From a5a35648d712e851d706df5311c5d385b4060646 Mon Sep 17 00:00:00 2001 From: liangxu Date: Thu, 9 Jul 2026 02:20:23 +0800 Subject: [PATCH] fix(server): complete generator task when thread settles or project ready MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Treat a generator task as completed when it produces artifacts, when a task message reaches a terminal state, or when the project is ready — even if the project is still exploring another concurrent thread. Adds generatorTaskMessageCompleted to classify terminal task messages and covers ready-without-messages and concurrent-thread cases with tests. Co-Authored-By: Claude Opus 4.8 (1M context) --- server/internal/application/generator_task.go | 29 ++++++-- .../application/generator_task_test.go | 74 +++++++++++++++++++ 2 files changed, 97 insertions(+), 6 deletions(-) diff --git a/server/internal/application/generator_task.go b/server/internal/application/generator_task.go index a070647..97e8347 100644 --- a/server/internal/application/generator_task.go +++ b/server/internal/application/generator_task.go @@ -384,7 +384,7 @@ func generatorTaskMetadataFromMessages(messages []design.Message, taskID string) func generatorTaskStatus(project design.Project, taskID string) string { messages := project.Messages status := "submitted" - completed := false + completed := len(generatorTaskArtifacts(messages, taskID)) > 0 for _, message := range messages { if message.ThreadID != taskID { continue @@ -392,9 +392,11 @@ func generatorTaskStatus(project design.Project, taskID string) string { if message.Role == "error" || message.Status == "failed" { return "failed" } + if generatorTaskMessageCompleted(message) { + completed = true + } switch message.Status { case "success", "cancelled": - completed = true case "running", "streaming", "submitted": status = "submitted" } @@ -405,15 +407,30 @@ func generatorTaskStatus(project design.Project, taskID string) string { if project.Status == design.StatusFailed { return "failed" } - if project.Status == design.StatusExploring { - return status - } - if len(generatorTaskArtifacts(messages, taskID)) > 0 || completed { + if completed || project.Status == design.StatusReady { return "completed" } return status } +func generatorTaskMessageCompleted(message design.Message) bool { + status := strings.ToLower(strings.TrimSpace(message.Status)) + if status != "success" && status != "cancelled" { + return false + } + name := strings.TrimSpace(message.Name) + if name == "agent_planner" || name == "generator_task" { + return false + } + if isToolArtifactsMessage(message) { + return true + } + return name == "canvas_action" || + name == "text_extraction" || + name == "mockup_model" || + strings.TrimSpace(message.Role) == "assistant" +} + func generatorTaskHasActiveCanvasWork(project design.Project, taskID string) bool { taskID = strings.TrimSpace(taskID) lastThreadID := strings.TrimSpace(project.LastThreadID) diff --git a/server/internal/application/generator_task_test.go b/server/internal/application/generator_task_test.go index b9eea9c..6f68f27 100644 --- a/server/internal/application/generator_task_test.go +++ b/server/internal/application/generator_task_test.go @@ -114,3 +114,77 @@ func TestGeneratorTaskStatusCompletesWhenCanvasSettles(t *testing.T) { t.Fatalf("expected settled canvas task to complete, got %s", task.Status) } } + +func TestGeneratorTaskStatusCompletesReadyProjectWithoutTaskMessages(t *testing.T) { + threadID := "thread-without-messages" + project := design.Project{ + ID: "project-ready-without-messages", + LastThreadID: threadID, + Status: design.StatusReady, + UpdatedAt: time.Now(), + Nodes: []design.Node{ + { + ID: "image-1", + Type: design.NodeTypeImage, + Title: "Generated image", + Content: "http://localhost:19000/canvas-assets/done.png", + Status: "success", + }, + }, + } + + task := generatorTaskFromProject(project, threadID) + if task.Status != "completed" { + t.Fatalf("expected ready project without task messages to complete, got %s", task.Status) + } +} + +func TestGeneratorTaskStatusCompletesFinishedThreadWhileProjectStillExploring(t *testing.T) { + threadID := "thread-done" + activeThreadID := "thread-active" + now := time.Now() + project := design.Project{ + ID: "project-concurrent", + LastThreadID: activeThreadID, + Status: design.StatusExploring, + UpdatedAt: now, + Nodes: []design.Node{ + { + ID: "done-image", + Type: design.NodeTypeImage, + Title: "Done image", + Content: "http://localhost:19000/canvas-assets/done.png", + Status: "success", + }, + { + ID: "active-image", + Type: design.NodeTypeImage, + Title: "Active image", + Content: "pending prompt", + Status: "generating", + }, + }, + Messages: []design.Message{ + generatorTaskMetadataMessage(threadID, removeBackgroundGeneratorName, design.GeneratorTaskInputArgs{}, 4, now.Add(-2*time.Second)), + {ID: "user", Role: "user", Type: "user", Name: "canvas_action", Status: "running", ThreadID: threadID}, + toolArtifactsMessage(threadID, "remove background", []design.Node{ + { + ID: "done-image", + Type: design.NodeTypeImage, + Title: "Done image", + Content: "http://localhost:19000/canvas-assets/done.png", + Status: "success", + Width: 1024, + Height: 1024, + }, + }, now.Add(-time.Second)), + {ID: "assistant", Role: "assistant", Type: "assistant", Name: "canvas_action", Status: "success", ThreadID: threadID}, + {ID: "active-tool", Role: "tool", Type: "tool", Title: "GPT Image 2", Status: "running", ThreadID: activeThreadID}, + }, + } + + task := generatorTaskFromProject(project, threadID) + if task.Status != "completed" { + t.Fatalf("expected finished thread to complete while another task runs, got %s", task.Status) + } +}