From eb7ccf1dd2e1bb34e82fee20a9537ddf87813ea9 Mon Sep 17 00:00:00 2001 From: JOJO <1498581755@qq.com> Date: Tue, 2 Dec 2025 19:03:33 +0800 Subject: [PATCH] feat: restore run mode personalization --- core/main_terminal.py | 9 +++ core/web_terminal.py | 34 +++++++--- modules/personalization_manager.py | 18 ++++++ static/icons/brain-cog.svg | 1 + static/src/App.vue | 6 +- static/src/app.ts | 22 +++++-- static/src/components/input/InputComposer.vue | 10 ++- static/src/components/input/QuickMenu.vue | 63 ++++++++++++++++--- static/src/components/panels/LeftPanel.vue | 49 +++++++++++++-- .../personalization/PersonalizationDrawer.vue | 43 +++++++++++++ static/src/stores/connection.ts | 8 ++- static/src/stores/personalization.ts | 24 ++++++- .../styles/components/input/_composer.scss | 12 ++-- .../styles/components/overlays/_overlays.scss | 60 ++++++++++++++++++ static/src/utils/icons.ts | 1 + 15 files changed, 324 insertions(+), 36 deletions(-) create mode 100644 static/icons/brain-cog.svg diff --git a/core/main_terminal.py b/core/main_terminal.py index a3c6de0..3033ff1 100644 --- a/core/main_terminal.py +++ b/core/main_terminal.py @@ -357,6 +357,15 @@ class MainTerminal: self.tool_category_states[key] = False if key in disabled_categories else category.default_enabled self._refresh_disabled_tools() + preferred_mode = effective_config.get("default_run_mode") + if isinstance(preferred_mode, str): + normalized_mode = preferred_mode.strip().lower() + if normalized_mode in {"fast", "thinking", "deep"} and normalized_mode != self.run_mode: + try: + self.set_run_mode(normalized_mode) + except ValueError: + logger.warning("忽略无效默认运行模式: %s", preferred_mode) + def _handle_read_tool(self, arguments: Dict) -> Dict: """集中处理 read_file 工具的三种模式。""" diff --git a/core/web_terminal.py b/core/web_terminal.py index ffbfa6e..b9e89fd 100644 --- a/core/web_terminal.py +++ b/core/web_terminal.py @@ -48,13 +48,21 @@ class WebTerminal(MainTerminal): self, project_path: str, thinking_mode: bool = False, + run_mode: Optional[str] = None, message_callback: Optional[Callable] = None, data_dir: Optional[str] = None, container_session: Optional["ContainerHandle"] = None, usage_tracker: Optional[object] = None, ): # 调用父类初始化(包含对话持久化功能) - super().__init__(project_path, thinking_mode, data_dir=data_dir, container_session=container_session, usage_tracker=usage_tracker) + super().__init__( + project_path, + thinking_mode, + run_mode=run_mode, + data_dir=data_dir, + container_session=container_session, + usage_tracker=usage_tracker + ) # Web特有属性 self.message_callback = message_callback @@ -74,7 +82,7 @@ class WebTerminal(MainTerminal): ) print(f"[WebTerminal] 初始化完成,项目路径: {project_path}") - print(f"[WebTerminal] 思考模式: {'开启' if thinking_mode else '关闭'}") + print(f"[WebTerminal] 初始模式: {self.run_mode}") print(f"[WebTerminal] 对话管理已就绪") # 设置token更新回调 @@ -88,25 +96,34 @@ class WebTerminal(MainTerminal): # 新增:对话管理相关方法(Web版本) # =========================================== - def create_new_conversation(self, thinking_mode: bool = None) -> Dict: + def create_new_conversation(self, thinking_mode: bool = None, run_mode: Optional[str] = None) -> Dict: """ 创建新对话(Web版本) - + Args: thinking_mode: 思考模式,None则使用当前设置 - + run_mode: 显式的运行模式(fast/thinking/deep) + Returns: Dict: 包含新对话信息 """ if thinking_mode is None: thinking_mode = self.thinking_mode - + + if isinstance(run_mode, str): + try: + self.set_run_mode(run_mode) + thinking_mode = self.thinking_mode + except ValueError: + logger.warning("无效的 run_mode 参数: %s", run_mode) + try: conversation_id = self.context_manager.start_new_conversation( project_path=self.project_path, - thinking_mode=thinking_mode + thinking_mode=thinking_mode, + run_mode=self.run_mode ) - + # 重置相关状态 if self.thinking_mode: self.api_client.start_new_task() @@ -268,6 +285,7 @@ class WebTerminal(MainTerminal): "project_path": self.project_path, "thinking_mode": self.thinking_mode, "thinking_status": self.get_thinking_mode_status(), + "run_mode": self.run_mode, "context": { "usage_percent": context_status['usage_percent'], "total_size": context_status['sizes']['total'], diff --git a/modules/personalization_manager.py b/modules/personalization_manager.py index 3555e3a..4bb3cad 100644 --- a/modules/personalization_manager.py +++ b/modules/personalization_manager.py @@ -14,6 +14,8 @@ except ImportError: from core.tool_config import TOOL_CATEGORIES +ALLOWED_RUN_MODES = {"fast", "thinking", "deep"} + PERSONALIZATION_FILENAME = "personalization.json" MAX_SHORT_FIELD_LENGTH = 20 MAX_CONSIDERATION_LENGTH = 50 @@ -31,6 +33,7 @@ DEFAULT_PERSONALIZATION_CONFIG: Dict[str, Any] = { "considerations": [], "thinking_interval": None, "disabled_tool_categories": [], + "default_run_mode": None, } __all__ = [ @@ -123,6 +126,11 @@ def sanitize_personalization_payload( base["disabled_tool_categories"] = _sanitize_tool_categories(data.get("disabled_tool_categories"), allowed_tool_categories) else: base["disabled_tool_categories"] = _sanitize_tool_categories(base.get("disabled_tool_categories"), allowed_tool_categories) + + if "default_run_mode" in data: + base["default_run_mode"] = _sanitize_run_mode(data.get("default_run_mode")) + else: + base["default_run_mode"] = _sanitize_run_mode(base.get("default_run_mode")) return base @@ -222,3 +230,13 @@ def _sanitize_tool_categories(value: Any, allowed: set) -> list: if candidate not in result: result.append(candidate) return result + + +def _sanitize_run_mode(value: Any) -> Optional[str]: + if value is None: + return None + if isinstance(value, str): + candidate = value.strip().lower() + if candidate in ALLOWED_RUN_MODES: + return candidate + return None diff --git a/static/icons/brain-cog.svg b/static/icons/brain-cog.svg new file mode 100644 index 0000000..777a2a7 --- /dev/null +++ b/static/icons/brain-cog.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/src/App.vue b/static/src/App.vue index c3e2901..d4226ef 100644 --- a/static/src/App.vue +++ b/static/src/App.vue @@ -63,6 +63,7 @@ :icon-style="iconStyle" :agent-version="agentVersion" :thinking-mode="thinkingMode" + :run-mode="resolvedRunMode" :is-connected="isConnected" :panel-menu-open="panelMenuOpen" :panel-mode="panelMode" @@ -148,8 +149,10 @@ :streaming-message="streamingMessage" :uploading="uploading" :thinking-mode="thinkingMode" + :run-mode="resolvedRunMode" :quick-menu-open="quickMenuOpen" :tool-menu-open="toolMenuOpen" + :mode-menu-open="modeMenuOpen" :tool-settings="toolSettings" :tool-settings-loading="toolSettingsLoading" :settings-open="settingsOpen" @@ -166,7 +169,8 @@ @send-or-stop="handleSendOrStop" @quick-upload="handleQuickUpload" @toggle-tool-menu="toggleToolMenu" - @quick-mode-toggle="handleQuickModeToggle" + @toggle-mode-menu="toggleModeMenu" + @select-run-mode="handleModeSelect" @toggle-settings="toggleSettings" @update-tool-category="updateToolCategory" @realtime-terminal="handleRealtimeTerminalClick" diff --git a/static/src/app.ts b/static/src/app.ts index 5f94b21..e74f997 100644 --- a/static/src/app.ts +++ b/static/src/app.ts @@ -256,6 +256,13 @@ const appOptions = { 'toolMenuOpen', 'settingsOpen' ]), + resolvedRunMode() { + const allowed = ['fast', 'thinking', 'deep']; + if (allowed.includes(this.runMode)) { + return this.runMode; + } + return this.thinkingMode ? 'thinking' : 'fast'; + }, ...mapWritableState(useToolStore, [ 'preparingTools', 'activeTools', @@ -1824,8 +1831,14 @@ const appOptions = { }, async toggleThinkingMode() { - const target = this.thinkingMode ? 'fast' : 'thinking'; - await this.setRunMode(target); + await this.handleCycleRunMode(); + }, + + handleQuickModeToggle() { + if (!this.isConnected || this.streamingMessage) { + return; + } + this.handleCycleRunMode(); }, triggerFileUpload() { @@ -2034,7 +2047,8 @@ const appOptions = { async handleCycleRunMode() { const modes: Array<'fast' | 'thinking' | 'deep'> = ['fast', 'thinking', 'deep']; - const currentIndex = modes.indexOf(this.runMode); + const currentMode = this.resolvedRunMode; + const currentIndex = modes.indexOf(currentMode); const nextMode = modes[(currentIndex + 1) % modes.length]; await this.setRunMode(nextMode); }, @@ -2044,7 +2058,7 @@ const appOptions = { this.modeMenuOpen = false; return; } - if (mode === this.runMode) { + if (mode === this.resolvedRunMode) { this.modeMenuOpen = false; this.closeQuickMenu(); return; diff --git a/static/src/components/input/InputComposer.vue b/static/src/components/input/InputComposer.vue index 11f314f..bafc374 100644 --- a/static/src/components/input/InputComposer.vue +++ b/static/src/components/input/InputComposer.vue @@ -42,18 +42,21 @@ :uploading="uploading" :streaming-message="streamingMessage" :thinking-mode="thinkingMode" + :run-mode="runMode" :tool-menu-open="toolMenuOpen" :tool-settings="toolSettings" :tool-settings-loading="toolSettingsLoading" :settings-open="settingsOpen" + :mode-menu-open="modeMenuOpen" :compressing="compressing" :current-conversation-id="currentConversationId" :icon-style="iconStyle" :tool-category-icon="toolCategoryIcon" @quick-upload="triggerQuickUpload" @toggle-tool-menu="$emit('toggle-tool-menu')" - @quick-mode-toggle="$emit('quick-mode-toggle')" @toggle-settings="$emit('toggle-settings')" + @toggle-mode-menu="$emit('toggle-mode-menu')" + @select-run-mode="(mode) => $emit('select-run-mode', mode)" @update-tool-category="(id, enabled) => $emit('update-tool-category', id, enabled)" @realtime-terminal="$emit('realtime-terminal')" @toggle-focus-panel="$emit('toggle-focus-panel')" @@ -81,7 +84,8 @@ const emit = defineEmits([ 'send-or-stop', 'quick-upload', 'toggle-tool-menu', - 'quick-mode-toggle', + 'toggle-mode-menu', + 'select-run-mode', 'toggle-settings', 'update-tool-category', 'realtime-terminal', @@ -99,8 +103,10 @@ const props = defineProps<{ streamingMessage: boolean; uploading: boolean; thinkingMode: boolean; + runMode: 'fast' | 'thinking' | 'deep'; quickMenuOpen: boolean; toolMenuOpen: boolean; + modeMenuOpen: boolean; toolSettings: Array<{ id: string; label: string; enabled: boolean }>; toolSettingsLoading: boolean; settingsOpen: boolean; diff --git a/static/src/components/input/QuickMenu.vue b/static/src/components/input/QuickMenu.vue index 28f1496..3d34ba2 100644 --- a/static/src/components/input/QuickMenu.vue +++ b/static/src/components/input/QuickMenu.vue @@ -4,6 +4,15 @@ + - + +
+ +
+
+
@@ -98,6 +117,8 @@ diff --git a/static/src/components/panels/LeftPanel.vue b/static/src/components/panels/LeftPanel.vue index e4cdf91..8692f26 100644 --- a/static/src/components/panels/LeftPanel.vue +++ b/static/src/components/panels/LeftPanel.vue @@ -14,15 +14,15 @@ +
+
思考频率 @@ -303,6 +326,15 @@ const { const activeTab = ref<'preferences' | 'behavior'>('preferences'); const swipeState = ref<{ startY: number; active: boolean }>({ startY: 0, active: false }); +type RunModeValue = 'fast' | 'thinking' | 'deep' | null; + +const runModeOptions: Array<{ id: string; label: string; desc: string; value: RunModeValue; badge?: string }> = [ + { id: 'auto', label: '跟随系统', desc: '沿用工作区默认设置', value: null }, + { id: 'fast', label: '快速模式', desc: '追求响应速度,跳过思考模型', value: 'fast' }, + { id: 'thinking', label: '思考模式', desc: '首轮回复会先输出思考过程', value: 'thinking', badge: '推荐' }, + { id: 'deep', label: '深度思考', desc: '整轮对话都使用思考模型', value: 'deep' } +]; + const thinkingPresets = [ { id: 'low', label: '低', value: 10 }, { id: 'medium', label: '中', value: 5 }, @@ -340,6 +372,17 @@ const applyThinkingPreset = (value: number) => { personalization.setThinkingInterval(value); }; +const isRunModeActive = (value: RunModeValue) => { + if (value === null) { + return !form.value.default_run_mode; + } + return form.value.default_run_mode === value; +}; + +const setDefaultRunMode = (value: RunModeValue) => { + personalization.setDefaultRunMode(value); +}; + const handleThinkingInput = (event: Event) => { const target = event.target as HTMLInputElement; if (!target.value) { diff --git a/static/src/stores/connection.ts b/static/src/stores/connection.ts index 31a4812..f8a0121 100644 --- a/static/src/stores/connection.ts +++ b/static/src/stores/connection.ts @@ -8,6 +8,7 @@ interface ConnectionState { projectPath: string; agentVersion: string; thinkingMode: boolean; + runMode: 'fast' | 'thinking' | 'deep'; } export const useConnectionStore = defineStore('connection', { @@ -17,7 +18,8 @@ export const useConnectionStore = defineStore('connection', { stopRequested: false, projectPath: '', agentVersion: '', - thinkingMode: true + thinkingMode: true, + runMode: 'thinking' }), actions: { setSocket(socket: Socket | null) { @@ -46,6 +48,10 @@ export const useConnectionStore = defineStore('connection', { }, toggleThinkingMode() { this.thinkingMode = !this.thinkingMode; + }, + setRunMode(mode: 'fast' | 'thinking' | 'deep') { + this.runMode = mode; + this.thinkingMode = mode !== 'fast'; } } }); diff --git a/static/src/stores/personalization.ts b/static/src/stores/personalization.ts index 893629a..a8f1dec 100644 --- a/static/src/stores/personalization.ts +++ b/static/src/stores/personalization.ts @@ -1,5 +1,7 @@ import { defineStore } from 'pinia'; +type RunMode = 'fast' | 'thinking' | 'deep'; + interface PersonalForm { enabled: boolean; self_identify: string; @@ -9,6 +11,7 @@ interface PersonalForm { considerations: string[]; thinking_interval: number | null; disabled_tool_categories: string[]; + default_run_mode: RunMode | null; } interface PersonalizationState { @@ -32,6 +35,7 @@ interface PersonalizationState { const DEFAULT_INTERVAL = 10; const DEFAULT_INTERVAL_RANGE = { min: 1, max: 50 }; +const RUN_MODE_OPTIONS: RunMode[] = ['fast', 'thinking', 'deep']; const defaultForm = (): PersonalForm => ({ enabled: false, @@ -41,7 +45,8 @@ const defaultForm = (): PersonalForm => ({ tone: '', considerations: [], thinking_interval: null, - disabled_tool_categories: [] + disabled_tool_categories: [], + default_run_mode: null }); export const usePersonalizationStore = defineStore('personalization', { @@ -121,7 +126,11 @@ export const usePersonalizationStore = defineStore('personalization', { tone: data.tone || '', considerations: Array.isArray(data.considerations) ? [...data.considerations] : [], thinking_interval: typeof data.thinking_interval === 'number' ? data.thinking_interval : null, - disabled_tool_categories: Array.isArray(data.disabled_tool_categories) ? data.disabled_tool_categories.filter((item: unknown) => typeof item === 'string') : [] + disabled_tool_categories: Array.isArray(data.disabled_tool_categories) ? data.disabled_tool_categories.filter((item: unknown) => typeof item === 'string') : [], + default_run_mode: + typeof data.default_run_mode === 'string' && RUN_MODE_OPTIONS.includes(data.default_run_mode as RunMode) + ? data.default_run_mode as RunMode + : null }; this.clearFeedback(); }, @@ -271,6 +280,17 @@ export const usePersonalizationStore = defineStore('personalization', { }; this.clearFeedback(); }, + setDefaultRunMode(mode: RunMode | null) { + let target: RunMode | null = null; + if (typeof mode === 'string' && RUN_MODE_OPTIONS.includes(mode as RunMode)) { + target = mode as RunMode; + } + this.form = { + ...this.form, + default_run_mode: target + }; + this.clearFeedback(); + }, applyTonePreset(preset: string) { if (!preset) { return; diff --git a/static/src/styles/components/input/_composer.scss b/static/src/styles/components/input/_composer.scss index b3c4fef..aa5672f 100644 --- a/static/src/styles/components/input/_composer.scss +++ b/static/src/styles/components/input/_composer.scss @@ -218,6 +218,12 @@ background: rgba(0, 0, 0, 0.05); } +.menu-entry.active { + background: rgba(118, 103, 84, 0.12); + color: var(--claude-text); + font-weight: 600; +} + .menu-entry:disabled { opacity: 0.45; cursor: not-allowed; @@ -272,11 +278,7 @@ opacity: 0.5; } -.submenu-label { - display: inline-flex; - align-items: center; - gap: 8px; -} + .quick-menu-enter-active, .quick-menu-leave-active { diff --git a/static/src/styles/components/overlays/_overlays.scss b/static/src/styles/components/overlays/_overlays.scss index b30666f..df9d817 100644 --- a/static/src/styles/components/overlays/_overlays.scss +++ b/static/src/styles/components/overlays/_overlays.scss @@ -295,6 +295,66 @@ font-size: 13px; } +.run-mode-options { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); + gap: 12px; +} + +.run-mode-card { + border: 1px solid rgba(118, 103, 84, 0.2); + border-radius: 16px; + background: rgba(255, 255, 255, 0.95); + padding: 16px; + text-align: left; + cursor: pointer; + transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease; + width: 100%; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 6px; +} + +.run-mode-card:hover { + border-color: rgba(118, 103, 84, 0.4); + transform: translateY(-1px); +} + +.run-mode-card.active { + border-color: var(--claude-accent); + background: rgba(118, 103, 84, 0.08); + box-shadow: 0 8px 20px rgba(118, 103, 84, 0.18); +} + +.run-mode-card-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + margin-bottom: 6px; +} + +.run-mode-title { + font-weight: 600; + font-size: 15px; +} + +.run-mode-badge { + font-size: 12px; + color: var(--claude-accent); + font-weight: 600; + background: rgba(118, 103, 84, 0.12); + border-radius: 999px; + padding: 2px 8px; +} + +.run-mode-desc { + color: var(--claude-text-secondary); + font-size: 13px; + line-height: 1.4; +} + .thinking-presets { display: flex; gap: 12px; diff --git a/static/src/utils/icons.ts b/static/src/utils/icons.ts index c4f9d19..4e4a59d 100644 --- a/static/src/utils/icons.ts +++ b/static/src/utils/icons.ts @@ -2,6 +2,7 @@ export const ICONS = Object.freeze({ bot: '/static/icons/bot.svg', book: '/static/icons/book.svg', brain: '/static/icons/brain.svg', + brainCog: '/static/icons/brain-cog.svg', camera: '/static/icons/camera.svg', check: '/static/icons/check.svg', checkbox: '/static/icons/checkbox.svg',