From 07a0a0c68ff95c51d355de1e4c677784112f3ed0 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Wed, 3 Jun 2026 09:51:26 -0700 Subject: [PATCH] feat(dashboard): surface workflow selection on tasks and project settings (U8) Add WorkflowSelector: a per-task picker in the task detail workflow tab that applies a workflow (selection returns the resulting enabledWorkflowSteps so the controlled steps list refreshes in place), and a ProjectDefaultWorkflowField in Project General settings for the default new tasks inherit. --- packages/core/src/store.ts | 3 +- packages/dashboard/app/api/legacy.ts | 16 +- .../app/components/SettingsModal.tsx | 5 + .../app/components/WorkflowResultsTab.tsx | 38 ++++- .../app/components/WorkflowSelector.css | 48 ++++++ .../app/components/WorkflowSelector.tsx | 137 ++++++++++++++++++ .../__tests__/WorkflowResultsTab.test.tsx | 3 + .../src/routes/register-workflow-routes.ts | 7 +- 8 files changed, 246 insertions(+), 11 deletions(-) create mode 100644 packages/dashboard/app/components/WorkflowSelector.css create mode 100644 packages/dashboard/app/components/WorkflowSelector.tsx diff --git a/packages/core/src/store.ts b/packages/core/src/store.ts index 0e4ceb1f63..0e80d56158 100644 --- a/packages/core/src/store.ts +++ b/packages/core/src/store.ts @@ -11161,7 +11161,7 @@ ${stepsSection}`; * (no orphaned steps). Throws WorkflowCompileError for non-linear graphs * before any state is written. */ - async selectTaskWorkflow(taskId: string, workflowId: string): Promise { + async selectTaskWorkflow(taskId: string, workflowId: string): Promise { const def = await this.getWorkflowDefinition(workflowId); if (!def) throw new Error(`Workflow '${workflowId}' not found`); // Compile first so a non-linear graph aborts before we mutate anything. @@ -11181,6 +11181,7 @@ ${stepsSection}`; await this.updateTask(taskId, { enabledWorkflowSteps: ids }); this.writeTaskWorkflowSelection(taskId, workflowId, ids); + return ids; } /** Clear a task's workflow selection and its enabled steps. */ diff --git a/packages/dashboard/app/api/legacy.ts b/packages/dashboard/app/api/legacy.ts index d2350e96e9..d8e010616c 100644 --- a/packages/dashboard/app/api/legacy.ts +++ b/packages/dashboard/app/api/legacy.ts @@ -4959,16 +4959,20 @@ export function fetchTaskWorkflow(taskId: string, projectId?: string): Promise<{ ); } -/** Select (or clear, with null) a workflow for a task. */ +/** Select (or clear, with null) a workflow for a task. Returns the resulting + * enabled step ids so callers can reflect the change without a refetch. */ export function selectTaskWorkflow( taskId: string, workflowId: string | null, projectId?: string, -): Promise<{ workflowId: string | null }> { - return api<{ workflowId: string | null }>(withProjectId(`/tasks/${encodeURIComponent(taskId)}/workflow`, projectId), { - method: "PUT", - body: JSON.stringify({ workflowId }), - }); +): Promise<{ workflowId: string | null; enabledWorkflowSteps: string[] }> { + return api<{ workflowId: string | null; enabledWorkflowSteps: string[] }>( + withProjectId(`/tasks/${encodeURIComponent(taskId)}/workflow`, projectId), + { + method: "PUT", + body: JSON.stringify({ workflowId }), + }, + ); } /** Read the project default workflow. */ diff --git a/packages/dashboard/app/components/SettingsModal.tsx b/packages/dashboard/app/components/SettingsModal.tsx index fcdd2df02b..93fb01ff84 100644 --- a/packages/dashboard/app/components/SettingsModal.tsx +++ b/packages/dashboard/app/components/SettingsModal.tsx @@ -16,6 +16,7 @@ import { import type { AgentPermissionPolicyRules, Settings, GlobalSettings, ThemeMode, ColorTheme, ModelPreset, NtfyNotificationEvent, AgentPromptsConfig, ThinkingLevel } from "@fusion/core"; import { fetchSettings, fetchSettingsByScope, updateSettings, updateGlobalSettings, fetchAuthStatus, loginProvider, logoutProvider, cancelProviderLogin, saveApiKey, clearApiKey, fetchModels, testNotification, fetchBackups, createBackup, exportSettings, importSettings, fetchMemoryFile, fetchMemoryFiles, saveMemoryFile, compactMemory, fetchGlobalConcurrency, updateGlobalConcurrency, installQmd, testMemoryRetrieval, triggerMemoryDreams, fetchGitRemotes, fetchGitRemotesDetailed, fetchGitBranches, fetchProjects, fetchDashboardHealth, checkForUpdates, fetchRemoteSettings, updateRemoteSettings, fetchRemoteStatus, installCloudflared, startRemoteTunnel, stopRemoteTunnel, killExternalTunnel, regenerateRemotePersistentToken, generateShortLivedRemoteToken, fetchRemoteQr, fetchRemoteUrl, submitProviderManualCode } from "../api"; import type { AuthProvider, ManualOAuthCodeInfo, ModelInfo, BackupListResponse, SettingsExportData, MemoryFileInfo, MemoryRetrievalTestResult, GitRemote, GitRemoteDetailed, ProjectInfo, RemoteSettings, RemoteStatus, UpdateCheckResponse, OAuthDeviceCodeInfo } from "../api"; +import { ProjectDefaultWorkflowField } from "./WorkflowSelector"; import { useMemoryBackendStatus } from "../hooks/useMemoryBackendStatus"; import { useOverlayDismiss } from "../hooks/useOverlayDismiss"; import type { ToastType } from "../hooks/useToast"; @@ -2347,6 +2348,10 @@ export function SettingsModal({ {prefixError && {prefixError}} {!prefixError && Prefix for new task IDs (e.g. KB, PROJ)} +
+ + New tasks inherit this custom workflow's steps (overridable per task) +