From 6c0e0917edc28db0104845ad3c79fcca3e16d146 Mon Sep 17 00:00:00 2001 From: Bsyy95 <300071466+Bsyy95@users.noreply.github.com> Date: Tue, 21 Jul 2026 04:18:54 +0800 Subject: [PATCH] feat(ui): sidebar work-mode switcher (coding/writing/design) with per-mode prompt, tools, and model memory Co-authored-by: Cursor --- .../agent-gateway/web/src/app/GatewayApp.tsx | 40 +++- .../app/sidebar/GatewaySidebarContainer.tsx | 6 +- .../web/src/components/GatewayTranscript.tsx | 5 + .../src/components/chat/ChatEmptyState.tsx | 111 +++++----- .../components/chat/ChatHistorySidebar.tsx | 41 +++- .../web/src/components/icons.tsx | 2 + crates/agent-gateway/web/src/i18n/config.ts | 51 +++++ .../web/src/lib/settings/index.ts | 95 +++++++++ .../web/src/lib/settings/sync.ts | 2 + .../web/src/lib/settings/workModes.ts | 190 ++++++++++++++++++ .../components/chat/ChatHistorySidebar.tsx | 41 +++- crates/agent-gui/src/i18n/config.ts | 51 +++++ crates/agent-gui/src/lib/settings/index.ts | 94 +++++++++ crates/agent-gui/src/lib/settings/storage.ts | 2 + crates/agent-gui/src/lib/settings/sync.ts | 2 + .../agent-gui/src/lib/settings/workModes.ts | 190 ++++++++++++++++++ .../src/lib/tools/builtinRegistry.ts | 17 +- crates/agent-gui/src/pages/ChatPage.tsx | 48 ++++- .../chat/sidebar/ChatSidebarContainer.tsx | 6 +- .../pages/chat/transcript/ChatEmptyState.tsx | 111 +++++----- .../pages/chat/transcript/ChatTranscript.tsx | 2 + .../pages/chat/transcript/transcriptTypes.ts | 3 + .../chat/turns/runAgentConversationTurn.ts | 4 + .../test/settings/work-modes.test.mjs | 148 ++++++++++++++ .../builtin-registry-subagent-mcp.test.mjs | 31 +++ docs/features/work-modes.md | 29 +++ scripts/mirror-manifest.json | 3 +- 27 files changed, 1215 insertions(+), 110 deletions(-) create mode 100644 crates/agent-gateway/web/src/lib/settings/workModes.ts create mode 100644 crates/agent-gui/src/lib/settings/workModes.ts create mode 100644 crates/agent-gui/test/settings/work-modes.test.mjs create mode 100644 docs/features/work-modes.md diff --git a/crates/agent-gateway/web/src/app/GatewayApp.tsx b/crates/agent-gateway/web/src/app/GatewayApp.tsx index 54ad789ab..243c03a25 100644 --- a/crates/agent-gateway/web/src/app/GatewayApp.tsx +++ b/crates/agent-gateway/web/src/app/GatewayApp.tsx @@ -73,6 +73,7 @@ import { getRightDockFileTreeState, getRightDockProjectState, getSshProjectHostIds, + getWorkModeDefinition, isAgentDevMode, isRightDockSingletonTabOpen, isThinkingAlwaysOnForModel, @@ -85,6 +86,7 @@ import { resolveEffectiveTheme, resolveWorkspaceProjects, type SelectedModel, + setActiveWorkMode, setSelectedModel, updateChatRuntimeControlsForProvider, updateCustomSettings, @@ -94,6 +96,8 @@ import { updateSkills, updateSshProjectHostIds, updateSystem, + WORK_MODE_IDS, + type WorkModeId, type WorkspaceProject, workspaceProjectPathKey, } from "@/lib/settings"; @@ -3442,6 +3446,31 @@ export default function GatewayApp() { [displayedConversationId, setSettings], ); + const activeWorkMode = useMemo( + () => getWorkModeDefinition(settings.customSettings.workMode.activeModeId), + [settings.customSettings.workMode.activeModeId], + ); + const handleWorkModeChange = useCallback( + (modeId: WorkModeId) => { + setSettings((prev) => setActiveWorkMode(prev, modeId)); + }, + [setSettings], + ); + // Ctrl/⌘+Alt+1/2/3 快速切换工作模式(避开浏览器 Ctrl+数字 的标签页快捷键)。 + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if (!(event.ctrlKey || event.metaKey) || !event.altKey || event.repeat) return; + const index = ["Digit1", "Digit2", "Digit3"].indexOf(event.code); + if (index < 0) return; + const modeId = WORK_MODE_IDS[index]; + if (!modeId) return; + event.preventDefault(); + handleWorkModeChange(modeId); + }; + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [handleWorkModeChange]); + const skillsEnabled = settings.skills.enabled && isAgentMode; const selectedSkillNames = useMemo( () => (skillsEnabled ? mergeAlwaysEnabledSkillNames(settings.skills.selected) : []), @@ -3855,9 +3884,11 @@ export default function GatewayApp() { ? translate("chat.compactingContextWait", settings.locale) : historyDetailLoading ? "正在加载会话历史,请稍候..." - : enabledComposerSkills.length > 0 - ? translate("chat.inputHintWithSkills", settings.locale) - : translate("chat.inputHint", settings.locale); + : activeWorkMode.composerHintKey + ? translate(activeWorkMode.composerHintKey, settings.locale) + : enabledComposerSkills.length > 0 + ? translate("chat.inputHintWithSkills", settings.locale) + : translate("chat.inputHint", settings.locale); const canDropUpload = status?.online === true && isAgentMode && @@ -4038,6 +4069,8 @@ export default function GatewayApp() { onConversationsRemoved={handleSidebarConversationsRemoved} onCloseSidebar={() => setSidebarOpen(false)} onOpenSettings={() => openSettings()} + activeWorkModeId={settings.customSettings.workMode.activeModeId} + onWorkModeChange={handleWorkModeChange} onOpenSkillsHub={handleSidebarOpenSkillsHub} onOpenMcpHub={handleSidebarOpenMcpHub} /> @@ -4240,6 +4273,7 @@ export default function GatewayApp() { branchPendingMessageId={branchPendingMessageId} onSuggestionSelect={handleEmptyStateSuggestion} suggestionsDisabled={isSuggestionTyping} + workMode={activeWorkMode} /> {displayedTranscriptRowCount > 0 && !conversationOpenState.showOverlay ? ( diff --git a/crates/agent-gateway/web/src/app/sidebar/GatewaySidebarContainer.tsx b/crates/agent-gateway/web/src/app/sidebar/GatewaySidebarContainer.tsx index 9d6a45b90..b87d8d369 100644 --- a/crates/agent-gateway/web/src/app/sidebar/GatewaySidebarContainer.tsx +++ b/crates/agent-gateway/web/src/app/sidebar/GatewaySidebarContainer.tsx @@ -7,7 +7,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { ChatHistorySidebar } from "@/components/chat/ChatHistorySidebar"; import { useLocale } from "@/i18n"; import type { ChatHistorySummary } from "@/lib/chat/chatHistory"; -import type { WorkspaceProject } from "@/lib/settings"; +import type { WorkModeId, WorkspaceProject } from "@/lib/settings"; import { selectConversations, selectListState, @@ -125,6 +125,8 @@ export type GatewaySidebarContainerProps = { onConversationsRemoved: (ids: readonly string[]) => void; onCloseSidebar: () => void; onOpenSettings: () => void; + activeWorkModeId: WorkModeId; + onWorkModeChange: (modeId: WorkModeId) => void; onOpenSkillsHub: () => void; onOpenMcpHub: () => void; }; @@ -353,6 +355,8 @@ export function GatewaySidebarContainer(props: GatewaySidebarContainerProps) { onLoadMore={handleLoadMore} onCloseSidebar={props.onCloseSidebar} onOpenSettings={props.onOpenSettings} + activeWorkModeId={props.activeWorkModeId} + onWorkModeChange={props.onWorkModeChange} onOpenSkillsHub={props.onOpenSkillsHub} onOpenMcpHub={props.onOpenMcpHub} /> diff --git a/crates/agent-gateway/web/src/components/GatewayTranscript.tsx b/crates/agent-gateway/web/src/components/GatewayTranscript.tsx index 99f38188e..3971cf7bf 100644 --- a/crates/agent-gateway/web/src/components/GatewayTranscript.tsx +++ b/crates/agent-gateway/web/src/components/GatewayTranscript.tsx @@ -56,6 +56,7 @@ import { import type { RetryAttemptRecord, TranscriptRow } from "../lib/chat/transcript/types"; import type { GatewayTranscriptRound } from "../lib/chatUi"; +import type { WorkModeDefinition } from "../lib/settings"; import type { SectionId } from "../pages/settings/types"; import { ChatEmptyState } from "./chat/ChatEmptyState"; import { @@ -122,6 +123,8 @@ type GatewayTranscriptProps = { branchPendingMessageId?: string | null; onSuggestionSelect?: (text: string) => void; suggestionsDisabled?: boolean; + /** 当前工作模式:驱动空态问候语、建议卡片与光晕色。 */ + workMode?: WorkModeDefinition; readOnly?: boolean; redactToolContent?: boolean; }; @@ -1747,6 +1750,7 @@ export function GatewayTranscript({ branchPendingMessageId, onSuggestionSelect, suggestionsDisabled = false, + workMode, readOnly = false, redactToolContent = false, }: GatewayTranscriptProps) { @@ -1785,6 +1789,7 @@ export function GatewayTranscript({ onOpenSettings={onOpenSettings} onSuggestionSelect={readOnly ? undefined : onSuggestionSelect} suggestionsDisabled={suggestionsDisabled} + workMode={workMode} /> diff --git a/crates/agent-gateway/web/src/components/chat/ChatEmptyState.tsx b/crates/agent-gateway/web/src/components/chat/ChatEmptyState.tsx index ffb92dd0a..1d9651c66 100644 --- a/crates/agent-gateway/web/src/components/chat/ChatEmptyState.tsx +++ b/crates/agent-gateway/web/src/components/chat/ChatEmptyState.tsx @@ -1,7 +1,24 @@ import { type CSSProperties, useEffect, useState } from "react"; -import { FolderTree, Lightbulb, Settings, Wrench } from "@/components/icons"; +import { + BookOpen, + Edit3, + FolderTree, + GitBranch, + LayoutGrid, + Lightbulb, + ListChecks, + Palette, + Settings, + Wrench, +} from "@/components/icons"; import { useLocale } from "@/i18n/LocaleContext"; +import { + getWorkModeDefinition, + type WorkModeDefinition, + type WorkModeSuggestionIconId, +} from "@/lib/settings"; +import { cn } from "@/lib/shared/utils"; import type { SectionId } from "@/pages/settings/types"; type GreetingPeriod = "morning" | "noon" | "afternoon" | "evening" | "night"; @@ -37,29 +54,19 @@ function useGreetingPeriod() { return period; } -const SUGGESTION_CARDS = [ - { - key: "explore", - icon: FolderTree, - chipClassName: "text-sky-600 dark:text-sky-400", - titleKey: "chat.suggestExploreTitle", - promptKey: "chat.suggestExplorePrompt", - }, - { - key: "fix", - icon: Wrench, - chipClassName: "text-amber-600 dark:text-amber-400", - titleKey: "chat.suggestFixTitle", - promptKey: "chat.suggestFixPrompt", - }, - { - key: "ideate", - icon: Lightbulb, - chipClassName: "text-emerald-600 dark:text-emerald-400", - titleKey: "chat.suggestIdeateTitle", - promptKey: "chat.suggestIdeatePrompt", - }, -] as const; +// 工作模式建议卡片图标 id → 图标组件(模式定义位于镜像的 workModes.ts, +// 不能直接携带各端的组件引用)。 +const SUGGESTION_ICONS: Record = { + folderTree: FolderTree, + wrench: Wrench, + lightbulb: Lightbulb, + listChecks: ListChecks, + penLine: Edit3, + bookOpen: BookOpen, + layoutGrid: LayoutGrid, + gitBranch: GitBranch, + palette: Palette, +}; export type ChatEmptyStateProps = { variant: "no-models" | "start-chat"; @@ -67,6 +74,8 @@ export type ChatEmptyStateProps = { onSuggestionSelect?: (text: string) => void; /** Locks the suggestion cards while a picked prompt is still typing in. */ suggestionsDisabled?: boolean; + /** 当前工作模式:驱动问候语、建议卡片与光晕色;缺省为编程模式。 */ + workMode?: WorkModeDefinition; }; export function ChatEmptyState({ @@ -74,9 +83,11 @@ export function ChatEmptyState({ onOpenSettings, onSuggestionSelect, suggestionsDisabled = false, + workMode, }: ChatEmptyStateProps) { const { t } = useLocale(); const period = useGreetingPeriod(); + const mode = workMode ?? getWorkModeDefinition("coding"); return (
@@ -86,7 +97,10 @@ export function ChatEmptyState({