Compare commits

..

5 Commits

Author SHA1 Message Date
89bcca744d feat(sub_agent): skill 与提示词允许按任务边界修改核心代码
原「严格禁止子智能体修改代码」改为视情况而定:大规模、重复性高、边界与任务明确的修改可由子智能体承担,需明确工作范围防冲突,最终由主智能体验证。同步更新运行态 skill 副本。

Co-authored-by: Astrion powered by DeepSeek-V4-Flash <astrion-agent@users.noreply.github.com>
2026-08-29 23:12:15 +08:00
af01030424 feat(quickdock): 子智能体时间线支持压缩提示与状态图标右置
上下文压缩发生时在时间线对应位置显示提示条(layers 图标 + 轮次);工具栏去掉右侧完成/进行中文字,状态图标(转圈/对勾/叉)移至行尾。

Co-authored-by: Astrion powered by DeepSeek-V4-Flash <astrion-agent@users.noreply.github.com>
2026-08-29 23:12:05 +08:00
10a2763f00 feat(sub_agent): 交付目录改为可选,未指定时自动创建
deliverables_dir 移出必填:留空时自动在 .astrion/sub_agent_results/ 下创建(撞名自动加后缀),最终路径作为工具结果返回;显式传入已存在目录仍报错。

Co-authored-by: Astrion powered by DeepSeek-V4-Flash <astrion-agent@users.noreply.github.com>
2026-08-29 23:11:54 +08:00
04770e0af9 fix(sub_agent): finish_task 补终态进度事件,修复活动窗口永久转圈
finish_task 在模型流式阶段会 emit「calling」进度事件,但执行分支直接收尾不补终态,导致前端活动窗口该条目永远停留在调用中。现在补 emit status=completed。

Co-authored-by: Astrion powered by DeepSeek-V4-Flash <astrion-agent@users.noreply.github.com>
2026-08-29 23:11:45 +08:00
f5bfab4056 feat(settings): 个人空间重构为紧凑窄版布局,完善移动端适配
- 卡片窄化至 648px、圆角 14px、侧栏 184px,控件行高紧凑化
- 4429 行主文件拆分为 13 个 tab 子组件 + 共享样式表
- 移动端(≤760px)改为 iOS 设置式「列表→子页」导航
- 修复内容区缺 flex:1 导致设置行无法占满
- 修复交流语气行 label 被 grid auto 列挤压成竖排
- 修复移动端弹窗顶栏被主界面顶栏(z-index 1500)遮挡
- 窄屏禁用选项 hover/active 视觉态与 tab 切换过渡动画
- 修复上下文图标 28 viewBox/1.7 描边导致的视觉偏小偏细
2026-08-29 21:58:53 +08:00
28 changed files with 3903 additions and 3128 deletions

View File

