From 33c9d0b48d99dbd429820bf63572f24021db36c5 Mon Sep 17 00:00:00 2001 From: Jorben Date: Sun, 7 Jun 2026 17:49:18 +0800 Subject: [PATCH] =?UTF-8?q?refactor(workbench):=20=E2=99=BB=EF=B8=8F=20rem?= =?UTF-8?q?ove=20runMode=20from=20frontend=20state=20management?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the runMode concept from composer store, thread surface state, submission types, and helper functions. The backend now determines the run mode, so the frontend no longer needs to track or pass it. - Remove newThreadRunMode from composer store - Remove runMode from ComposerSubmission, PendingThreadRun, NewThreadSubmission - Remove deriveSelectedRunMode and selectedRunMode state - Remove runMode parameter from buildComposerSubmission - Remove runMode from InitialPromptRequest type - Hardcode 'default' as the fixed run mode sent to backend --- .../model/composer-commands.test.ts | 11 ++++------ .../model/composer-commands.ts | 6 +----- .../model/composer-store.test.ts | 8 ------- .../workbench-shell/model/composer-store.ts | 8 ------- .../model/thread-store.test.ts | 1 - .../model/workbench-actions.test.ts | 9 ++------ .../model/workbench-actions.ts | 6 +----- .../ui/dashboard-workbench-logic.ts | 3 +-- .../ui/dashboard-workbench.tsx | 2 -- .../ui/runtime-thread-surface-state.ts | 18 ---------------- .../ui/runtime-thread-surface.tsx | 21 +++---------------- .../ui/workbench-prompt-composer.tsx | 7 ++----- 12 files changed, 14 insertions(+), 86 deletions(-) diff --git a/src/modules/workbench-shell/model/composer-commands.test.ts b/src/modules/workbench-shell/model/composer-commands.test.ts index 5b9a14fe..5ce79982 100644 --- a/src/modules/workbench-shell/model/composer-commands.test.ts +++ b/src/modules/workbench-shell/model/composer-commands.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it } from "vitest"; import type { PromptInputMessage } from "@/components/ai-elements/prompt-input"; -import type { RunMode } from "@/shared/types/api"; import { buildCommandEffectivePrompt, buildComposerCommandRegistry, @@ -10,8 +9,6 @@ import { parseSlashCommandInput, } from "@/modules/workbench-shell/model/composer-commands"; -const RUN_MODE: RunMode = "default"; - function createMessage(text: string): PromptInputMessage { return { text, @@ -22,7 +19,7 @@ function createMessage(text: string): PromptInputMessage { describe("buildComposerSubmission", () => { it("preserves plain multi-line Markdown exactly", () => { const text = " 1. First\n2. Second\n\n- Bullet\n```ts\nconst value = 1;\n```\n "; - const submission = buildComposerSubmission(createMessage(text), [], RUN_MODE); + const submission = buildComposerSubmission(createMessage(text), []); expect(submission).not.toBeNull(); expect(submission?.kind).toBe("plain"); @@ -31,7 +28,7 @@ describe("buildComposerSubmission", () => { }); it("rejects whitespace-only messages without attachments", () => { - const submission = buildComposerSubmission(createMessage(" \n\t "), [], RUN_MODE); + const submission = buildComposerSubmission(createMessage(" \n\t "), []); expect(submission).toBeNull(); }); @@ -39,7 +36,7 @@ describe("buildComposerSubmission", () => { it("parses slash commands from trimmed text while preserving the original display text", () => { const registry = buildComposerCommandRegistry([]); const text = " /init \n"; - const submission = buildComposerSubmission(createMessage(text), registry, RUN_MODE); + const submission = buildComposerSubmission(createMessage(text), registry); expect(submission).not.toBeNull(); expect(submission?.kind).toBe("command"); @@ -51,7 +48,7 @@ describe("buildComposerSubmission", () => { it("preserves multi-line /goal objectives as command arguments", () => { const registry = buildComposerCommandRegistry([]); const text = "/goal First goal line\nSecond goal line\n- checklist item"; - const submission = buildComposerSubmission(createMessage(text), registry, RUN_MODE); + const submission = buildComposerSubmission(createMessage(text), registry); expect(submission).not.toBeNull(); expect(submission?.kind).toBe("command"); diff --git a/src/modules/workbench-shell/model/composer-commands.ts b/src/modules/workbench-shell/model/composer-commands.ts index 0c1c7cd8..5e5a26f1 100644 --- a/src/modules/workbench-shell/model/composer-commands.ts +++ b/src/modules/workbench-shell/model/composer-commands.ts @@ -1,6 +1,6 @@ import type { PromptInputMessage } from "@/components/ai-elements/prompt-input"; import type { CommandEntry } from "@/modules/settings-center/model/types"; -import type { MessageAttachmentDto, RunMode } from "@/shared/types/api"; +import type { MessageAttachmentDto } from "@/shared/types/api"; export type ComposerReferencedFile = { name: string; @@ -139,7 +139,6 @@ export type ComposerSubmission = { attachments: MessageAttachmentDto[]; command?: ComposerCommandInvocation; metadata?: Record | null; - runMode?: RunMode; }; const BUILTIN_COMMANDS: ReadonlyArray = [ @@ -551,7 +550,6 @@ export function buildCommandEffectivePrompt( export function buildComposerSubmission( message: PromptInputMessage, registry: ReadonlyArray, - runMode?: RunMode, ): ComposerSubmission | null { const rawText = message.text ?? ""; const trimmedText = rawText.trim(); @@ -574,7 +572,6 @@ export function buildComposerSubmission( rawMessage: message, attachments, metadata: null, - runMode, }; } @@ -605,6 +602,5 @@ export function buildComposerSubmission( command: invocation, }, }, - runMode, }; } diff --git a/src/modules/workbench-shell/model/composer-store.test.ts b/src/modules/workbench-shell/model/composer-store.test.ts index d0b0ca5a..c0b805a4 100644 --- a/src/modules/workbench-shell/model/composer-store.test.ts +++ b/src/modules/workbench-shell/model/composer-store.test.ts @@ -2,7 +2,6 @@ import { describe, it, expect, beforeEach } from "vitest"; import { composerStore, setNewThreadValue, - setNewThreadRunMode, setNewThreadReferencedFiles, setNewThreadAttachmentData, setDraft, @@ -25,13 +24,6 @@ describe("composerStore", () => { expect(composerStore.getState().newThreadValue).toBe("hello world"); }); - it("should set new thread run mode", () => { - setNewThreadRunMode("plan"); - expect(composerStore.getState().newThreadRunMode).toBe("plan"); - setNewThreadRunMode("default"); - expect(composerStore.getState().newThreadRunMode).toBe("default"); - }); - it("should clear new thread composer", () => { setNewThreadValue("some value"); setNewThreadReferencedFiles([{ name: "test.ts", path: "/test.ts", parentPath: "/" }]); diff --git a/src/modules/workbench-shell/model/composer-store.ts b/src/modules/workbench-shell/model/composer-store.ts index fea3cbed..c48adcd8 100644 --- a/src/modules/workbench-shell/model/composer-store.ts +++ b/src/modules/workbench-shell/model/composer-store.ts @@ -1,5 +1,4 @@ import { createStore, useStore as useStoreBase, shallowEqual } from "@/shared/lib/create-store"; -import type { RunMode } from "@/shared/types/api"; import type { ComposerReferencedFile } from "@/modules/workbench-shell/model/composer-commands"; // --------------------------------------------------------------------------- @@ -28,8 +27,6 @@ export interface ComposerStoreState { [key: string]: unknown; /** Input value for new-thread mode. */ newThreadValue: string; - /** Run mode for new threads (default / plan). */ - newThreadRunMode: RunMode; /** @file references for new-thread composer. */ newThreadReferencedFiles: ComposerReferencedFile[]; /** Serialized attachment data for new-thread composer. */ @@ -46,7 +43,6 @@ export interface ComposerStoreState { export const composerStore = createStore({ newThreadValue: "", - newThreadRunMode: "default", newThreadReferencedFiles: [], newThreadAttachmentData: [], drafts: {}, @@ -67,10 +63,6 @@ export function setNewThreadValue(value: string): void { composerStore.setState({ newThreadValue: value }); } -export function setNewThreadRunMode(mode: RunMode): void { - composerStore.setState({ newThreadRunMode: mode }); -} - export function setNewThreadReferencedFiles(files: ReadonlyArray): void { composerStore.setState({ newThreadReferencedFiles: files as ComposerReferencedFile[] }); } diff --git a/src/modules/workbench-shell/model/thread-store.test.ts b/src/modules/workbench-shell/model/thread-store.test.ts index 7980ec71..24e8f3c3 100644 --- a/src/modules/workbench-shell/model/thread-store.test.ts +++ b/src/modules/workbench-shell/model/thread-store.test.ts @@ -42,7 +42,6 @@ function makePendingRun(overrides?: Partial): PendingThreadRun effectivePrompt: "test prompt", attachments: [], metadata: null, - runMode: "default", threadId: "thread-1", ...overrides, }; diff --git a/src/modules/workbench-shell/model/workbench-actions.test.ts b/src/modules/workbench-shell/model/workbench-actions.test.ts index 5950c7a9..009ce0e8 100644 --- a/src/modules/workbench-shell/model/workbench-actions.test.ts +++ b/src/modules/workbench-shell/model/workbench-actions.test.ts @@ -387,7 +387,7 @@ describe("deleteThread", () => { threadStore.setState({ activeThreadId: null, workspaces: [makeWorkspace("ws-1", [thread])], - pendingRuns: { "thread-1": { id: "run-1", prompt: "test", runMode: "auto" } as any }, + pendingRuns: { "thread-1": { id: "run-1", prompt: "test" } as any }, }); await deleteThread("thread-1", { skipIpc: true }); @@ -461,7 +461,7 @@ describe("removeWorkspace", () => { threadStore.setState({ activeThreadId: null, workspaces: [ws1], - pendingRuns: { "thread-1": { id: "run-1", prompt: "test", runMode: "auto" } as any }, + pendingRuns: { "thread-1": { id: "run-1", prompt: "test" } as any }, }); await removeWorkspace(ws1 as any); @@ -583,7 +583,6 @@ describe("submitNewThread", () => { ): NewThreadSubmission { return { value: "test prompt", - runMode: "default", effectivePrompt: "test prompt", ...overrides, }; @@ -594,7 +593,6 @@ describe("submitNewThread", () => { workspaces: [], isNewThreadMode: true, }); - composerStore.setState({ newThreadRunMode: "default" }); settingsStore.setState({ activeAgentProfileId: "default-profile" }); await submitNewThread(makeSubmission({ value: "hello" })); @@ -626,7 +624,6 @@ describe("submitNewThread", () => { workspaces: [workspace], isNewThreadMode: true, }); - composerStore.setState({ newThreadRunMode: "default" }); settingsStore.setState({ activeAgentProfileId: "default-profile" }); await submitNewThread(makeSubmission({ value: "hello" })); @@ -659,7 +656,6 @@ describe("submitNewThread", () => { isNewThreadMode: true, }); projectStore.setState({ selectedProject: project, recentProjects: [project] }); - composerStore.setState({ newThreadRunMode: "default" }); settingsStore.setState({ activeAgentProfileId: "default-profile" }); await submitNewThread(makeSubmission({ @@ -703,7 +699,6 @@ describe("submitNewThread", () => { workspaces: [workspace], isNewThreadMode: true, }); - composerStore.setState({ newThreadRunMode: "default" }); settingsStore.setState({ activeAgentProfileId: "default-profile" }); await submitNewThread(makeSubmission({ value: "new thread" })); diff --git a/src/modules/workbench-shell/model/workbench-actions.ts b/src/modules/workbench-shell/model/workbench-actions.ts index da71fc2c..02f3803e 100644 --- a/src/modules/workbench-shell/model/workbench-actions.ts +++ b/src/modules/workbench-shell/model/workbench-actions.ts @@ -51,7 +51,7 @@ import { findWorkspaceByPath, } from "@/modules/workbench-shell/model/workspace-path-bindings"; import type { ProjectOption, WorkspaceItem } from "@/modules/workbench-shell/model/types"; -import type { MessageAttachmentDto, RunMode, ThreadSummaryDto, WorkspaceDto } from "@/shared/types/api"; +import type { MessageAttachmentDto, ThreadSummaryDto, WorkspaceDto } from "@/shared/types/api"; import type { LanguagePreference } from "@/app/providers/language-provider"; import type { ComposerCommandInvocation } from "@/modules/workbench-shell/model/composer-commands"; @@ -61,7 +61,6 @@ import type { ComposerCommandInvocation } from "@/modules/workbench-shell/model/ export interface NewThreadSubmission { value: string; - runMode: RunMode; displayText?: string; effectivePrompt: string; attachments?: MessageAttachmentDto[]; @@ -574,7 +573,6 @@ export async function submitNewThread(submission: NewThreadSubmission): Promise< // Re-read workspaces after async IPC to avoid stale state const { workspaces } = threadStore.getState(); - const { newThreadRunMode } = composerStore.getState(); // Find or match the workspace in the sidebar const existingWorkspace = @@ -674,7 +672,6 @@ export async function submitNewThread(submission: NewThreadSubmission): Promise< attachments: (submission.attachments ?? []) as unknown as PendingThreadRun["attachments"], metadata: submission.metadata ?? null, command: submission.command, - runMode: submission.runMode ?? newThreadRunMode, threadId, }; return { @@ -717,7 +714,6 @@ export async function submitNewThread(submission: NewThreadSubmission): Promise< // Reset composer composerStore.setState({ newThreadValue: "", - newThreadRunMode: "default", newThreadReferencedFiles: [], newThreadAttachmentData: [], error: null, diff --git a/src/modules/workbench-shell/ui/dashboard-workbench-logic.ts b/src/modules/workbench-shell/ui/dashboard-workbench-logic.ts index 55658cbe..9dfc20f1 100644 --- a/src/modules/workbench-shell/ui/dashboard-workbench-logic.ts +++ b/src/modules/workbench-shell/ui/dashboard-workbench-logic.ts @@ -1,5 +1,5 @@ import type { LanguagePreference } from "@/app/providers/language-provider"; -import type { MessageAttachmentDto, RunMode, WorkspaceDto } from "@/shared/types/api"; +import type { MessageAttachmentDto, WorkspaceDto } from "@/shared/types/api"; import { buildProjectOptionFromPath } from "@/modules/workbench-shell/model/helpers"; import type { ProjectOption, @@ -188,6 +188,5 @@ export type PendingThreadRun = { attachments: MessageAttachmentDto[]; metadata: Record | null; command?: import("@/modules/workbench-shell/model/composer-commands").ComposerCommandInvocation; - runMode: RunMode; threadId: string; }; diff --git a/src/modules/workbench-shell/ui/dashboard-workbench.tsx b/src/modules/workbench-shell/ui/dashboard-workbench.tsx index b8129cac..f2dd227c 100644 --- a/src/modules/workbench-shell/ui/dashboard-workbench.tsx +++ b/src/modules/workbench-shell/ui/dashboard-workbench.tsx @@ -262,7 +262,6 @@ const drawerWidth = useStore(uiLayoutStore, (s) => s.drawerWidth); const composerValue = useStore(composerStore, (s) => s.newThreadValue); const composerError = useStore(composerStore, (s) => s.error); - const newThreadRunMode = useStore(composerStore, (s) => s.newThreadRunMode); const newThreadReferencedFiles = useStore(composerStore, (s) => s.newThreadReferencedFiles); const newThreadAttachmentData = useStore(composerStore, (s) => s.newThreadAttachmentData); @@ -714,7 +713,6 @@ const drawerWidth = useStore(uiLayoutStore, (s) => s.drawerWidth); void submitNewThread({ value: trimmedValue, - runMode: newThreadRunMode, displayText: submission.displayText, effectivePrompt, attachments: submission.attachments, diff --git a/src/modules/workbench-shell/ui/runtime-thread-surface-state.ts b/src/modules/workbench-shell/ui/runtime-thread-surface-state.ts index 1b862dbe..dab0e8be 100644 --- a/src/modules/workbench-shell/ui/runtime-thread-surface-state.ts +++ b/src/modules/workbench-shell/ui/runtime-thread-surface-state.ts @@ -7,7 +7,6 @@ import type { MessageAttachmentDto, MessageDto, MessagePartDto, - RunMode, RunSummaryDto, ThreadSnapshotDto, ToolCallDto, @@ -279,7 +278,6 @@ export type InitialPromptRequest = { effectivePrompt: string; attachments: MessageAttachmentDto[]; metadata: Record | null; - runMode?: RunMode; }; export type ThinkingPlaceholder = { @@ -392,22 +390,6 @@ export function mapRecordedUserMessage(event: RecordedUserMessageEvent): Surface -export function deriveSelectedRunMode(snapshot: ThreadSnapshotDto, currentMode: RunMode) { - if ( - snapshot.thread.status === "waiting_approval" - && !snapshot.activeRun - && snapshot.latestRun?.runMode === "plan" - ) { - return "plan"; - } - - if (snapshot.activeRun?.runMode === "default" || snapshot.activeRun?.runMode === "plan") { - return snapshot.activeRun.runMode; - } - - return currentMode; -} - export function formatApprovalPromptState(state: string, approvedAction: PlanApprovalAction | null, t: (key: TranslationKey) => string) { switch (state) { case "approved": diff --git a/src/modules/workbench-shell/ui/runtime-thread-surface.tsx b/src/modules/workbench-shell/ui/runtime-thread-surface.tsx index 6fbfb3e3..ef06efe8 100644 --- a/src/modules/workbench-shell/ui/runtime-thread-surface.tsx +++ b/src/modules/workbench-shell/ui/runtime-thread-surface.tsx @@ -38,7 +38,6 @@ import { } from "@/services/thread-stream"; import type { MessageAttachmentDto, - RunMode, RuntimeQueueMessageKind, RuntimeQueueMessageDto, RuntimeQueueSnapshotDto, @@ -118,7 +117,6 @@ import { TaskHistoryTimeline } from "@/modules/workbench-shell/ui/task-stage-his import { appendOrReplaceMessage, compareTimelineEntries, - deriveSelectedRunMode, formatApprovalPromptState, formatToolStatusLabel, getApprovalReason, @@ -356,7 +354,6 @@ export function RuntimeThreadSurface({ threadStore, (s) => (threadId ? s.threadStatuses[threadId]?.runId ?? null : null), ); - const [selectedRunMode, setSelectedRunMode] = useState("default"); const [snapshotReady, setSnapshotReady] = useState(false); const [snapshotThreadId, setSnapshotThreadId] = useState(null); @@ -376,7 +373,6 @@ export function RuntimeThreadSurface({ useEffect(() => { if (prevThreadIdRef.current !== threadId) { prevThreadIdRef.current = threadId; - setSelectedRunMode("default"); setRuntimeQueueSubmitMode(defaultAppendMessageKind); setRequestRetryEntries([]); setRequestRetryOpen({}); @@ -694,7 +690,6 @@ export function RuntimeThreadSurface({ } eventBufferRef.current = []; } - setSelectedRunMode((current) => deriveSelectedRunMode(snapshot, current)); if (!shouldPreserveContextUsage) { threadStore.setState({ runtimeContextUsage: nextContextUsage }); } @@ -836,7 +831,6 @@ export function RuntimeThreadSurface({ errorMessage: null, retryCount: 0, }); - setSelectedRunMode((current) => deriveSelectedRunMode(snapshot, current)); const latestVisibleRun = getLatestVisibleRun(snapshot); if (latestVisibleRun?.id === runId) { @@ -1017,9 +1011,6 @@ export function RuntimeThreadSurface({ if (event.type === "run_started") { setApprovingPlanMessageId(null); - if (event.runMode === "default" || event.runMode === "plan") { - setSelectedRunMode(event.runMode); - } } if (event.type === "stream_resync_required") { @@ -1505,7 +1496,6 @@ export function RuntimeThreadSurface({ const submitPrompt = useCallback(async ( submissionOrPrompt: ComposerSubmission | string, - runModeOverride?: RunMode, ): Promise => { if (!threadId) { setComposerError("This thread is still preparing. Try again in a moment."); @@ -1520,7 +1510,6 @@ export function RuntimeThreadSurface({ rawMessage: { text: submissionOrPrompt, files: [] }, attachments: [], metadata: null, - runMode: runModeOverride, } : submissionOrPrompt; const prompt = submission.effectivePrompt ?? ""; @@ -1726,7 +1715,7 @@ export function RuntimeThreadSurface({ promptMetadata: submission.metadata ?? null, attachments: submission.attachments, }, - runModeOverride ?? submission.runMode ?? selectedRunMode, + "default", modelPlan, ); } catch (error) { @@ -1736,7 +1725,7 @@ export function RuntimeThreadSurface({ submittingRef.current = false; } return true; - }, [activeAgentProfileId, activeProfile, agentProfiles, appendOptimisticUserMessage, loadSnapshot, providers, runState, runtimeQueueSubmitMode, selectedRunMode, t, threadId]); + }, [activeAgentProfileId, activeProfile, agentProfiles, appendOptimisticUserMessage, loadSnapshot, providers, runState, runtimeQueueSubmitMode, t, threadId]); const cancelRuntimeQueueMessage = useCallback(async (messageId: string) => { if (!threadId || !streamRef.current) { @@ -1884,9 +1873,6 @@ export function RuntimeThreadSurface({ // Mark as handled at the store level — survives component unmount/remount. markPendingRunHandled(initialPromptRequestId!); - if (initialPromptRequest.runMode) { - setSelectedRunMode(initialPromptRequest.runMode); - } const pendingRunSubmission: ComposerSubmission = { kind: initialPromptRequest.command ? "command" : "plain", displayText: initialPromptRequest.displayText, @@ -1895,9 +1881,8 @@ export function RuntimeThreadSurface({ attachments: initialPromptRequest.attachments, command: initialPromptRequest.command, metadata: initialPromptRequest.metadata, - runMode: initialPromptRequest.runMode, }; - void submitPrompt(pendingRunSubmission, initialPromptRequest.runMode) + void submitPrompt(pendingRunSubmission) .finally(() => { threadStore.setState((prev) => { const next = Object.fromEntries( diff --git a/src/modules/workbench-shell/ui/workbench-prompt-composer.tsx b/src/modules/workbench-shell/ui/workbench-prompt-composer.tsx index 6acd577a..25df8b42 100644 --- a/src/modules/workbench-shell/ui/workbench-prompt-composer.tsx +++ b/src/modules/workbench-shell/ui/workbench-prompt-composer.tsx @@ -72,7 +72,7 @@ import type { updateAgentProfile as updateAgentProfileAction } from "@/modules/s import { sortAgentProfilesByName } from "@/modules/settings-center/model/profile-utils"; import { profileSubagentAccessGet } from "@/services/bridge/subagent-commands"; import type { SkillRecord } from "@/shared/types/extensions"; -import type { RunMode, RuntimeQueueMessageKind } from "@/shared/types/api"; +import type { RuntimeQueueMessageKind } from "@/shared/types/api"; import { indexFilterFiles, type FileFilterMatch } from "@/services/bridge"; import type { SerializableAttachment } from "@/modules/workbench-shell/model/composer-store"; import { useT } from "@/i18n"; @@ -182,7 +182,6 @@ function isSlashCommandActive(value: string) { function buildSubmissionFromPromptInput( message: PromptInputMessage, registry: ReadonlyArray, - runMode: RunMode, referencedFiles: ReadonlyArray, ): ComposerSubmission { const rawText = message.text ?? ""; @@ -215,7 +214,6 @@ function buildSubmissionFromPromptInput( }, } : null, - runMode, }; } @@ -227,7 +225,6 @@ function buildSubmissionFromPromptInput( effectivePrompt, rawMessage: message, attachments, - runMode, command: { source: parsedCommand.command.source, name: parsedCommand.command.name, @@ -2143,7 +2140,7 @@ export function WorkbenchPromptComposer({ // must be called inside a PromptInput descendant. const handlePromptSubmit = async (message: PromptInputMessage) => { - const submission = buildSubmissionFromPromptInput(message, commandRegistry, "default", referencedFiles); + const submission = buildSubmissionFromPromptInput(message, commandRegistry, referencedFiles); await onSubmit(submission); // Only clear referenced files and sources after a successful submit. // If onSubmit throws (e.g. thread has an active run), the composer