feat: restore run mode personalization

This commit is contained in:
JOJO 2025-12-02 19:03:33 +08:00
parent c1e9f33208
commit eb7ccf1dd2
15 changed files with 324 additions and 36 deletions

View File

@ -357,6 +357,15 @@ class MainTerminal:
self.tool_category_states[key] = False if key in disabled_categories else category.default_enabled self.tool_category_states[key] = False if key in disabled_categories else category.default_enabled
self._refresh_disabled_tools() 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: def _handle_read_tool(self, arguments: Dict) -> Dict:
"""集中处理 read_file 工具的三种模式。""" """集中处理 read_file 工具的三种模式。"""

View File

@ -48,13 +48,21 @@ class WebTerminal(MainTerminal):
self, self,
project_path: str, project_path: str,
thinking_mode: bool = False, thinking_mode: bool = False,
run_mode: Optional[str] = None,
message_callback: Optional[Callable] = None, message_callback: Optional[Callable] = None,
data_dir: Optional[str] = None, data_dir: Optional[str] = None,
container_session: Optional["ContainerHandle"] = None, container_session: Optional["ContainerHandle"] = None,
usage_tracker: Optional[object] = 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特有属性 # Web特有属性
self.message_callback = message_callback self.message_callback = message_callback
@ -74,7 +82,7 @@ class WebTerminal(MainTerminal):
) )
print(f"[WebTerminal] 初始化完成,项目路径: {project_path}") print(f"[WebTerminal] 初始化完成,项目路径: {project_path}")
print(f"[WebTerminal] 思考模式: {'开启' if thinking_mode else '关闭'}") print(f"[WebTerminal] 初始模式: {self.run_mode}")
print(f"[WebTerminal] 对话管理已就绪") print(f"[WebTerminal] 对话管理已就绪")
# 设置token更新回调 # 设置token更新回调
@ -88,12 +96,13 @@ class WebTerminal(MainTerminal):
# 新增对话管理相关方法Web版本 # 新增对话管理相关方法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版本 创建新对话Web版本
Args: Args:
thinking_mode: 思考模式None则使用当前设置 thinking_mode: 思考模式None则使用当前设置
run_mode: 显式的运行模式fast/thinking/deep
Returns: Returns:
Dict: 包含新对话信息 Dict: 包含新对话信息
@ -101,10 +110,18 @@ class WebTerminal(MainTerminal):
if thinking_mode is None: if thinking_mode is None:
thinking_mode = self.thinking_mode 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: try:
conversation_id = self.context_manager.start_new_conversation( conversation_id = self.context_manager.start_new_conversation(
project_path=self.project_path, project_path=self.project_path,
thinking_mode=thinking_mode thinking_mode=thinking_mode,
run_mode=self.run_mode
) )
# 重置相关状态 # 重置相关状态
@ -268,6 +285,7 @@ class WebTerminal(MainTerminal):
"project_path": self.project_path, "project_path": self.project_path,
"thinking_mode": self.thinking_mode, "thinking_mode": self.thinking_mode,
"thinking_status": self.get_thinking_mode_status(), "thinking_status": self.get_thinking_mode_status(),
"run_mode": self.run_mode,
"context": { "context": {
"usage_percent": context_status['usage_percent'], "usage_percent": context_status['usage_percent'],
"total_size": context_status['sizes']['total'], "total_size": context_status['sizes']['total'],

View File

@ -14,6 +14,8 @@ except ImportError:
from core.tool_config import TOOL_CATEGORIES from core.tool_config import TOOL_CATEGORIES
ALLOWED_RUN_MODES = {"fast", "thinking", "deep"}
PERSONALIZATION_FILENAME = "personalization.json" PERSONALIZATION_FILENAME = "personalization.json"
MAX_SHORT_FIELD_LENGTH = 20 MAX_SHORT_FIELD_LENGTH = 20
MAX_CONSIDERATION_LENGTH = 50 MAX_CONSIDERATION_LENGTH = 50
@ -31,6 +33,7 @@ DEFAULT_PERSONALIZATION_CONFIG: Dict[str, Any] = {
"considerations": [], "considerations": [],
"thinking_interval": None, "thinking_interval": None,
"disabled_tool_categories": [], "disabled_tool_categories": [],
"default_run_mode": None,
} }
__all__ = [ __all__ = [
@ -123,6 +126,11 @@ def sanitize_personalization_payload(
base["disabled_tool_categories"] = _sanitize_tool_categories(data.get("disabled_tool_categories"), allowed_tool_categories) base["disabled_tool_categories"] = _sanitize_tool_categories(data.get("disabled_tool_categories"), allowed_tool_categories)
else: else:
base["disabled_tool_categories"] = _sanitize_tool_categories(base.get("disabled_tool_categories"), allowed_tool_categories) 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 return base
@ -222,3 +230,13 @@ def _sanitize_tool_categories(value: Any, allowed: set) -> list:
if candidate not in result: if candidate not in result:
result.append(candidate) result.append(candidate)
return result 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

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="48" height="48" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="opacity:1;"><path d="m10.852 14.772l-.383.923m.383-6.467l-.383-.923m2.679 6.467l.382.924m.001-7.391l-.383.923m1.624 1.624l.923-.383m-.923 2.679l.923.383M17.598 6.5A3 3 0 1 0 12 5a3 3 0 0 0-5.63-1.446a3 3 0 0 0-.368 1.571a4 4 0 0 0-2.525 5.771"/><path d="M17.998 5.125a4 4 0 0 1 2.525 5.771"/><path d="M19.505 10.294a4 4 0 0 1-1.5 7.706"/><path d="M4.032 17.483A4 4 0 0 0 11.464 20c.18-.311.892-.311 1.072 0a4 4 0 0 0 7.432-2.516"/><path d="M4.5 10.291A4 4 0 0 0 6 18m.002-12.875a3 3 0 0 0 .4 1.375m2.826 4.352l-.923-.383m.923 2.679l-.923.383"/><circle cx="12" cy="12" r="3"/></svg>

After

Width:  |  Height:  |  Size: 764 B

View File

@ -63,6 +63,7 @@
:icon-style="iconStyle" :icon-style="iconStyle"
:agent-version="agentVersion" :agent-version="agentVersion"
:thinking-mode="thinkingMode" :thinking-mode="thinkingMode"
:run-mode="resolvedRunMode"
:is-connected="isConnected" :is-connected="isConnected"
:panel-menu-open="panelMenuOpen" :panel-menu-open="panelMenuOpen"
:panel-mode="panelMode" :panel-mode="panelMode"
@ -148,8 +149,10 @@
:streaming-message="streamingMessage" :streaming-message="streamingMessage"
:uploading="uploading" :uploading="uploading"
:thinking-mode="thinkingMode" :thinking-mode="thinkingMode"
:run-mode="resolvedRunMode"
:quick-menu-open="quickMenuOpen" :quick-menu-open="quickMenuOpen"
:tool-menu-open="toolMenuOpen" :tool-menu-open="toolMenuOpen"
:mode-menu-open="modeMenuOpen"
:tool-settings="toolSettings" :tool-settings="toolSettings"
:tool-settings-loading="toolSettingsLoading" :tool-settings-loading="toolSettingsLoading"
:settings-open="settingsOpen" :settings-open="settingsOpen"
@ -166,7 +169,8 @@
@send-or-stop="handleSendOrStop" @send-or-stop="handleSendOrStop"
@quick-upload="handleQuickUpload" @quick-upload="handleQuickUpload"
@toggle-tool-menu="toggleToolMenu" @toggle-tool-menu="toggleToolMenu"
@quick-mode-toggle="handleQuickModeToggle" @toggle-mode-menu="toggleModeMenu"
@select-run-mode="handleModeSelect"
@toggle-settings="toggleSettings" @toggle-settings="toggleSettings"
@update-tool-category="updateToolCategory" @update-tool-category="updateToolCategory"
@realtime-terminal="handleRealtimeTerminalClick" @realtime-terminal="handleRealtimeTerminalClick"

View File

@ -256,6 +256,13 @@ const appOptions = {
'toolMenuOpen', 'toolMenuOpen',
'settingsOpen' 'settingsOpen'
]), ]),
resolvedRunMode() {
const allowed = ['fast', 'thinking', 'deep'];
if (allowed.includes(this.runMode)) {
return this.runMode;
}
return this.thinkingMode ? 'thinking' : 'fast';
},
...mapWritableState(useToolStore, [ ...mapWritableState(useToolStore, [
'preparingTools', 'preparingTools',
'activeTools', 'activeTools',
@ -1824,8 +1831,14 @@ const appOptions = {
}, },
async toggleThinkingMode() { async toggleThinkingMode() {
const target = this.thinkingMode ? 'fast' : 'thinking'; await this.handleCycleRunMode();
await this.setRunMode(target); },
handleQuickModeToggle() {
if (!this.isConnected || this.streamingMessage) {
return;
}
this.handleCycleRunMode();
}, },
triggerFileUpload() { triggerFileUpload() {
@ -2034,7 +2047,8 @@ const appOptions = {
async handleCycleRunMode() { async handleCycleRunMode() {
const modes: Array<'fast' | 'thinking' | 'deep'> = ['fast', 'thinking', 'deep']; 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]; const nextMode = modes[(currentIndex + 1) % modes.length];
await this.setRunMode(nextMode); await this.setRunMode(nextMode);
}, },
@ -2044,7 +2058,7 @@ const appOptions = {
this.modeMenuOpen = false; this.modeMenuOpen = false;
return; return;
} }
if (mode === this.runMode) { if (mode === this.resolvedRunMode) {
this.modeMenuOpen = false; this.modeMenuOpen = false;
this.closeQuickMenu(); this.closeQuickMenu();
return; return;

View File

@ -42,18 +42,21 @@
:uploading="uploading" :uploading="uploading"
:streaming-message="streamingMessage" :streaming-message="streamingMessage"
:thinking-mode="thinkingMode" :thinking-mode="thinkingMode"
:run-mode="runMode"
:tool-menu-open="toolMenuOpen" :tool-menu-open="toolMenuOpen"
:tool-settings="toolSettings" :tool-settings="toolSettings"
:tool-settings-loading="toolSettingsLoading" :tool-settings-loading="toolSettingsLoading"
:settings-open="settingsOpen" :settings-open="settingsOpen"
:mode-menu-open="modeMenuOpen"
:compressing="compressing" :compressing="compressing"
:current-conversation-id="currentConversationId" :current-conversation-id="currentConversationId"
:icon-style="iconStyle" :icon-style="iconStyle"
:tool-category-icon="toolCategoryIcon" :tool-category-icon="toolCategoryIcon"
@quick-upload="triggerQuickUpload" @quick-upload="triggerQuickUpload"
@toggle-tool-menu="$emit('toggle-tool-menu')" @toggle-tool-menu="$emit('toggle-tool-menu')"
@quick-mode-toggle="$emit('quick-mode-toggle')"
@toggle-settings="$emit('toggle-settings')" @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)" @update-tool-category="(id, enabled) => $emit('update-tool-category', id, enabled)"
@realtime-terminal="$emit('realtime-terminal')" @realtime-terminal="$emit('realtime-terminal')"
@toggle-focus-panel="$emit('toggle-focus-panel')" @toggle-focus-panel="$emit('toggle-focus-panel')"
@ -81,7 +84,8 @@ const emit = defineEmits([
'send-or-stop', 'send-or-stop',
'quick-upload', 'quick-upload',
'toggle-tool-menu', 'toggle-tool-menu',
'quick-mode-toggle', 'toggle-mode-menu',
'select-run-mode',
'toggle-settings', 'toggle-settings',
'update-tool-category', 'update-tool-category',
'realtime-terminal', 'realtime-terminal',
@ -99,8 +103,10 @@ const props = defineProps<{
streamingMessage: boolean; streamingMessage: boolean;
uploading: boolean; uploading: boolean;
thinkingMode: boolean; thinkingMode: boolean;
runMode: 'fast' | 'thinking' | 'deep';
quickMenuOpen: boolean; quickMenuOpen: boolean;
toolMenuOpen: boolean; toolMenuOpen: boolean;
modeMenuOpen: boolean;
toolSettings: Array<{ id: string; label: string; enabled: boolean }>; toolSettings: Array<{ id: string; label: string; enabled: boolean }>;
toolSettingsLoading: boolean; toolSettingsLoading: boolean;
settingsOpen: boolean; settingsOpen: boolean;

View File

@ -4,6 +4,15 @@
<button type="button" class="menu-entry" @click="$emit('quick-upload')" :disabled="!isConnected || uploading"> <button type="button" class="menu-entry" @click="$emit('quick-upload')" :disabled="!isConnected || uploading">
{{ uploading ? '上传中...' : '上传文件' }} {{ uploading ? '上传中...' : '上传文件' }}
</button> </button>
<button
type="button"
class="menu-entry has-submenu"
@click.stop="$emit('toggle-mode-menu')"
:disabled="!isConnected || streamingMessage"
>
<span>运行模式</span>
<span class="entry-arrow">{{ runModeLabel }}</span>
</button>
<button <button
type="button" type="button"
class="menu-entry has-submenu" class="menu-entry has-submenu"
@ -13,14 +22,6 @@
工具禁用 工具禁用
<span class="entry-arrow"></span> <span class="entry-arrow"></span>
</button> </button>
<button
type="button"
class="menu-entry"
@click="$emit('quick-mode-toggle')"
:disabled="streamingMessage || !isConnected"
>
{{ thinkingMode ? '快速模式' : '思考模式' }}
</button>
<button <button
type="button" type="button"
class="menu-entry has-submenu" class="menu-entry has-submenu"
@ -31,6 +32,24 @@
<span class="entry-arrow"></span> <span class="entry-arrow"></span>
</button> </button>
<transition name="submenu-slide">
<div class="quick-submenu mode-submenu" v-if="modeMenuOpen">
<div class="submenu-list">
<button
v-for="option in runModeOptions"
:key="option.value"
type="button"
class="menu-entry submenu-entry"
:class="{ active: option.value === resolvedRunMode }"
@click.stop="$emit('select-run-mode', option.value)"
:disabled="streamingMessage || !isConnected"
>
{{ option.label }}
</button>
</div>
</div>
</transition>
<transition name="submenu-slide"> <transition name="submenu-slide">
<div class="quick-submenu tool-submenu" v-if="toolMenuOpen"> <div class="quick-submenu tool-submenu" v-if="toolMenuOpen">
<div class="submenu-status" v-if="toolSettingsLoading">正在同步工具状态...</div> <div class="submenu-status" v-if="toolSettingsLoading">正在同步工具状态...</div>
@ -98,6 +117,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue';
defineOptions({ name: 'QuickMenu' }); defineOptions({ name: 'QuickMenu' });
const props = defineProps<{ const props = defineProps<{
@ -114,19 +135,43 @@ const props = defineProps<{
currentConversationId: string | null; currentConversationId: string | null;
iconStyle?: (key: string) => Record<string, string>; iconStyle?: (key: string) => Record<string, string>;
toolCategoryIcon: (categoryId: string) => string; toolCategoryIcon: (categoryId: string) => string;
modeMenuOpen: boolean;
runMode?: 'fast' | 'thinking' | 'deep';
}>(); }>();
defineEmits<{ defineEmits<{
(event: 'quick-upload'): void; (event: 'quick-upload'): void;
(event: 'toggle-tool-menu'): void; (event: 'toggle-tool-menu'): void;
(event: 'quick-mode-toggle'): void;
(event: 'toggle-settings'): void; (event: 'toggle-settings'): void;
(event: 'update-tool-category', id: string, enabled: boolean): void; (event: 'update-tool-category', id: string, enabled: boolean): void;
(event: 'realtime-terminal'): void; (event: 'realtime-terminal'): void;
(event: 'toggle-focus-panel'): void; (event: 'toggle-focus-panel'): void;
(event: 'toggle-token-panel'): void; (event: 'toggle-token-panel'): void;
(event: 'compress-conversation'): void; (event: 'compress-conversation'): void;
(event: 'toggle-mode-menu'): void;
(event: 'select-run-mode', mode: 'fast' | 'thinking' | 'deep'): void;
}>(); }>();
const runModeOptions = [
{ value: 'fast', label: '快速模式' },
{ value: 'thinking', label: '思考模式' },
{ value: 'deep', label: '深度思考模式' }
] as const;
const runModeLabelMap: Record<'fast' | 'thinking' | 'deep', string> = {
fast: '快速模式',
thinking: '思考模式',
deep: '深度思考'
};
const resolvedRunMode = computed<'fast' | 'thinking' | 'deep'>(() => {
if (props.runMode === 'deep' || props.runMode === 'thinking' || props.runMode === 'fast') {
return props.runMode;
}
return props.thinkingMode ? 'thinking' : 'fast';
});
const runModeLabel = computed(() => runModeLabelMap[resolvedRunMode.value]);
const getIconStyle = (key: string) => (props.iconStyle ? props.iconStyle(key) : {}); const getIconStyle = (key: string) => (props.iconStyle ? props.iconStyle(key) : {});
</script> </script>

View File

@ -14,15 +14,15 @@
<button <button
type="button" type="button"
class="mode-indicator" class="mode-indicator"
:class="{ thinking: thinkingMode, fast: !thinkingMode }" :class="modeIndicatorClass"
:title="thinkingMode ? '思考模式(点击切换)' : '快速模式(点击切换)'" :title="modeIndicatorTitle"
@click="$emit('toggle-thinking-mode')" @click="$emit('toggle-thinking-mode')"
> >
<transition name="mode-icon" mode="out-in"> <transition name="mode-icon" mode="out-in">
<span <span
class="icon icon-sm" class="icon icon-sm"
:style="iconStyle(thinkingMode ? 'brain' : 'zap')" :style="iconStyle(modeIndicatorIcon)"
:key="thinkingMode ? 'brain' : 'zap'" :key="modeIndicatorIcon"
aria-hidden="true" aria-hidden="true"
></span> ></span>
</transition> </transition>
@ -145,6 +145,7 @@ const props = defineProps<{
isConnected: boolean; isConnected: boolean;
panelMenuOpen: boolean; panelMenuOpen: boolean;
panelMode: 'files' | 'todo' | 'subAgents'; panelMode: 'files' | 'todo' | 'subAgents';
runMode: 'fast' | 'thinking' | 'deep';
}>(); }>();
defineEmits<{ defineEmits<{
@ -168,6 +169,46 @@ const panelStyle = computed(() => {
minWidth: px minWidth: px
}; };
}); });
const resolveRunMode = () => {
if (props.runMode === 'deep' || props.runMode === 'thinking' || props.runMode === 'fast') {
return props.runMode;
}
return props.thinkingMode ? 'thinking' : 'fast';
};
const modeIndicatorClass = computed(() => {
const mode = resolveRunMode();
if (mode === 'deep') {
return 'deep';
}
if (mode === 'thinking') {
return 'thinking';
}
return 'fast';
});
const modeIndicatorIcon = computed(() => {
const mode = resolveRunMode();
if (mode === 'deep') {
return 'brainCog';
}
if (mode === 'thinking') {
return 'brain';
}
return 'zap';
});
const modeIndicatorTitle = computed(() => {
const mode = resolveRunMode();
if (mode === 'deep') {
return '深度思考模式(点击切换)';
}
if (mode === 'thinking') {
return '思考模式(点击切换)';
}
return '快速模式(点击切换)';
});
const fileStore = useFileStore(); const fileStore = useFileStore();
const subAgentStore = useSubAgentStore(); const subAgentStore = useSubAgentStore();
const { fileTree, expandedFolders, todoList } = storeToRefs(fileStore); const { fileTree, expandedFolders, todoList } = storeToRefs(fileStore);

View File

@ -190,6 +190,29 @@
</section> </section>
<section v-else key="behavior" class="personal-page behavior-page"> <section v-else key="behavior" class="personal-page behavior-page">
<div class="behavior-section"> <div class="behavior-section">
<div class="behavior-field">
<div class="behavior-field-header">
<span class="field-title">默认思考模型</span>
<p class="field-desc">设定登录或新建任务时的初始运行模式仍可通过主界面随时切换</p>
</div>
<div class="run-mode-options">
<button
v-for="option in runModeOptions"
:key="option.id"
type="button"
class="run-mode-card"
:class="{ active: isRunModeActive(option.value) }"
:aria-pressed="isRunModeActive(option.value)"
@click.prevent="setDefaultRunMode(option.value)"
>
<div class="run-mode-card-header">
<span class="run-mode-title">{{ option.label }}</span>
<span v-if="option.badge" class="run-mode-badge">{{ option.badge }}</span>
</div>
<p class="run-mode-desc">{{ option.desc }}</p>
</button>
</div>
</div>
<div class="behavior-field"> <div class="behavior-field">
<div class="behavior-field-header"> <div class="behavior-field-header">
<span class="field-title">思考频率</span> <span class="field-title">思考频率</span>
@ -303,6 +326,15 @@ const {
const activeTab = ref<'preferences' | 'behavior'>('preferences'); const activeTab = ref<'preferences' | 'behavior'>('preferences');
const swipeState = ref<{ startY: number; active: boolean }>({ startY: 0, active: false }); 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 = [ const thinkingPresets = [
{ id: 'low', label: '低', value: 10 }, { id: 'low', label: '低', value: 10 },
{ id: 'medium', label: '中', value: 5 }, { id: 'medium', label: '中', value: 5 },
@ -340,6 +372,17 @@ const applyThinkingPreset = (value: number) => {
personalization.setThinkingInterval(value); 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 handleThinkingInput = (event: Event) => {
const target = event.target as HTMLInputElement; const target = event.target as HTMLInputElement;
if (!target.value) { if (!target.value) {

View File

@ -8,6 +8,7 @@ interface ConnectionState {
projectPath: string; projectPath: string;
agentVersion: string; agentVersion: string;
thinkingMode: boolean; thinkingMode: boolean;
runMode: 'fast' | 'thinking' | 'deep';
} }
export const useConnectionStore = defineStore('connection', { export const useConnectionStore = defineStore('connection', {
@ -17,7 +18,8 @@ export const useConnectionStore = defineStore('connection', {
stopRequested: false, stopRequested: false,
projectPath: '', projectPath: '',
agentVersion: '', agentVersion: '',
thinkingMode: true thinkingMode: true,
runMode: 'thinking'
}), }),
actions: { actions: {
setSocket(socket: Socket | null) { setSocket(socket: Socket | null) {
@ -46,6 +48,10 @@ export const useConnectionStore = defineStore('connection', {
}, },
toggleThinkingMode() { toggleThinkingMode() {
this.thinkingMode = !this.thinkingMode; this.thinkingMode = !this.thinkingMode;
},
setRunMode(mode: 'fast' | 'thinking' | 'deep') {
this.runMode = mode;
this.thinkingMode = mode !== 'fast';
} }
} }
}); });

View File

@ -1,5 +1,7 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
type RunMode = 'fast' | 'thinking' | 'deep';
interface PersonalForm { interface PersonalForm {
enabled: boolean; enabled: boolean;
self_identify: string; self_identify: string;
@ -9,6 +11,7 @@ interface PersonalForm {
considerations: string[]; considerations: string[];
thinking_interval: number | null; thinking_interval: number | null;
disabled_tool_categories: string[]; disabled_tool_categories: string[];
default_run_mode: RunMode | null;
} }
interface PersonalizationState { interface PersonalizationState {
@ -32,6 +35,7 @@ interface PersonalizationState {
const DEFAULT_INTERVAL = 10; const DEFAULT_INTERVAL = 10;
const DEFAULT_INTERVAL_RANGE = { min: 1, max: 50 }; const DEFAULT_INTERVAL_RANGE = { min: 1, max: 50 };
const RUN_MODE_OPTIONS: RunMode[] = ['fast', 'thinking', 'deep'];
const defaultForm = (): PersonalForm => ({ const defaultForm = (): PersonalForm => ({
enabled: false, enabled: false,
@ -41,7 +45,8 @@ const defaultForm = (): PersonalForm => ({
tone: '', tone: '',
considerations: [], considerations: [],
thinking_interval: null, thinking_interval: null,
disabled_tool_categories: [] disabled_tool_categories: [],
default_run_mode: null
}); });
export const usePersonalizationStore = defineStore('personalization', { export const usePersonalizationStore = defineStore('personalization', {
@ -121,7 +126,11 @@ export const usePersonalizationStore = defineStore('personalization', {
tone: data.tone || '', tone: data.tone || '',
considerations: Array.isArray(data.considerations) ? [...data.considerations] : [], considerations: Array.isArray(data.considerations) ? [...data.considerations] : [],
thinking_interval: typeof data.thinking_interval === 'number' ? data.thinking_interval : null, 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(); this.clearFeedback();
}, },
@ -271,6 +280,17 @@ export const usePersonalizationStore = defineStore('personalization', {
}; };
this.clearFeedback(); 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) { applyTonePreset(preset: string) {
if (!preset) { if (!preset) {
return; return;

View File

@ -218,6 +218,12 @@
background: rgba(0, 0, 0, 0.05); 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 { .menu-entry:disabled {
opacity: 0.45; opacity: 0.45;
cursor: not-allowed; cursor: not-allowed;
@ -272,11 +278,7 @@
opacity: 0.5; opacity: 0.5;
} }
.submenu-label {
display: inline-flex;
align-items: center;
gap: 8px;
}
.quick-menu-enter-active, .quick-menu-enter-active,
.quick-menu-leave-active { .quick-menu-leave-active {

View File

@ -295,6 +295,66 @@
font-size: 13px; 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 { .thinking-presets {
display: flex; display: flex;
gap: 12px; gap: 12px;

View File

@ -2,6 +2,7 @@ export const ICONS = Object.freeze({
bot: '/static/icons/bot.svg', bot: '/static/icons/bot.svg',
book: '/static/icons/book.svg', book: '/static/icons/book.svg',
brain: '/static/icons/brain.svg', brain: '/static/icons/brain.svg',
brainCog: '/static/icons/brain-cog.svg',
camera: '/static/icons/camera.svg', camera: '/static/icons/camera.svg',
check: '/static/icons/check.svg', check: '/static/icons/check.svg',
checkbox: '/static/icons/checkbox.svg', checkbox: '/static/icons/checkbox.svg',