@ -1,6 +1,6 @@
--- ---
name: sub-agent-guide name: sub-agent-guide
description: 子智能体使用指南。创建子智能体之前必须阅读此技能。子智能体用于并行处理独立的“非代码编写”任务(项目初步探索、网络搜索与信息收集、程序测试执行)。关键限制:子智能体之间无法通信,看不到主对话历史。严格禁止将子智能体用于项目代码编写、代码修改、重构或实现功能。**如果运行模式为多智能体模式,禁止阅读并参考该 skill。** description: 子智能体使用指南。创建子智能体之前必须阅读此技能。子智能体用于并行处理独立任务(项目探索、网络搜索与信息收集、程序测试执行)。代码修改视情况而定:一般性代码编写由主智能体完成;大规模、重复性高、边界与任务明确的代码修改可由子智能体承担,但必须明确工作范围防止冲突,最终由主智能体验证。关键限制:子智能体之间无法通信,看不到主对话历史。**如果运行模式为多智能体模式,禁止阅读并参考该 skill。**
--- ---
# 子智能体使用指南 # 子智能体使用指南
@ -11,7 +11,19 @@ description: 子智能体使用指南。创建子智能体之前必须阅读此
子智能体拥有完整工具能力,但与主智能体和其他子智能体完全隔离。它们看不到你的对话历史,无法互相通信,只能通过文件系统共享信息。 子智能体拥有完整工具能力,但与主智能体和其他子智能体完全隔离。它们看不到你的对话历史,无法互相通信,只能通过文件系统共享信息。
**硬性限制:子智能体严格禁止用于项目代码编写/修改。** **代码修改纪律(视情况而定)**
子智能体**不是**代码编写的主力——一般性代码编写、重构、功能实现仍由主智能体承担。但当任务**同时满足**以下条件时,**允许子智能体修改核心代码**
1. **大规模、重复性高**:改动量大或模式重复(如批量迁移、统一错误处理、机械式重构),交给子智能体执行效率远高于主智能体逐文件处理
2. **边界与任务明确**:任务描述清楚指定要操作的文件/目录范围,无歧义
3. **范围防冲突**:与主智能体、其他子智能体的工作范围不重叠
4. **最终验证**:子智能体的智能程度取决于用户配置的模型,其改动**必须由主智能体审查验证**(读 diff / 跑测试),不能直接信以为真
**禁止场景(不可放宽):**
- 修改任务描述明确范围之外的文件
- 影响面不可控的架构级改动、跨模块协调
- 子智能体无法自主验证正确性的改动
## 何时使用 ## 何时使用
@ -21,13 +33,14 @@ description: 子智能体使用指南。创建子智能体之前必须阅读此
- **重要**:进行大范围搜索或调查前,必须先自己进行一次浅层搜索/调查,摸清关键词、主要来源和任务边界,再创建子智能体并给出精确指导。 - **重要**:进行大范围搜索或调查前,必须先自己进行一次浅层搜索/调查,摸清关键词、主要来源和任务边界,再创建子智能体并给出精确指导。
- **程序测试与验证**:执行测试命令、收集日志、汇总失败用例与错误模式 - **程序测试与验证**:执行测试命令、收集日志、汇总失败用例与错误模式
- **并行独立的文档/分析任务**:生成说明文档、运行报告、排查清单 - **并行独立的文档/分析任务**:生成说明文档、运行报告、排查清单
- **大规模、重复性高的代码修改**:批量迁移、统一错误处理、机械式重构等边界明确的任务(任务描述必须精确指定文件/目录范围,完成后审查验证改动)
**不适合:** **不适合:**
- 任务之间需要频繁协调(子智能体无法互相通信) - 任务之间需要频繁协调(子智能体无法互相通信)
- 简单快速的任务(创建子智能体有开销) - 简单快速的任务(创建子智能体有开销)
- 需要你参与决策的任务(子智能体是自主运行的) - 需要你参与决策的任务(子智能体是自主运行的)
- 任何项目代码编写、代码修改、代码重构、功能实现任务(必须由主智能体处理 - 边界模糊、影响面不可控的代码修改(必须先由主智能体划定范围
- 任何会直接改动项目源码的开发工作 - 需要跨模块协调或反复验证的架构级改动
## 任务描述的艺术 ## 任务描述的艺术
@ -71,17 +84,19 @@ description: 子智能体使用指南。创建子智能体之前必须阅读此
## 交付目录设计 ## 交付目录设计
`deliverables_dir` 必须是**不存在的新目录**,子智能体会把所有成果放在这里 `deliverables_dir` 可以**留空**:留空时系统自动在 `.astrion/sub_agent_results/` 下创建(如 `agent_1`,撞名自动加后缀),并把最终路径作为工具结果返回。该目录是运行时内部目录,不进入 git 版本控制
**命名建议:** 显式传入时:必须是相对项目根目录的**不存在的新目录**,已存在会报错。
**命名建议**(显式传入时,用清晰描述任务的相对路径):
``` ```
sub_agent_results/task_description/ # 清晰描述任务 research/python312/ # 清晰描述任务
sub_agent_results/module_a_docs/ # 按模块组织 docs/module_a/ # 按模块组织
sub_agent_results/test_auth_report/ # 按测试/报告组织 analysis/frontend_auth/ # 按功能分析组织
``` ```
**避免:** **避免:**
- ❌ 使用已存在的目录(会报错) - ❌ 传入已存在的目录(会报错)
- ❌ 使用绝对路径(必须是相对于项目根目录的相对路径) - ❌ 使用绝对路径(必须是相对于项目根目录的相对路径)
- ❌ 使用通用名称如 "output" "result"(多个子智能体会冲突) - ❌ 使用通用名称如 "output" "result"(多个子智能体会冲突)
@ -251,7 +266,7 @@ create_sub_agent(task="""
- 梳理认证相关入口、调用链和关键配置 - 梳理认证相关入口、调用链和关键配置
- 运行现有测试并记录失败项 - 运行现有测试并记录失败项
- 生成诊断报告auth_diagnosis.md - 生成诊断报告auth_diagnosis.md
- 不允许修改任何项目源码 - 本任务为只读诊断,不修改任何文件
""") """)
``` ```
@ -286,7 +301,7 @@ create_sub_agent(agent_id=1, ...) # 失败了
result = create_sub_agent(agent_id=1, run_in_background=false, ...) result = create_sub_agent(agent_id=1, run_in_background=false, ...)
if result["success"]: if result["success"]:
# 读取交付目录中的文件 # 读取交付目录中的文件(显式传入或自动创建的路径都在这里)
deliverables = result["deliverables_dir"] deliverables = result["deliverables_dir"]
# 处理生成的文件... # 处理生成的文件...
else: else:

View File

@ -133,7 +133,7 @@ class ToolsDefinitionAgentToolsMixin:
}, },
"deliverables_dir": { "deliverables_dir": {
"type": "string", "type": "string",
"description": "交付文件夹的相对路径(相对于项目根目录)。子智能体会将所有结果文件放在此目录。\n\n要求:必须是不存在的新目录;若目录不存在会自动创建;若目录已存在(无论是否为空)将报错。\n\n留空则使用默认路径sub_agent_results/agent_{agent_id}\n\n示例:'docs/api''reports/performance''tests/generated'" "description": "交付文件夹的相对路径(相对于项目根目录)。子智能体会将所有结果文件放在此目录。\n\n显式传入时:必须是不存在的新目录,若已存在(无论是否为空)将报错。\n\n留空时:自动在 .astrion/sub_agent_results/ 下创建(如 agent_{agent_id}),最终路径会作为工具结果返回。\n\n示例:'docs/api''reports/performance''tests/generated'"
}, },
"run_in_background": { "run_in_background": {
"type": "boolean", "type": "boolean",
@ -149,7 +149,7 @@ class ToolsDefinitionAgentToolsMixin:
"description": "子智能体思考模式,根据任务复杂度选择:\n\nfast快速模式- 适合简单明确的任务:\n- 网络信息搜集和整理\n- 批量文件读取和简单处理\n- 执行已知的命令序列\n- 生成简单的文档或报告\n- 数据格式转换\n\nthinking思考模式- 适合复杂任务:\n- 代码架构分析和重构设计\n- 复杂算法实现和优化\n- 多步骤问题诊断和调试\n- 技术方案选型和对比\n- 需要深度推理的代码审查\n\n不填则使用默认模式。" "description": "子智能体思考模式,根据任务复杂度选择:\n\nfast快速模式- 适合简单明确的任务:\n- 网络信息搜集和整理\n- 批量文件读取和简单处理\n- 执行已知的命令序列\n- 生成简单的文档或报告\n- 数据格式转换\n\nthinking思考模式- 适合复杂任务:\n- 代码架构分析和重构设计\n- 复杂算法实现和优化\n- 多步骤问题诊断和调试\n- 技术方案选型和对比\n- 需要深度推理的代码审查\n\n不填则使用默认模式。"
} }
}), }),
"required": ["agent_id", "summary", "task", "deliverables_dir", "thinking_mode"] "required": ["agent_id", "summary", "task", "thinking_mode"]
} }
} }
}, },

View File

@ -103,22 +103,22 @@ class SubAgentCreationMixin:
return "任务摘要不能为空" return "任务摘要不能为空"
if not task or not task.strip(): if not task or not task.strip():
return "任务详情不能为空" return "任务详情不能为空"
# 多智能体模式不需要交付目录 # 交付目录不再必填:传统模式未指定时自动创建(见 _resolve_deliverables_dir
if not multi_agent_mode and target_dir is None:
return "指定文件夹不能为空"
return None return None
def _generate_task_id(self, agent_id: int) -> str: def _generate_task_id(self, agent_id: int) -> str:
suffix = uuid.uuid4().hex[:6] suffix = uuid.uuid4().hex[:6]
return f"sub_{agent_id}_{int(time.time())}_{suffix}" return f"sub_{agent_id}_{int(time.time())}_{suffix}"
def _resolve_deliverables_dir(self, relative_dir: Optional[str], *, multi_agent_mode: bool = False) -> Path: def _resolve_deliverables_dir(self, relative_dir: Optional[str], *, agent_id: int = 0, multi_agent_mode: bool = False) -> Path:
relative_dir = (relative_dir or "").strip() relative_dir = (relative_dir or "").strip()
# 多智能体模式:没有交付目录概念,直接使用项目根目录 # 多智能体模式:没有交付目录概念,直接使用项目根目录
if multi_agent_mode and not relative_dir: if multi_agent_mode and not relative_dir:
return self.project_path.resolve() return self.project_path.resolve()
if not relative_dir: if not relative_dir:
raise ValueError(tr("sub_agent_creation.deliverables_dir_required")) # 未显式指定交付目录:在运行时目录 .astrion/sub_agent_results/ 下自动创建
#(撞名自动加后缀,保证总能建成),实际路径由 create_sub_agent 结果返回
return self._auto_deliverables_dir(agent_id)
deliverables_path = (self.project_path / relative_dir).resolve() deliverables_path = (self.project_path / relative_dir).resolve()
if not str(deliverables_path).startswith(str(self.project_path)): if not str(deliverables_path).startswith(str(self.project_path)):
raise ValueError(tr("sub_agent_creation.deliverables_dir_outside")) raise ValueError(tr("sub_agent_creation.deliverables_dir_outside"))
@ -126,3 +126,15 @@ class SubAgentCreationMixin:
raise ValueError(tr("sub_agent_creation.deliverables_dir_not_new")) raise ValueError(tr("sub_agent_creation.deliverables_dir_not_new"))
deliverables_path.mkdir(parents=True, exist_ok=True) deliverables_path.mkdir(parents=True, exist_ok=True)
return deliverables_path return deliverables_path
def _auto_deliverables_dir(self, agent_id: int) -> Path:
"""未指定交付目录时自动创建:.astrion/sub_agent_results/agent_{id}(撞名追加后缀)。"""
base = (self.project_path / ".astrion" / "sub_agent_results").resolve()
base.mkdir(parents=True, exist_ok=True)
candidate = base / f"agent_{agent_id}"
suffix = 2
while candidate.exists():
candidate = base / f"agent_{agent_id}_{suffix}"
suffix += 1
candidate.mkdir(parents=True, exist_ok=True)
return candidate

View File

@ -284,7 +284,7 @@ class SubAgentManager(SubAgentStateMixin, SubAgentStatsMixin, SubAgentCreationMi
task_root.mkdir(parents=True, exist_ok=True) task_root.mkdir(parents=True, exist_ok=True)
try: try:
deliverables_path = self._resolve_deliverables_dir(deliverables_dir, multi_agent_mode=multi_agent_mode) deliverables_path = self._resolve_deliverables_dir(deliverables_dir, agent_id=agent_id, multi_agent_mode=multi_agent_mode)
except ValueError as exc: except ValueError as exc:
# 回滚已创建的任务目录,避免残留状态(交付目录校验失败时 deliverables 不会创建) # 回滚已创建的任务目录,避免残留状态(交付目录校验失败时 deliverables 不会创建)
shutil.rmtree(task_root, ignore_errors=True) shutil.rmtree(task_root, ignore_errors=True)

View File

@ -413,6 +413,12 @@ class SubAgentTask:
if self.current_context_tokens > 0 and self.current_context_tokens >= self.compress_threshold_tokens: if self.current_context_tokens > 0 and self.current_context_tokens >= self.compress_threshold_tokens:
compressed = self._deep_compress_messages() compressed = self._deep_compress_messages()
if compressed: if compressed:
# 写入进度文件:前端活动窗口在压缩发生的位置显示压缩提示
self.emit("progress", {
"subtype": "compression",
"round": self._compress_round,
"ts": int(time.time() * 1000),
})
ma_debug( ma_debug(
"sub_agent_deep_compressed", "sub_agent_deep_compressed",
task_id=self.task_id, task_id=self.task_id,
@ -477,6 +483,9 @@ class SubAgentTask:
progress_id = tool_call.get("id") or f"tool_{int(time.time() * 1000)}_{uuid.uuid4().hex[:6]}" progress_id = tool_call.get("id") or f"tool_{int(time.time() * 1000)}_{uuid.uuid4().hex[:6]}"
if name == "finish_task": if name == "finish_task":
# finish_task 在模型流式阶段已 emit「calling」进度事件_call_model
# 此处必须补终态,否则前端活动窗口该条目永远停留在「调用中」转圈
self.emit("progress", {"id": progress_id, "tool": name, "status": "completed", "args": args, "ts": int(time.time() * 1000)})
await self._write_finish(args, elapsed) await self._write_finish(args, elapsed)
return return

View File

@ -24,6 +24,13 @@
- 你的工作范围应该与其他子智能体不重叠 - 你的工作范围应该与其他子智能体不重叠
- 不要修改任务描述之外的文件 - 不要修改任务描述之外的文件
## 代码修改纪律(视情况而定)
- 你有完整的读写工具,主智能体可能分配给你修改核心代码的任务(典型:大规模、重复性高、边界明确的工作)
- 允许修改代码的前提:任务描述明确划定了文件/目录范围,且改动面在该范围之内
- 修改前先阅读相关代码,理解上下文再动手;只做任务要求的改动,不顺手扩大改动面
- 修改完成后,在 finish_task 的 summary 中列出改动清单(文件、改动点),由主智能体审查验证
- 任务未指定代码修改时,保持只读分析,不要擅自改动代码
## 效率性 ## 效率性
- 直接开始工作,不要过度解释 - 直接开始工作,不要过度解释
- 合理使用工具,避免重复操作 - 合理使用工具,避免重复操作

View File

@ -30,15 +30,23 @@
v-for="item in timelineItems" v-for="item in timelineItems"
:key="item.key" :key="item.key"
class="qd-feed-row" class="qd-feed-row"
:class="[item.kind === 'tool' ? 'feed-tool' : 'feed-text', { 'is-new': animatedKeys.has(item.key) }]" :class="[item.kind === 'tool' ? 'feed-tool' : item.kind === 'compression' ? 'feed-compressed' : 'feed-text', { 'is-new': animatedKeys.has(item.key) }]"
> >
<template v-if="item.kind === 'tool'"> <template v-if="item.kind === 'tool'">
<span v-if="item.state === 'running' || item.state === 'calling'" class="qd-tool-spinner"></span>
<span v-else class="qd-tool-done">{{ item.state === 'failed' ? '✕' : '✓' }}</span>
<span class="tool-name">{{ item.toolName }}</span> <span class="tool-name">{{ item.toolName }}</span>
<span class="tool-param" :title="item.text">{{ item.text }}</span> <span class="tool-param" :title="item.text">{{ item.text }}</span>
<span class="tool-result" :class="{ 'is-error': item.state === 'failed' }"> <span class="tool-status" :class="{ 'is-error': item.state === 'failed' }">
{{ item.stateLabel }} <span v-if="item.state === 'running' || item.state === 'calling'" class="qd-tool-spinner"></span>
<span v-else class="qd-tool-done">{{ item.state === 'failed' ? '✕' : '✓' }}</span>
</span>
</template>
<template v-else-if="item.kind === 'compression'">
<span class="feed-compressed__label">
<svg class="feed-compressed__icon" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<path d="M8 1.5 13.5 4v4.5c0 3-2.2 5-5.5 6-3.3-1-5.5-3-5.5-6V4L8 1.5Z" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/>
<path d="M5.5 8.2 7.2 9.9 11 5.8" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
{{ t('quickdock.contextCompressed', { round: item.round }) }}
</span> </span>
</template> </template>
<template v-else>{{ item.content }}</template> <template v-else>{{ item.content }}</template>
@ -283,14 +291,6 @@ function isEntryTerminal(status?: string) {
].includes(normalized); ].includes(normalized);
} }
/** 工具条目状态标签:映射表存 key、使用处解析规范 §3.3,模块顶层禁止调 t() */
const TOOL_STATE_LABEL_KEYS: Record<string, string> = {
completed: 'quickdock.toolStateDone',
failed: 'quickdock.toolStateFailed',
calling: 'quickdock.toolStateCalling',
running: 'quickdock.toolStateProgress'
};
function buildText(entry: any): string { function buildText(entry: any): string {
const tool = entry.tool || ''; const tool = entry.tool || '';
const args = entry.args || {}; const args = entry.args || {};
@ -309,7 +309,6 @@ interface ToolTimelineItem {
kind: 'tool'; kind: 'tool';
key: string; key: string;
state: string; state: string;
stateLabel: string;
toolName: string; toolName: string;
text: string; text: string;
} }
@ -320,10 +319,16 @@ interface OutputTimelineItem {
content: string; content: string;
} }
const timelineItems = computed<(ToolTimelineItem | OutputTimelineItem)[]>(() => { interface CompressionTimelineItem {
kind: 'compression';
key: string;
round: number;
}
const timelineItems = computed<(ToolTimelineItem | OutputTimelineItem | CompressionTimelineItem)[]>(() => {
void currentLocale.value; void currentLocale.value;
const entries = activityEntries.value || []; const entries = activityEntries.value || [];
const rawItems: ({ kind: 'tool'; key: string; entry: any } | OutputTimelineItem)[] = []; const rawItems: ({ kind: 'tool'; key: string; entry: any } | OutputTimelineItem | CompressionTimelineItem)[] = [];
let currentToolGroup: { kind: 'tool'; key: string; entry: any } | null = null; let currentToolGroup: { kind: 'tool'; key: string; entry: any } | null = null;
const flushToolGroup = () => { const flushToolGroup = () => {
@ -361,6 +366,17 @@ const timelineItems = computed<(ToolTimelineItem | OutputTimelineItem)[]>(() =>
return; return;
} }
//
if (entry?.type === 'progress' && entry?.subtype === 'compression') {
flushToolGroup();
rawItems.push({
kind: 'compression',
key: `compression-${entry.ts || index}`,
round: Number(entry.round) || 0,
});
return;
}
if (!entry || entry.type !== 'progress' || !entry.tool) { if (!entry || entry.type !== 'progress' || !entry.tool) {
return; return;
} }
@ -401,7 +417,7 @@ const timelineItems = computed<(ToolTimelineItem | OutputTimelineItem)[]>(() =>
flushToolGroup(); flushToolGroup();
return rawItems.map((item) => { return rawItems.map((item) => {
if (item.kind === 'output') { if (item.kind === 'output' || item.kind === 'compression') {
return item; return item;
} }
const state = normalizeStatus(item.entry.status); const state = normalizeStatus(item.entry.status);
@ -409,7 +425,6 @@ const timelineItems = computed<(ToolTimelineItem | OutputTimelineItem)[]>(() =>
kind: 'tool' as const, kind: 'tool' as const,
key: item.key, key: item.key,
state, state,
stateLabel: t(TOOL_STATE_LABEL_KEYS[state] || 'quickdock.toolStateProgress'),
toolName: item.entry.tool || t('common.tool'), toolName: item.entry.tool || t('common.tool'),
text: buildText(item.entry) text: buildText(item.entry)
}; };

View File

@ -697,15 +697,16 @@
min-width: 0; min-width: 0;
} }
.qd-feed-row.feed-tool .tool-result { .qd-feed-row.feed-tool .tool-status {
flex: none; flex: none;
margin-left: auto; margin-left: auto;
font-size: 11.5px; display: inline-flex;
color: var(--text-tertiary); align-items: center;
font-variant-numeric: tabular-nums; justify-content: center;
min-width: 14px;
} }
.qd-feed-row.feed-tool .tool-result.is-error { .qd-feed-row.feed-tool .tool-status.is-error .qd-tool-done {
color: var(--state-danger); color: var(--state-danger);
} }
@ -733,6 +734,25 @@
color: var(--accent); color: var(--accent);
} }
.qd-feed-row.feed-compressed {
justify-content: center;
padding: 2px 14px;
}
.qd-feed-row.feed-compressed .feed-compressed__label {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 11.5px;
color: var(--text-tertiary);
}
.qd-feed-row.feed-compressed .feed-compressed__icon {
width: 12px;
height: 12px;
flex: none;
}
.qd-feed-row.feed-term { .qd-feed-row.feed-term {
font-family: ui-monospace, 'SF Mono', Menlo, monospace; font-family: ui-monospace, 'SF Mono', Menlo, monospace;
font-size: 12px; font-size: 12px;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,937 @@
/*
* 个人空间抽屉共享样式骨架 / 设置行 / 控件 / 浮层 / 响应式
* PersonalizationDrawer.vue 抽出scoped 样式无法穿透 tab 子组件故改为全局 CSS
* 所有类名均为 settings-* / personal-* 前缀全局冲突风险低
*/
/* 卡片尺寸/圆角/内边距的唯一事实源在全局 .personal-page-card_overlays.scss
这里只保留 redesign 特有的变量容器查询上下文与内部布局 */
.settings-redesign-card {
--settings-floating-menu-bg: color-mix(in srgb, var(--surface-soft) 90%, var(--surface-raised));
--settings-floating-menu-hover: var(--hover-bg);
--settings-floating-menu-shadow: none;
--settings-tab-hover-bg: var(--theme-tab-active);
--settings-tab-active-bg: var(--theme-tab-active);
position: relative;
/* 容器查询上下文:内部布局按卡片自身宽度响应挤压,与视口脱耦 */
container-type: inline-size;
container-name: settings-card;
}
html[data-theme='dark'] .settings-redesign-card,
body[data-theme='dark'] .settings-redesign-card {
--settings-floating-menu-bg: var(--surface-panel);
--settings-floating-menu-hover: var(--hover-bg);
--settings-floating-menu-shadow: none;
--settings-tab-hover-bg: var(--hover-bg);
--settings-tab-active-bg: var(--hover-bg);
}
/* 桌面端关闭按钮位于侧栏顶部流内(不再是绝对定位大圆钮) */
.settings-nav-head {
flex: 0 0 auto;
padding: 12px 12px 4px;
}
.settings-close-button {
width: 32px;
height: 32px;
border: 0;
border-radius: 8px;
background: transparent;
color: var(--text-secondary);
display: grid;
place-items: center;
cursor: pointer;
}
@media (hover: hover) {
.settings-close-button:hover {
background: var(--hover-bg);
color: var(--text-primary);
}
}
.settings-close-button svg {
width: 17px;
height: 17px;
stroke-width: 2;
}
/* 移动端顶栏:桌面端隐藏 */
.settings-mobile-bar {
display: none;
}
.settings-tab-chevron {
display: none;
}
.settings-redesign-body,
.settings-redesign-form {
height: 100%;
min-height: 0;
}
.settings-redesign-layout {
height: 100%;
display: flex;
min-height: 0;
}
.settings-redesign-nav {
flex: 0 0 184px;
min-width: 0;
min-height: 0;
height: 100%;
display: flex;
flex-direction: column;
border-right: 1px solid var(--theme-control-border);
}
.settings-redesign-tabs {
flex: 1 1 auto;
min-height: 0;
padding: 4px 10px 12px;
display: flex;
flex-direction: column;
gap: 2px;
overflow-y: auto;
overflow-x: hidden;
scrollbar-width: none;
-ms-overflow-style: none;
}
.settings-redesign-tab {
width: 100%;
height: 36px;
flex: 0 0 auto;
border: 0;
border-radius: 8px;
background: transparent;
color: var(--text-primary);
padding: 0 10px;
display: flex;
align-items: center;
gap: 10px;
cursor: pointer;
text-align: left;
font-size: 14px;
letter-spacing: -0.006em;
}
.settings-tab-label {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
@media (hover: hover) {
.settings-redesign-tab:hover {
background: var(--settings-tab-hover-bg);
}
}
.settings-redesign-tab.active {
background: var(--settings-tab-active-bg);
font-weight: 640;
}
.settings-tab-icon {
--icon-size: 18px;
flex: 0 0 auto;
}
/* 与其他图标外框统一 18px不再特殊放大图标外框对齐规范 */
.settings-tab-icon--chat {
width: 18px;
height: 18px;
display: inline-flex;
align-items: center;
justify-content: center;
}
.settings-tab-icon--chat svg {
width: 18px;
height: 18px;
}
.settings-redesign-content {
flex: 1 1 0;
min-width: 0;
min-height: 0;
height: 100%;
overflow: visible;
display: flex;
flex-direction: column;
position: relative;
}
.settings-redesign-content-header {
height: 58px;
flex: 0 0 58px;
display: flex;
align-items: center;
padding: 0 20px;
border-bottom: 1px solid var(--theme-control-border);
}
.settings-redesign-content-header h2 {
margin: 0;
color: var(--text-primary);
font-size: 19px;
line-height: 1;
letter-spacing: -0.015em;
font-weight: 650;
}
.settings-redesign-scroll {
min-height: 0;
flex: 1 1 auto;
overflow-y: auto;
overflow-x: hidden;
padding: 2px 20px 96px;
scroll-padding-bottom: 96px;
scrollbar-width: none;
-ms-overflow-style: none;
}
.settings-redesign-tabs::-webkit-scrollbar,
.settings-redesign-scroll::-webkit-scrollbar,
.settings-floating-menu::-webkit-scrollbar,
.role-editor-body::-webkit-scrollbar {
display: none;
}
.settings-floating-menu {
scrollbar-width: none;
-ms-overflow-style: none;
}
.settings-page {
min-height: 0;
}
.settings-select-row,
.settings-input-row,
.settings-list-row,
.settings-action-row,
.settings-toggle-row {
min-height: 0;
border-bottom: 1px solid var(--theme-control-border);
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: 18px;
padding: 13px 0;
color: var(--text-primary);
}
.settings-list-row.tall,
.settings-input-row.stackable {
align-items: flex-start;
}
.settings-list-row.tall {
grid-template-columns: minmax(180px, 1fr) minmax(0, 360px);
}
.settings-row-copy {
min-width: 0;
display: block;
}
.settings-row-title {
display: block;
color: var(--text-primary);
font-size: 14px;
font-weight: 550;
letter-spacing: -0.01em;
line-height: 1.32;
}
.settings-row-desc {
display: block;
margin-top: 3px;
color: var(--text-secondary);
font-size: 13px;
line-height: 1.45;
}
.settings-select-wrap {
position: relative;
display: inline-flex;
justify-self: end;
}
.settings-select-button {
width: max-content;
min-width: 0;
height: 32px;
border: 0;
border-radius: 8px;
background: transparent;
color: var(--text-primary);
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 0 8px 0 10px;
cursor: pointer;
font-size: 14px;
white-space: nowrap;
}
@media (hover: hover) {
.settings-select-button:hover {
background: var(--hover-bg);
}
}
.settings-select-button .select-chevron {
width: 12px;
height: 12px;
display: inline-flex;
align-items: center;
justify-content: center;
transition: transform 0.15s ease;
}
.settings-select-button .select-chevron::before {
content: '';
width: 7px;
height: 7px;
border-right: 1.5px solid currentColor;
border-bottom: 1.5px solid currentColor;
transform: translateY(-1.5px) rotate(45deg);
}
.settings-select-wrap.open .settings-select-button .select-chevron {
transform: rotate(180deg);
}
.settings-floating-menu {
position: fixed;
right: auto;
top: auto;
width: 300px;
max-height: calc(34px * 4 + 12px);
overflow-y: auto;
overflow-x: hidden;
padding: 5px;
border-radius: 10px;
background: var(--settings-floating-menu-bg);
border: 1px solid var(--theme-control-border);
box-shadow: var(--settings-floating-menu-shadow);
opacity: 1;
backdrop-filter: none;
z-index: 2147483647;
display: none;
}
.settings-select-wrap.open .settings-floating-menu {
display: block;
}
.settings-floating-menu.dark {
background: var(--surface-panel) !important;
box-shadow: none !important;
}
@media (hover: hover) {
.settings-floating-menu.dark .settings-menu-option:hover {
background: var(--hover-bg) !important;
}
}
html[data-theme='dark'] .settings-floating-menu,
body[data-theme='dark'] .settings-floating-menu {
background: var(--surface-panel) !important;
box-shadow: none !important;
}
@media (hover: hover) {
html[data-theme='dark'] .settings-menu-option:hover,
body[data-theme='dark'] .settings-menu-option:hover {
background: var(--hover-bg) !important;
}
}
.settings-menu-option {
width: 100%;
min-height: 34px;
border: 0;
border-radius: 8px;
background: transparent;
color: var(--text-primary);
display: grid;
grid-template-columns: minmax(0, 1fr) 22px;
row-gap: 0;
column-gap: 8px;
align-items: center;
padding: 5px 9px;
text-align: left;
cursor: pointer;
}
@media (hover: hover) {
.settings-menu-option:hover {
background: var(--settings-floating-menu-hover);
}
}
.settings-menu-option:disabled {
opacity: 0.48;
cursor: not-allowed;
}
.settings-menu-option strong {
grid-column: 1;
grid-row: 1;
display: block;
color: var(--text-primary);
font-size: 14px;
font-weight: 550;
letter-spacing: -0.01em;
min-width: 0;
}
.settings-menu-option span {
grid-column: 1;
grid-row: 2;
display: block;
margin-top: 0;
color: var(--text-secondary);
font-size: 11px;
line-height: 1.15;
min-width: 0;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.settings-menu-option svg {
grid-column: 2;
grid-row: 1 / span 2;
align-self: center;
width: 21px;
height: 21px;
fill: none;
stroke: var(--text-primary);
stroke-width: 2.25;
stroke-linecap: round;
stroke-linejoin: round;
opacity: 0;
}
.settings-menu-option.selected svg {
opacity: 1;
}
.settings-input-row input,
.settings-add-row input,
.settings-number-row input,
.settings-number-input,
.settings-compression-grid input {
height: 32px;
border: 1px solid var(--theme-control-border);
border-radius: 8px;
background: transparent;
color: var(--text-primary);
padding: 0 11px;
outline: none;
font-size: 13px;
}
.settings-number-input {
width: 120px;
text-align: right;
}
.settings-section-divider {
display: flex;
align-items: center;
margin: 18px 0 6px;
}
.settings-section-divider__label {
font-size: 12px;
font-weight: 600;
color: var(--text-secondary);
letter-spacing: 0.04em;
}
.settings-section-divider::after {
content: '';
flex: 1;
height: 1px;
margin-left: 10px;
background: var(--theme-control-border);
}
.settings-input-row > input {
width: min(220px, 100%);
text-align: left;
}
.settings-input-row input:focus,
.settings-add-row input:focus,
.settings-number-row input:focus,
.settings-number-input:focus,
.settings-compression-grid input:focus {
border-color: var(--text-secondary);
box-shadow: none;
}
/* width 用绝对值min(280px, 100%) 的百分比在 grid auto 列固有尺寸计算中会失效
导致列宽被内容chips 撑到 404pxlabel 列被挤成 0px 竖排 */
.settings-input-stack {
width: 280px;
max-width: 100%;
display: grid;
gap: 8px;
justify-self: end;
}
.settings-input-stack.wide {
width: 240px;
max-width: 100%;
}
.settings-input-stack input {
width: 100%;
}
.settings-chip-row {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.settings-chip-row.right {
justify-content: flex-end;
}
.settings-chip-row button {
border: 1px solid var(--theme-control-border);
border-radius: 999px;
background: transparent;
color: var(--text-primary);
min-height: 28px;
padding: 0 12px;
font-size: 12.5px;
cursor: pointer;
}
.settings-number-row button,
.settings-add-row button,
.settings-secondary-button,
.settings-primary-button {
border: 1px solid var(--theme-control-border);
border-radius: 8px;
background: transparent;
color: var(--text-primary);
height: 32px;
padding: 0 14px;
font-size: 13px;
cursor: pointer;
}
@media (hover: hover) {
.settings-chip-row button:hover,
.settings-number-row button:hover,
.settings-add-row button:hover,
.settings-secondary-button:hover {
background: var(--hover-bg);
}
}
.settings-chip-row button.active,
.settings-primary-button {
border-color: var(--accent);
background: var(--accent);
color: var(--on-accent);
}
.settings-secondary-button.danger {
color: var(--state-warning);
}
.settings-inline-actions {
display: inline-flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
flex-wrap: wrap;
}
.settings-inline-actions.right {
margin-top: 10px;
}
.settings-mini-status {
color: var(--text-secondary);
font-size: 12px;
}
.settings-mini-status.warning {
color: var(--state-warning);
}
.settings-add-row,
.settings-number-row {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
}
.settings-add-row input,
.settings-number-row input {
min-width: 0;
flex: 1 1 auto;
}
.settings-textarea-row {
min-height: 0;
border-bottom: 1px solid var(--theme-control-border);
display: flex;
flex-direction: column;
gap: 10px;
padding: 14px 0;
color: var(--text-primary);
}
.settings-textarea-row textarea {
width: 100%;
min-height: 96px;
max-height: 320px;
border: 1px solid var(--theme-control-border);
border-radius: 8px;
background: transparent;
color: var(--text-primary);
padding: 12px;
outline: none;
font-size: 13px;
line-height: 1.55;
resize: vertical;
overflow-y: auto;
font-family: inherit;
}
.settings-textarea-row textarea:focus {
border-color: var(--text-secondary);
box-shadow: none;
}
.settings-textarea-row textarea::placeholder {
color: var(--text-secondary);
opacity: 0.6;
}
.settings-toggle-row {
cursor: pointer;
position: relative;
}
.settings-toggle-row input[type='checkbox'] {
position: absolute;
opacity: 0;
pointer-events: none;
}
.settings-group-block {
border-bottom: 1px solid var(--theme-control-border);
padding: 14px 0 16px;
}
.settings-group-title {
margin-bottom: 6px;
}
.settings-toggle-row.inner {
min-height: 48px;
padding: 7px 0 7px 18px;
border-bottom: 0;
}
.settings-check-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0 26px;
}
.settings-compression-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px 14px;
margin-top: 10px;
}
.settings-compression-grid label {
display: grid;
gap: 5px;
color: var(--text-secondary);
font-size: 12px;
}
.settings-save-bar {
position: absolute;
right: 18px;
bottom: 0;
left: 0;
min-height: 54px;
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
background: transparent;
pointer-events: none;
}
.settings-save-bar > * {
pointer-events: auto;
}
.personal-status-group {
min-height: 20px;
font-size: 12px;
}
.status.success {
color: var(--state-success);
}
.status.error,
.usage-summary-error {
color: var(--state-warning);
}
/* 卡片常态最大宽度 648px以下断点均在此基础上进一步响应
窗口拖窄520px 卡片侧栏再收窄内容留白再压缩输入框收窄 */
@container settings-card (max-width: 520px) {
.settings-redesign-nav {
flex-basis: 164px;
}
.settings-redesign-content-header {
padding: 0 16px;
}
.settings-redesign-scroll {
padding-left: 16px;
padding-right: 16px;
}
.settings-select-row,
.settings-input-row,
.settings-list-row,
.settings-action-row,
.settings-toggle-row {
gap: 12px;
}
.settings-input-row > input {
width: min(180px, 100%);
}
.settings-input-stack,
.settings-input-stack.wide {
width: 220px;
max-width: 100%;
}
}
/* 极限挤压(手机全屏 / 窗口极窄输入类行上下堆叠控件全宽开关行保持同行iOS 设置式) */
@container settings-card (max-width: 440px) {
.settings-select-row,
.settings-input-row,
.settings-list-row,
.settings-action-row {
grid-template-columns: minmax(0, 1fr);
gap: 9px;
}
.settings-select-wrap,
.settings-input-stack,
.settings-input-stack.wide,
.settings-input-row > input {
justify-self: stretch;
width: 100%;
}
.settings-select-wrap .settings-select-button {
width: 100%;
justify-content: space-between;
}
.settings-check-grid,
.settings-compression-grid,
.usage-summary-grid--tokens,
.usage-summary-grid--counts {
grid-template-columns: 1fr;
}
}
/* ═══ 手机端 / 窄屏≤760px全屏 + iOS 设置式「列表 → 子页」导航 ═══ */
@media (max-width: 760px) {
/* 卡片全屏化由全局 _overlays.scss 同断点媒体查询负责(统一断点 760px */
/* 导航列表与内容子页并排,直接切换(触屏不做平移动画,避免看到上一屏滑走) */
.settings-redesign-layout {
width: 200%;
}
.settings-redesign-card.mobile-in-sub .settings-redesign-layout {
transform: translateX(-50%);
}
.settings-redesign-nav,
.settings-redesign-content {
flex: 0 0 50%;
width: 50%;
}
/* iOS 点击默认灰色高亮闪tap-highlight观感类似 hover 粘滞,禁用 */
.settings-redesign-tab {
-webkit-tap-highlight-color: transparent;
}
/* 窄屏布局下禁用选项 hover 桌面窄窗口 hover:hover 仍匹配
触屏已由 @media (hover: hover) 包裹兑底这里按布局模式再禁一层 */
.settings-redesign-tab:hover,
.settings-menu-option:hover {
background: transparent;
}
/* 移动端列表态不需要「当前选中项」常驻高亮iOS 设置式列表无此概念) */
.settings-redesign-tab.active {
background: transparent;
font-weight: inherit;
}
.settings-floating-menu.dark .settings-menu-option:hover,
html[data-theme='dark'] .settings-menu-option:hover,
body[data-theme='dark'] .settings-menu-option:hover {
background: transparent !important;
}
.settings-redesign-nav {
border-right: 0;
}
/* 桌面端头部隐藏,移动端顶栏接管 */
.settings-nav-head,
.settings-redesign-content-header {
display: none;
}
.settings-mobile-bar {
display: flex;
align-items: center;
gap: 4px;
flex: 0 0 auto;
padding: calc(8px + env(safe-area-inset-top, 0px)) 12px 8px;
border-bottom: 1px solid var(--theme-control-border);
min-height: 52px;
}
.settings-mobile-bar-title {
flex: 1 1 auto;
min-width: 0;
text-align: center;
font-size: 16px;
font-weight: 650;
letter-spacing: -0.01em;
color: var(--text-primary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.settings-mobile-bar-cell {
width: 44px;
flex: 0 0 auto;
}
.settings-mobile-bar-btn {
height: 36px;
min-width: 36px;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 2px;
padding: 0 6px;
border: 0;
border-radius: 8px;
background: transparent;
color: var(--text-primary);
font-size: 15px;
cursor: pointer;
}
.settings-mobile-bar-btn:active {
background: var(--hover-bg);
}
.settings-mobile-bar-btn svg {
width: 20px;
height: 20px;
}
.settings-mobile-back {
color: var(--accent);
}
/* 导航变为分组列表样式48px 触控目标 + 右箭头 */
.settings-redesign-tabs {
padding: 8px 12px calc(20px + env(safe-area-inset-bottom, 0px));
gap: 3px;
}
.settings-redesign-tab {
height: 48px;
padding: 0 12px;
font-size: 15px;
border-radius: 10px;
gap: 12px;
}
.settings-tab-icon {
--icon-size: 20px;
}
.settings-tab-icon--chat {
width: 20px;
height: 20px;
}
.settings-tab-icon--chat svg {
width: 20px;
height: 20px;
}
.settings-tab-chevron {
display: block;
width: 16px;
height: 16px;
margin-left: auto;
flex: 0 0 auto;
color: var(--text-secondary);
}
.settings-redesign-scroll {
padding: 4px 16px calc(48px + env(safe-area-inset-bottom, 0px));
}
.settings-save-bar {
right: 16px;
}
.settings-floating-menu {
width: min(340px, calc(100vw - 48px));
}
}
/* ── 语音模型下载进度条 ── */

View File

@ -0,0 +1,82 @@
<script setup lang="ts">
import { inject } from 'vue';
defineOptions({ name: 'AdminTab' });
/**
* 共享上下文由 PersonalizationDrawer.vue 通过 provide 注入
* 解构出的名称与主文件 script 顶层绑定一致模板可直接引用
*/
const ctx = inject<Record<string, any>>('personalizationDrawer')!;
const {
openAdminPanel,
openApiAdmin,
openCustomTools,
openMcpConfig,
showMcpConfigEntry
} = ctx;
</script>
<template>
<section class="settings-page admin-monitor-page">
<div class="settings-action-row" v-if="showMcpConfigEntry">
<span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.mcpConfigTitle') }}</span
><span class="settings-row-desc"
>{{ $t('personalization.mcpConfigDesc') }}</span
></span
><button
type="button"
class="settings-secondary-button"
@click="openMcpConfig"
>
{{ $t('personalization.open') }}
</button>
</div>
<div class="settings-action-row">
<span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.adminMonitorTitle') }}</span
><span class="settings-row-desc"
>{{ $t('personalization.adminMonitorDesc') }}</span
></span
><button
type="button"
class="settings-secondary-button"
@click="openAdminPanel"
>
{{ $t('personalization.open') }}
</button>
</div>
<div class="settings-action-row">
<span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.apiAdminTitle') }}</span
><span class="settings-row-desc">{{ $t('personalization.apiAdminDesc') }}</span></span
><button
type="button"
class="settings-secondary-button"
@click="openApiAdmin"
>
{{ $t('personalization.open') }}
</button>
</div>
<div class="settings-action-row">
<span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.customToolsAdminTitle') }}</span
><span class="settings-row-desc">{{ $t('personalization.customToolsAdminDesc') }}</span></span
><button
type="button"
class="settings-secondary-button"
@click="openCustomTools"
>
{{ $t('personalization.open') }}
</button>
</div>
</section>
</template>
<style scoped>
.admin-monitor-page {
display: block;
padding: 0;
}
</style>

View File

@ -0,0 +1,375 @@
<script setup lang="ts">
import { inject } from 'vue';
import FancyCheck from '@/components/common/FancyCheck.vue';
defineOptions({ name: 'AppearanceTab' });
/**
* 共享上下文由 PersonalizationDrawer.vue 通过 provide 注入
* 解构出的名称与主文件 script 顶层绑定一致模板可直接引用
*/
const ctx = inject<Record<string, any>>('personalizationDrawer')!;
const {
activeDropdown,
activeTheme,
blockDisplayLabel,
blockDisplayOptions,
compactMessageDisplayLabel,
compactMessageDisplayOptions,
currentBlockDisplayMode,
currentCompactMessageDisplay,
floatingMenuStyle,
form,
handleMinimalExpandHeightLimitedChange,
handleStackedHideBordersChange,
locale,
localeLabel,
localeOptions,
minimalExpandHeightLimited,
personalization,
selectBlockDisplayMode,
selectCompactMessageDisplay,
selectLocaleOption,
selectThemeOption,
stackedHideBorders,
themeLabel,
themeOptions,
toggleDropdown
} = ctx;
</script>
<template>
<section class="settings-page">
<div class="settings-select-row">
<span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.themeTitle') }}</span
><span class="settings-row-desc">{{ $t('personalization.themeDesc') }}</span></span
>
<div
class="settings-select-wrap"
:class="{ open: activeDropdown === 'theme' }"
@click.stop
>
<button
type="button"
class="settings-select-button"
@click="toggleDropdown('theme')"
>
{{ themeLabel }} <span class="select-chevron" aria-hidden="true"></span>
</button>
<div
:class="['settings-floating-menu', { dark: activeTheme === 'dark' }]"
:style="activeDropdown ? floatingMenuStyle : undefined"
>
<button
v-for="option in themeOptions"
:key="option.id"
type="button"
class="settings-menu-option"
:class="{ selected: activeTheme === option.id }"
@click="selectThemeOption(option.id)"
>
<strong>{{ $t(option.labelKey) }}</strong
><span>{{ $t(option.descKey) }}</span
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
</div>
</div>
</div>
<div class="settings-select-row">
<span class="settings-row-copy"
><span class="settings-row-title">{{ $t('common.settingsLanguage') }}</span
><span class="settings-row-desc">{{
$t('common.settingsLanguageDesc')
}}</span></span
>
<div
class="settings-select-wrap"
:class="{ open: activeDropdown === 'language' }"
@click.stop
>
<button
type="button"
class="settings-select-button"
@click="toggleDropdown('language')"
>
{{ localeLabel }}
<span class="select-chevron" aria-hidden="true"></span>
</button>
<div
:class="['settings-floating-menu', { dark: activeTheme === 'dark' }]"
:style="activeDropdown ? floatingMenuStyle : undefined"
>
<button
v-for="option in localeOptions"
:key="option.id"
type="button"
class="settings-menu-option"
:class="{ selected: locale === option.id }"
@click="selectLocaleOption(option.id)"
>
<strong>{{ $t(option.labelKey) }}</strong
><span>{{ $t(option.descKey) }}</span
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
</div>
</div>
</div>
<label class="settings-toggle-row"
><span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.groupByWorkspaceTitle') }}</span
><span class="settings-row-desc"
>{{ $t('personalization.groupByWorkspaceDesc') }}</span
></span
><input
type="checkbox"
:checked="form.group_sidebar_by_workspace"
@change="
personalization.updateField({
key: 'group_sidebar_by_workspace',
value: $event.target.checked
})
" /><FancyCheck :checked="form.group_sidebar_by_workspace" /></label>
<label class="settings-toggle-row"
><span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.newChatBlankTitle') }}</span
><span class="settings-row-desc"
>{{ $t('personalization.newChatBlankDesc') }}</span
></span
><input
type="checkbox"
:checked="form.new_chat_button_behavior === 'route'"
@change="
personalization.updateField({
key: 'new_chat_button_behavior',
value: $event.target.checked ? 'route' : 'blank'
})
" /><FancyCheck :checked="form.new_chat_button_behavior === 'route'" /></label>
<label class="settings-toggle-row"
><span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.useCustomNamesTitle') }}</span
><span class="settings-row-desc"
>{{ $t('personalization.useCustomNamesDesc') }}</span
></span
><input
type="checkbox"
:checked="form.use_custom_names"
@change="
personalization.updateField({
key: 'use_custom_names',
value: $event.target.checked
})
" /><FancyCheck :checked="form.use_custom_names" /></label>
<label class="settings-toggle-row"
><span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.enhancedToolDisplayTitle') }}</span
><span class="settings-row-desc"
>{{ $t('personalization.enhancedToolDisplayDesc') }}</span
></span
><input
type="checkbox"
:checked="form.enhanced_tool_display"
@change="
personalization.updateField({
key: 'enhanced_tool_display',
value: $event.target.checked
})
" /><FancyCheck :checked="form.enhanced_tool_display" /></label>
<label class="settings-toggle-row"
><span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.showStatusAvatarTitle') }}</span
><span class="settings-row-desc"
>{{ $t('personalization.showStatusAvatarDesc') }}</span
></span
><input
type="checkbox"
:checked="form.show_status_avatar"
@change="
personalization.updateField({
key: 'show_status_avatar',
value: $event.target.checked
})
" /><FancyCheck :checked="form.show_status_avatar" /></label>
<label class="settings-toggle-row"
><span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.showGitStatusBarTitle') }}</span
><span class="settings-row-desc"
>{{ $t('personalization.showGitStatusBarDesc') }}</span
></span
><input
type="checkbox"
:checked="form.show_git_status_bar"
@change="
personalization.updateField({
key: 'show_git_status_bar',
value: $event.target.checked
})
" /><FancyCheck :checked="form.show_git_status_bar" /></label>
<label class="settings-toggle-row"
><span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.autoOpenTerminalTitle') }}</span
><span class="settings-row-desc"
>{{ $t('personalization.autoOpenTerminalDesc') }}</span
></span
><input
type="checkbox"
:checked="form.auto_open_terminal_panel"
@change="
personalization.updateField({
key: 'auto_open_terminal_panel',
value: $event.target.checked
})
" /><FancyCheck :checked="form.auto_open_terminal_panel" /></label>
<label class="settings-toggle-row"
><span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.quickDockAutoExpandTitle') }}</span
><span class="settings-row-desc"
>{{ $t('personalization.quickDockAutoExpandDesc') }}</span
></span
><input
type="checkbox"
:checked="form.quick_dock_auto_expand"
@change="
personalization.updateField({
key: 'quick_dock_auto_expand',
value: $event.target.checked
})
" /><FancyCheck :checked="form.quick_dock_auto_expand" /></label>
<label class="settings-toggle-row"
><span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.editSummaryLiveTitle') }}</span
><span class="settings-row-desc"
>{{ $t('personalization.editSummaryLiveDesc') }}</span
></span
><input
type="checkbox"
:checked="form.edit_summary_live_display"
@change="
personalization.updateField({
key: 'edit_summary_live_display',
value: $event.target.checked
})
" /><FancyCheck :checked="form.edit_summary_live_display" /></label>
<label class="settings-toggle-row"
><span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.filePreviewWrapTitle') }}</span
><span class="settings-row-desc"
>{{ $t('personalization.filePreviewWrapDesc') }}</span
></span
><input
type="checkbox"
:checked="form.file_preview_auto_wrap"
@change="
personalization.updateField({
key: 'file_preview_auto_wrap',
value: $event.target.checked
})
" /><FancyCheck :checked="form.file_preview_auto_wrap" /></label>
<div class="settings-select-row">
<span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.blockDisplayModeTitle') }}</span
><span class="settings-row-desc">{{ $t('personalization.blockDisplayModeDesc') }}</span></span
>
<div
class="settings-select-wrap"
:class="{ open: activeDropdown === 'block-display' }"
@click.stop
>
<button
type="button"
class="settings-select-button"
@click="toggleDropdown('block-display')"
>
{{ blockDisplayLabel }}
<span class="select-chevron" aria-hidden="true"></span>
</button>
<div
:class="['settings-floating-menu', { dark: activeTheme === 'dark' }]"
:style="activeDropdown ? floatingMenuStyle : undefined"
>
<button
v-for="option in blockDisplayOptions"
:key="option.id"
type="button"
class="settings-menu-option"
:class="{ selected: currentBlockDisplayMode === option.value }"
@click="selectBlockDisplayMode(option.value)"
>
<strong>{{ $t(option.labelKey) }}</strong
><span>{{ $t(option.descKey) }}</span
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
</div>
</div>
</div>
<label
v-if="currentBlockDisplayMode === 'stacked'"
class="settings-toggle-row"
>
<span class="settings-row-copy">
<span class="settings-row-title">{{ $t('personalization.hideBlockBordersTitle') }}</span>
<span class="settings-row-desc">{{ $t('personalization.hideBlockBordersDesc') }}</span>
</span>
<input
type="checkbox"
:checked="stackedHideBorders"
@change="handleStackedHideBordersChange($event)"
/>
<FancyCheck :checked="stackedHideBorders" />
</label>
<label
v-if="currentBlockDisplayMode === 'minimal'"
class="settings-toggle-row"
>
<span class="settings-row-copy">
<span class="settings-row-title">{{ $t('personalization.minimalExpandHeightTitle') }}</span>
<span class="settings-row-desc">{{ $t('personalization.minimalExpandHeightDesc') }}</span>
</span>
<input
type="checkbox"
:checked="minimalExpandHeightLimited"
@change="handleMinimalExpandHeightLimitedChange($event)"
/>
<FancyCheck :checked="minimalExpandHeightLimited" />
</label>
<div class="settings-select-row">
<span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.compactMessageTitle') }}</span
><span class="settings-row-desc"
>{{ $t('personalization.compactMessageDesc') }}</span
></span
>
<div
class="settings-select-wrap"
:class="{ open: activeDropdown === 'compact-message' }"
@click.stop
>
<button
type="button"
class="settings-select-button"
@click="toggleDropdown('compact-message')"
>
{{ compactMessageDisplayLabel }}
<span class="select-chevron" aria-hidden="true"></span>
</button>
<div
:class="['settings-floating-menu', { dark: activeTheme === 'dark' }]"
:style="activeDropdown ? floatingMenuStyle : undefined"
>
<button
v-for="option in compactMessageDisplayOptions"
:key="option.id"
type="button"
class="settings-menu-option"
:class="{ selected: currentCompactMessageDisplay === option.value }"
@click="selectCompactMessageDisplay(option.value)"
>
<strong>{{ $t(option.labelKey) }}</strong
><span>{{ $t(option.descKey) }}</span
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
</div>
</div>
</div>
</section>
</template>

View File

@ -0,0 +1,203 @@
<script setup lang="ts">
import { inject } from 'vue';
import FancyCheck from '@/components/common/FancyCheck.vue';
defineOptions({ name: 'ContextTab' });
/**
* 共享上下文由 PersonalizationDrawer.vue 通过 provide 注入
* 解构出的名称与主文件 script 顶层绑定一致模板可直接引用
*/
const ctx = inject<Record<string, any>>('personalizationDrawer')!;
const {
commitProjectMemoryInjectLimitInput,
commitRecentConversationsPromptLimitInput,
form,
handleCompressionNumberInput,
handleProjectMemoryInjectLimitInput,
handleRecentConversationsPromptLimitInput,
personalization,
projectMemoryInjectLimitMin,
recentConversationsPromptLimitRange,
restoreCompressionDefaults,
restoreProjectMemoryInjectLimit,
restoreRecentConversationsPromptLimit
} = ctx;
</script>
<template>
<section class="settings-page">
<label class="settings-toggle-row"
><span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.recentConversationsTitle') }}</span
><span class="settings-row-desc"
>{{ $t('personalization.recentConversationsDesc') }}</span
></span
><input
type="checkbox"
:checked="form.recent_conversations_prompt_enabled"
@change="
personalization.updateField({
key: 'recent_conversations_prompt_enabled',
value: $event.target.checked
})
" /><FancyCheck :checked="form.recent_conversations_prompt_enabled" /></label>
<div
class="settings-input-row"
v-if="form.recent_conversations_prompt_enabled"
>
<span class="settings-row-title">{{ $t('personalization.recentConversationsCountTitle') }}</span>
<div class="settings-number-row">
<input
type="number"
:min="recentConversationsPromptLimitRange.min"
:max="recentConversationsPromptLimitRange.max"
:value="form.recent_conversations_prompt_limit"
@input="handleRecentConversationsPromptLimitInput"
@blur="commitRecentConversationsPromptLimitInput"
/><button type="button" @click="restoreRecentConversationsPromptLimit">{{ $t('personalization.restoreDefault') }}
</button>
</div>
</div>
<div class="settings-input-row">
<span class="settings-row-title">{{ $t('personalization.maxMemoryInjectTitle') }}</span>
<div class="settings-number-row">
<input
type="number"
:min="projectMemoryInjectLimitMin"
:value="form.project_memory_inject_limit ?? ''"
::placeholder="$t('personalization.noLimitPlaceholder')"
@input="handleProjectMemoryInjectLimitInput"
@blur="commitProjectMemoryInjectLimitInput"
/><button type="button" @click="restoreProjectMemoryInjectLimit">{{ $t('personalization.restoreDefault') }}
</button>
</div>
</div>
<div class="settings-group-block">
<div class="settings-group-title">
<span class="settings-row-title">{{ $t('personalization.compressionStrategyTitle') }}</span
><span class="settings-row-desc"
>{{ $t('personalization.compressionStrategyDesc') }}</span
>
</div>
<label class="settings-toggle-row inner"
><span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.autoShallowCompressTitle') }}</span></span
><input
type="checkbox"
:checked="form.auto_shallow_compress_enabled"
@change="
personalization.updateField({
key: 'auto_shallow_compress_enabled',
value: $event.target.checked
})
" /><FancyCheck :checked="form.auto_shallow_compress_enabled" /></label>
<label class="settings-toggle-row inner"
><span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.autoDeepCompressTitle') }}</span></span
><input
type="checkbox"
:checked="form.auto_deep_compress_enabled"
@change="
personalization.updateField({
key: 'auto_deep_compress_enabled',
value: $event.target.checked
})
" /><FancyCheck :checked="form.auto_deep_compress_enabled" /></label>
<div class="settings-compression-grid">
<label
><span>{{ $t('personalization.shallowTriggerTokens') }}</span
><input
type="number"
:value="form.shallow_compress_trigger_tokens ?? ''"
@input="
handleCompressionNumberInput(
'shallow_compress_trigger_tokens',
$event
)
"
/></label>
<label
><span>{{ $t('personalization.shallowKeepRecentTools') }}</span
><input
type="number"
:value="form.shallow_compress_keep_recent_tools ?? ''"
@input="
handleCompressionNumberInput(
'shallow_compress_keep_recent_tools',
$event
)
"
/></label>
<label
><span>{{ $t('personalization.shallowKeepTurnTools') }}</span
><input
type="number"
:value="form.shallow_compress_keep_user_turn_tools ?? ''"
@input="
handleCompressionNumberInput(
'shallow_compress_keep_user_turn_tools',
$event
)
"
/></label>
<label
><span>{{ $t('personalization.shallowMaxReplace') }}</span
><input
type="number"
:value="form.shallow_compress_max_replace_per_round ?? ''"
@input="
handleCompressionNumberInput(
'shallow_compress_max_replace_per_round',
$event
)
"
/></label>
<label
><span>{{ $t('personalization.shallowToolInterval') }}</span
><input
type="number"
:value="form.shallow_compress_trigger_tool_calls_interval ?? ''"
@input="
handleCompressionNumberInput(
'shallow_compress_trigger_tool_calls_interval',
$event
)
"
/></label>
<label
><span>{{ $t('personalization.deepTriggerTokens') }}</span
><input
type="number"
:value="form.deep_compress_trigger_tokens ?? ''"
@input="
handleCompressionNumberInput('deep_compress_trigger_tokens', $event)
"
/></label>
</div>
<label class="settings-toggle-row inner"
><span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.deepCompressInjectTitle') }}</span
><span class="settings-row-desc"
>{{ $t('personalization.deepCompressInjectDesc') }}</span
></span
><input
type="checkbox"
:checked="form.deep_compress_form === 'inject'"
@change="
personalization.updateField({
key: 'deep_compress_form',
value: $event.target.checked ? 'inject' : 'file'
})
" /><FancyCheck :checked="form.deep_compress_form === 'inject'" /></label>
<div class="settings-inline-actions right">
<button
type="button"
class="settings-secondary-button"
@click="restoreCompressionDefaults"
>{{ $t('personalization.restoreCompressionDefaults') }}
</button>
</div>
</div>
</section>
</template>

View File

@ -0,0 +1,182 @@
<script setup lang="ts">
import { inject } from 'vue';
defineOptions({ name: 'DataTab' });
/**
* 共享上下文由 PersonalizationDrawer.vue 通过 provide 注入
* 解构出的名称与主文件 script 顶层绑定一致模板可直接引用
*/
const ctx = inject<Record<string, any>>('personalizationDrawer')!;
const {
personalization,
fetchUsageSummary,
formatTokenCount,
usageError,
usageLoading,
usageSummary,
usageUpdatedText
} = ctx;
</script>
<template>
<section
class="settings-page usage-summary-page"
data-tutorial="personal-page-usage"
>
<div class="usage-summary-card settings-stats-card">
<div class="usage-summary-header">
<div>
<p class="usage-summary-eyebrow">{{ $t('personalization.usageEyebrow') }}</p>
<h3>{{ $t('personalization.usageTitle') }}</h3>
<p class="usage-summary-desc">{{ $t('personalization.usageDesc') }}
</p>
</div>
</div>
<div class="usage-summary-grid usage-summary-grid--tokens">
<div class="usage-summary-item">
<div class="label">{{ $t('personalization.usageInputLabel') }}</div>
<div class="value value--success">
{{ formatTokenCount(usageSummary.total_input_tokens) }}
</div>
</div>
<div class="usage-summary-item">
<div class="label">{{ $t('personalization.usageOutputLabel') }}</div>
<div class="value value--warning">
{{ formatTokenCount(usageSummary.total_output_tokens) }}
</div>
</div>
</div>
<div class="usage-summary-grid usage-summary-grid--counts">
<div class="usage-summary-item">
<div class="label">{{ $t('personalization.usageConversationsLabel') }}</div>
<div class="value">
{{ formatTokenCount(usageSummary.total_conversations) }}
</div>
</div>
<div class="usage-summary-item">
<div class="label">{{ $t('personalization.usageMessagesLabel') }}</div>
<div class="value">
{{ formatTokenCount(usageSummary.total_user_messages) }}
</div>
</div>
<div class="usage-summary-item">
<div class="label">{{ $t('personalization.usageToolsLabel') }}</div>
<div class="value">
{{ formatTokenCount(usageSummary.total_tools) }}
</div>
</div>
</div>
<div class="usage-summary-meta">
<span v-if="usageError" class="usage-summary-error">{{ usageError }}</span
><span v-else-if="usageLoading">{{ $t('personalization.usageSyncing') }}</span
><span v-else>{{ $t('personalization.usageUpdated', { time: usageUpdatedText }) }}</span
><button
type="button"
class="usage-summary-refresh"
@click="fetchUsageSummary"
:disabled="usageLoading"
>
{{ usageLoading ? $t('personalization.usageRefreshing') : $t('personalization.usageRefresh') }}
</button>
</div>
</div>
</section>
</template>
<style scoped>
.usage-summary-page {
display: block;
padding: 0;
}
.settings-stats-card,
.usage-summary-card {
width: 100%;
max-width: 720px;
padding: 0;
min-height: 0;
display: flex;
flex-direction: column;
gap: 14px;
}
.usage-summary-header h3 {
margin: 4px 0 6px;
font-size: 20px;
}
.usage-summary-eyebrow {
margin: 0;
font-size: 11px;
color: var(--accent-strong);
letter-spacing: 0.16em;
text-transform: uppercase;
}
.usage-summary-desc,
.usage-summary-note,
.usage-summary-meta {
margin: 0;
color: var(--text-secondary);
font-size: 12px;
}
.usage-summary-grid {
display: grid;
gap: 10px;
}
.usage-summary-grid--tokens {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.usage-summary-grid--counts {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.usage-summary-item {
padding: 12px 14px;
border-radius: 14px;
border: 1px solid var(--theme-control-border);
background: transparent;
}
.usage-summary-item .label {
font-size: 12px;
color: var(--text-secondary);
}
.usage-summary-item .value {
margin-top: 4px;
font-size: 20px;
font-weight: 600;
color: var(--text-primary);
}
.usage-summary-item .value--success {
color: var(--state-success);
}
.usage-summary-item .value--warning {
color: var(--state-warning);
}
.usage-summary-meta {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.usage-summary-refresh {
border: 1px solid var(--theme-control-border);
border-radius: 999px;
padding: 7px 14px;
background: transparent;
color: var(--text-primary);
font-size: 12px;
cursor: pointer;
}
</style>

View File

@ -0,0 +1,69 @@
<script setup lang="ts">
import { inject } from 'vue';
defineOptions({ name: 'FilesTab' });
/**
* 共享上下文由 PersonalizationDrawer.vue 通过 provide 注入
* 解构出的名称与主文件 script 顶层绑定一致模板可直接引用
*/
const ctx = inject<Record<string, any>>('personalizationDrawer')!;
const {
personalization,
form,
activeDropdown,
floatingMenuStyle,
imageCompressionLabel,
imageCompressionOptions,
selectImageCompression,
toggleDropdown,
activeTheme
} = ctx;
</script>
<template>
<section
class="settings-page"
data-tutorial="personal-page-image"
>
<div class="settings-select-row">
<span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.imageCompressionTitle') }}</span
><span class="settings-row-desc"
>{{ $t('personalization.imageCompressionDesc') }}</span
></span
>
<div
class="settings-select-wrap"
:class="{ open: activeDropdown === 'image' }"
@click.stop
>
<button
type="button"
class="settings-select-button"
@click="toggleDropdown('image')"
>
{{ imageCompressionLabel }}
<span class="select-chevron" aria-hidden="true"></span>
</button>
<div
:class="['settings-floating-menu', { dark: activeTheme === 'dark' }]"
:style="activeDropdown ? floatingMenuStyle : undefined"
>
<button
v-for="option in imageCompressionOptions"
:key="option.id"
type="button"
class="settings-menu-option"
:class="{ selected: form.image_compression === option.id }"
@click="selectImageCompression(option.id)"
>
<strong>{{ $t(option.labelKey) }}</strong
><span>{{ $t(option.descKey) }}</span
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
</div>
</div>
</div>
</section>
</template>

View File

@ -0,0 +1,101 @@
<script setup lang="ts">
import { inject } from 'vue';
import FancyCheck from '@/components/common/FancyCheck.vue';
defineOptions({ name: 'GeneralTab' });
/**
* 共享上下文由 PersonalizationDrawer.vue 通过 provide 注入
* 解构出的名称与主文件 script 顶层绑定一致模板可直接引用
*/
const ctx = inject<Record<string, any>>('personalizationDrawer')!;
const {
personalization,
form,
startTutorial,
isAppShell,
appCurrentVersionText,
appUpdateCheckedText,
appHasUpdate,
appUpdateStateText,
appUpdateChecking,
checkAppUpdate,
downloadLatestApp
} = ctx;
</script>
<template>
<section class="settings-page">
<label class="settings-toggle-row">
<span class="settings-row-copy">
<span class="settings-row-title">{{ $t('personalization.autoTitleTitle') }}</span>
<span class="settings-row-desc">{{ $t('personalization.autoTitleDesc') }}</span>
</span>
<input
type="checkbox"
:checked="form.auto_generate_title"
@change="
personalization.updateField({
key: 'auto_generate_title',
value: ($event.target as HTMLInputElement).checked
})
"
/>
<FancyCheck :checked="form.auto_generate_title" />
</label>
<div class="settings-action-row">
<span class="settings-row-copy">
<span class="settings-row-title">{{ $t('personalization.tutorialTitle') }}</span>
<span class="settings-row-desc">{{ $t('personalization.tutorialDesc') }}</span>
</span>
<button type="button" class="settings-secondary-button" @click="startTutorial">
{{ $t('personalization.tutorialButton') }}
</button>
</div>
<div class="settings-action-row" v-if="isAppShell">
<span class="settings-row-copy">
<span class="settings-row-title">{{ $t('personalization.appUpdateTitle') }}</span>
<span class="settings-row-desc">{{
$t('personalization.appUpdateDesc', {
version: appCurrentVersionText,
checked: appUpdateCheckedText
})
}}</span>
</span>
<div class="settings-inline-actions">
<span class="settings-mini-status" :class="{ warning: appHasUpdate }">{{
appUpdateStateText
}}</span>
<button
type="button"
class="settings-secondary-button"
:disabled="appUpdateChecking"
@click="checkAppUpdate"
>
{{ appUpdateChecking ? $t('personalization.appChecking') : $t('personalization.appCheckUpdate') }}
</button>
<button
v-if="appHasUpdate"
type="button"
class="settings-primary-button"
@click="downloadLatestApp"
>
{{ $t('common.download') }}
</button>
</div>
</div>
<div class="settings-action-row danger-zone">
<span class="settings-row-copy">
<span class="settings-row-title">{{ $t('personalization.logoutTitle') }}</span>
<span class="settings-row-desc">{{ $t('personalization.logoutDesc') }}</span>
</span>
<button
type="button"
class="settings-secondary-button danger"
@click="personalization.logout()"
>
{{ $t('personalization.logoutTitle') }}
</button>
</div>
</section>
</template>

View File

@ -0,0 +1,159 @@
<script setup lang="ts">
import { inject } from 'vue';
defineOptions({ name: 'ModelTab' });
/**
* 共享上下文由 PersonalizationDrawer.vue 通过 provide 注入
* 解构出的名称与主文件 script 顶层绑定一致模板可直接引用
*/
const ctx = inject<Record<string, any>>('personalizationDrawer')!;
const {
personalization,
form,
activeDropdown,
defaultModelLabel,
filteredModelOptions,
floatingMenuStyle,
isEffortActive,
isRunModeActive,
reasoningEffortLabel,
runModeLabel,
selectDefaultModel,
selectDefaultReasoningEffort,
selectDefaultRunMode,
toggleDropdown,
runModeOptions,
reasoningEffortOptions,
activeTheme
} = ctx;
</script>
<template>
<section
class="settings-page"
data-tutorial="personal-page-model"
>
<div class="settings-select-row">
<span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.defaultModelTitle') }}</span
><span class="settings-row-desc"
>{{ $t('personalization.defaultModelDesc') }}</span
></span
>
<div
class="settings-select-wrap"
:class="{ open: activeDropdown === 'model' }"
@click.stop
>
<button
type="button"
class="settings-select-button"
@click="toggleDropdown('model')"
>
{{ defaultModelLabel }}
<span class="select-chevron" aria-hidden="true"></span>
</button>
<div
:class="['settings-floating-menu', { dark: activeTheme === 'dark' }]"
:style="activeDropdown ? floatingMenuStyle : undefined"
>
<button
v-for="option in filteredModelOptions"
:key="option.id"
type="button"
class="settings-menu-option"
:class="{
selected: form.default_model === option.value,
disabled: option.disabled
}"
:disabled="option.disabled"
@click="selectDefaultModel(option.value)"
>
<strong>{{ option.label }}</strong
><span>{{ option.disabled ? $t('personalization.modelDisabled') : option.desc }}</span
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
</div>
</div>
</div>
<div class="settings-select-row">
<span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.defaultRunModeTitle') }}</span
><span class="settings-row-desc"
>{{ $t('personalization.defaultRunModeDesc') }}</span
></span
>
<div
class="settings-select-wrap"
:class="{ open: activeDropdown === 'run-mode' }"
@click.stop
>
<button
type="button"
class="settings-select-button"
@click="toggleDropdown('run-mode')"
>
{{ runModeLabel }}
<span class="select-chevron" aria-hidden="true"></span>
</button>
<div
:class="['settings-floating-menu', { dark: activeTheme === 'dark' }]"
:style="activeDropdown ? floatingMenuStyle : undefined"
>
<button
v-for="option in runModeOptions"
:key="option.id"
type="button"
class="settings-menu-option"
:class="{ selected: isRunModeActive(option.value) }"
@click="selectDefaultRunMode(option.value)"
>
<strong>{{ $t(option.labelKey) }}</strong
><span>{{ $t(option.descKey) }}</span
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
</div>
</div>
</div>
<div class="settings-select-row">
<span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.reasoningEffortTitle') }}</span
><span class="settings-row-desc"
>{{ $t('personalization.reasoningEffortDesc') }}</span
></span
>
<div
class="settings-select-wrap"
:class="{ open: activeDropdown === 'reasoning-effort' }"
@click.stop
>
<button
type="button"
class="settings-select-button"
@click="toggleDropdown('reasoning-effort')"
>
{{ reasoningEffortLabel }}
<span class="select-chevron" aria-hidden="true"></span>
</button>
<div
:class="['settings-floating-menu', { dark: activeTheme === 'dark' }]"
:style="activeDropdown ? floatingMenuStyle : undefined"
>
<button
v-for="option in reasoningEffortOptions"
:key="option.id"
type="button"
class="settings-menu-option"
:class="{ selected: isEffortActive(option.value) }"
@click="selectDefaultReasoningEffort(option.value)"
>
<strong>{{ $t(option.labelKey) }}</strong
><span>{{ $t(option.descKey) }}</span
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
</div>
</div>
</div>
</section>
</template>

View File

@ -0,0 +1,276 @@
<script setup lang="ts">
import { inject } from 'vue';
import FancyCheck from '@/components/common/FancyCheck.vue';
defineOptions({ name: 'PreferencesTab' });
/**
* 共享上下文由 PersonalizationDrawer.vue 通过 provide 注入
* 解构出的名称与主文件 script 顶层绑定一致模板可直接引用
*/
const ctx = inject<Record<string, any>>('personalizationDrawer')!;
const {
personalization,
form,
tonePresets,
toggleUpdating,
activeDropdown,
toggleDropdown,
floatingMenuStyle,
activeTheme,
communicationStyleLabel,
conversationContinuityLabel,
currentBlockDisplayMode,
stackedHideBorders,
handleStackedHideBordersChange,
minimalExpandHeightLimited,
handleMinimalExpandHeightLimitedChange,
selectCommunicationStyle,
selectConversationContinuity
} = ctx;
</script>
<template>
<section
class="settings-page"
data-tutorial="personal-page-preferences"
>
<label class="settings-toggle-row">
<span class="settings-row-copy">
<span class="settings-row-title">{{ $t('personalization.preferencesTitle') }}</span>
<span class="settings-row-desc">{{ $t('personalization.preferencesDesc') }}</span>
</span>
<input
type="checkbox"
:checked="form.enabled"
:disabled="toggleUpdating"
@change="personalization.toggleEnabled()"
/>
<FancyCheck :checked="form.enabled" />
</label>
<label class="settings-input-row">
<span class="settings-row-title">{{ $t('personalization.selfIdentifyTitle') }}</span>
<input
type="text"
:value="form.self_identify"
maxlength="20"
:placeholder="$t('personalization.selfIdentifyPlaceholder')"
@input="
personalization.updateField({
key: 'self_identify',
value: $event.target.value
})
"
@focus="personalization.clearFeedback()"
/>
</label>
<label class="settings-input-row">
<span class="settings-row-title">{{ $t('personalization.userNameTitle') }}</span>
<input
type="text"
:value="form.user_name"
maxlength="20"
:placeholder="$t('personalization.userNamePlaceholder')"
@input="
personalization.updateField({
key: 'user_name',
value: $event.target.value
})
"
@focus="personalization.clearFeedback()"
/>
</label>
<label class="settings-input-row">
<span class="settings-row-title">{{ $t('personalization.professionTitle') }}</span>
<input
type="text"
:value="form.profession"
maxlength="20"
:placeholder="$t('personalization.professionPlaceholder')"
@input="
personalization.updateField({
key: 'profession',
value: $event.target.value
})
"
@focus="personalization.clearFeedback()"
/>
</label>
<div class="settings-input-row stackable">
<span class="settings-row-title">{{ $t('personalization.toneTitle') }}</span>
<div class="settings-input-stack">
<input
type="text"
:value="form.tone"
maxlength="20"
:placeholder="$t('personalization.tonePlaceholder')"
@input="
personalization.updateField({
key: 'tone',
value: $event.target.value
})
"
@focus="personalization.clearFeedback()"
/>
<div class="settings-chip-row">
<button
v-for="preset in tonePresets"
:key="preset"
type="button"
@click.prevent="personalization.applyTonePreset(preset)"
>
{{ preset }}
</button>
</div>
</div>
</div>
<div class="settings-textarea-row">
<div class="settings-row-copy">
<span class="settings-row-title">{{ $t('personalization.considerationsTitle') }}</span>
<span class="settings-row-desc">{{ $t('personalization.considerationsDesc') }}</span>
</div>
<textarea
:value="form.considerations"
rows="6"
maxlength="2000"
:placeholder="$t('personalization.considerationsPlaceholder')"
@input="personalization.updateConsiderations($event.target.value)"
@focus="personalization.clearFeedback()"
></textarea>
</div>
<label
v-if="currentBlockDisplayMode === 'stacked'"
class="settings-toggle-row"
>
<span class="settings-row-copy">
<span class="settings-row-title">{{ $t('personalization.hideBlockBordersTitle') }}</span>
<span class="settings-row-desc">{{ $t('personalization.hideBlockBordersDesc') }}</span>
</span>
<input
type="checkbox"
:checked="stackedHideBorders"
@change="handleStackedHideBordersChange($event)"
/>
<FancyCheck :checked="stackedHideBorders" />
</label>
<label
v-if="currentBlockDisplayMode === 'minimal'"
class="settings-toggle-row"
>
<span class="settings-row-copy">
<span class="settings-row-title">{{ $t('personalization.minimalExpandHeightTitle') }}</span>
<span class="settings-row-desc">{{ $t('personalization.minimalExpandHeightDesc') }}</span>
</span>
<input
type="checkbox"
:checked="minimalExpandHeightLimited"
@change="handleMinimalExpandHeightLimitedChange($event)"
/>
<FancyCheck :checked="minimalExpandHeightLimited" />
</label>
<div class="settings-select-row">
<span class="settings-row-copy">
<span class="settings-row-title">{{ $t('personalization.communicationStyleTitle') }}</span>
<span class="settings-row-desc">{{ $t('personalization.communicationStyleDesc') }}</span>
</span>
<div
class="settings-select-wrap"
:class="{ open: activeDropdown === 'communication' }"
@click.stop
>
<button
type="button"
class="settings-select-button"
@click="toggleDropdown('communication')"
>
{{ communicationStyleLabel }}
<span class="select-chevron" aria-hidden="true"></span>
</button>
<div
:class="['settings-floating-menu', { dark: activeTheme === 'dark' }]"
:style="activeDropdown ? floatingMenuStyle : undefined"
>
<button
type="button"
class="settings-menu-option"
:class="{ selected: form.communication_style === 'default' }"
@click="selectCommunicationStyle('default')"
>
<strong>{{ $t('personalization.communicationDefault') }}</strong><span>{{ $t('personalization.communicationDefaultDesc') }}</span
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
<button
type="button"
class="settings-menu-option"
:class="{ selected: form.communication_style === 'human_like' }"
@click="selectCommunicationStyle('human_like')"
>
<strong>{{ $t('personalization.communicationHumanLike') }}</strong><span>{{ $t('personalization.communicationHumanLikeDesc') }}</span
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
<button
type="button"
class="settings-menu-option"
:class="{ selected: form.communication_style === 'auto' }"
@click="selectCommunicationStyle('auto')"
>
<strong>{{ $t('personalization.communicationAuto') }}</strong><span>{{ $t('personalization.communicationAutoDesc') }}</span
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
</div>
</div>
</div>
<div class="settings-select-row">
<span class="settings-row-copy">
<span class="settings-row-title">{{ $t('personalization.conversationContinuityTitle') }}</span>
<span class="settings-row-desc">{{ $t('personalization.conversationContinuityDesc') }}</span>
</span>
<div
class="settings-select-wrap"
:class="{ open: activeDropdown === 'conversation-continuity' }"
@click.stop
>
<button
type="button"
class="settings-select-button"
@click="toggleDropdown('conversation-continuity')"
>
{{ conversationContinuityLabel }}
<span class="select-chevron" aria-hidden="true"></span>
</button>
<div
:class="['settings-floating-menu', { dark: activeTheme === 'dark' }]"
:style="activeDropdown ? floatingMenuStyle : undefined"
>
<button
type="button"
class="settings-menu-option"
:class="{ selected: form.conversation_continuity === 'high' }"
@click="selectConversationContinuity('high')"
>
<strong>{{ $t('personalization.continuityHigh') }}</strong><span>{{ $t('personalization.continuityHighDesc') }}</span
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
<button
type="button"
class="settings-menu-option"
:class="{ selected: form.conversation_continuity === 'medium' }"
@click="selectConversationContinuity('medium')"
>
<strong>{{ $t('personalization.continuityMedium') }}</strong><span>{{ $t('personalization.continuityMediumDesc') }}</span
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
<button
type="button"
class="settings-menu-option"
:class="{ selected: form.conversation_continuity === 'low' }"
@click="selectConversationContinuity('low')"
>
<strong>{{ $t('personalization.continuityLow') }}</strong><span>{{ $t('personalization.continuityLowDesc') }}</span
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
</div>
</div>
</div>
</section>
</template>

View File

@ -0,0 +1,170 @@
<script setup lang="ts">
import { inject } from 'vue';
defineOptions({ name: 'ReviewAgentsTab' });
/**
* 共享上下文由 PersonalizationDrawer.vue 通过 provide 注入
* 解构出的名称与主文件 script 顶层绑定一致模板可直接引用
*/
const ctx = inject<Record<string, any>>('personalizationDrawer')!;
const {
activeDropdown,
activeTheme,
closeDropdown,
floatingMenuStyle,
reviewAgentDefs,
reviewAgentOf,
subAgentModels,
toggleDropdown,
updateReviewAgent,
updateReviewAgentInt
} = ctx;
</script>
<template>
<section class="settings-page">
<div class="settings-section-desc" style="margin: 0 0 16px; color: var(--text-secondary); font-size: 13px; line-height: 1.6">{{ $t('personalization.reviewIntro') }}
</div>
<template v-for="agent in reviewAgentDefs" :key="agent.key">
<div class="settings-section-divider">
<span class="settings-section-divider__label">{{ $t(agent.nameKey) }}</span>
</div>
<div style="margin: -4px 0 12px; color: var(--text-secondary); font-size: 12px; line-height: 1.5">
{{ $t(agent.descKey) }}
</div>
<div class="settings-select-row">
<span class="settings-row-copy">
<span class="settings-row-title">{{ $t('personalization.modelTitle') }}</span>
<span class="settings-row-desc">{{ $t('personalization.reviewModelEmptyDesc') }}</span>
</span>
<div
class="settings-select-wrap"
:class="{ open: activeDropdown === `review-model-${agent.key}` }"
@click.stop
>
<button
type="button"
class="settings-select-button"
@click="toggleDropdown(`review-model-${agent.key}`)"
>
{{ reviewAgentOf(agent.key).model || $t('personalization.defaultModelOption') }}
<span class="select-chevron" aria-hidden="true"></span>
</button>
<div
:class="['settings-floating-menu', { dark: activeTheme === 'dark' }]"
:style="activeDropdown === `review-model-${agent.key}` ? floatingMenuStyle : undefined"
>
<button
type="button"
class="settings-menu-option"
:class="{ selected: !reviewAgentOf(agent.key).model }"
@click="updateReviewAgent(agent.key, { model: '' }); closeDropdown()"
>
<strong>{{ $t('personalization.defaultModelOption') }}</strong><span>{{ $t('personalization.reviewDefaultModelDesc') }}</span><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
<button
v-for="m in subAgentModels"
:key="m.name"
type="button"
class="settings-menu-option"
:class="{ selected: reviewAgentOf(agent.key).model === m.name }"
@click="updateReviewAgent(agent.key, { model: m.name }); closeDropdown()"
>
<strong>{{ m.name }}</strong><span>{{ m.modes }} · {{ m.multimodal || $t('personalization.textOnly') }}</span><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
</div>
</div>
</div>
<div class="settings-select-row">
<span class="settings-row-copy">
<span class="settings-row-title">{{ $t('personalization.thinkingModeTitle') }}</span>
<span class="settings-row-desc">{{ $t('personalization.reviewThinkingDesc') }}</span>
</span>
<div
class="settings-select-wrap"
:class="{ open: activeDropdown === `review-thinking-${agent.key}` }"
@click.stop
>
<button
type="button"
class="settings-select-button"
@click="toggleDropdown(`review-thinking-${agent.key}`)"
>
{{ reviewAgentOf(agent.key).thinking ? 'thinking' : 'fast' }}
<span class="select-chevron" aria-hidden="true"></span>
</button>
<div
:class="['settings-floating-menu', { dark: activeTheme === 'dark' }]"
:style="activeDropdown === `review-thinking-${agent.key}` ? floatingMenuStyle : undefined"
>
<button
type="button"
class="settings-menu-option"
:class="{ selected: !reviewAgentOf(agent.key).thinking }"
@click="updateReviewAgent(agent.key, { thinking: false }); closeDropdown()"
>
<strong>fast</strong><span>{{ $t('personalization.fastResponseMode') }}</span><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
<button
type="button"
class="settings-menu-option"
:class="{ selected: reviewAgentOf(agent.key).thinking }"
@click="updateReviewAgent(agent.key, { thinking: true }); closeDropdown()"
>
<strong>thinking</strong><span>{{ $t('personalization.thinkingReasoningMode') }}</span><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
</div>
</div>
</div>
<div class="settings-select-row">
<span class="settings-row-copy">
<span class="settings-row-title">{{ $t('personalization.timeoutTitle') }}</span>
<span class="settings-row-desc">{{ $t('personalization.timeoutDesc') }}</span>
</span>
<input
type="number"
class="settings-number-input"
min="5"
max="3600"
:value="reviewAgentOf(agent.key).timeout_seconds"
@change="updateReviewAgentInt(agent.key, 'timeout_seconds', ($event.target as HTMLInputElement).value, 5, 3600)"
/>
</div>
<div class="settings-select-row">
<span class="settings-row-copy">
<span class="settings-row-title">{{ $t('personalization.maxRoundsTitle') }}</span>
<span class="settings-row-desc">{{ $t('personalization.maxRoundsDesc') }}</span>
</span>
<input
type="number"
class="settings-number-input"
min="1"
max="50"
:value="reviewAgentOf(agent.key).max_rounds"
@change="updateReviewAgentInt(agent.key, 'max_rounds', ($event.target as HTMLInputElement).value, 1, 50)"
/>
</div>
<div class="settings-select-row" style="margin-bottom: 8px">
<span class="settings-row-copy">
<span class="settings-row-title">{{ $t('personalization.commandTimeoutTitle') }}</span>
<span class="settings-row-desc">{{ $t('personalization.commandTimeoutDesc') }}</span>
</span>
<input
type="number"
class="settings-number-input"
min="1"
max="600"
:value="reviewAgentOf(agent.key).max_command_timeout"
@change="updateReviewAgentInt(agent.key, 'max_command_timeout', ($event.target as HTMLInputElement).value, 1, 600)"
/>
</div>
</template>
</section>
</template>

View File

@ -0,0 +1,442 @@
<script setup lang="ts">
import { inject } from 'vue';
defineOptions({ name: 'SubAgentsTab' });
/**
* 共享上下文由 PersonalizationDrawer.vue 通过 provide 注入
* 解构出的名称与主文件 script 顶层绑定一致模板可直接引用
*/
const ctx = inject<Record<string, any>>('personalizationDrawer')!;
const {
activeDropdown,
activeTheme,
closeDropdown,
closeRoleEditor,
deleteRole,
editingRole,
handleRoleEditorOverlayPressEnd,
handleRoleEditorOverlayPressStart,
openRoleEditor,
personalization,
roleEditorOpen,
roleForm,
roleSaving,
saveRole,
saveSubAgentSettings,
subAgentCompressThreshold,
subAgentMaxTurns,
subAgentModels,
subAgentRoles,
subAgentRolesLoading,
subAgentSettingsSaving,
toggleDropdown
} = ctx;
</script>
<template>
<section class="settings-page">
<div class="settings-section-desc" style="margin: 0 0 16px; color: var(--text-secondary); font-size: 13px; line-height: 1.6">{{ $t('personalization.subAgentsIntro') }}
</div>
<!-- 压缩阈值配置 -->
<div class="settings-action-row" style="margin-bottom: 16px">
<span class="settings-row-copy">
<span class="settings-row-title">{{ $t('personalization.compressThresholdTitle') }}</span>
<span class="settings-row-desc">{{ $t('personalization.compressThresholdDesc') }}
</span>
</span>
<div style="display: flex; gap: 6px; align-items: center">
<input
type="number"
class="settings-number-input"
v-model.number="subAgentCompressThreshold"
:min="10000"
:step="10000"
style="width: 120px"
/>
<button
type="button"
class="settings-secondary-button"
:disabled="subAgentSettingsSaving"
@click="saveSubAgentSettings"
>
{{ subAgentSettingsSaving ? $t('personalization.saving') : $t('common.save') }}
</button>
</div>
</div>
<!-- 最大执行轮次配置 -->
<div class="settings-action-row" style="margin-bottom: 16px">
<span class="settings-row-copy">
<span class="settings-row-title">{{ $t('personalization.maxTurnsTitle') }}</span>
<span class="settings-row-desc">{{ $t('personalization.maxTurnsDesc') }}
</span>
</span>
<div style="display: flex; gap: 6px; align-items: center">
<input
type="number"
class="settings-number-input"
v-model.number="subAgentMaxTurns"
:min="0"
:step="10"
placeholder="50"
style="width: 120px"
/>
<button
type="button"
class="settings-secondary-button"
:disabled="subAgentSettingsSaving"
@click="saveSubAgentSettings"
>
{{ subAgentSettingsSaving ? $t('personalization.saving') : $t('common.save') }}
</button>
</div>
</div>
<!-- 角色列表 -->
<div class="settings-section-header" style="margin-bottom: 8px">
<span class="settings-section-title">{{ $t('personalization.roleListTitle') }}</span>
<button
type="button"
class="settings-secondary-button"
@click="openRoleEditor()"
>
{{ $t('personalization.newRole') }}
</button>
</div>
<div v-if="subAgentRolesLoading" class="settings-empty-hint">{{ $t('common.loading') }}</div>
<div v-else-if="subAgentRoles.length === 0" class="settings-empty-hint">{{ $t('personalization.noRoles') }}
</div>
<div v-else class="sub-agent-role-list">
<div
v-for="role in subAgentRoles"
:key="role.role_id"
class="sub-agent-role-card"
>
<div class="sub-agent-role-info">
<div class="sub-agent-role-name">
{{ role.name }}
<span
class="sub-agent-role-tag"
:class="role.is_custom ? 'tag-custom' : 'tag-preset'"
>
{{ role.is_custom ? $t('personalization.customTag') : $t('personalization.presetTag') }}
</span>
</div>
<div class="sub-agent-role-desc">{{ role.description || $t('personalization.noDescription') }}</div>
<div class="sub-agent-role-meta">
{{ $t('personalization.roleMetaId', { id: role.role_id, mode: role.thinking_mode }) }}<template v-if="role.model_key">{{ $t('personalization.roleMetaModel', { model: role.model_key }) }}</template>
</div>
</div>
<div class="sub-agent-role-actions">
<button
type="button"
class="settings-secondary-button"
@click="openRoleEditor(role)"
>{{ $t('personalization.edit') }}</button>
<button
v-if="role.is_custom"
type="button"
class="settings-secondary-button danger"
@click="deleteRole(role)"
>{{ $t('common.delete') }}</button>
</div>
</div>
</div>
<!-- 角色编辑弹窗与个人空间同尺寸 -->
<transition name="personal-page-fade" appear>
<div
v-if="roleEditorOpen"
class="role-editor-drawer-overlay"
@click="closeDropdown"
@mousedown="handleRoleEditorOverlayPressStart"
@mouseup="handleRoleEditorOverlayPressEnd"
>
<div class="role-editor-drawer-card settings-redesign-card" @click.stop>
<div class="role-editor-header">
<h2>{{ editingRole ? $t('personalization.roleEditorEditTitle') : $t('personalization.roleEditorCreateTitle') }}</h2>
<button type="button" class="settings-secondary-button" @click="closeRoleEditor">{{ $t('common.close') }}</button>
</div>
<div class="role-editor-body" @click="closeDropdown">
<label class="settings-input-row">
<span class="settings-row-title">{{ $t('personalization.roleIdTitle') }}</span>
<input
v-if="!editingRole"
type="text"
:value="roleForm.role_id"
maxlength="40"
::placeholder="$t('personalization.roleIdPlaceholder')"
@input="roleForm.role_id = ($event.target as HTMLInputElement).value"
/>
<span v-else class="settings-row-desc" style="text-align: right; font-size: 14px;">{{ roleForm.role_id }}</span>
</label>
<label class="settings-input-row">
<span class="settings-row-title">{{ $t('personalization.roleNameTitle') }}</span>
<input
type="text"
:value="roleForm.name"
maxlength="40"
::placeholder="$t('personalization.roleNamePlaceholder')"
@input="roleForm.name = ($event.target as HTMLInputElement).value"
/>
</label>
<label class="settings-input-row">
<span class="settings-row-title">{{ $t('personalization.roleDescTitle') }}</span>
<input
type="text"
:value="roleForm.description"
maxlength="100"
::placeholder="$t('personalization.roleDescPlaceholder')"
@input="roleForm.description = ($event.target as HTMLInputElement).value"
/>
</label>
<div class="settings-select-row">
<span class="settings-row-copy">
<span class="settings-row-title">{{ $t('personalization.thinkingModeTitle') }}</span>
<span class="settings-row-desc">{{ $t('personalization.roleThinkingDesc') }}</span>
</span>
<div
class="settings-select-wrap"
:class="{ open: activeDropdown === 'role-thinking' }"
@click.stop
>
<button
type="button"
class="settings-select-button"
@click="toggleDropdown('role-thinking')"
>
{{ roleForm.thinking_mode === 'thinking' ? 'thinking' : 'fast' }}
<span class="select-chevron" aria-hidden="true"></span>
</button>
<div
:class="['settings-floating-menu', { dark: activeTheme === 'dark' }]"
:style="activeDropdown === 'role-thinking' ? floatingMenuStyle : undefined"
>
<button
type="button"
class="settings-menu-option"
:class="{ selected: roleForm.thinking_mode === 'fast' }"
@click="roleForm.thinking_mode = 'fast'; closeDropdown()"
>
<strong>fast</strong><span>{{ $t('personalization.fastResponseMode') }}</span><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
<button
type="button"
class="settings-menu-option"
:class="{ selected: roleForm.thinking_mode === 'thinking' }"
@click="roleForm.thinking_mode = 'thinking'; closeDropdown()"
>
<strong>thinking</strong><span>{{ $t('personalization.thinkingReasoningMode') }}</span><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
</div>
</div>
</div>
<div class="settings-select-row">
<span class="settings-row-copy">
<span class="settings-row-title">{{ $t('personalization.modelTitle') }}</span>>
<span class="settings-row-desc">{{ $t('personalization.roleModelEmptyDesc') }}</span>
</span>
<div
class="settings-select-wrap"
:class="{ open: activeDropdown === 'role-model' }"
@click.stop
>
<button
type="button"
class="settings-select-button"
@click="toggleDropdown('role-model')"
>
{{ roleForm.model_key || $t('personalization.defaultModelOption') }}
<span class="select-chevron" aria-hidden="true"></span>
</button>
<div
:class="['settings-floating-menu', { dark: activeTheme === 'dark' }]"
:style="activeDropdown === 'role-model' ? floatingMenuStyle : undefined"
>
<button
type="button"
class="settings-menu-option"
:class="{ selected: !roleForm.model_key }"
@click="roleForm.model_key = ''; closeDropdown()"
>
<strong>{{ $t('personalization.defaultModelOption') }}</strong><span>{{ $t('personalization.roleDefaultModelDesc') }}</span><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
<button
v-for="m in subAgentModels"
:key="m.name"
type="button"
class="settings-menu-option"
:class="{ selected: roleForm.model_key === m.name }"
@click="roleForm.model_key = m.name; closeDropdown()"
>
<strong>{{ m.name }}</strong><span>{{ m.modes }} · {{ m.multimodal || $t('personalization.textOnly') }}</span><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
</div>
</div>
</div>
<div class="settings-textarea-row">
<span class="settings-row-title">{{ $t('personalization.promptBodyTitle') }}</span>
<textarea
:value="roleForm.body_prompt"
::placeholder="$t('personalization.promptBodyPlaceholder')"
@input="roleForm.body_prompt = ($event.target as HTMLTextAreaElement).value"
></textarea>
</div>
</div>
<div class="role-editor-footer">
<button type="button" class="settings-secondary-button" @click="closeRoleEditor">{{ $t('common.cancel') }}</button>
<button
type="button"
class="settings-primary-button"
:disabled="roleSaving || !roleForm.role_id || !roleForm.name || !roleForm.body_prompt"
@click="saveRole"
>
{{ roleSaving ? $t('personalization.saving') : $t('common.save') }}
</button>
</div>
</div>
</div>
</transition>
</section>
</template>
<style scoped>
.sub-agent-role-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.sub-agent-role-card {
display: flex;
align-items: flex-start;
justify-content: space-between;
padding: 11px 0;
border-bottom: 1px solid var(--theme-control-border);
}
.sub-agent-role-info {
flex: 1;
min-width: 0;
}
.sub-agent-role-name {
font-size: 14px;
font-weight: 550;
color: var(--text-primary);
display: flex;
align-items: center;
gap: 6px;
}
.sub-agent-role-tag {
font-size: 10px;
padding: 1px 8px;
border-radius: 999px;
font-weight: 400;
}
.sub-agent-role-tag.tag-preset {
background: color-mix(in srgb, var(--text-secondary) 15%, transparent);
color: var(--text-secondary);
}
.sub-agent-role-tag.tag-custom {
background: color-mix(in srgb, var(--accent) 15%, transparent);
color: var(--accent);
}
.sub-agent-role-desc {
font-size: 12.5px;
color: var(--text-secondary);
margin-top: 3px;
line-height: 1.38;
}
.sub-agent-role-meta {
font-size: 11px;
color: var(--text-secondary);
opacity: 0.7;
margin-top: 2px;
}
.sub-agent-role-actions {
display: flex;
gap: 6px;
flex-shrink: 0;
}
/* 角色编辑弹窗 — 复用 settings-redesign-card */
.role-editor-drawer-overlay {
position: fixed;
inset: 0;
background: var(--overlay-scrim);
display: flex;
align-items: center;
justify-content: center;
z-index: 2147483647;
}
.role-editor-drawer-card {
display: flex;
flex-direction: column;
overflow: hidden;
width: min(648px, calc(100vw - 40px));
height: min(646px, calc(100vh - 40px));
height: min(646px, calc(100dvh - 40px));
padding: 22px 24px 18px;
border-radius: 14px;
background: var(--theme-surface-soft);
border: 1px solid var(--theme-control-border);
}
@media (max-width: 760px) {
.role-editor-drawer-card {
width: 100%;
height: 100vh;
height: 100dvh;
border-radius: 0;
border: 0;
padding: calc(14px + env(safe-area-inset-top, 0px)) 16px calc(18px + env(safe-area-inset-bottom, 0px));
}
}
.role-editor-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
flex-shrink: 0;
}
.role-editor-header h2 {
font-size: 20px;
font-weight: 600;
color: var(--text-primary);
margin: 0;
}
.role-editor-body {
flex: 1;
overflow-y: auto;
overflow-x: visible;
min-height: 0;
scrollbar-width: none;
-ms-overflow-style: none;
}
.role-editor-footer {
display: flex;
justify-content: flex-end;
gap: 8px;
padding-top: 12px;
flex-shrink: 0;
}
.settings-section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.settings-section-title {
font-size: 14px;
font-weight: 550;
color: var(--text-primary);
}
.settings-empty-hint {
color: var(--text-secondary);
font-size: 13px;
padding: 24px 0;
text-align: center;
}
</style>

View File

@ -0,0 +1,170 @@
<script setup lang="ts">
import { inject } from 'vue';
import FancyCheck from '@/components/common/FancyCheck.vue';
defineOptions({ name: 'ToolsTab' });
/**
* 共享上下文由 PersonalizationDrawer.vue 通过 provide 注入
* 解构出的名称与主文件 script 顶层绑定一致模板可直接引用
*/
const ctx = inject<Record<string, any>>('personalizationDrawer')!;
const {
form,
personalization,
skillsCatalog,
toggleCategory,
toolCategories
} = ctx;
</script>
<template>
<section class="settings-page">
<label class="settings-toggle-row"
><span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.silentToolDisableTitle') }}</span
><span class="settings-row-desc"
>{{ $t('personalization.silentToolDisableDesc') }}</span
></span
><input
type="checkbox"
:checked="form.silent_tool_disable"
@change="
personalization.updateField({
key: 'silent_tool_disable',
value: $event.target.checked
})
" /><FancyCheck :checked="form.silent_tool_disable" /></label>
<label class="settings-toggle-row"
><span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.hideToolApprovalTitle') }}</span
><span class="settings-row-desc"
>{{ $t('personalization.hideToolApprovalDesc') }}</span
></span
><input
type="checkbox"
:checked="form.hide_tool_approval_panel"
@change="
personalization.updateField({
key: 'hide_tool_approval_panel',
value: $event.target.checked
})
" /><FancyCheck :checked="form.hide_tool_approval_panel" /></label>
<label class="settings-toggle-row"
><span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.toolIntentTitle') }}</span
><span class="settings-row-desc"
>{{ $t('personalization.toolIntentDesc') }}</span
></span
><input
type="checkbox"
:checked="form.tool_intent_enabled"
@change="
personalization.updateField({
key: 'tool_intent_enabled',
value: $event.target.checked
})
" /><FancyCheck :checked="form.tool_intent_enabled" /></label>
<label class="settings-toggle-row"
><span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.skillHintsTitle') }}</span
><span class="settings-row-desc"
>{{ $t('personalization.skillHintsDesc') }}</span
></span
><input
type="checkbox"
:checked="form.skill_hints_enabled"
@change="
personalization.updateField({
key: 'skill_hints_enabled',
value: $event.target.checked
})
" /><FancyCheck :checked="form.skill_hints_enabled" /></label>
<div class="settings-group-block">
<div class="settings-group-title">
<span class="settings-row-title">{{ $t('personalization.strictSkillTitle') }}</span
><span class="settings-row-desc">{{ $t('personalization.strictSkillDesc') }}</span>
</div>
<label class="settings-toggle-row inner"
><span class="settings-row-title">{{ $t('personalization.terminalToolsTitle') }}</span
><input
type="checkbox"
:checked="form.skill_strict_terminal_enabled"
@change="
personalization.updateField({
key: 'skill_strict_terminal_enabled',
value: $event.target.checked
})
" /><FancyCheck :checked="form.skill_strict_terminal_enabled" /></label>
<label class="settings-toggle-row inner"
><span class="settings-row-title">{{ $t('personalization.subAgentToolsTitle') }}</span
><input
type="checkbox"
:checked="form.skill_strict_sub_agent_enabled"
@change="
personalization.updateField({
key: 'skill_strict_sub_agent_enabled',
value: $event.target.checked
})
" /><FancyCheck :checked="form.skill_strict_sub_agent_enabled" /></label>
<label class="settings-toggle-row inner"
><span class="settings-row-title">{{ $t('personalization.runCommandFgTitle') }}</span
><input
type="checkbox"
:checked="form.skill_strict_run_command_foreground_enabled"
@change="
personalization.updateField({
key: 'skill_strict_run_command_foreground_enabled',
value: $event.target.checked
})
" /><FancyCheck :checked="form.skill_strict_run_command_foreground_enabled" /></label>
<label class="settings-toggle-row inner"
><span class="settings-row-title">{{ $t('personalization.runCommandBgTitle') }}</span
><input
type="checkbox"
:checked="form.skill_strict_run_command_background_enabled"
@change="
personalization.updateField({
key: 'skill_strict_run_command_background_enabled',
value: $event.target.checked
})
" /><FancyCheck :checked="form.skill_strict_run_command_background_enabled" /></label>
</div>
<div class="settings-group-block" v-if="skillsCatalog.length">
<div class="settings-group-title">
<span class="settings-row-title">{{ $t('personalization.availableSkillsTitle') }}</span
><span class="settings-row-desc"
>{{ $t('personalization.availableSkillsDesc') }}</span
>
</div>
<div class="settings-check-grid">
<label
v-for="skill in skillsCatalog"
:key="skill.id"
class="settings-toggle-row inner"
><span class="settings-row-title">{{ skill.label }}</span
><input
type="checkbox"
:checked="form.enabled_skills.includes(skill.id)"
@change="personalization.toggleSkill(skill.id)" /><FancyCheck :checked="form.enabled_skills.includes(skill.id)" /></label>
</div>
</div>
<div class="settings-group-block" v-if="toolCategories.length">
<div class="settings-group-title">
<span class="settings-row-title">{{ $t('personalization.disabledToolCategoriesTitle') }}</span
><span class="settings-row-desc">{{ $t('personalization.disabledToolCategoriesDesc') }}</span>
</div>
<div class="settings-check-grid">
<label
v-for="category in toolCategories"
:key="category.id"
class="settings-toggle-row inner"
><span class="settings-row-title">{{ category.label }}</span
><input
type="checkbox"
:checked="form.disabled_tool_categories.includes(category.id)"
@change="toggleCategory(category.id)" /><FancyCheck :checked="form.disabled_tool_categories.includes(category.id)" /></label>
</div>
</div>
</section>
</template>

View File

@ -0,0 +1,114 @@
<script setup lang="ts">
import { inject } from 'vue';
defineOptions({ name: 'VoiceTab' });
/**
* 共享上下文由 PersonalizationDrawer.vue 通过 provide 注入
* 解构出的名称与主文件 script 顶层绑定一致模板可直接引用
*/
const ctx = inject<Record<string, any>>('personalizationDrawer')!;
const {
deleteVoiceModel,
downloadVoiceModel,
voiceDownloadMsg,
voiceDownloadPercent,
voiceDownloading,
voiceModelPartial,
voiceModelReady
} = ctx;
</script>
<template>
<section class="settings-page">
<div class="settings-section" style="margin-bottom: 16px">
<p
class="settings-section-desc"
style="
margin: 0;
color: var(--text-secondary);
font-size: 13px;
line-height: 1.6;
"
>{{ $t('personalization.voiceModelIntro') }}
</p>
</div>
<div class="settings-action-row">
<span class="settings-row-copy">
<span class="settings-row-title">{{ $t('personalization.voiceModelTitle') }}</span>
<span class="settings-row-desc">
<template v-if="voiceModelReady">{{ $t('personalization.voiceModelReady') }}</template>
<template v-else-if="voiceDownloading"
>{{ $t('personalization.voiceModelDownloading', { percent: voiceDownloadPercent, msg: voiceDownloadMsg }) }}</template
>
<template v-else-if="voiceModelPartial"
>{{ $t('personalization.voiceModelPartial') }}</template
>
<template v-else>{{ $t('personalization.voiceModelNotDownloaded') }}</template>
</span>
</span>
<div style="display: flex; gap: 6px">
<button
type="button"
class="settings-secondary-button"
:disabled="voiceDownloading"
@click="downloadVoiceModel"
>
{{
voiceDownloading
? $t('personalization.voiceDownloadingBtn')
: voiceModelReady
? $t('personalization.voiceRedownload')
: $t('personalization.voiceDownloadModel')
}}
</button>
<button
v-if="voiceModelReady || voiceModelPartial"
type="button"
class="settings-secondary-button"
style="color: var(--state-error)"
:disabled="voiceDownloading"
@click="deleteVoiceModel"
>
{{ $t('common.delete') }}
</button>
</div>
</div>
<div
v-if="voiceDownloading"
class="voice-download-bar"
style="margin: 8px 0 0"
>
<div class="voice-download-track">
<div
class="voice-download-fill"
:style="{ width: voiceDownloadPercent + '%' }"
></div>
</div>
<span style="font-size: 11px; color: var(--text-secondary); margin-top: 4px"
>{{ voiceDownloadPercent }}%</span
>
</div>
</section>
</template>
<style scoped>
.voice-download-bar {
display: flex;
flex-direction: column;
gap: 4px;
}
.voice-download-track {
width: 100%;
height: 6px;
background: var(--surface-muted);
border-radius: 3px;
overflow: hidden;
}
.voice-download-fill {
height: 100%;
background: var(--accent-primary);
border-radius: 3px;
transition: width 0.3s ease;
}
</style>

View File

@ -0,0 +1,317 @@
<script setup lang="ts">
import { inject } from 'vue';
import FancyCheck from '@/components/common/FancyCheck.vue';
defineOptions({ name: 'WorkspaceTab' });
/**
* 共享上下文由 PersonalizationDrawer.vue 通过 provide 注入
* 解构出的名称与主文件 script 顶层绑定一致模板可直接引用
*/
const ctx = inject<Record<string, any>>('personalizationDrawer')!;
const {
activeDropdown,
floatingMenuStyle,
form,
goalTokenLimitEnabled,
permissionModeLabel,
personalization,
selectDefaultPermissionMode,
selectDefaultWorkMode,
selectVersioningBackupMode,
toggleDropdown,
toggleGoalTokenLimit,
versioningBackupModeLabel,
workModeLabel,
permissionModeOptions,
workModeOptions,
// activeTheme drawerContext
// clampGoalMaxTurns / clampGoalMaxTokens const drawerContext report.md
activeTheme,
clampGoalMaxTurns,
clampGoalMaxTokens
} = ctx;
</script>
<template>
<section class="settings-page" data-tutorial="personal-page-workspace">
<div class="settings-select-row">
<span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.defaultPermissionTitle') }}</span
><span class="settings-row-desc">{{ $t('personalization.defaultPermissionDesc') }}</span></span
>
<div
class="settings-select-wrap"
:class="{ open: activeDropdown === 'permission' }"
@click.stop
>
<button
type="button"
class="settings-select-button"
@click="toggleDropdown('permission')"
>
{{ permissionModeLabel }}
<span class="select-chevron" aria-hidden="true"></span>
</button>
<div
:class="['settings-floating-menu', { dark: activeTheme === 'dark' }]"
:style="activeDropdown ? floatingMenuStyle : undefined"
>
<button
v-for="option in permissionModeOptions"
:key="option.id"
type="button"
class="settings-menu-option"
:class="{ selected: form.default_permission_mode === option.id }"
@click="selectDefaultPermissionMode(option.id)"
>
<strong>{{ $t(option.labelKey) }}</strong
><span>{{ $t(option.descKey) }}</span
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
</div>
</div>
</div>
<div class="settings-select-row">
<span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.defaultRunModeTitle') }}</span
><span class="settings-row-desc"
>{{ $t('personalization.workRunModeDesc') }}</span
></span
>
<div
class="settings-select-wrap"
:class="{ open: activeDropdown === 'work-mode' }"
@click.stop
>
<button
type="button"
class="settings-select-button"
@click="toggleDropdown('work-mode')"
>
{{ workModeLabel }}
<span class="select-chevron" aria-hidden="true"></span>
</button>
<div
:class="['settings-floating-menu', { dark: activeTheme === 'dark' }]"
:style="activeDropdown ? floatingMenuStyle : undefined"
>
<button
v-for="option in workModeOptions"
:key="option.id"
type="button"
class="settings-menu-option"
:class="{ selected: form.default_work_mode === option.id }"
@click="selectDefaultWorkMode(option.id)"
>
<strong>{{ $t(option.labelKey) }}</strong
><span>{{ $t(option.descKey) }}</span
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
</div>
</div>
</div>
<label class="settings-toggle-row"
><span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.agentsMdInjectTitle') }}</span
><span class="settings-row-desc"
>{{ $t('personalization.agentsMdInjectDesc') }}</span
></span
><input
type="checkbox"
:checked="form.agents_md_auto_inject"
@change="
personalization.updateField({
key: 'agents_md_auto_inject',
value: $event.target.checked
})
" /><FancyCheck :checked="form.agents_md_auto_inject" /></label>
<label class="settings-toggle-row"
><span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.claudeMdInjectTitle') }}</span
><span class="settings-row-desc"
>{{ $t('personalization.claudeMdInjectDesc') }}</span
></span
><input
type="checkbox"
:checked="form.claude_md_auto_inject"
@change="
personalization.updateField({
key: 'claude_md_auto_inject',
value: $event.target.checked
})
" /><FancyCheck :checked="form.claude_md_auto_inject" /></label>
<label class="settings-toggle-row"
><span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.agentsSkillsScanTitle') }}</span
><span class="settings-row-desc"
>{{ $t('personalization.agentsSkillsScanDesc') }}</span
></span
><input
type="checkbox"
:checked="form.agents_skills_scan_enabled"
@change="
personalization.updateField({
key: 'agents_skills_scan_enabled',
value: $event.target.checked
})
" /><FancyCheck :checked="form.agents_skills_scan_enabled" /></label>
<label class="settings-toggle-row"
><span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.modifyHistoryTitle') }}</span
><span class="settings-row-desc"
>{{ $t('personalization.modifyHistoryDesc') }}</span
></span
><input
type="checkbox"
:checked="form.modify_history_enabled"
@change="
personalization.updateField({
key: 'modify_history_enabled',
value: $event.target.checked
})
" /><FancyCheck :checked="form.modify_history_enabled" /></label>
<div class="settings-section-divider">
<span class="settings-section-divider__label">{{ $t('personalization.versionControlDivider') }}</span>
</div>
<label class="settings-toggle-row"
><span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.versioningByDefaultTitle') }}</span
><span class="settings-row-desc"
>{{ $t('personalization.versioningByDefaultDesc') }}</span
></span
><input
type="checkbox"
:checked="form.versioning_enabled_by_default"
@change="
personalization.updateField({
key: 'versioning_enabled_by_default',
value: $event.target.checked
})
" /><FancyCheck :checked="form.versioning_enabled_by_default" /></label>
<div class="settings-select-row">
<span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.backupModeTitle') }}</span
><span class="settings-row-desc"
>{{ $t('personalization.backupModeDesc') }}</span
></span
>
<div
class="settings-select-wrap"
:class="{ open: activeDropdown === 'versioning-backup-mode' }"
@click.stop
>
<button
type="button"
class="settings-select-button"
@click="toggleDropdown('versioning-backup-mode')"
>
{{ versioningBackupModeLabel }}
<span class="select-chevron" aria-hidden="true"></span>
</button>
<div
:class="['settings-floating-menu', { dark: activeTheme === 'dark' }]"
:style="activeDropdown ? floatingMenuStyle : undefined"
>
<button
type="button"
class="settings-menu-option"
:class="{ selected: form.versioning_backup_mode === 'shallow' }"
@click="selectVersioningBackupMode('shallow')"
>
<strong>{{ $t('personalization.shallowBackup') }}</strong><span>{{ $t('personalization.shallowBackupDesc') }}</span
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
<button
type="button"
class="settings-menu-option"
:class="{ selected: form.versioning_backup_mode === 'full' }"
@click="selectVersioningBackupMode('full')"
>
<strong>{{ $t('personalization.fullBackup') }}</strong><span>{{ $t('personalization.fullBackupDesc') }}</span
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
</div>
</div>
</div>
<div class="settings-section-divider">
<span class="settings-section-divider__label">{{ $t('personalization.goalModeDivider') }}</span>
</div>
<label class="settings-toggle-row"
><span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.goalReviewActiveTitle') }}</span
><span class="settings-row-desc"
>{{ $t('personalization.goalReviewActiveDesc') }}</span
></span
><input
type="checkbox"
:checked="form.goal_review_mode === 'active'"
@change="
personalization.updateField({
key: 'goal_review_mode',
value: $event.target.checked ? 'active' : 'readonly'
})
" /><FancyCheck :checked="form.goal_review_mode === 'active'" /></label>
<div class="settings-select-row">
<span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.goalMaxTurnsTitle') }}</span
><span class="settings-row-desc"
>{{ $t('personalization.goalMaxTurnsDesc') }}</span
></span
>
<input
type="number"
class="settings-number-input"
min="1"
max="100"
:value="form.goal_max_turns"
@change="
personalization.updateField({
key: 'goal_max_turns',
value: clampGoalMaxTurns($event.target.value)
})
"
/>
</div>
<label class="settings-toggle-row"
><span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.goalTokenLimitTitle') }}</span
><span class="settings-row-desc"
>{{ $t('personalization.goalTokenLimitDesc') }}</span
></span
><input
type="checkbox"
:checked="goalTokenLimitEnabled"
@change="toggleGoalTokenLimit($event.target.checked)" /><FancyCheck :checked="goalTokenLimitEnabled" /></label>
<div v-if="goalTokenLimitEnabled" class="settings-select-row">
<span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.goalTokenLimitValueTitle') }}</span
><span class="settings-row-desc">{{ $t('personalization.goalTokenLimitValueDesc') }}</span></span
>
<input
type="number"
class="settings-number-input"
min="1000"
step="1000"
:value="form.goal_max_tokens || 100000"
@change="
personalization.updateField({
key: 'goal_max_tokens',
value: clampGoalMaxTokens($event.target.value)
})
"
/>
</div>
</section>
</template>

View File

@ -28,6 +28,8 @@ export default {
tool: 'Tool', tool: 'Tool',
versioning: 'Versioning', versioning: 'Versioning',
unset: 'Not set', unset: 'Not set',
settings: 'Settings',
back: 'Back',
settingsLanguage: 'Language', settingsLanguage: 'Language',
settingsLanguageDesc: 'Choose the display language', settingsLanguageDesc: 'Choose the display language',

View File

@ -39,10 +39,9 @@ export default {
toolRunCommand: 'Run command {command}', toolRunCommand: 'Run command {command}',
toolEditFile: 'Edit {path}', toolEditFile: 'Edit {path}',
toolReadMedia: 'Read media file {path}', toolReadMedia: 'Read media file {path}',
toolStateDone: 'Done',
toolStateFailed: 'Failed', // —— Sub-agent detail panel: context compression notice ——
toolStateCalling: 'Calling...', contextCompressed: 'Context compressed (round {round})',
toolStateProgress: 'In progress',
// —— File preview panel —— // —— File preview panel ——
resizeWidthHint: 'Drag to resize width', resizeWidthHint: 'Drag to resize width',

View File

@ -30,6 +30,8 @@ export default {
tool: '工具', tool: '工具',
versioning: '版本管理', versioning: '版本管理',
unset: '未设置', unset: '未设置',
settings: '设置',
back: '返回',
settingsLanguage: '界面语言', settingsLanguage: '界面语言',
settingsLanguageDesc: '选择界面显示语言', settingsLanguageDesc: '选择界面显示语言',

View File

@ -46,10 +46,9 @@ export default {
toolRunCommand: '运行命令 {command}', toolRunCommand: '运行命令 {command}',
toolEditFile: '编辑 {path}', toolEditFile: '编辑 {path}',
toolReadMedia: '读取媒体文件 {path}', toolReadMedia: '读取媒体文件 {path}',
toolStateDone: '完成',
toolStateFailed: '失败', // —— 子智能体详情面板:上下文压缩提示 ——
toolStateCalling: '调用中', contextCompressed: '上下文已压缩(第 {round} 轮)',
toolStateProgress: '进行中',
// —— 文件预览面板FilePreviewPanel —— // —— 文件预览面板FilePreviewPanel ——
resizeWidthHint: '拖拽调整宽度', resizeWidthHint: '拖拽调整宽度',

View File

@ -15,14 +15,14 @@
} }
.personal-page-card { .personal-page-card {
width: min(96vw, 1020px); width: min(648px, calc(100vw - 40px));
height: calc(100vh - 40px); height: min(646px, calc(100vh - 40px));
max-height: 760px; height: min(646px, calc(100dvh - 40px));
background: var(--theme-surface-card); background: var(--theme-surface-soft);
border-radius: 24px; border-radius: 14px;
border: 1px solid var(--theme-control-border); border: 1px solid var(--theme-control-border);
box-shadow: var(--theme-shadow-strong); box-shadow: none;
padding: 40px; padding: 0;
text-align: left; text-align: left;
color: var(--text-primary); color: var(--text-primary);
display: flex; display: flex;
@ -329,11 +329,13 @@
} }
} }
/* 手机端完整适配 */ /* 手机端完整适配(与个人空间组件移动端断点 760px 统一) */
@media (max-width: 768px) { @media (max-width: 760px) {
.personal-page-overlay { .personal-page-overlay {
padding: 0; padding: 0;
align-items: stretch; align-items: stretch;
/* 盖过移动端顶栏 mobile-panel-trigger (1500) 及其下拉菜单 (1600) */
z-index: 1700;
} }
.personal-page-card { .personal-page-card {
@ -341,8 +343,6 @@
height: 100%; height: 100%;
max-height: none; max-height: none;
border-radius: 0; border-radius: 0;
padding: 16px;
padding-top: 60px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
@ -2555,6 +2555,21 @@
transform: translateY(-26px); transform: translateY(-26px);
} }
/* 触屏760px不做内容切换过渡mode=out-in 的离场动画在手机上观感拖沓
必须放在原规则之后同特异性后者胜
离场元素直接 display:none窄屏下 transition 组件为默认同时模式
旧内容瞬间消失新内容瞬间出现不会先闪旧内容 */
@media (max-width: 760px) {
.personal-page-vertical-enter-active,
.personal-page-vertical-leave-active {
transition: none;
}
.personal-page-vertical-leave-active {
display: none;
}
}
.personal-page-fade-enter-active, .personal-page-fade-enter-active,
.personal-page-fade-leave-active { .personal-page-fade-leave-active {
transition: transition: