From b309d2beb6e2ebdb2025477b5eb94939d54d16ea Mon Sep 17 00:00:00 2001 From: nova28 Date: Sun, 9 Aug 2026 18:49:04 +0800 Subject: [PATCH] fix(web): render workflow description in settings (#2418) --- .../internal/task/service/service_events.go | 1 + .../internal/task/service/service_test.go | 31 +++ .../workspace/workspace-workflows-client.tsx | 6 +- .../workspace-workflows-order.test.ts | 50 +++- .../web/components/settings/workflow-card.tsx | 7 + .../settings/workflow-description-field.tsx | 41 +++ .../tests/gitlab/mr-task-card-badge.spec.ts | 1 - .../tests/workflow/workflow-settings.spec.ts | 85 ++++++ .../workflow/workflow-sync-gitlab.spec.ts | 10 + .../settings/use-workflow-settings.test.ts | 252 ++++++++++++++++++ .../domains/settings/use-workflow-settings.ts | 117 +++++++- apps/web/hooks/use-workflows.test.ts | 15 ++ apps/web/hooks/use-workflows.ts | 1 + apps/web/lib/state/slices/kanban/types.ts | 1 + apps/web/lib/ws/handlers/workflows.test.ts | 22 ++ apps/web/lib/ws/handlers/workflows.ts | 2 + apps/web/src/locales/en/workflows.json | 2 + apps/web/src/locales/pseudo/workflows.json | 2 + 18 files changed, 627 insertions(+), 19 deletions(-) create mode 100644 apps/web/components/settings/workflow-description-field.tsx diff --git a/apps/backend/internal/task/service/service_events.go b/apps/backend/internal/task/service/service_events.go index 3e427f731d..467cb6d07b 100644 --- a/apps/backend/internal/task/service/service_events.go +++ b/apps/backend/internal/task/service/service_events.go @@ -762,6 +762,7 @@ func (s *Service) publishWorkflowEvent(ctx context.Context, eventType string, wo "workspace_id": workflow.WorkspaceID, "name": workflow.Name, "description": workflow.Description, + "prompt": workflow.Prompt, "agent_profile_id": workflow.AgentProfileID, "hidden": workflow.Hidden, "source": workflow.Source, diff --git a/apps/backend/internal/task/service/service_test.go b/apps/backend/internal/task/service/service_test.go index 3531166c7c..32ff09af89 100644 --- a/apps/backend/internal/task/service/service_test.go +++ b/apps/backend/internal/task/service/service_test.go @@ -2824,6 +2824,37 @@ func TestService_UpdateWorkflow(t *testing.T) { t.Errorf("expected name 'Updated', got %s", updated.Name) } } + +func TestService_UpdateWorkflow_PublishesPromptOnWorkflowEvent(t *testing.T) { + // Cross-tab WS sync (use-workflow-settings.ts) relies on workflow.updated + // carrying the current prompt, the same way it already carries + // description and agent_profile_id. + svc, eventBus, repo := createTestService(t) + ctx := context.Background() + + _ = repo.CreateWorkspace(ctx, &models.Workspace{ID: "ws-1", Name: "Workspace"}) + workflow := &models.Workflow{ID: "wf-123", WorkspaceID: "ws-1", Name: "Original"} + _ = repo.CreateWorkflow(ctx, workflow) + + newPrompt := "Updated prompt" + req := &UpdateWorkflowRequest{Prompt: &newPrompt} + + if _, err := svc.UpdateWorkflow(ctx, "wf-123", req); err != nil { + t.Fatalf("failed to update workflow: %v", err) + } + + if len(eventBus.publishedEvents) == 0 { + t.Fatal("expected a workflow.updated event to be published") + } + last := eventBus.publishedEvents[len(eventBus.publishedEvents)-1] + data, ok := last.Data.(map[string]interface{}) + if !ok { + t.Fatalf("expected event data to be a map, got %T", last.Data) + } + if data["prompt"] != newPrompt { + t.Errorf("expected published prompt %q, got %v", newPrompt, data["prompt"]) + } +} func TestService_DeleteWorkflow(t *testing.T) { svc, _, repo := createTestService(t) ctx := context.Background() diff --git a/apps/web/app/settings/workspace/workspace-workflows-client.tsx b/apps/web/app/settings/workspace/workspace-workflows-client.tsx index 4a32ee3dfa..04e4584d13 100644 --- a/apps/web/app/settings/workspace/workspace-workflows-client.tsx +++ b/apps/web/app/settings/workspace/workspace-workflows-client.tsx @@ -161,12 +161,12 @@ function useWorkflowImportExport( }; } -function hasNewerWorkflowMetadata(current: Workflow, savedFrom: Workflow) { +export function hasNewerWorkflowMetadata(current: Workflow, savedFrom: Workflow) { return ( current.name !== savedFrom.name || - current.description !== savedFrom.description || + (current.description ?? "") !== (savedFrom.description ?? "") || (current.prompt ?? "") !== (savedFrom.prompt ?? "") || - current.agent_profile_id !== savedFrom.agent_profile_id + (current.agent_profile_id ?? "") !== (savedFrom.agent_profile_id ?? "") ); } diff --git a/apps/web/app/settings/workspace/workspace-workflows-order.test.ts b/apps/web/app/settings/workspace/workspace-workflows-order.test.ts index 147ee0bc6a..7a71d7a665 100644 --- a/apps/web/app/settings/workspace/workspace-workflows-order.test.ts +++ b/apps/web/app/settings/workspace/workspace-workflows-order.test.ts @@ -1,8 +1,13 @@ import { describe, expect, it } from "vitest"; -import { workflowId as toWorkflowId, type Workflow } from "@/lib/types/http"; +import { + agentProfileId as toAgentProfileId, + workflowId as toWorkflowId, + type Workflow, +} from "@/lib/types/http"; import { alignSavedWorkflowsToDraftOrder, getWorkflowOrderDirtyIds, + hasNewerWorkflowMetadata, } from "./workspace-workflows-client"; function workflow(id: string, name = id): Workflow { @@ -68,3 +73,46 @@ describe("getWorkflowOrderDirtyIds", () => { expect([...getWorkflowOrderDirtyIds([first, draft], [first])]).toEqual([draft.id]); }); }); + +describe("hasNewerWorkflowMetadata", () => { + it("treats an empty description as unchanged from an absent saved description", () => { + // Backend omits `description` when empty (omitempty), so a save response's + // description is undefined, not "". A draft that was never edited beyond + // that (or was edited then cleared) must not be treated as "newer" than + // what was just submitted, or a save reconciliation discards the response. + const saved: Workflow = { ...workflow("wf-1"), description: undefined }; + const current: Workflow = { ...workflow("wf-1"), description: "" }; + + expect(hasNewerWorkflowMetadata(current, saved)).toBe(false); + }); + + it("still detects a real description change", () => { + const saved: Workflow = { ...workflow("wf-1"), description: "old" }; + const current: Workflow = { ...workflow("wf-1"), description: "new" }; + + expect(hasNewerWorkflowMetadata(current, saved)).toBe(true); + }); + + it("treats an empty agent_profile_id as unchanged from an absent saved agent_profile_id", () => { + // Same omitempty class of bug as description: clearing a profile override + // produces agent_profile_id === "" locally, but the save response omits + // the empty field, so saved.agent_profile_id is undefined. + const saved: Workflow = { ...workflow("wf-1"), agent_profile_id: undefined }; + const current: Workflow = { ...workflow("wf-1"), agent_profile_id: toAgentProfileId("") }; + + expect(hasNewerWorkflowMetadata(current, saved)).toBe(false); + }); + + it("still detects a real agent_profile_id change", () => { + const saved: Workflow = { + ...workflow("wf-1"), + agent_profile_id: toAgentProfileId("profile-a"), + }; + const current: Workflow = { + ...workflow("wf-1"), + agent_profile_id: toAgentProfileId("profile-b"), + }; + + expect(hasNewerWorkflowMetadata(current, saved)).toBe(true); + }); +}); diff --git a/apps/web/components/settings/workflow-card.tsx b/apps/web/components/settings/workflow-card.tsx index 1a48942537..804b9df439 100644 --- a/apps/web/components/settings/workflow-card.tsx +++ b/apps/web/components/settings/workflow-card.tsx @@ -30,6 +30,7 @@ import { useWorkflowMutationGuard } from "./workflow-mutation-guard"; import { WorkflowCycleGuardDialog } from "./workflow-cycle-diagnostic"; import { useWorkflowDraftContributor } from "./use-workflow-draft-contributor"; import { WorkflowPromptSection } from "./workflow-prompt-section"; +import { WorkflowDescriptionField } from "./workflow-description-field"; const TEMP_WORKFLOW_PREFIX = "temp-workflow-"; @@ -342,6 +343,12 @@ function WorkflowCardBody({ + onUpdateWorkflow({ description })} + /> void; +}; + +// Always visible, unlike WorkflowPromptSection's collapsible — every synced workflow carries one. +export function WorkflowDescriptionField({ + workflow, + savedWorkflow, + readOnly, + onUpdate, +}: WorkflowDescriptionFieldProps) { + const { t } = useTranslation(); + const inputId = `workflow-description-input-${workflow.id}`; + return ( +
+ +