diff --git a/.trellis/tasks/10-19-conversation-document-title/check.jsonl b/.trellis/tasks/10-19-conversation-document-title/check.jsonl new file mode 100644 index 0000000..174b7ac --- /dev/null +++ b/.trellis/tasks/10-19-conversation-document-title/check.jsonl @@ -0,0 +1,2 @@ +{"file":".trellis/spec/frontend/state-management.md","reason":"检查当前会话、后台更新与切换隔离"} +{"file":".trellis/spec/frontend/quality-guidelines.md","reason":"检查语法和行为验证"} diff --git a/.trellis/tasks/10-19-conversation-document-title/findings.md b/.trellis/tasks/10-19-conversation-document-title/findings.md new file mode 100644 index 0000000..efa8500 --- /dev/null +++ b/.trellis/tasks/10-19-conversation-document-title/findings.md @@ -0,0 +1,9 @@ +# 调查记录 + +- 项目为原生 JavaScript 前端,入口 public/index.html 默认 title 为 CC-Web。 +- 初步索引搜索未发现 document.title 动态更新逻辑。 +- codebase-memory 索引状态 ready。 +- public/app.js 中 resetChatView、applySessionSnapshot、handleServerMessage 的 title_updated 分支以及聊天标题编辑完成逻辑直接写入 chatTitle.textContent,应统一维护标签页标题。 +- 实际事件名为 session_renamed;session_list 当前不主动刷新聊天标题,需要同步当前会话名称且避开编辑中的草稿。 +- 服务端每次计算 app.js 内容哈希并用于入口页版本,可刷新页面加载本次改动,无需重启服务。 +- 主代理读取源码片段时 codebase-memory 出现 Transport closed,已按已定位符号用本地读取降级;实现子代理仍可访问索引。 diff --git a/.trellis/tasks/10-19-conversation-document-title/implement.jsonl b/.trellis/tasks/10-19-conversation-document-title/implement.jsonl new file mode 100644 index 0000000..8e07326 --- /dev/null +++ b/.trellis/tasks/10-19-conversation-document-title/implement.jsonl @@ -0,0 +1,2 @@ +{"file":".trellis/spec/frontend/state-management.md","reason":"沿用当前会话状态更新链路"} +{"file":".trellis/spec/frontend/quality-guidelines.md","reason":"遵守原生前端与静态资源版本约定"} diff --git a/.trellis/tasks/10-19-conversation-document-title/prd.md b/.trellis/tasks/10-19-conversation-document-title/prd.md new file mode 100644 index 0000000..2551e74 --- /dev/null +++ b/.trellis/tasks/10-19-conversation-document-title/prd.md @@ -0,0 +1,5 @@ +# 当前对话同步浏览器标题 + +浏览器标签页的 title 随当前打开的对话名称更新;切换对话、自动生成标题和手动重命名后保持同步。没有当前对话时使用 CC-Web。复用现有当前对话标题入口,不改变页面视觉样式。 + +验收:检查初始加载、切换、重命名、后台会话更新与无当前会话的标题表现;完成语法检查与必要行为验证。 diff --git a/.trellis/tasks/10-19-conversation-document-title/progress.md b/.trellis/tasks/10-19-conversation-document-title/progress.md new file mode 100644 index 0000000..a0c40ee --- /dev/null +++ b/.trellis/tasks/10-19-conversation-document-title/progress.md @@ -0,0 +1,23 @@ +# 进度记录 + +- 已确认需求是浏览器标签页标题随当前对话名称同步。 +- 已读取项目约定并建立任务记录、计划和 CSV;验证探索交由只读子代理并行进行。 +- todo-list-csv 技能未安装;按项目约定直接维护 CSV。 +- 已定位 public/app.js 的会话快照、列表刷新、重命名和重置路径;实现子代理仅修改 app.js,主代理负责文档与验证。 +- 只读验证发现现有回归没有 document.title 行为断言;采用临时浏览器验证,不新增长期测试文件。 +- app.js 已通过 setChatTitle 统一维护页头与浏览器标题,覆盖载入、重命名、列表刷新与重置;编辑中不由列表刷新覆盖草稿。 +- node --check public/app.js 通过。 +- Chromium 149.0.7827.55 通过 11 项真实 DOM 行为检查:初始、打开、切换、当前重命名、后台隔离、列表同步、编辑草稿、提交、取消、无名称回退与重置默认;无页面脚本异常。 +- 浏览器使用本地夹具、模拟通信和临时暴露入口,未访问真实会话或调用生产 API;临时脚本在 /tmp,未加入项目。 +- 独立审查通过:currentSessionId 变化均覆盖标题更新,后台会话隔离正确,Escape 恢复后通过 blur 同步;git diff --check 通过。 +- 已评估规范更新:本次沿用既有会话状态更新模式,无需新增规范条目。 +- 工作完成,无需重启服务;用户刷新页面即可加载更新。 + +## 后续交付 + +- 按用户修正,标签页仅显示当前对话名称,已去掉 CC-Web 后缀。 +- 已按用户原文更新设置标题、发送消息、列出对话、更新计划、创建持久对话和多问题表单六项 MCP 工具描述。 +- 已重启 ccweb,并在当前模型工具列表确认六项新描述生效。 +- 用户要求重新打包并提交推送全部修改,不再进行额外审计。 +- 使用本地 Bun 1.4.0 按 bun-linux-x64-baseline 目标重新生成发布包;必要语法检查、可执行文件 MCP initialize、压缩包完整性及前端源码一致性验证均通过。 +- 发布包 42.96 MiB,SHA-256:2ee61a827b56f2ff538ced64c344ff73470e3df042db61d2f9360d67eebef49f。 diff --git a/.trellis/tasks/10-19-conversation-document-title/task.json b/.trellis/tasks/10-19-conversation-document-title/task.json new file mode 100644 index 0000000..9a1016e --- /dev/null +++ b/.trellis/tasks/10-19-conversation-document-title/task.json @@ -0,0 +1,26 @@ +{ + "id": "conversation-document-title", + "name": "conversation-document-title", + "title": "当前对话同步浏览器标题", + "description": "", + "status": "completed", + "dev_type": null, + "scope": null, + "package": null, + "priority": "P2", + "creator": "shiyue", + "assignee": "shiyue", + "createdAt": "2026-10-19", + "completedAt": "2026-10-19", + "branch": null, + "base_branch": "main", + "worktree_path": null, + "commit": null, + "pr_url": null, + "subtasks": [], + "children": [], + "parent": null, + "relatedFiles": [], + "notes": "", + "meta": {} +} diff --git a/.trellis/tasks/10-19-conversation-document-title/task_plan.md b/.trellis/tasks/10-19-conversation-document-title/task_plan.md new file mode 100644 index 0000000..461b6a1 --- /dev/null +++ b/.trellis/tasks/10-19-conversation-document-title/task_plan.md @@ -0,0 +1,13 @@ +# 任务计划 + +1. [completed] 定位当前对话名称与浏览器标题的更新入口。 +2. [completed] 实现浏览器标题随当前对话同步。 +3. [completed] 验证切换、重命名和默认标题并收尾。 + +## 约束 + +只修改相关前端逻辑,不重启正在承载其他运行会话的服务。 + +## 工具异常 + +- codebase-memory 的 get_code_snippet 在主代理端连接关闭,已使用本地文件读取;子代理端索引正常。 diff --git a/dist-exe/cc-web-bun-linux-x64-baseline.tar.gz b/dist-exe/cc-web-bun-linux-x64-baseline.tar.gz index e7aaee4..e84ee11 100644 Binary files a/dist-exe/cc-web-bun-linux-x64-baseline.tar.gz and b/dist-exe/cc-web-bun-linux-x64-baseline.tar.gz differ diff --git a/lib/ccweb-mcp-server.js b/lib/ccweb-mcp-server.js index b5aaa68..4c4fb6e 100644 --- a/lib/ccweb-mcp-server.js +++ b/lib/ccweb-mcp-server.js @@ -16,7 +16,7 @@ const CCWEB_REPLY_MODES = Object.freeze({ ONE_WAY: 'one_way', RETURN_AND_CONTINUE: 'return_and_continue', }); -const CCWEB_SEND_MESSAGE_DESCRIPTION = '向指定 ccweb 对话发送一条消息,并以“来自某对话”的气泡在目标对话中展示。必须填写 replyMode:仅当来源不需要目标结果时使用 one_way;涉及分析、实现、测试、验收、完成后汇报或来源后续依赖目标结果时必须使用 return_and_continue。两种模式的工具调用都会立即返回,不阻塞也不等待目标对话完成;return_and_continue 只表示目标完成后由系统异步回传并触发来源继续运行,不表示当前调用需要等待。调用后不要使用 sleep、不要轮询等待,也不要反复查询 pending reply;继续当前工作或结束本轮即可。'; +const CCWEB_SEND_MESSAGE_DESCRIPTION = '向指定对话发送一条消息,并以“来自某对话”的气泡在目标对话中展示。必须填写 replyMode:仅当来源不需要目标结果时使用 one_way;涉及分析、实现、测试、验收、完成后汇报或来源后续依赖目标结果时必须使用 return_and_continue。两种模式的工具调用都会立即返回,不阻塞也不等待目标对话完成;return_and_continue 只表示目标完成后由系统异步回传并触发来源继续运行,不表示当前调用需要等待。调用后不要使用 sleep、不要轮询等待,也不要反复查询 pending reply;继续当前工作或结束本轮即可。'; const CODEX_APP_COMMUNICATION_TOOL_NAMES = new Set([ 'ccweb_list_conversations', 'ccweb_list_user_inputs', @@ -55,7 +55,7 @@ const TOOLS = [ }, { name: 'ccweb_list_conversations', - description: '列出当前 ccweb 中可投递消息的对话。默认返回全部对话;scope=children 时只返回当前来源对话通过 ccweb_create_conversation 直接创建的持久子对话。只返回 ID、标题、Agent、运行状态和更新时间,不返回对话正文。', + description: '列出当前对话列表中可投递消息的对话。默认返回全部对话;scope=children 时只返回当前来源对话通过 ccweb_create_conversation 直接创建的持久子对话。只返回 ID、标题、Agent、运行状态和更新时间,不返回对话正文。', inputSchema: { type: 'object', properties: { @@ -112,7 +112,7 @@ const TOOLS = [ }, { name: 'ccweb_set_title', - description: '设置当前 ccweb 对话标题。只作用于当前来源对话;用户手动重命名后会成功返回但忽略,不再覆盖用户标题。', + description: '设置当前对话标题,新对话明确需求后调用一次;普通进展、子步骤、实现细节都不要改名,只有主要目标发生明显变化才再次调用。', inputSchema: { type: 'object', properties: { @@ -128,7 +128,7 @@ const TOOLS = [ }, { name: 'ccweb_update_plan', - description: '当原生 update_plan 工具不可用时,在当前 ccweb 对话展示并更新多步骤计划。每次传入完整计划;开始、完成步骤时重新调用以同步状态。简单任务无需调用。', + description: '当原生 update_plan 工具不可用时,优先调用此工具展示并更新多步骤计划。每次传入完整计划;开始、完成步骤时重新调用以同步状态。简单任务无需调用。', inputSchema: { type: 'object', properties: { @@ -155,7 +155,7 @@ const TOOLS = [ }, { name: 'ccweb_create_conversation', - description: '创建一个新的 ccweb 持久对话。Agent 固定继承来源对话,不作为参数指定;只用于需要在会话列表中长期追踪、后续可继续对话的工作流;一次性并行研究应优先使用子代能力。工具调用会立即返回。启用 requestReply 后,默认由系统异步回传结果;除非用户明确要求轮询,否则不主动轮询等待。', + description: '创建一个新的持久对话。只用于需要在会话列表中长期追踪、后续可继续对话的工作流;一次性并行研究应优先使用子代能力。工具调用会立即返回。启用 requestReply 后,默认由系统异步回传结果;任何尝试轮询等待持久对话返回的行为都是禁止的', inputSchema: { type: 'object', properties: { @@ -241,7 +241,7 @@ const TOOLS = [ }, { name: 'ccweb_prompt_user', - description: '在当前来源 ccweb 对话前台渲染一个多问题表单。工具会立即返回,不等待用户;用户提交后,ccweb 会把问题、选择和答案作为一条普通用户消息发回当前对话。', + description: '调用此工具实时渲染一个多问题表单,工具会立即返回一个待定状态;用户提交后,会把问题、选择和答案作为一普通用户消息发回当前对话。', inputSchema: { type: 'object', properties: { diff --git a/public/app.js b/public/app.js index 3a15d46..9d1bcdb 100644 --- a/public/app.js +++ b/public/app.js @@ -7142,6 +7142,11 @@ chatAgentBtn.setAttribute('aria-expanded', willOpen ? 'true' : 'false'); } + function setChatTitle(title) { + chatTitle.textContent = title || '新会话'; + document.title = currentSessionId ? chatTitle.textContent : 'CC-Web'; + } + function resetChatView(agent) { abortToolDetailRequests(); clearPendingMissingSessionConfirmation(); @@ -7177,7 +7182,7 @@ collabAgentIdsByToolUseId = new Map(); closedCollabAgentIdsByToolUseId = new Map(); updateGenerationControls(); - chatTitle.textContent = '新会话'; + setChatTitle('新会话'); updateSessionIdBadge(); updateCwdBadge(); updateReloadMcpButtonUI(); @@ -7235,7 +7240,7 @@ (snapshot.messages || []).length, ); setLastSessionForAgent(snapshot.agent, currentSessionId); - chatTitle.textContent = snapshot.title || '新会话'; + setChatTitle(snapshot.title); updateSessionIdBadge(); setCurrentAgent(snapshotAgent); migratePendingNotesToSession(snapshot.sessionId, snapshotAgent); @@ -8554,7 +8559,11 @@ reconcileSessionCacheWithSessions(); renderSessionList(); if (currentSessionId) { - setCurrentSessionRunningState(!!getSessionMeta(currentSessionId)?.isRunning); + const currentSession = getSessionMeta(currentSessionId); + setCurrentSessionRunningState(!!currentSession?.isRunning); + if (currentSession && chatTitle.contentEditable !== 'true') { + setChatTitle(currentSession.title); + } } if (pendingInitialSessionLoad) { pendingInitialSessionLoad = false; @@ -8884,7 +8893,7 @@ } }); if (msg.sessionId === currentSessionId) { - chatTitle.textContent = msg.title; + setChatTitle(msg.title); if (msg.titleEvent) { currentOutlineTitleHistory = normalizeOutlineTitleHistory([...currentOutlineTitleHistory, msg.titleEvent]); updateUserOutlinePanel(); @@ -12763,7 +12772,7 @@ chatTitle.style.overflow = ''; chatTitle.style.textOverflow = ''; const newTitle = chatTitle.textContent.trim() || originalText; - chatTitle.textContent = newTitle; + setChatTitle(newTitle); if (save && newTitle !== originalText && currentSessionId) { send({ type: 'rename_session', sessionId: currentSessionId, title: newTitle }); }