From ead46f9d9e54016cfba7a20e29a494f3c7d21f2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E7=A3=8A?= Date: Wed, 18 Mar 2026 10:42:37 +0800 Subject: [PATCH] feat: add mode selector dropdown to chat input toolbar (closes #320, closes #317) Multiple users reported they couldn't find the mode switching UI (Code/Plan). The backend (ChatView.handleModeChange + /api/chat/mode) already works, but there was no visible entry point in the chat input toolbar. Changes: - New ModeSelectorDropdown component (follows EffortSelectorDropdown pattern) - MessageInput: accept mode/onModeChange props, render selector in toolbar - ChatView: pass mode + handleModeChange down to MessageInput - i18n: add 'messageInput.modeLabel' key for en/zh Co-Authored-By: Claude Opus 4.6 (1M context) --- src/components/chat/ChatView.tsx | 2 + src/components/chat/MessageInput.tsx | 14 ++++ src/components/chat/ModeSelectorDropdown.tsx | 75 ++++++++++++++++++++ src/i18n/en.ts | 1 + src/i18n/zh.ts | 1 + 5 files changed, 93 insertions(+) create mode 100644 src/components/chat/ModeSelectorDropdown.tsx diff --git a/src/components/chat/ChatView.tsx b/src/components/chat/ChatView.tsx index 4870077b..f58a1dc9 100644 --- a/src/components/chat/ChatView.tsx +++ b/src/components/chat/ChatView.tsx @@ -436,6 +436,8 @@ export function ChatView({ sessionId, initialMessages = [], initialHasMore = fal effort={selectedEffort} onEffortChange={setSelectedEffort} sdkInitMeta={initMetaRef.current} + mode={mode} + onModeChange={handleModeChange} /> } diff --git a/src/components/chat/MessageInput.tsx b/src/components/chat/MessageInput.tsx index d54d1776..bf2c0503 100644 --- a/src/components/chat/MessageInput.tsx +++ b/src/components/chat/MessageInput.tsx @@ -18,6 +18,7 @@ import { SlashCommandPopover } from './SlashCommandPopover'; import { CliToolsPopover } from './CliToolsPopover'; import { ModelSelectorDropdown } from './ModelSelectorDropdown'; import { EffortSelectorDropdown } from './EffortSelectorDropdown'; +import { ModeSelectorDropdown } from './ModeSelectorDropdown'; import { FileAwareSubmitButton, AttachFileButton, FileTreeAttachmentBridge, FileAttachmentsCapsules, CommandBadge, CliBadge } from './MessageInputParts'; import { Tooltip, @@ -53,6 +54,9 @@ interface MessageInputProps { onEffortChange?: (effort: string | undefined) => void; /** SDK init metadata — when available, used to validate command/skill availability */ sdkInitMeta?: { tools?: unknown; slash_commands?: unknown; skills?: unknown } | null; + /** Current chat mode (code / plan) */ + mode?: string; + onModeChange?: (mode: string) => void; } export function MessageInput({ @@ -71,6 +75,8 @@ export function MessageInput({ effort: effortProp, onEffortChange, sdkInitMeta, + mode, + onModeChange, }: MessageInputProps) { const { t, locale } = useTranslation(); const imageGen = useImageGen(); @@ -417,6 +423,14 @@ export function MessageInput({ + {/* Mode selector (Code / Plan) */} + {mode && onModeChange && ( + + )} + {/* Model selector */} void; +} + +export function ModeSelectorDropdown({ + currentMode, + onModeChange, +}: ModeSelectorDropdownProps) { + const { t } = useTranslation(); + const menuRef = useRef(null); + const [menuOpen, setMenuOpen] = useState(false); + + // Close on outside click + useEffect(() => { + if (!menuOpen) return; + const handler = (e: MouseEvent) => { + if (menuRef.current && !menuRef.current.contains(e.target as Node)) { + setMenuOpen(false); + } + }; + document.addEventListener('mousedown', handler); + return () => document.removeEventListener('mousedown', handler); + }, [menuOpen]); + + const labelKey = `messageInput.mode${currentMode.charAt(0).toUpperCase() + currentMode.slice(1)}` as TranslationKey; + + return ( +
+ setMenuOpen((prev) => !prev)}> + {t(labelKey)} + + + + {menuOpen && ( + + +
+ {MODES.map((mode) => ( + { + onModeChange(mode); + setMenuOpen(false); + }} + className="justify-between" + > + {t(`messageInput.mode${mode.charAt(0).toUpperCase() + mode.slice(1)}` as TranslationKey)} + {currentMode === mode && } + + ))} +
+
+
+ )} +
+ ); +} diff --git a/src/i18n/en.ts b/src/i18n/en.ts index 479d50f5..a69cfd11 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -47,6 +47,7 @@ const en = { 'messageInput.memoryDesc': 'Edit project memory file', 'messageInput.modeCode': 'Code', 'messageInput.modePlan': 'Plan', + 'messageInput.modeLabel': 'Mode', 'messageInput.aiSuggested': 'AI Suggested', // ── Streaming message ─────────────────────────────────────── diff --git a/src/i18n/zh.ts b/src/i18n/zh.ts index c239873d..1e83f957 100644 --- a/src/i18n/zh.ts +++ b/src/i18n/zh.ts @@ -44,6 +44,7 @@ const zh: Record = { 'messageInput.memoryDesc': '编辑项目记忆文件', 'messageInput.modeCode': '代码', 'messageInput.modePlan': '计划', + 'messageInput.modeLabel': '模式', 'messageInput.aiSuggested': 'AI 推荐', // ── Streaming message ───────────────────────────────────────