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._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 工具的三种模式。"""

View File

@ -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'],

View File

@ -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

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"
: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"

View File

@ -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;

View File

@ -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;

View File

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

View File

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

View File

@ -190,6 +190,29 @@
</section>
<section v-else key="behavior" class="personal-page behavior-page">
<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-header">
<span class="field-title">思考频率</span>
@ -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) {

View File

@ -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';
}
}
});

View File

@ -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;

View File

@ -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 {

View File

@ -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;

View File

@ -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',