From 35ef1d9e2a183fb173a34afa083f02007a285cfd Mon Sep 17 00:00:00 2001 From: tmoonlight Date: Mon, 28 Sep 2026 10:58:16 +0800 Subject: [PATCH 1/6] =?UTF-8?q?refactor20260927(layers/P2-07):=20=E6=8B=86?= =?UTF-8?q?=E5=87=BA=E6=8F=90=E7=A4=BA=E8=AF=8D=E5=92=8C=E4=BC=9A=E8=AF=9D?= =?UTF-8?q?=E6=A0=87=E9=A2=98=E5=8F=8A=20LSP=20=E6=96=87=E6=9C=AC=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 2 + src/commands/builtin_commands.cpp | 2 +- src/commands/init_command.cpp | 133 +---------- src/commands/init_command.hpp | 17 -- src/commands/lsp_command.cpp | 51 +---- src/commands/lsp_command.hpp | 10 - src/lsp/lsp_status_text.cpp | 56 +++++ src/lsp/lsp_status_text.hpp | 15 ++ src/platform/terminal/terminal_title.cpp | 65 ------ src/platform/terminal/terminal_title.hpp | 12 - src/prompt/init_prompt.cpp | 137 +++++++++++ src/prompt/init_prompt.hpp | 22 ++ src/session/session_manager.cpp | 2 +- src/session/session_registry.cpp | 4 +- src/session/session_storage.cpp | 2 +- src/session/session_title_generator.cpp | 194 +--------------- src/session/session_title_generator.hpp | 10 - src/session/session_title_text.cpp | 263 ++++++++++++++++++++++ src/session/session_title_text.hpp | 32 +++ src/web/server_helpers.cpp | 1 + src/web/server_impl.hpp | 1 - tests/commands/init_command_test.cpp | 5 +- tests/lsp/lsp_diagnostics_format_test.cpp | 4 +- tests/platform/terminal_title_test.cpp | 4 +- tests/session/session_title_test.cpp | 1 + 25 files changed, 548 insertions(+), 497 deletions(-) create mode 100644 src/lsp/lsp_status_text.cpp create mode 100644 src/lsp/lsp_status_text.hpp create mode 100644 src/prompt/init_prompt.cpp create mode 100644 src/prompt/init_prompt.hpp create mode 100644 src/session/session_title_text.cpp create mode 100644 src/session/session_title_text.hpp diff --git a/CLAUDE.md b/CLAUDE.md index 699f4cacd..49937b170 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -229,6 +229,8 @@ Canonical shared transcripts are protected by a lightweight writer lease under t **会话标题有两份且内存那份优先(改名回滚的坑)**:`SessionRegistry::list_active()` 报的是 `SessionManager` 内存里的 title,`session_info_to_json` 让它盖过磁盘 meta。所以任何**只写磁盘**的改名都会被内存那份盖掉:活跃会话的下一次 `update_meta()` 把旧标题写回去,列表也立刻显示旧标题 —— 用户看到的就是「重命名无效」。两条路径会走到只写磁盘:(1) `set_session_title_response` 的 workspace 归属判定失败(`no_workspace` 会话永远匹配不上任何 workspace,cwd 形态不同也匹配不上),(2) Desktop 每个 workspace 一个 daemon、只有 active 的那个服务 UI,改别的 workspace 的会话时落到的是没有这份内存的 daemon。修法两端都在:web 层在磁盘分支后按 id 同步本进程的活跃 entry;`SessionManager::update_meta` 写前调 `adopt_foreign_user_title_locked()`,除本进程正在提交的显式用户改名外都采纳磁盘上的 `user`/`user-cleared` 标题,因此已手动命名过的会话也能继续跨 daemon 改名;`session_info_to_json` 里磁盘 user 标题压过旧的内存标题。回归测试:`tests/session/session_title_test.cpp` + `tests/web/web_server_smoke_test.cpp` 的 title 用例。 +P2-07 将可复用的文本逻辑归回实际模块:标题清洗与空输入判定在 `src/session/session_title_text.{hpp,cpp}`,终端标题 OS 调用仍在 `src/platform/terminal/terminal_title`;`/init` 的共享提示词在 `src/prompt/init_prompt`,LSP 状态格式化与子命令分发在 `src/lsp/lsp_status_text`。TUI 命令层仅负责注册与交互适配,会话宿主直接依赖这些共享接口。 + **会话显示标题只有一个字段,前端不许从消息正文现推。** 侧栏、顶部标题栏、网格卡片、搜索面板、托盘菜单显示的都是 `sessionDisplayTitle(session)`:服务端 `title`(用户改名 / 大模型生成,generated 的 `[Error]` 标题忽略)优先,否则 `summary`。`summary` 由 `SessionStorage::summarize_user_message_text` 从最近一条可见用户消息的**显示文本**(`metadata.display_text` 优先于 `content`,`SessionStorage::visible_user_message_text`)折成单行、UTF-8 安全截到 80 字节 + "..."; `SessionManager` 的内存值与 `list_sessions` 补齐旧 meta 走同一实现。实时同步只有一条路:用户消息落盘后 AgentLoop 发 `session_updated{summary}`(`emit_session_summary_updated`,正常回合与 soft-steer 插话都发),大模型标题就绪时 SessionRegistry 发 `session_updated{title,title_source}`,`list_active` 与 `GET /messages?since=0` 快照都带这三个字段;Web 的 transcript store(`transcriptDisplayTitle`)与 Sidebar 列表对同一事件做同样的合并。两条被删掉的歧路别再加回来:(1) transcript 加载历史后用 `titleFromMessages` 把**最后一条 user 消息全文**当顶部标题 —— 会话 20260923-163126-e7e3 的首条消息是 `@session` 引用展开后的 9k 字符长文,顶部标题就是它、长度不受限,侧栏却显示 80 字节的 `summary`,而 `summary` 又是按展开后的 `content` 截的 "Referenced ACECode session context follows...";(2) 侧栏 hover 跑马灯再拉整份消息把标题「水合」成全文(`sidebarFullTitle.js`,已删)。ChatView 发送时也不再本地改写标题。回归:`tests/session/session_summary_test.cpp`、`tests/agent_loop/agent_loop_session_summary_event_test.cpp`、前端 `sessionTitleSingleSourceArchitecture.test.js` + `sessionTranscript.test.js` 的「会话显示标题」组。 Rewind support uses per-user-turn checkpoints. `SessionManager::track_file_write_before` is the hook file-mutating tools call so `/rewind` can restore file state. diff --git a/src/commands/builtin_commands.cpp b/src/commands/builtin_commands.cpp index 847f492bf..1bc2c5e62 100644 --- a/src/commands/builtin_commands.cpp +++ b/src/commands/builtin_commands.cpp @@ -33,6 +33,7 @@ #include "session/thread_goal_store.hpp" #include "utils/logger.hpp" #include "platform/terminal/terminal_title.hpp" +#include "session/session_title_text.hpp" #include "utils/utf8_path.hpp" #include "version.hpp" #include @@ -44,7 +45,6 @@ #include #include #include -#include #include #include diff --git a/src/commands/init_command.cpp b/src/commands/init_command.cpp index c9e959a4c..b0ba5d431 100644 --- a/src/commands/init_command.cpp +++ b/src/commands/init_command.cpp @@ -1,12 +1,12 @@ #include "init_command.hpp" #include "config/config.hpp" +#include "prompt/init_prompt.hpp" #include "utils/utf8_path.hpp" #include #include #include -#include namespace fs = std::filesystem; @@ -20,107 +20,6 @@ void emit(CommandContext& ctx, const std::string& msg) { ctx.state.chat_follow_tail = true; } -std::string skeleton_body_internal(bool claude_exists) { - std::ostringstream oss; - if (claude_exists) { - oss << "\n\n"; - } - oss << "# Project Overview\n\n" - << "\n\n" - << "# Build Instructions\n\n" - << "\n\n" - << "# Architecture\n\n" - << "\n\n" - << "# Conventions\n\n" - << "\n"; - return oss.str(); -} - -// Mirrors the preconditions AgentLoop relies on when calling chat_stream, not a -// comprehensive auth probe — false positives just reach AgentLoop which -// surfaces the HTTP error normally. Rationale in design.md D4. -// Body adapted from claudecodehaha/src/commands/init.ts OLD_INIT_PROMPT with -// Claude Code → acecode and an acecode-specific AGENT.md file prefix. The -// wording ("not obvious instructions", "avoid listing every -// component") carries over verbatim because it is the load-bearing part that -// keeps the LLM from producing boilerplate. -std::string build_init_prompt_body() { - return - "Please analyze this codebase and create an AGENT.md file, which " - "will be given to future instances of acecode to operate in this " - "repository.\n" - "\n" - "What to add:\n" - "1. Commands that will be commonly used, such as how to build, lint, " - "and run tests. Include the necessary commands to develop in this " - "codebase, such as how to run a single test.\n" - "2. High-level code architecture and structure so that future " - "instances can be productive more quickly. Focus on the \"big " - "picture\" architecture that requires reading multiple files to " - "understand.\n" - "\n" - "Usage notes:\n" - "- If there's already an AGENT.md, suggest improvements to it.\n" - "- When you make the initial AGENT.md, do not repeat yourself and " - "do not include obvious instructions like \"Provide helpful error " - "messages to users\", \"Write unit tests for all new utilities\", " - "\"Never include sensitive information (API keys, tokens) in code or " - "commits\".\n" - "- Avoid listing every component or file structure that can be easily " - "discovered.\n" - "- Don't include generic development practices.\n" - "- If there are Cursor rules (in .cursor/rules/ or .cursorrules) or " - "Copilot rules (in .github/copilot-instructions.md), make sure to " - "include the important parts.\n" - "- If there is a README.md, make sure to include the important " - "parts.\n" - "- Do not make up information such as \"Common Development Tasks\", " - "\"Tips for Development\", \"Support and Documentation\" unless this " - "is expressly included in other files that you read.\n" - "- Be sure to prefix the file with the following text:\n" - "\n" - "```\n" - "# AGENT.md\n" - "\n" - "This file provides guidance to acecode " - "(https://github.com/tmoonlight/acecode) when working with code in " - "this repository.\n" - "```\n"; -} - -std::string improvement_suffix() { - return - "\n" - "NOTE: AGENT.md already exists in this directory. Read it first " - "with the file_read tool, identify specific gaps against what the " - "codebase actually shows, and apply targeted edits via the " - "file_edit_tool. If the file is already accurate and well-written, " - "report that it looks good and do not edit it — do not overwrite " - "silently.\n"; -} - -std::string migration_suffix(bool claude_exists) { - std::ostringstream oss; - oss << "\n" - << "NOTE: "; - if (claude_exists) { - oss << "CLAUDE.md already exists in this directory but AGENT.md " - "does not. Read CLAUDE.md first and adapt its content into a " - "new AGENT.md via the file_write_tool rather than writing " - "from scratch. "; - } - oss << "Do not delete or modify CLAUDE.md on disk — leave it alone. After " - "writing AGENT.md, tell the user that AGENT.md now takes precedence " - "so they can delete CLAUDE.md or keep it as a fallback.\n"; - return oss.str(); -} - void cmd_init(CommandContext& ctx, const std::string& /*args*/) { fs::path cwd = path_from_utf8(ctx.agent_loop.cwd()); std::error_code ec; @@ -183,33 +82,9 @@ void cmd_init(CommandContext& ctx, const std::string& /*args*/) { } // namespace -std::string build_agent_md_skeleton(const fs::path& cwd) { - std::error_code ec; - bool claude = fs::exists(cwd / "CLAUDE.md", ec); - return skeleton_body_internal(claude); -} - -std::string build_init_prompt(const fs::path& cwd) { - std::error_code ec; - bool agent_exists = fs::exists(cwd / "AGENT.md", ec); - bool claude_exists = fs::exists(cwd / "CLAUDE.md", ec); - - std::string out = build_init_prompt_body(); - if (agent_exists) { - out += improvement_suffix(); - if (claude_exists) { - out += - "\nAlso read CLAUDE.md and cross-check it for additional " - "project guidance not already present in AGENT.md. Preserve " - "AGENT.md as the file you edit; do not delete or modify " - "CLAUDE.md.\n"; - } - } else if (claude_exists) { - out += migration_suffix(claude_exists); - } - return out; -} - +// Mirrors the preconditions AgentLoop relies on when calling chat_stream, not a +// comprehensive auth probe — false positives just reach AgentLoop which +// surfaces the HTTP error normally. Rationale in design.md D4. bool has_usable_init_provider(const AppConfig& cfg) { if (cfg.provider == "copilot") { // Background token refresh handles validity; treat as usable if chosen. diff --git a/src/commands/init_command.hpp b/src/commands/init_command.hpp index 0bdecfdd2..d3a59fd1c 100644 --- a/src/commands/init_command.hpp +++ b/src/commands/init_command.hpp @@ -2,25 +2,8 @@ #include "command_registry.hpp" -#include -#include - namespace acecode { -// Generate the AGENT.md skeleton text for a directory. The hint block at the -// top is included only when a legacy `CLAUDE.md` already lives in the same -// directory. Pure function — unit tested directly. Used by the no-provider -// fallback path of `/init`. -std::string build_agent_md_skeleton(const std::filesystem::path& cwd); - -// Build the LLM prompt that `/init` submits when a provider is configured. The -// returned string is the full user-role message sent to the agent loop; the -// LLM uses its read tools to survey the codebase and writes / edits AGENT.md -// via file_write_tool / file_edit_tool. Pure function — unit tested directly. -// The prompt body is stable; only the suffix varies based on whether -// AGENT.md / CLAUDE.md already exist in `cwd`. -std::string build_init_prompt(const std::filesystem::path& cwd); - bool has_usable_init_provider(const AppConfig& cfg); // Register `/init` — generate or improve AGENT.md in cwd. With a configured diff --git a/src/commands/lsp_command.cpp b/src/commands/lsp_command.cpp index 200901e98..535c59361 100644 --- a/src/commands/lsp_command.cpp +++ b/src/commands/lsp_command.cpp @@ -1,10 +1,10 @@ // /lsp 命令实现:状态文本组装是纯函数,TUI 与 daemon builtin 共用。 #include "commands/lsp_command.hpp" +#include "lsp/lsp_status_text.hpp" #include #include -#include namespace acecode { @@ -33,55 +33,6 @@ void cmd_lsp(CommandContext& ctx, const std::string& args) { } // namespace -std::string format_lsp_status(const lsp::LspService::Status& status) { - std::ostringstream oss; - oss << "LSP:\n"; - oss << " Enabled : " << (status.enabled ? "yes" : "no") << "\n"; - if (!status.enabled) { - oss << " Enable with config.json: {\"lsp\": {\"enabled\": true}}\n"; - return oss.str(); - } - - oss << " Connected : "; - if (status.connected.empty()) { - oss << "(none — servers start lazily on first matching file)\n"; - } else { - oss << "\n"; - for (const auto& entry : status.connected) { - oss << " * " << entry.server_id << " root=" << entry.root - << " files=" << entry.open_files << "\n"; - } - } - if (!status.broken.empty()) { - oss << " Broken : "; - for (std::size_t i = 0; i < status.broken.size(); ++i) { - if (i) oss << ", "; - oss << status.broken[i].server_id << " (root=" << status.broken[i].root << ")"; - } - oss << " — failed to start; restart acecode to retry\n"; - } - if (!status.not_installed.empty()) { - oss << " Not found : "; - for (std::size_t i = 0; i < status.not_installed.size(); ++i) { - if (i) oss << ", "; - oss << status.not_installed[i]; - } - oss << " — executable not on PATH; install to enable\n"; - } - return oss.str(); -} - -std::string dispatch_lsp_subcommand(const std::string& sub) { - if (!lsp::is_initialized()) { - return "LSP runtime is not initialized in this process."; - } - if (sub.empty() || sub == "show" || sub == "status") { - return format_lsp_status(lsp::service().status_snapshot()); - } - return "Unknown subcommand. Usage:\n" - " /lsp Show LSP integration status"; -} - void register_lsp_command(CommandRegistry& registry) { registry.register_command({ "lsp", diff --git a/src/commands/lsp_command.hpp b/src/commands/lsp_command.hpp index bbc68276b..a902878b2 100644 --- a/src/commands/lsp_command.hpp +++ b/src/commands/lsp_command.hpp @@ -10,18 +10,8 @@ #include "command_registry.hpp" -#include "lsp/lsp_service.hpp" - -#include - namespace acecode { void register_lsp_command(CommandRegistry& registry); -// 纯函数:状态快照 → 用户可读多行文本(单测覆盖)。 -std::string format_lsp_status(const lsp::LspService::Status& status); - -// 子命令分发。runtime 未初始化时返回提示文本,不抛。 -std::string dispatch_lsp_subcommand(const std::string& sub); - } // namespace acecode diff --git a/src/lsp/lsp_status_text.cpp b/src/lsp/lsp_status_text.cpp new file mode 100644 index 000000000..5902a267e --- /dev/null +++ b/src/lsp/lsp_status_text.cpp @@ -0,0 +1,56 @@ +#include "lsp_status_text.hpp" + +#include + +namespace acecode { + +std::string format_lsp_status(const lsp::LspService::Status& status) { + std::ostringstream oss; + oss << "LSP:\n"; + oss << " Enabled : " << (status.enabled ? "yes" : "no") << "\n"; + if (!status.enabled) { + oss << " Enable with config.json: {\"lsp\": {\"enabled\": true}}\n"; + return oss.str(); + } + + oss << " Connected : "; + if (status.connected.empty()) { + oss << "(none — servers start lazily on first matching file)\n"; + } else { + oss << "\n"; + for (const auto& entry : status.connected) { + oss << " * " << entry.server_id << " root=" << entry.root + << " files=" << entry.open_files << "\n"; + } + } + if (!status.broken.empty()) { + oss << " Broken : "; + for (std::size_t i = 0; i < status.broken.size(); ++i) { + if (i) oss << ", "; + oss << status.broken[i].server_id << " (root=" << status.broken[i].root << ")"; + } + oss << " — failed to start; restart acecode to retry\n"; + } + if (!status.not_installed.empty()) { + oss << " Not found : "; + for (std::size_t i = 0; i < status.not_installed.size(); ++i) { + if (i) oss << ", "; + oss << status.not_installed[i]; + } + oss << " — executable not on PATH; install to enable\n"; + } + return oss.str(); +} + +std::string dispatch_lsp_subcommand(const std::string& sub) { + if (!lsp::is_initialized()) { + return "LSP runtime is not initialized in this process."; + } + if (sub.empty() || sub == "show" || sub == "status") { + return format_lsp_status(lsp::service().status_snapshot()); + } + return "Unknown subcommand. Usage:\n" + " /lsp Show LSP integration status"; +} + +} // namespace acecode diff --git a/src/lsp/lsp_status_text.hpp b/src/lsp/lsp_status_text.hpp new file mode 100644 index 000000000..84dc7bae3 --- /dev/null +++ b/src/lsp/lsp_status_text.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include "lsp_service.hpp" + +#include + +namespace acecode { + +// 纯函数:状态快照 → 用户可读多行文本(单测覆盖)。 +std::string format_lsp_status(const lsp::LspService::Status& status); + +// 子命令分发。runtime 未初始化时返回提示文本,不抛。 +std::string dispatch_lsp_subcommand(const std::string& sub); + +} // namespace acecode diff --git a/src/platform/terminal/terminal_title.cpp b/src/platform/terminal/terminal_title.cpp index 8b13920aa..57cfd6245 100644 --- a/src/platform/terminal/terminal_title.cpp +++ b/src/platform/terminal/terminal_title.cpp @@ -1,7 +1,6 @@ #include "terminal_title.hpp" #include -#include #ifdef _WIN32 #ifndef NOMINMAX @@ -12,48 +11,6 @@ namespace acecode { -namespace { - -constexpr size_t kMaxTitleBytes = 256; - -size_t utf8_safe_prefix(const std::string& text, size_t max_bytes) { - const size_t limit = std::min(max_bytes, text.size()); - size_t i = 0; - size_t last_valid = 0; - - while (i < limit) { - const unsigned char c = static_cast(text[i]); - size_t seq_len = 0; - - if ((c & 0x80u) == 0) { - seq_len = 1; - } else if ((c & 0xE0u) == 0xC0u) { - seq_len = 2; - } else if ((c & 0xF0u) == 0xE0u) { - seq_len = 3; - } else if ((c & 0xF8u) == 0xF0u) { - seq_len = 4; - } else { - break; - } - - if (i + seq_len > limit || i + seq_len > text.size()) break; - - bool valid = true; - for (size_t j = 1; j < seq_len; ++j) { - const unsigned char cc = static_cast(text[i + j]); - if ((cc & 0xC0u) != 0x80u) { valid = false; break; } - } - if (!valid) break; - - i += seq_len; - last_valid = i; - } - return last_valid; -} - -} // namespace - void set_terminal_title(std::string_view text) { #ifdef _WIN32 if (text.empty()) { @@ -87,26 +44,4 @@ void clear_terminal_title() { set_terminal_title(std::string_view{}); } -bool sanitize_title(std::string& inout, std::string& error_out) { - error_out.clear(); - for (unsigned char c : inout) { - // Reject any C0 control byte. OSC 2 is single-line; tabs/newlines are - // also rejected to keep the rendered title predictable. - if (c < 0x20 || c == 0x7F) { - error_out = "invalid control character"; - return false; - } - } - if (inout.size() > kMaxTitleBytes) { - size_t cut = utf8_safe_prefix(inout, kMaxTitleBytes); - if (cut == 0) { - error_out = "invalid encoding"; - return false; - } - inout.resize(cut); - error_out = "truncated"; - } - return true; -} - } // namespace acecode diff --git a/src/platform/terminal/terminal_title.hpp b/src/platform/terminal/terminal_title.hpp index 91f9ca66c..09291494e 100644 --- a/src/platform/terminal/terminal_title.hpp +++ b/src/platform/terminal/terminal_title.hpp @@ -13,16 +13,4 @@ void set_terminal_title(std::string_view text); // Convenience: set_terminal_title(""). void clear_terminal_title(); -// Validate and clamp a user-supplied title in place. -// -// Rejects (returns false, sets error_out): any C0 control byte except that the -// caller has already excluded — specifically rejects ESC (0x1B), BEL (0x07), -// NUL (0x00), CR (0x0D), LF (0x0A), and HT (0x09). Tabs/newlines are rejected -// because OSC 2 is a single-line construct. -// -// On success (returns true): if the byte length exceeds 256, truncates to a -// UTF-8 character boundary <= 256 bytes. error_out is set to a short note when -// truncation occurred ("truncated"), empty otherwise. -bool sanitize_title(std::string& inout, std::string& error_out); - } // namespace acecode diff --git a/src/prompt/init_prompt.cpp b/src/prompt/init_prompt.cpp new file mode 100644 index 000000000..885ae92b3 --- /dev/null +++ b/src/prompt/init_prompt.cpp @@ -0,0 +1,137 @@ +#include "init_prompt.hpp" + +#include + +namespace fs = std::filesystem; + +namespace acecode { +namespace { + +std::string skeleton_body_internal(bool claude_exists) { + std::ostringstream oss; + if (claude_exists) { + oss << "\n\n"; + } + oss << "# Project Overview\n\n" + << "\n\n" + << "# Build Instructions\n\n" + << "\n\n" + << "# Architecture\n\n" + << "\n\n" + << "# Conventions\n\n" + << "\n"; + return oss.str(); +} + +// Body adapted from claudecodehaha/src/commands/init.ts OLD_INIT_PROMPT with +// Claude Code → acecode and an acecode-specific AGENT.md file prefix. The +// wording ("not obvious instructions", "avoid listing every +// component") carries over verbatim because it is the load-bearing part that +// keeps the LLM from producing boilerplate. +std::string build_init_prompt_body() { + return + "Please analyze this codebase and create an AGENT.md file, which " + "will be given to future instances of acecode to operate in this " + "repository.\n" + "\n" + "What to add:\n" + "1. Commands that will be commonly used, such as how to build, lint, " + "and run tests. Include the necessary commands to develop in this " + "codebase, such as how to run a single test.\n" + "2. High-level code architecture and structure so that future " + "instances can be productive more quickly. Focus on the \"big " + "picture\" architecture that requires reading multiple files to " + "understand.\n" + "\n" + "Usage notes:\n" + "- If there's already an AGENT.md, suggest improvements to it.\n" + "- When you make the initial AGENT.md, do not repeat yourself and " + "do not include obvious instructions like \"Provide helpful error " + "messages to users\", \"Write unit tests for all new utilities\", " + "\"Never include sensitive information (API keys, tokens) in code or " + "commits\".\n" + "- Avoid listing every component or file structure that can be easily " + "discovered.\n" + "- Don't include generic development practices.\n" + "- If there are Cursor rules (in .cursor/rules/ or .cursorrules) or " + "Copilot rules (in .github/copilot-instructions.md), make sure to " + "include the important parts.\n" + "- If there is a README.md, make sure to include the important " + "parts.\n" + "- Do not make up information such as \"Common Development Tasks\", " + "\"Tips for Development\", \"Support and Documentation\" unless this " + "is expressly included in other files that you read.\n" + "- Be sure to prefix the file with the following text:\n" + "\n" + "```\n" + "# AGENT.md\n" + "\n" + "This file provides guidance to acecode " + "(https://github.com/tmoonlight/acecode) when working with code in " + "this repository.\n" + "```\n"; +} + +std::string improvement_suffix() { + return + "\n" + "NOTE: AGENT.md already exists in this directory. Read it first " + "with the file_read tool, identify specific gaps against what the " + "codebase actually shows, and apply targeted edits via the " + "file_edit_tool. If the file is already accurate and well-written, " + "report that it looks good and do not edit it — do not overwrite " + "silently.\n"; +} + +std::string migration_suffix(bool claude_exists) { + std::ostringstream oss; + oss << "\n" + << "NOTE: "; + if (claude_exists) { + oss << "CLAUDE.md already exists in this directory but AGENT.md " + "does not. Read CLAUDE.md first and adapt its content into a " + "new AGENT.md via the file_write_tool rather than writing " + "from scratch. "; + } + oss << "Do not delete or modify CLAUDE.md on disk — leave it alone. After " + "writing AGENT.md, tell the user that AGENT.md now takes precedence " + "so they can delete CLAUDE.md or keep it as a fallback.\n"; + return oss.str(); +} + +} // namespace + +std::string build_agent_md_skeleton(const fs::path& cwd) { + std::error_code ec; + bool claude = fs::exists(cwd / "CLAUDE.md", ec); + return skeleton_body_internal(claude); +} + +std::string build_init_prompt(const fs::path& cwd) { + std::error_code ec; + bool agent_exists = fs::exists(cwd / "AGENT.md", ec); + bool claude_exists = fs::exists(cwd / "CLAUDE.md", ec); + + std::string out = build_init_prompt_body(); + if (agent_exists) { + out += improvement_suffix(); + if (claude_exists) { + out += + "\nAlso read CLAUDE.md and cross-check it for additional " + "project guidance not already present in AGENT.md. Preserve " + "AGENT.md as the file you edit; do not delete or modify " + "CLAUDE.md.\n"; + } + } else if (claude_exists) { + out += migration_suffix(claude_exists); + } + return out; +} + +} // namespace acecode diff --git a/src/prompt/init_prompt.hpp b/src/prompt/init_prompt.hpp new file mode 100644 index 000000000..a8f4b0e8e --- /dev/null +++ b/src/prompt/init_prompt.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include +#include + +namespace acecode { + +// Generate the AGENT.md skeleton text for a directory. The hint block at the +// top is included only when a legacy `CLAUDE.md` already lives in the same +// directory. Pure function — unit tested directly. Used by the no-provider +// fallback path of `/init`. +std::string build_agent_md_skeleton(const std::filesystem::path& cwd); + +// Build the LLM prompt that `/init` submits when a provider is configured. The +// returned string is the full user-role message sent to the agent loop; the +// LLM uses its read tools to survey the codebase and writes / edits AGENT.md +// via file_write_tool / file_edit_tool. Pure function — unit tested directly. +// The prompt body is stable; only the suffix varies based on whether +// AGENT.md / CLAUDE.md already exist in `cwd`. +std::string build_init_prompt(const std::filesystem::path& cwd); + +} // namespace acecode diff --git a/src/session/session_manager.cpp b/src/session/session_manager.cpp index 88f69918d..0e1611fdd 100644 --- a/src/session/session_manager.cpp +++ b/src/session/session_manager.cpp @@ -3,7 +3,7 @@ #include "session_serializer.hpp" #include "fork_attachment_context.hpp" #include "session_rewind.hpp" -#include "session_title_generator.hpp" +#include "session_title_text.hpp" #include "session_user_message_search.hpp" #include "tool_result_storage.hpp" #include "turn_net_diff.hpp" diff --git a/src/session/session_registry.cpp b/src/session/session_registry.cpp index 297979654..93a956a0b 100644 --- a/src/session/session_registry.cpp +++ b/src/session/session_registry.cpp @@ -9,8 +9,8 @@ #include "system_notice.hpp" #include "tool_result_storage.hpp" #include "turn_timing.hpp" -#include "commands/init_command.hpp" -#include "commands/lsp_command.hpp" +#include "prompt/init_prompt.hpp" +#include "lsp/lsp_status_text.hpp" #include "provider/apply_model_to_session.hpp" #include "provider/cwd_model_override.hpp" #include "provider/model_context_resolver.hpp" diff --git a/src/session/session_storage.cpp b/src/session/session_storage.cpp index 0086a8ed4..8bef1010c 100644 --- a/src/session/session_storage.cpp +++ b/src/session/session_storage.cpp @@ -1,7 +1,7 @@ #include "session_storage.hpp" #include "permissions/permissions.hpp" #include "session_serializer.hpp" -#include "session_title_generator.hpp" +#include "session_title_text.hpp" #include "task_suggestion_store.hpp" #include "llm/context_usage.hpp" #include "utils/atomic_file.hpp" diff --git a/src/session/session_title_generator.cpp b/src/session/session_title_generator.cpp index c3529b797..0ddc92224 100644 --- a/src/session/session_title_generator.cpp +++ b/src/session/session_title_generator.cpp @@ -1,202 +1,12 @@ #include "session_title_generator.hpp" +#include "session_title_text.hpp" #include "platform/locale.hpp" #include "utils/encoding.hpp" -#include "platform/terminal/terminal_title.hpp" #include -#include -#include -#include -#include -#include namespace acecode { -namespace { - -constexpr std::size_t kMaxGeneratedTitleBytes = 120; - -std::string trim_ascii(std::string s) { - auto is_space = [](unsigned char c) { return std::isspace(c) != 0; }; - while (!s.empty() && is_space(static_cast(s.front()))) s.erase(s.begin()); - while (!s.empty() && is_space(static_cast(s.back()))) s.pop_back(); - return s; -} - -std::string collapse_whitespace(std::string s) { - std::string out; - out.reserve(s.size()); - bool in_space = false; - for (unsigned char c : s) { - if (std::isspace(c)) { - if (!out.empty() && !in_space) out.push_back(' '); - in_space = true; - } else { - out.push_back(static_cast(c)); - in_space = false; - } - } - if (!out.empty() && out.back() == ' ') out.pop_back(); - return out; -} - -std::string strip_wrapping_quotes(std::string s) { - s = trim_ascii(std::move(s)); - while (s.size() >= 2 && - ((s.front() == '"' && s.back() == '"') || - (s.front() == '\'' && s.back() == '\''))) { - s = trim_ascii(s.substr(1, s.size() - 2)); - } - return s; -} - -std::optional title_from_json(const std::string& raw) { - try { - auto j = nlohmann::json::parse(raw); - if (j.is_object()) { - for (const auto* key : {"title", "name", "summary"}) { - if (j.contains(key) && j[key].is_string()) { - return j[key].get(); - } - } - } - if (j.is_string()) return j.get(); - } catch (...) { - } - return std::nullopt; -} - -std::optional unwrap_markdown_json_fence(const std::string& raw) { - std::string text = trim_ascii(raw); - if (text.rfind("```", 0) != 0) return std::nullopt; - - const std::size_t closing = text.rfind("```"); - if (closing <= 3 || !trim_ascii(text.substr(closing + 3)).empty()) { - return std::nullopt; - } - - std::string body = trim_ascii(text.substr(3, closing - 3)); - if (body.empty()) return std::nullopt; - - if (body.size() >= 4) { - std::string language = body.substr(0, 4); - std::transform(language.begin(), language.end(), language.begin(), - [](unsigned char c) { - return static_cast(std::tolower(c)); - }); - if (language == "json" && - (body.size() == 4 || - std::isspace(static_cast(body[4])) != 0)) { - body = trim_ascii(body.substr(4)); - } - } - - if (body.empty() || (body.front() != '{' && body.front() != '"')) { - return std::nullopt; - } - return body; -} - -bool contains_line_break(const std::string& text) { - return text.find('\n') != std::string::npos || - text.find('\r') != std::string::npos; -} - -bool contains_sentence_ending(const std::string& text) { - if (text.find("\xE3\x80\x82") != std::string::npos || // CJK full stop - text.find("\xEF\xBC\x81") != std::string::npos || // full-width exclamation - text.find("\xEF\xBC\x9F") != std::string::npos) { // full-width question mark - return true; - } - if (text.empty()) return false; - const char last = text.back(); - return last == '.' || last == '!' || last == '?'; -} - -std::size_t whitespace_word_count(const std::string& text) { - std::size_t count = 0; - bool in_word = false; - for (unsigned char c : text) { - if (std::isspace(c) != 0) { - in_word = false; - } else if (!in_word) { - ++count; - in_word = true; - } - } - return count; -} - -bool looks_like_generated_title(const std::string& title) { - if (title.empty() || title.size() > kMaxGeneratedTitleBytes) return false; - if (title.find("```") != std::string::npos || - title.find("**") != std::string::npos || - contains_line_break(title) || - contains_sentence_ending(title)) { - return false; - } - return whitespace_word_count(title) <= 12; -} - -std::string strip_common_prefix(std::string s) { - const std::string lower = [&] { - std::string v = s; - std::transform(v.begin(), v.end(), v.begin(), [](unsigned char c) { - return static_cast(std::tolower(c)); - }); - return v; - }(); - for (const auto* prefix : {"title:", "session title:", "标题:"}) { - const std::string p(prefix); - if (lower.rfind(p, 0) == 0) { - return trim_ascii(s.substr(p.size())); - } - } - return s; -} - -} // namespace - -bool is_generated_session_error_title(const std::string& title) { - std::size_t i = 0; - while (i < title.size() && - std::isspace(static_cast(title[i])) != 0) { - ++i; - } - return title.compare(i, 7, "[Error]") == 0; -} - -std::string sanitize_generated_session_title(std::string raw) { - std::string normalized = trim_ascii(ensure_utf8(raw)); - if (normalized.empty() || is_generated_session_error_title(normalized)) return {}; - - bool fenced = false; - if (normalized.rfind("```", 0) == 0) { - auto payload = unwrap_markdown_json_fence(normalized); - if (!payload.has_value()) return {}; - normalized = std::move(*payload); - fenced = true; - } else if (normalized.find("```") != std::string::npos) { - return {}; - } - - auto json_title = title_from_json(normalized); - if (!json_title.has_value() && - (fenced || normalized.front() == '{' || normalized.front() == '[')) { - return {}; - } - - std::string title = json_title.has_value() ? std::move(*json_title) : normalized; - if (contains_line_break(title)) return {}; - title = strip_wrapping_quotes(strip_common_prefix(collapse_whitespace(title))); - if (!looks_like_generated_title(title) || - is_generated_session_error_title(title)) { - return {}; - } - std::string err; - if (!sanitize_title(title, err)) return {}; - return trim_ascii(title); -} std::optional generate_session_title( LlmProvider& provider, @@ -208,7 +18,7 @@ std::optional generate_session_title( first_user_text, static_cast(bounded_input), ""); - if (trim_ascii(input).empty()) return std::nullopt; + if (!has_session_title_input(input)) return std::nullopt; ChatMessage system; system.role = "system"; diff --git a/src/session/session_title_generator.hpp b/src/session/session_title_generator.hpp index 4d2412540..6900e26e7 100644 --- a/src/session/session_title_generator.hpp +++ b/src/session/session_title_generator.hpp @@ -7,16 +7,6 @@ namespace acecode { -// Provider implementations use a bracketed error marker for non-streaming -// failures. Keep this deliberately narrower than a generic "Error" prefix so -// legitimate titles such as "Error handling cleanup" remain valid. -bool is_generated_session_error_title(const std::string& title); - -// Normalize model-generated title output. JSON and a single Markdown-wrapped -// JSON object are accepted for compatibility; explanations, code blocks, -// malformed structured output, and overlong text are rejected. -std::string sanitize_generated_session_title(std::string raw); - std::optional generate_session_title( LlmProvider& provider, const std::string& first_user_text, diff --git a/src/session/session_title_text.cpp b/src/session/session_title_text.cpp new file mode 100644 index 000000000..7a694ee8f --- /dev/null +++ b/src/session/session_title_text.cpp @@ -0,0 +1,263 @@ +#include "session_title_text.hpp" + +#include "utils/encoding.hpp" + +#include +#include +#include +#include + +#include + +namespace acecode { +namespace { + +constexpr size_t kMaxTitleBytes = 256; + +size_t utf8_safe_prefix(const std::string& text, size_t max_bytes) { + const size_t limit = std::min(max_bytes, text.size()); + size_t i = 0; + size_t last_valid = 0; + + while (i < limit) { + const unsigned char c = static_cast(text[i]); + size_t seq_len = 0; + + if ((c & 0x80u) == 0) { + seq_len = 1; + } else if ((c & 0xE0u) == 0xC0u) { + seq_len = 2; + } else if ((c & 0xF0u) == 0xE0u) { + seq_len = 3; + } else if ((c & 0xF8u) == 0xF0u) { + seq_len = 4; + } else { + break; + } + + if (i + seq_len > limit || i + seq_len > text.size()) break; + + bool valid = true; + for (size_t j = 1; j < seq_len; ++j) { + const unsigned char cc = static_cast(text[i + j]); + if ((cc & 0xC0u) != 0x80u) { valid = false; break; } + } + if (!valid) break; + + i += seq_len; + last_valid = i; + } + return last_valid; +} + +constexpr std::size_t kMaxGeneratedTitleBytes = 120; + +std::string trim_ascii(std::string s) { + auto is_space = [](unsigned char c) { return std::isspace(c) != 0; }; + while (!s.empty() && is_space(static_cast(s.front()))) s.erase(s.begin()); + while (!s.empty() && is_space(static_cast(s.back()))) s.pop_back(); + return s; +} + +std::string collapse_whitespace(std::string s) { + std::string out; + out.reserve(s.size()); + bool in_space = false; + for (unsigned char c : s) { + if (std::isspace(c)) { + if (!out.empty() && !in_space) out.push_back(' '); + in_space = true; + } else { + out.push_back(static_cast(c)); + in_space = false; + } + } + if (!out.empty() && out.back() == ' ') out.pop_back(); + return out; +} + +std::string strip_wrapping_quotes(std::string s) { + s = trim_ascii(std::move(s)); + while (s.size() >= 2 && + ((s.front() == '"' && s.back() == '"') || + (s.front() == '\'' && s.back() == '\''))) { + s = trim_ascii(s.substr(1, s.size() - 2)); + } + return s; +} + +std::optional title_from_json(const std::string& raw) { + try { + auto j = nlohmann::json::parse(raw); + if (j.is_object()) { + for (const auto* key : {"title", "name", "summary"}) { + if (j.contains(key) && j[key].is_string()) { + return j[key].get(); + } + } + } + if (j.is_string()) return j.get(); + } catch (...) { + } + return std::nullopt; +} + +std::optional unwrap_markdown_json_fence(const std::string& raw) { + std::string text = trim_ascii(raw); + if (text.rfind("```", 0) != 0) return std::nullopt; + + const std::size_t closing = text.rfind("```"); + if (closing <= 3 || !trim_ascii(text.substr(closing + 3)).empty()) { + return std::nullopt; + } + + std::string body = trim_ascii(text.substr(3, closing - 3)); + if (body.empty()) return std::nullopt; + + if (body.size() >= 4) { + std::string language = body.substr(0, 4); + std::transform(language.begin(), language.end(), language.begin(), + [](unsigned char c) { + return static_cast(std::tolower(c)); + }); + if (language == "json" && + (body.size() == 4 || + std::isspace(static_cast(body[4])) != 0)) { + body = trim_ascii(body.substr(4)); + } + } + + if (body.empty() || (body.front() != '{' && body.front() != '"')) { + return std::nullopt; + } + return body; +} + +bool contains_line_break(const std::string& text) { + return text.find('\n') != std::string::npos || + text.find('\r') != std::string::npos; +} + +bool contains_sentence_ending(const std::string& text) { + if (text.find("\xE3\x80\x82") != std::string::npos || // CJK full stop + text.find("\xEF\xBC\x81") != std::string::npos || // full-width exclamation + text.find("\xEF\xBC\x9F") != std::string::npos) { // full-width question mark + return true; + } + if (text.empty()) return false; + const char last = text.back(); + return last == '.' || last == '!' || last == '?'; +} + +std::size_t whitespace_word_count(const std::string& text) { + std::size_t count = 0; + bool in_word = false; + for (unsigned char c : text) { + if (std::isspace(c) != 0) { + in_word = false; + } else if (!in_word) { + ++count; + in_word = true; + } + } + return count; +} + +bool looks_like_generated_title(const std::string& title) { + if (title.empty() || title.size() > kMaxGeneratedTitleBytes) return false; + if (title.find("```") != std::string::npos || + title.find("**") != std::string::npos || + contains_line_break(title) || + contains_sentence_ending(title)) { + return false; + } + return whitespace_word_count(title) <= 12; +} + +std::string strip_common_prefix(std::string s) { + const std::string lower = [&] { + std::string v = s; + std::transform(v.begin(), v.end(), v.begin(), [](unsigned char c) { + return static_cast(std::tolower(c)); + }); + return v; + }(); + for (const auto* prefix : {"title:", "session title:", "标题:"}) { + const std::string p(prefix); + if (lower.rfind(p, 0) == 0) { + return trim_ascii(s.substr(p.size())); + } + } + return s; +} + +} // namespace + +bool sanitize_title(std::string& inout, std::string& error_out) { + error_out.clear(); + for (unsigned char c : inout) { + // Reject any C0 control byte. OSC 2 is single-line; tabs/newlines are + // also rejected to keep the rendered title predictable. + if (c < 0x20 || c == 0x7F) { + error_out = "invalid control character"; + return false; + } + } + if (inout.size() > kMaxTitleBytes) { + size_t cut = utf8_safe_prefix(inout, kMaxTitleBytes); + if (cut == 0) { + error_out = "invalid encoding"; + return false; + } + inout.resize(cut); + error_out = "truncated"; + } + return true; +} + +bool has_session_title_input(const std::string& text) { + return !trim_ascii(text).empty(); +} + +bool is_generated_session_error_title(const std::string& title) { + std::size_t i = 0; + while (i < title.size() && + std::isspace(static_cast(title[i])) != 0) { + ++i; + } + return title.compare(i, 7, "[Error]") == 0; +} + +std::string sanitize_generated_session_title(std::string raw) { + std::string normalized = trim_ascii(ensure_utf8(raw)); + if (normalized.empty() || is_generated_session_error_title(normalized)) return {}; + + bool fenced = false; + if (normalized.rfind("```", 0) == 0) { + auto payload = unwrap_markdown_json_fence(normalized); + if (!payload.has_value()) return {}; + normalized = std::move(*payload); + fenced = true; + } else if (normalized.find("```") != std::string::npos) { + return {}; + } + + auto json_title = title_from_json(normalized); + if (!json_title.has_value() && + (fenced || normalized.front() == '{' || normalized.front() == '[')) { + return {}; + } + + std::string title = json_title.has_value() ? std::move(*json_title) : normalized; + if (contains_line_break(title)) return {}; + title = strip_wrapping_quotes(strip_common_prefix(collapse_whitespace(title))); + if (!looks_like_generated_title(title) || + is_generated_session_error_title(title)) { + return {}; + } + std::string err; + if (!sanitize_title(title, err)) return {}; + return trim_ascii(title); +} + +} // namespace acecode diff --git a/src/session/session_title_text.hpp b/src/session/session_title_text.hpp new file mode 100644 index 000000000..320cbab5f --- /dev/null +++ b/src/session/session_title_text.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include + +namespace acecode { + +// 标题生成前的输入判定,沿用既有空白规则,避免为空输入调用模型。 +bool has_session_title_input(const std::string& text); + +// Validate and clamp a user-supplied title in place. +// +// Rejects (returns false, sets error_out): any C0 control byte except that the +// caller has already excluded — specifically rejects ESC (0x1B), BEL (0x07), +// NUL (0x00), CR (0x0D), LF (0x0A), and HT (0x09). Tabs/newlines are rejected +// because OSC 2 is a single-line construct. +// +// On success (returns true): if the byte length exceeds 256, truncates to a +// UTF-8 character boundary <= 256 bytes. error_out is set to a short note when +// truncation occurred ("truncated"), empty otherwise. +bool sanitize_title(std::string& inout, std::string& error_out); + +// Provider implementations use a bracketed error marker for non-streaming +// failures. Keep this deliberately narrower than a generic "Error" prefix so +// legitimate titles such as "Error handling cleanup" remain valid. +bool is_generated_session_error_title(const std::string& title); + +// Normalize model-generated title output. JSON and a single Markdown-wrapped +// JSON object are accepted for compatibility; explanations, code blocks, +// malformed structured output, and overlong text are rejected. +std::string sanitize_generated_session_title(std::string raw); + +} // namespace acecode diff --git a/src/web/server_helpers.cpp b/src/web/server_helpers.cpp index b061d1c48..f0cd68323 100644 --- a/src/web/server_helpers.cpp +++ b/src/web/server_helpers.cpp @@ -11,6 +11,7 @@ #include "prompt/context_usage_breakdown.hpp" #include "session/session_user_message_search.hpp" #include "session/composer_content.hpp" +#include "session/session_title_text.hpp" #include "utils/encoding.hpp" namespace acecode::web { diff --git a/src/web/server_impl.hpp b/src/web/server_impl.hpp index f005fb990..78146c50a 100644 --- a/src/web/server_impl.hpp +++ b/src/web/server_impl.hpp @@ -48,7 +48,6 @@ #include "utils/logger.hpp" #include "utils/base64.hpp" #include "utils/cwd_hash.hpp" -#include "platform/terminal/terminal_title.hpp" #include "workspace/files_handler.hpp" #include "web/handlers/fork_handler.hpp" #include "web/handlers/history_handler.hpp" diff --git a/tests/commands/init_command_test.cpp b/tests/commands/init_command_test.cpp index 0e0356099..1bc73e666 100644 --- a/tests/commands/init_command_test.cpp +++ b/tests/commands/init_command_test.cpp @@ -1,4 +1,4 @@ -// 覆盖 src/commands/init_command.{hpp,cpp} 的两个纯函数: +// 覆盖 src/prompt/init_prompt.{hpp,cpp} 的两个纯函数: // - build_agent_md_skeleton: 离线骨架生成,根据目录下 CLAUDE.md 是否存在拼出迁移提示 // - build_init_prompt: 交给 LLM 的 /init prompt 构建;根据目录下 AGENT.md / // CLAUDE.md 存在情况在基础 prompt 末尾选一条 suffix @@ -8,8 +8,9 @@ #include -#include "commands/init_command.hpp" +#include "prompt/init_prompt.hpp" +#include #include #include diff --git a/tests/lsp/lsp_diagnostics_format_test.cpp b/tests/lsp/lsp_diagnostics_format_test.cpp index bdd2a8eb8..195a86154 100644 --- a/tests/lsp/lsp_diagnostics_format_test.cpp +++ b/tests/lsp/lsp_diagnostics_format_test.cpp @@ -1,5 +1,5 @@ // 覆盖 src/lsp/lsp_diagnostics.{hpp,cpp} 的纯格式化函数 + -// src/commands/lsp_command.cpp 的 format_lsp_status。 +// src/lsp/lsp_status_text.cpp 的 format_lsp_status。 // // 覆盖项: // - pretty_diagnostic:0-based → 1-based 行列换算、severity 标签、 @@ -12,7 +12,7 @@ #include -#include "commands/lsp_command.hpp" +#include "lsp/lsp_status_text.hpp" #include "lsp/lsp_diagnostics.hpp" #include "lsp/lsp_service.hpp" diff --git a/tests/platform/terminal_title_test.cpp b/tests/platform/terminal_title_test.cpp index f3374bfc9..edd2a1ce8 100644 --- a/tests/platform/terminal_title_test.cpp +++ b/tests/platform/terminal_title_test.cpp @@ -1,4 +1,4 @@ -// 覆盖 src/platform/terminal/terminal_title.cpp 的 sanitize_title: +// 覆盖 src/session/session_title_text.cpp 的 sanitize_title: // - 合法 ASCII / UTF-8 必须原样通过 // - 任何 C0 控制字符(含换行/制表)必须被拒绝 // - 超长输入必须按 UTF-8 边界安全截断,不留半字符 @@ -7,7 +7,7 @@ #include -#include "platform/terminal/terminal_title.hpp" +#include "session/session_title_text.hpp" #include "utils/encoding.hpp" #include diff --git a/tests/session/session_title_test.cpp b/tests/session/session_title_test.cpp index 16236aaab..fc98ec649 100644 --- a/tests/session/session_title_test.cpp +++ b/tests/session/session_title_test.cpp @@ -5,6 +5,7 @@ #include "session/session_auto_title.hpp" #include "session/session_storage.hpp" #include "session/session_title_generator.hpp" +#include "session/session_title_text.hpp" #include #include From dfe8a5c14113d68822ff78ecaeddac965c11fb51 Mon Sep 17 00:00:00 2001 From: tmoonlight Date: Mon, 28 Sep 2026 10:59:41 +0800 Subject: [PATCH 2/6] =?UTF-8?q?refactor20260927(layers/P2-07):=20=E6=90=AC?= =?UTF-8?q?=E8=BF=81=E4=BC=9A=E8=AF=9D=E5=AE=BF=E4=B8=BB=E4=B8=8E=E9=95=9C?= =?UTF-8?q?=E5=83=8F=E6=B5=8B=E8=AF=95=20[mechanical]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/{provider => session_host}/apply_model_to_session.cpp | 0 src/{provider => session_host}/apply_model_to_session.hpp | 0 src/{session => session_host}/local_session_client.cpp | 0 src/{session => session_host}/local_session_client.hpp | 0 src/{session => session_host}/session_auto_title.cpp | 0 src/{session => session_host}/session_auto_title.hpp | 0 src/{session => session_host}/session_registry.cpp | 0 src/{session => session_host}/session_registry.hpp | 0 src/{session => session_host}/session_title_generator.cpp | 0 src/{session => session_host}/session_title_generator.hpp | 0 src/{session => session_host}/task_suggestion_service.cpp | 0 src/{session => session_host}/task_suggestion_service.hpp | 0 src/{session => session_host}/thread_service.cpp | 0 src/{session => session_host}/thread_service.hpp | 0 src/{tool => session_host/tools}/spawn_subagent_tool.cpp | 0 src/{tool => session_host/tools}/spawn_subagent_tool.hpp | 0 src/{tool => session_host/tools}/task_suggestion_tools.cpp | 0 src/{tool => session_host/tools}/task_suggestion_tools.hpp | 0 src/{tool => session_host/tools}/thread_tools.cpp | 0 src/{tool => session_host/tools}/thread_tools.hpp | 0 .../init_command_test.cpp => prompt/init_prompt_test.cpp} | 0 .../session_title_text_test.cpp} | 0 tests/{provider => session_host}/apply_model_to_session_test.cpp | 0 tests/{session => session_host}/session_registry_test.cpp | 0 tests/{tool => session_host}/spawn_subagent_tool_test.cpp | 0 tests/{session => session_host}/task_suggestion_service_test.cpp | 0 tests/{tool => session_host}/thread_tools_test.cpp | 0 27 files changed, 0 insertions(+), 0 deletions(-) rename src/{provider => session_host}/apply_model_to_session.cpp (100%) rename src/{provider => session_host}/apply_model_to_session.hpp (100%) rename src/{session => session_host}/local_session_client.cpp (100%) rename src/{session => session_host}/local_session_client.hpp (100%) rename src/{session => session_host}/session_auto_title.cpp (100%) rename src/{session => session_host}/session_auto_title.hpp (100%) rename src/{session => session_host}/session_registry.cpp (100%) rename src/{session => session_host}/session_registry.hpp (100%) rename src/{session => session_host}/session_title_generator.cpp (100%) rename src/{session => session_host}/session_title_generator.hpp (100%) rename src/{session => session_host}/task_suggestion_service.cpp (100%) rename src/{session => session_host}/task_suggestion_service.hpp (100%) rename src/{session => session_host}/thread_service.cpp (100%) rename src/{session => session_host}/thread_service.hpp (100%) rename src/{tool => session_host/tools}/spawn_subagent_tool.cpp (100%) rename src/{tool => session_host/tools}/spawn_subagent_tool.hpp (100%) rename src/{tool => session_host/tools}/task_suggestion_tools.cpp (100%) rename src/{tool => session_host/tools}/task_suggestion_tools.hpp (100%) rename src/{tool => session_host/tools}/thread_tools.cpp (100%) rename src/{tool => session_host/tools}/thread_tools.hpp (100%) rename tests/{commands/init_command_test.cpp => prompt/init_prompt_test.cpp} (100%) rename tests/{platform/terminal_title_test.cpp => session/session_title_text_test.cpp} (100%) rename tests/{provider => session_host}/apply_model_to_session_test.cpp (100%) rename tests/{session => session_host}/session_registry_test.cpp (100%) rename tests/{tool => session_host}/spawn_subagent_tool_test.cpp (100%) rename tests/{session => session_host}/task_suggestion_service_test.cpp (100%) rename tests/{tool => session_host}/thread_tools_test.cpp (100%) diff --git a/src/provider/apply_model_to_session.cpp b/src/session_host/apply_model_to_session.cpp similarity index 100% rename from src/provider/apply_model_to_session.cpp rename to src/session_host/apply_model_to_session.cpp diff --git a/src/provider/apply_model_to_session.hpp b/src/session_host/apply_model_to_session.hpp similarity index 100% rename from src/provider/apply_model_to_session.hpp rename to src/session_host/apply_model_to_session.hpp diff --git a/src/session/local_session_client.cpp b/src/session_host/local_session_client.cpp similarity index 100% rename from src/session/local_session_client.cpp rename to src/session_host/local_session_client.cpp diff --git a/src/session/local_session_client.hpp b/src/session_host/local_session_client.hpp similarity index 100% rename from src/session/local_session_client.hpp rename to src/session_host/local_session_client.hpp diff --git a/src/session/session_auto_title.cpp b/src/session_host/session_auto_title.cpp similarity index 100% rename from src/session/session_auto_title.cpp rename to src/session_host/session_auto_title.cpp diff --git a/src/session/session_auto_title.hpp b/src/session_host/session_auto_title.hpp similarity index 100% rename from src/session/session_auto_title.hpp rename to src/session_host/session_auto_title.hpp diff --git a/src/session/session_registry.cpp b/src/session_host/session_registry.cpp similarity index 100% rename from src/session/session_registry.cpp rename to src/session_host/session_registry.cpp diff --git a/src/session/session_registry.hpp b/src/session_host/session_registry.hpp similarity index 100% rename from src/session/session_registry.hpp rename to src/session_host/session_registry.hpp diff --git a/src/session/session_title_generator.cpp b/src/session_host/session_title_generator.cpp similarity index 100% rename from src/session/session_title_generator.cpp rename to src/session_host/session_title_generator.cpp diff --git a/src/session/session_title_generator.hpp b/src/session_host/session_title_generator.hpp similarity index 100% rename from src/session/session_title_generator.hpp rename to src/session_host/session_title_generator.hpp diff --git a/src/session/task_suggestion_service.cpp b/src/session_host/task_suggestion_service.cpp similarity index 100% rename from src/session/task_suggestion_service.cpp rename to src/session_host/task_suggestion_service.cpp diff --git a/src/session/task_suggestion_service.hpp b/src/session_host/task_suggestion_service.hpp similarity index 100% rename from src/session/task_suggestion_service.hpp rename to src/session_host/task_suggestion_service.hpp diff --git a/src/session/thread_service.cpp b/src/session_host/thread_service.cpp similarity index 100% rename from src/session/thread_service.cpp rename to src/session_host/thread_service.cpp diff --git a/src/session/thread_service.hpp b/src/session_host/thread_service.hpp similarity index 100% rename from src/session/thread_service.hpp rename to src/session_host/thread_service.hpp diff --git a/src/tool/spawn_subagent_tool.cpp b/src/session_host/tools/spawn_subagent_tool.cpp similarity index 100% rename from src/tool/spawn_subagent_tool.cpp rename to src/session_host/tools/spawn_subagent_tool.cpp diff --git a/src/tool/spawn_subagent_tool.hpp b/src/session_host/tools/spawn_subagent_tool.hpp similarity index 100% rename from src/tool/spawn_subagent_tool.hpp rename to src/session_host/tools/spawn_subagent_tool.hpp diff --git a/src/tool/task_suggestion_tools.cpp b/src/session_host/tools/task_suggestion_tools.cpp similarity index 100% rename from src/tool/task_suggestion_tools.cpp rename to src/session_host/tools/task_suggestion_tools.cpp diff --git a/src/tool/task_suggestion_tools.hpp b/src/session_host/tools/task_suggestion_tools.hpp similarity index 100% rename from src/tool/task_suggestion_tools.hpp rename to src/session_host/tools/task_suggestion_tools.hpp diff --git a/src/tool/thread_tools.cpp b/src/session_host/tools/thread_tools.cpp similarity index 100% rename from src/tool/thread_tools.cpp rename to src/session_host/tools/thread_tools.cpp diff --git a/src/tool/thread_tools.hpp b/src/session_host/tools/thread_tools.hpp similarity index 100% rename from src/tool/thread_tools.hpp rename to src/session_host/tools/thread_tools.hpp diff --git a/tests/commands/init_command_test.cpp b/tests/prompt/init_prompt_test.cpp similarity index 100% rename from tests/commands/init_command_test.cpp rename to tests/prompt/init_prompt_test.cpp diff --git a/tests/platform/terminal_title_test.cpp b/tests/session/session_title_text_test.cpp similarity index 100% rename from tests/platform/terminal_title_test.cpp rename to tests/session/session_title_text_test.cpp diff --git a/tests/provider/apply_model_to_session_test.cpp b/tests/session_host/apply_model_to_session_test.cpp similarity index 100% rename from tests/provider/apply_model_to_session_test.cpp rename to tests/session_host/apply_model_to_session_test.cpp diff --git a/tests/session/session_registry_test.cpp b/tests/session_host/session_registry_test.cpp similarity index 100% rename from tests/session/session_registry_test.cpp rename to tests/session_host/session_registry_test.cpp diff --git a/tests/tool/spawn_subagent_tool_test.cpp b/tests/session_host/spawn_subagent_tool_test.cpp similarity index 100% rename from tests/tool/spawn_subagent_tool_test.cpp rename to tests/session_host/spawn_subagent_tool_test.cpp diff --git a/tests/session/task_suggestion_service_test.cpp b/tests/session_host/task_suggestion_service_test.cpp similarity index 100% rename from tests/session/task_suggestion_service_test.cpp rename to tests/session_host/task_suggestion_service_test.cpp diff --git a/tests/tool/thread_tools_test.cpp b/tests/session_host/thread_tools_test.cpp similarity index 100% rename from tests/tool/thread_tools_test.cpp rename to tests/session_host/thread_tools_test.cpp From 88d0e6d212d726685dc664c9a5a1bf9593af5e3b Mon Sep 17 00:00:00 2001 From: tmoonlight Date: Mon, 28 Sep 2026 11:01:34 +0800 Subject: [PATCH 3/6] =?UTF-8?q?refactor20260927(layers/P2-07):=20=E6=94=B9?= =?UTF-8?q?=E5=86=99=E5=AE=BF=E4=B8=BB=E4=BE=9D=E8=B5=96=E8=B7=AF=E5=BE=84?= =?UTF-8?q?=E4=B8=8E=E6=B5=8B=E8=AF=95=E6=98=A0=E5=B0=84=20[mechanical]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/refactor/src_layout_map.tsv | 7 ++++++- src/commands/builtin_commands.cpp | 2 +- src/commands/model_command.cpp | 2 +- src/daemon/worker.cpp | 14 +++++++------- src/headless/headless_runner.cpp | 10 +++++----- src/loop/loop_scheduler.cpp | 2 +- src/main.cpp | 12 ++++++------ src/remote_control/session_channel_binder.cpp | 2 +- src/session_host/apply_model_to_session.hpp | 2 +- src/session_host/local_session_client.cpp | 6 +++--- src/session_host/local_session_client.hpp | 2 +- src/session_host/session_registry.cpp | 18 +++++++++--------- src/session_host/session_registry.hpp | 8 ++++---- src/session_host/session_title_generator.cpp | 2 +- src/session_host/task_suggestion_service.cpp | 10 +++++----- src/session_host/thread_service.cpp | 14 +++++++------- src/session_host/thread_service.hpp | 2 +- src/session_host/tools/spawn_subagent_tool.cpp | 2 +- src/session_host/tools/spawn_subagent_tool.hpp | 2 +- .../tools/task_suggestion_tools.cpp | 2 +- .../tools/task_suggestion_tools.hpp | 2 +- src/session_host/tools/thread_tools.cpp | 2 +- src/session_host/tools/thread_tools.hpp | 2 +- src/tui/subagent_host.hpp | 4 ++-- src/web/routes/routes_security.cpp | 2 +- src/web/routes/routes_tool_preamble.cpp | 2 +- src/web/server_impl.hpp | 6 +++--- tests/channels/gateway_test.cpp | 4 ++-- tests/experts/expert_registry_test.cpp | 2 +- tests/gitinfo/web_worktree_intent_test.cpp | 2 +- tests/loop/loop_scheduler_test.cpp | 2 +- .../session_channel_binder_test.cpp | 4 ++-- tests/session/session_title_test.cpp | 4 ++-- .../apply_model_to_session_test.cpp | 2 +- tests/session_host/session_registry_test.cpp | 4 ++-- .../session_host/spawn_subagent_tool_test.cpp | 6 +++--- .../task_suggestion_service_test.cpp | 6 +++--- tests/session_host/thread_tools_test.cpp | 8 ++++---- tests/web/summary_generation_handler_test.cpp | 2 +- tests/web/web_server_smoke_test.cpp | 10 +++++----- 40 files changed, 101 insertions(+), 96 deletions(-) diff --git a/scripts/refactor/src_layout_map.tsv b/scripts/refactor/src_layout_map.tsv index 1e77c04f6..44ad77665 100644 --- a/scripts/refactor/src_layout_map.tsv +++ b/scripts/refactor/src_layout_map.tsv @@ -269,7 +269,7 @@ tests/utils/open_url_test.cpp tests/platform/open_url_test.cpp P2-03 move Mirror tests/utils/power_inhibitor_test.cpp tests/platform/power_inhibitor_test.cpp P2-03 move Mirror the corresponding relocated source tests/utils/terminal_capability_test.cpp tests/platform/terminal_capability_test.cpp P2-03 move Mirror the corresponding relocated source tests/utils/terminal_theme_detect_test.cpp tests/platform/terminal_theme_detect_test.cpp P2-03 move Mirror the corresponding relocated source -tests/utils/terminal_title_test.cpp tests/platform/terminal_title_test.cpp P2-03 move Mirror the corresponding relocated source +tests/utils/terminal_title_test.cpp tests/session/session_title_text_test.cpp P2-03 move P2-03 platform intermediate path; pure title responsibility finalized in P2-07 tests/utils/text_input_ops_test.cpp tests/tui/text_input_ops_test.cpp P2-08 move Mirror the corresponding relocated source tests/utils/token_tracker_test.cpp tests/session/token_tracker_test.cpp P2-05 move Mirror the corresponding relocated source tests/utils/state_json_web_search_test.cpp tests/tool/state_json_web_search_test.cpp P2-05 move Mirror the extracted search region cache @@ -321,3 +321,8 @@ src/session/session_resume_restore.cpp src/adapters/tool/file_state_restore.cpp src/commands/init_command.cpp src/engine/prompt/init_prompt.cpp P2-07 extract Extract prompt builders src/commands/lsp_command.cpp src/adapters/lsp/lsp_status_text.cpp P2-07 extract Extract status format and dispatch src/cli/interactive_options.cpp src/adapters/tool/question_policy.cpp P2-08 extract Extract question policy parsing + +# P2-07 mirrors and the recorded P2-03 intermediate test path. +tests/commands/init_command_test.cpp tests/prompt/init_prompt_test.cpp P2-07 move Mirror the extracted prompt builders +tests/platform/terminal_title_test.cpp tests/session/session_title_text_test.cpp P2-07 move Intermediate alias for the original utils test; never coexist with final path +src/platform/terminal/terminal_title.cpp src/domain/session/session_title_text.cpp P2-07 extract Extract title validation and UTF-8 truncation helpers diff --git a/src/commands/builtin_commands.cpp b/src/commands/builtin_commands.cpp index 1bc2c5e62..64b8fbf1f 100644 --- a/src/commands/builtin_commands.cpp +++ b/src/commands/builtin_commands.cpp @@ -17,7 +17,7 @@ #include "tui/mode_picker.hpp" #include "tui/theme_palette.hpp" #include "config/saved_models.hpp" -#include "provider/apply_model_to_session.hpp" +#include "session_host/apply_model_to_session.hpp" #include "provider/cwd_model_override.hpp" #include "provider/model_resolver.hpp" #include "feedback/feedback_upload.hpp" diff --git a/src/commands/model_command.cpp b/src/commands/model_command.cpp index 8d9decb3b..2453fda93 100644 --- a/src/commands/model_command.cpp +++ b/src/commands/model_command.cpp @@ -5,7 +5,7 @@ #include "config/saved_models.hpp" #include "config/saved_models_editor.hpp" #include "config/settings_mutations.hpp" -#include "provider/apply_model_to_session.hpp" +#include "session_host/apply_model_to_session.hpp" #include "provider/cwd_model_override.hpp" #include "provider/model_context_resolver.hpp" #include "tui/model_picker.hpp" diff --git a/src/daemon/worker.cpp b/src/daemon/worker.cpp index 44c8c157f..e1e379b69 100644 --- a/src/daemon/worker.cpp +++ b/src/daemon/worker.cpp @@ -8,8 +8,8 @@ #include "ipc/daemon_protocol.hpp" #include "platform/native_ui/open_in_explorer.hpp" #include "version.hpp" -#include "tool/spawn_subagent_tool.hpp" -#include "tool/thread_tools.hpp" +#include "session_host/tools/spawn_subagent_tool.hpp" +#include "session_host/tools/thread_tools.hpp" #include "tool/workspace_tools.hpp" #include "workspace/workspace_registry.hpp" #include "experts/expert_registry.hpp" @@ -31,12 +31,12 @@ #include "platform/process/process_runner.hpp" #include "loop/loop_scheduler.hpp" #include "loop/loop_store.hpp" -#include "session/local_session_client.hpp" +#include "session_host/local_session_client.hpp" #include "session/global_session_catalog.hpp" -#include "session/session_registry.hpp" -#include "session/thread_service.hpp" -#include "session/task_suggestion_service.hpp" -#include "tool/task_suggestion_tools.hpp" +#include "session_host/session_registry.hpp" +#include "session_host/thread_service.hpp" +#include "session_host/task_suggestion_service.hpp" +#include "session_host/tools/task_suggestion_tools.hpp" #include "skills/skill_registry.hpp" #include "skills/skill_init.hpp" #include "skills/skill_usage_store.hpp" diff --git a/src/headless/headless_runner.cpp b/src/headless/headless_runner.cpp index 9e3c2fedb..5c7b9a188 100644 --- a/src/headless/headless_runner.cpp +++ b/src/headless/headless_runner.cpp @@ -16,20 +16,20 @@ #include "lsp/lsp_service.hpp" #include "network/proxy_resolver.hpp" #include "permissions/permissions.hpp" -#include "session/local_session_client.hpp" -#include "session/session_registry.hpp" -#include "session/thread_service.hpp" +#include "session_host/local_session_client.hpp" +#include "session_host/session_registry.hpp" +#include "session_host/thread_service.hpp" #include "skills/skill_init.hpp" #include "skills/skill_registry.hpp" #include "tool/ask_user_question_tool.hpp" #include "tool/builtin_tool_registry.hpp" #include "tool/tool_rewrites.hpp" #include "security/audit_log.hpp" -#include "tool/spawn_subagent_tool.hpp" +#include "session_host/tools/spawn_subagent_tool.hpp" #include "tool/skill_view_tool.hpp" #include "tool/skills_tool.hpp" #include "tool/tool_executor.hpp" -#include "tool/thread_tools.hpp" +#include "session_host/tools/thread_tools.hpp" #include "tool/workspace_tools.hpp" #include "tool/web_search/backend_router.hpp" #include "tool/web_search/region_detector.hpp" diff --git a/src/loop/loop_scheduler.cpp b/src/loop/loop_scheduler.cpp index d05dbdc97..3c3ccf8a4 100644 --- a/src/loop/loop_scheduler.cpp +++ b/src/loop/loop_scheduler.cpp @@ -1,7 +1,7 @@ #include "loop_scheduler.hpp" #include "config/config.hpp" -#include "session/session_registry.hpp" +#include "session_host/session_registry.hpp" #include "utils/logger.hpp" #include "utils/utf8_path.hpp" #include "utils/uuid.hpp" diff --git a/src/main.cpp b/src/main.cpp index 41b3e93c8..d1dbb1479 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -60,7 +60,7 @@ #include "provider/models_dev_registry.hpp" #include "provider/model_resolver.hpp" #include "provider/cwd_model_override.hpp" -#include "provider/apply_model_to_session.hpp" +#include "session_host/apply_model_to_session.hpp" #include "tool/tool_executor.hpp" #include "tool/bash_tool.hpp" #include "tool/builtin_tool_registry.hpp" @@ -75,7 +75,7 @@ #include "tool/goal_tool.hpp" #include "tool/mcp_manager.hpp" #include "tool/mcp_startup_coordination.hpp" -#include "tool/thread_tools.hpp" +#include "session_host/tools/thread_tools.hpp" #include "tool/workspace_tools.hpp" #include "tool/skills_tool.hpp" #include "tool/skill_view_tool.hpp" @@ -105,7 +105,7 @@ #include "permissions/permissions.hpp" #include "agent_loop.hpp" #include "tui/tui_ask_channel.hpp" -#include "session/thread_service.hpp" +#include "session_host/thread_service.hpp" #include "cli/interactive_options.hpp" #include "commands/configure.hpp" #include "daemon/cli.hpp" @@ -125,9 +125,9 @@ #include "session/token_tracker.hpp" #include "markdown/markdown_formatter.hpp" #include "session/session_manager.hpp" -#include "session/session_auto_title.hpp" +#include "session_host/session_auto_title.hpp" #include "tool_preamble/tool_preamble.hpp" -#include "session/session_registry.hpp" +#include "session_host/session_registry.hpp" #include "session/session_resume_restore.hpp" #include "worktree/worktree_core.hpp" #include "worktree/worktree_manager.hpp" @@ -174,7 +174,7 @@ #include "platform/terminal/terminal_theme_detect.hpp" #include "tui/subagent_host.hpp" #include "tui/terminal_key_event.hpp" -#include "tool/spawn_subagent_tool.hpp" +#include "session_host/tools/spawn_subagent_tool.hpp" #include "tui/todo_checklist_view.hpp" #include "tui/input_history_navigation.hpp" #include "tui/message_render_cache.hpp" diff --git a/src/remote_control/session_channel_binder.cpp b/src/remote_control/session_channel_binder.cpp index 3c3b0c655..8f4f4425e 100644 --- a/src/remote_control/session_channel_binder.cpp +++ b/src/remote_control/session_channel_binder.cpp @@ -1,6 +1,6 @@ #include "session_channel_binder.hpp" -#include "session/session_registry.hpp" +#include "session_host/session_registry.hpp" #include "session/system_notice.hpp" #include "utils/logger.hpp" diff --git a/src/session_host/apply_model_to_session.hpp b/src/session_host/apply_model_to_session.hpp index 104d25d96..2dd4c00cd 100644 --- a/src/session_host/apply_model_to_session.hpp +++ b/src/session_host/apply_model_to_session.hpp @@ -6,7 +6,7 @@ #include "config/config.hpp" #include "config/saved_models.hpp" #include "session/session_client.hpp" // for SessionModelState -#include "session_model_binding.hpp" +#include "provider/session_model_binding.hpp" #include #include diff --git a/src/session_host/local_session_client.cpp b/src/session_host/local_session_client.cpp index 001fe8675..c4651edef 100644 --- a/src/session_host/local_session_client.cpp +++ b/src/session_host/local_session_client.cpp @@ -1,9 +1,9 @@ #include "local_session_client.hpp" #include "environment/data_dir_migration.hpp" -#include "ask_user_question_prompter.hpp" -#include "session_storage.hpp" -#include "system_notice.hpp" +#include "session/ask_user_question_prompter.hpp" +#include "session/session_storage.hpp" +#include "session/system_notice.hpp" #include "utils/logger.hpp" namespace acecode { diff --git a/src/session_host/local_session_client.hpp b/src/session_host/local_session_client.hpp index 8a4431b41..fc0d7e588 100644 --- a/src/session_host/local_session_client.hpp +++ b/src/session_host/local_session_client.hpp @@ -7,7 +7,7 @@ // 浏览器侧的 RemoteSessionClient(走 HTTP/WebSocket 的 wire format)由 // add-web-chat-ui change 落地,接口契约相同。 -#include "session_client.hpp" +#include "session/session_client.hpp" #include "session_registry.hpp" #include diff --git a/src/session_host/session_registry.cpp b/src/session_host/session_registry.cpp index 93a956a0b..927ad4b1f 100644 --- a/src/session_host/session_registry.cpp +++ b/src/session_host/session_registry.cpp @@ -1,17 +1,17 @@ #include "session_registry.hpp" -#include "compact_checkpoint.hpp" -#include "session_rewind.hpp" -#include "session_resume_restore.hpp" -#include "session_storage.hpp" +#include "session/compact_checkpoint.hpp" +#include "session/session_rewind.hpp" +#include "session/session_resume_restore.hpp" +#include "session/session_storage.hpp" #include "session_auto_title.hpp" -#include "thread_goal_store.hpp" -#include "system_notice.hpp" -#include "tool_result_storage.hpp" -#include "turn_timing.hpp" +#include "session/thread_goal_store.hpp" +#include "session/system_notice.hpp" +#include "session/tool_result_storage.hpp" +#include "session/turn_timing.hpp" #include "prompt/init_prompt.hpp" #include "lsp/lsp_status_text.hpp" -#include "provider/apply_model_to_session.hpp" +#include "session_host/apply_model_to_session.hpp" #include "provider/cwd_model_override.hpp" #include "provider/model_context_resolver.hpp" #include "provider/model_pool_status.hpp" diff --git a/src/session_host/session_registry.hpp b/src/session_host/session_registry.hpp index ae2a953fb..2855e835d 100644 --- a/src/session_host/session_registry.hpp +++ b/src/session_host/session_registry.hpp @@ -24,10 +24,10 @@ #include "experts/expert_registry.hpp" #include "skills/skill_registry.hpp" #include "tool/tool_executor.hpp" -#include "ask_user_question_prompter.hpp" -#include "permission_prompter.hpp" -#include "session_client.hpp" -#include "session_manager.hpp" +#include "session/ask_user_question_prompter.hpp" +#include "session/permission_prompter.hpp" +#include "session/session_client.hpp" +#include "session/session_manager.hpp" #include #include diff --git a/src/session_host/session_title_generator.cpp b/src/session_host/session_title_generator.cpp index 0ddc92224..2da888a41 100644 --- a/src/session_host/session_title_generator.cpp +++ b/src/session_host/session_title_generator.cpp @@ -1,6 +1,6 @@ #include "session_title_generator.hpp" -#include "session_title_text.hpp" +#include "session/session_title_text.hpp" #include "platform/locale.hpp" #include "utils/encoding.hpp" diff --git a/src/session_host/task_suggestion_service.cpp b/src/session_host/task_suggestion_service.cpp index b4bb8d213..9835c26ab 100644 --- a/src/session_host/task_suggestion_service.cpp +++ b/src/session_host/task_suggestion_service.cpp @@ -1,11 +1,11 @@ #include "task_suggestion_service.hpp" -#include "compact_checkpoint.hpp" -#include "session_client.hpp" +#include "session/compact_checkpoint.hpp" +#include "session/session_client.hpp" #include "session_registry.hpp" -#include "task_suggestion_store.hpp" -#include "thread_goal_store.hpp" -#include "todo_state.hpp" +#include "session/task_suggestion_store.hpp" +#include "session/thread_goal_store.hpp" +#include "session/todo_state.hpp" #include "utils/encoding.hpp" #include "environment/data_dir_migration.hpp" #include "utils/logger.hpp" diff --git a/src/session_host/thread_service.cpp b/src/session_host/thread_service.cpp index 634ef5bd1..f31da4915 100644 --- a/src/session_host/thread_service.cpp +++ b/src/session_host/thread_service.cpp @@ -1,12 +1,12 @@ #include "thread_service.hpp" -#include "compact_checkpoint.hpp" -#include "global_session_catalog.hpp" -#include "session_manager.hpp" -#include "session_pin_store.hpp" +#include "session/compact_checkpoint.hpp" +#include "session/global_session_catalog.hpp" +#include "session/session_manager.hpp" +#include "session/session_pin_store.hpp" #include "session_registry.hpp" -#include "session_storage.hpp" -#include "session_user_message_search.hpp" -#include "thread_repair.hpp" +#include "session/session_storage.hpp" +#include "session/session_user_message_search.hpp" +#include "session/thread_repair.hpp" #include "llm/message_predicates.hpp" #include "llm/token_estimate.hpp" #include "utils/encoding.hpp" diff --git a/src/session_host/thread_service.hpp b/src/session_host/thread_service.hpp index b514cb4eb..f7faea608 100644 --- a/src/session_host/thread_service.hpp +++ b/src/session_host/thread_service.hpp @@ -1,6 +1,6 @@ #pragma once -#include "session_client.hpp" +#include "session/session_client.hpp" #include #include diff --git a/src/session_host/tools/spawn_subagent_tool.cpp b/src/session_host/tools/spawn_subagent_tool.cpp index 86d0fdc83..765c4fb0b 100644 --- a/src/session_host/tools/spawn_subagent_tool.cpp +++ b/src/session_host/tools/spawn_subagent_tool.cpp @@ -2,7 +2,7 @@ #include "config/config.hpp" #include "session/session_client.hpp" -#include "session/session_registry.hpp" +#include "session_host/session_registry.hpp" #include "skills/skill_init.hpp" #include "skills/skill_registry.hpp" #include "utils/encoding.hpp" diff --git a/src/session_host/tools/spawn_subagent_tool.hpp b/src/session_host/tools/spawn_subagent_tool.hpp index f08530ae8..9bc97699e 100644 --- a/src/session_host/tools/spawn_subagent_tool.hpp +++ b/src/session_host/tools/spawn_subagent_tool.hpp @@ -25,7 +25,7 @@ // tui::SubagentHost 提供进程内 SessionRegistry 后同样注册。TUI 主会话 // 不在 registry 里,权限模式经 fallback_permissions 继承。 -#include "tool_executor.hpp" +#include "tool/tool_executor.hpp" #include #include diff --git a/src/session_host/tools/task_suggestion_tools.cpp b/src/session_host/tools/task_suggestion_tools.cpp index ebbf0de46..093cb0ee6 100644 --- a/src/session_host/tools/task_suggestion_tools.cpp +++ b/src/session_host/tools/task_suggestion_tools.cpp @@ -1,7 +1,7 @@ #include "task_suggestion_tools.hpp" #include "session/session_manager.hpp" -#include "session/task_suggestion_service.hpp" +#include "session_host/task_suggestion_service.hpp" #include "tool/tool_args_parser.hpp" namespace acecode { diff --git a/src/session_host/tools/task_suggestion_tools.hpp b/src/session_host/tools/task_suggestion_tools.hpp index 3e36cea7d..2b85c5cb4 100644 --- a/src/session_host/tools/task_suggestion_tools.hpp +++ b/src/session_host/tools/task_suggestion_tools.hpp @@ -1,6 +1,6 @@ #pragma once -#include "tool_executor.hpp" +#include "tool/tool_executor.hpp" #include diff --git a/src/session_host/tools/thread_tools.cpp b/src/session_host/tools/thread_tools.cpp index 3f59afafb..c3c44248f 100644 --- a/src/session_host/tools/thread_tools.cpp +++ b/src/session_host/tools/thread_tools.cpp @@ -1,7 +1,7 @@ #include "thread_tools.hpp" #include "session/session_manager.hpp" -#include "session/thread_service.hpp" +#include "session_host/thread_service.hpp" #include "tool/tool_args_parser.hpp" #include diff --git a/src/session_host/tools/thread_tools.hpp b/src/session_host/tools/thread_tools.hpp index d87fd0c3a..924fbe149 100644 --- a/src/session_host/tools/thread_tools.hpp +++ b/src/session_host/tools/thread_tools.hpp @@ -1,6 +1,6 @@ #pragma once -#include "tool_executor.hpp" +#include "tool/tool_executor.hpp" #include diff --git a/src/tui/subagent_host.hpp b/src/tui/subagent_host.hpp index 1732c1335..c349e5c46 100644 --- a/src/tui/subagent_host.hpp +++ b/src/tui/subagent_host.hpp @@ -20,8 +20,8 @@ // mu_ 保护;所有对外交付都经 Deps 回调(由 main.cpp 负责 lock TuiState + // PostEvent)。本文件不依赖 FTXUI,编译进 acecode_testable 供单测。 -#include "session/local_session_client.hpp" -#include "session/session_registry.hpp" +#include "session_host/local_session_client.hpp" +#include "session_host/session_registry.hpp" #include #include diff --git a/src/web/routes/routes_security.cpp b/src/web/routes/routes_security.cpp index a20c9a075..1a111ce5a 100644 --- a/src/web/routes/routes_security.cpp +++ b/src/web/routes/routes_security.cpp @@ -15,7 +15,7 @@ #include "sandbox/exec_rules.hpp" #include "sandbox/sandbox_backend.hpp" #include "security/audit_log.hpp" -#include "session/session_registry.hpp" +#include "session_host/session_registry.hpp" #include "utils/utf8_path.hpp" #include diff --git a/src/web/routes/routes_tool_preamble.cpp b/src/web/routes/routes_tool_preamble.cpp index f7064753b..ca83f79d5 100644 --- a/src/web/routes/routes_tool_preamble.cpp +++ b/src/web/routes/routes_tool_preamble.cpp @@ -1,7 +1,7 @@ #include "web/server_impl.hpp" #include "web/handlers/tool_preamble_handler.hpp" #include "config/config_mutation.hpp" -#include "session/session_registry.hpp" +#include "session_host/session_registry.hpp" namespace acecode::web { using nlohmann::json; diff --git a/src/web/server_impl.hpp b/src/web/server_impl.hpp index 78146c50a..ea42104a5 100644 --- a/src/web/server_impl.hpp +++ b/src/web/server_impl.hpp @@ -24,12 +24,12 @@ #include "session/ask_user_question_prompter.hpp" #include "session/attachment_store.hpp" #include "session/global_session_search.hpp" -#include "session/local_session_client.hpp" +#include "session_host/local_session_client.hpp" #include "session/opencode_import.hpp" #include "session/session_attention.hpp" #include "session/session_client.hpp" -#include "session/session_registry.hpp" -#include "session/task_suggestion_service.hpp" +#include "session_host/session_registry.hpp" +#include "session_host/task_suggestion_service.hpp" #include "session/session_rewind.hpp" #include "session/session_serializer.hpp" #include "session/session_markdown_export.hpp" diff --git a/tests/channels/gateway_test.cpp b/tests/channels/gateway_test.cpp index 4d3926223..84e230b79 100644 --- a/tests/channels/gateway_test.cpp +++ b/tests/channels/gateway_test.cpp @@ -1,8 +1,8 @@ #include "test_support/channels/test_support.hpp" #include "channels/bridge.hpp" #include "session/attachment_store.hpp" -#include "session/local_session_client.hpp" -#include "session/session_registry.hpp" +#include "session_host/local_session_client.hpp" +#include "session_host/session_registry.hpp" #include #include #include diff --git a/tests/experts/expert_registry_test.cpp b/tests/experts/expert_registry_test.cpp index d217d8b40..2c4085954 100644 --- a/tests/experts/expert_registry_test.cpp +++ b/tests/experts/expert_registry_test.cpp @@ -4,7 +4,7 @@ #include "test_support/agent/stub_provider.hpp" #include "permissions/permissions.hpp" #include "prompt/system_prompt.hpp" -#include "session/session_registry.hpp" +#include "session_host/session_registry.hpp" #include "session/session_storage.hpp" #include "tool/tool_executor.hpp" #include "utils/utf8_path.hpp" diff --git a/tests/gitinfo/web_worktree_intent_test.cpp b/tests/gitinfo/web_worktree_intent_test.cpp index b72c28bd8..3f7159215 100644 --- a/tests/gitinfo/web_worktree_intent_test.cpp +++ b/tests/gitinfo/web_worktree_intent_test.cpp @@ -13,7 +13,7 @@ #include "permissions/permissions.hpp" #include "config/config.hpp" -#include "session/session_registry.hpp" +#include "session_host/session_registry.hpp" #include "tool/tool_executor.hpp" #include "worktree/worktree_manager.hpp" #include "utils/utf8_path.hpp" diff --git a/tests/loop/loop_scheduler_test.cpp b/tests/loop/loop_scheduler_test.cpp index 2e5f18b1c..8ddb7c33a 100644 --- a/tests/loop/loop_scheduler_test.cpp +++ b/tests/loop/loop_scheduler_test.cpp @@ -2,7 +2,7 @@ #include "config/config.hpp" #include "permissions/permissions.hpp" -#include "session/session_registry.hpp" +#include "session_host/session_registry.hpp" #include "tool/tool_executor.hpp" #include "utils/utf8_path.hpp" #include "utils/uuid.hpp" diff --git a/tests/remote_control/session_channel_binder_test.cpp b/tests/remote_control/session_channel_binder_test.cpp index 83a071f34..20bbc3a0f 100644 --- a/tests/remote_control/session_channel_binder_test.cpp +++ b/tests/remote_control/session_channel_binder_test.cpp @@ -278,8 +278,8 @@ TEST(KeepaliveDecider, HealthProbeDueAfterInterval) { #include "remote_control/remote_control_service.hpp" #include "config/config.hpp" #include "permissions/permissions.hpp" -#include "session/local_session_client.hpp" -#include "session/session_registry.hpp" +#include "session_host/local_session_client.hpp" +#include "session_host/session_registry.hpp" #include "tool/ask_user_question_tool.hpp" #include "tool/tool_executor.hpp" diff --git a/tests/session/session_title_test.cpp b/tests/session/session_title_test.cpp index fc98ec649..3a56dcdd8 100644 --- a/tests/session/session_title_test.cpp +++ b/tests/session/session_title_test.cpp @@ -2,9 +2,9 @@ #include "platform/locale.hpp" #include "session/session_manager.hpp" -#include "session/session_auto_title.hpp" +#include "session_host/session_auto_title.hpp" #include "session/session_storage.hpp" -#include "session/session_title_generator.hpp" +#include "session_host/session_title_generator.hpp" #include "session/session_title_text.hpp" #include diff --git a/tests/session_host/apply_model_to_session_test.cpp b/tests/session_host/apply_model_to_session_test.cpp index 1870730c7..1b15a0f4b 100644 --- a/tests/session_host/apply_model_to_session_test.cpp +++ b/tests/session_host/apply_model_to_session_test.cpp @@ -12,7 +12,7 @@ #include -#include "provider/apply_model_to_session.hpp" +#include "session_host/apply_model_to_session.hpp" #include "config/config.hpp" #include "config/saved_models.hpp" diff --git a/tests/session_host/session_registry_test.cpp b/tests/session_host/session_registry_test.cpp index e7de041ce..f21f4e6dd 100644 --- a/tests/session_host/session_registry_test.cpp +++ b/tests/session_host/session_registry_test.cpp @@ -20,9 +20,9 @@ #include "hooks/hook_runtime.hpp" #include "prompt/system_prompt.hpp" #include "session/compact_checkpoint.hpp" -#include "session/local_session_client.hpp" +#include "session_host/local_session_client.hpp" #include "session/session_manager.hpp" -#include "session/session_registry.hpp" +#include "session_host/session_registry.hpp" #include "session/session_storage.hpp" #include "session/thread_goal_store.hpp" #include "tool/goal_tool.hpp" diff --git a/tests/session_host/spawn_subagent_tool_test.cpp b/tests/session_host/spawn_subagent_tool_test.cpp index af0f47f01..28eb34bef 100644 --- a/tests/session_host/spawn_subagent_tool_test.cpp +++ b/tests/session_host/spawn_subagent_tool_test.cpp @@ -14,10 +14,10 @@ #include "config/config.hpp" #include "experts/expert_registry.hpp" #include "permissions/permissions.hpp" -#include "session/local_session_client.hpp" -#include "session/session_registry.hpp" +#include "session_host/local_session_client.hpp" +#include "session_host/session_registry.hpp" #include "session/session_storage.hpp" -#include "tool/spawn_subagent_tool.hpp" +#include "session_host/tools/spawn_subagent_tool.hpp" #include "tool/tool_executor.hpp" #include "utils/utf8_path.hpp" #include "worktree/worktree_manager.hpp" diff --git a/tests/session_host/task_suggestion_service_test.cpp b/tests/session_host/task_suggestion_service_test.cpp index 4f66d8fc0..21c2eb628 100644 --- a/tests/session_host/task_suggestion_service_test.cpp +++ b/tests/session_host/task_suggestion_service_test.cpp @@ -1,9 +1,9 @@ #include #include "session/compact_checkpoint.hpp" -#include "session/local_session_client.hpp" -#include "session/session_registry.hpp" -#include "session/task_suggestion_service.hpp" +#include "session_host/local_session_client.hpp" +#include "session_host/session_registry.hpp" +#include "session_host/task_suggestion_service.hpp" #include "session/task_suggestion_store.hpp" #include "session/thread_goal_store.hpp" #include "utils/utf8_path.hpp" diff --git a/tests/session_host/thread_tools_test.cpp b/tests/session_host/thread_tools_test.cpp index ec9a37be3..b379962b6 100644 --- a/tests/session_host/thread_tools_test.cpp +++ b/tests/session_host/thread_tools_test.cpp @@ -1,12 +1,12 @@ #include #include "session/session_manager.hpp" -#include "session/local_session_client.hpp" +#include "session_host/local_session_client.hpp" #include "session/session_pin_store.hpp" -#include "session/session_registry.hpp" +#include "session_host/session_registry.hpp" #include "session/session_storage.hpp" -#include "session/thread_service.hpp" -#include "tool/thread_tools.hpp" +#include "session_host/thread_service.hpp" +#include "session_host/tools/thread_tools.hpp" #include "utils/paths.hpp" #include diff --git a/tests/web/summary_generation_handler_test.cpp b/tests/web/summary_generation_handler_test.cpp index a925a93c0..5b52cdb51 100644 --- a/tests/web/summary_generation_handler_test.cpp +++ b/tests/web/summary_generation_handler_test.cpp @@ -1,5 +1,5 @@ #include "web/handlers/summary_generation_handler.hpp" -#include "session/session_auto_title.hpp" +#include "session_host/session_auto_title.hpp" #include "utils/utf8_path.hpp" #include diff --git a/tests/web/web_server_smoke_test.cpp b/tests/web/web_server_smoke_test.cpp index 5f285ee51..9f00106a2 100644 --- a/tests/web/web_server_smoke_test.cpp +++ b/tests/web/web_server_smoke_test.cpp @@ -41,18 +41,18 @@ #include "tool/tool_rewrites.hpp" #include "provider/cwd_model_override.hpp" #include "provider/models_dev_registry.hpp" -#include "session/local_session_client.hpp" +#include "session_host/local_session_client.hpp" #include "session/attachment_store.hpp" #include "session/pasted_text_attachment.hpp" #include "session/session_manager.hpp" #include "session/session_pin_store.hpp" -#include "session/session_registry.hpp" -#include "session/session_auto_title.hpp" +#include "session_host/session_registry.hpp" +#include "session_host/session_auto_title.hpp" #include "session/session_storage.hpp" #include "session/session_trajectory.hpp" #include "session/session_user_message_search.hpp" #include "session/todo_state.hpp" -#include "session/task_suggestion_service.hpp" +#include "session_host/task_suggestion_service.hpp" #include "session/session_usage_ledger.hpp" #include "skills/skill_registry.hpp" #include "test_support/repo_root.hpp" @@ -61,7 +61,7 @@ #include "tool/ask_user_question_tool.hpp" #include "tool/mtime_tracker.hpp" #include "tool/tool_executor.hpp" -#include "tool/task_suggestion_tools.hpp" +#include "session_host/tools/task_suggestion_tools.hpp" #include "upgrade/manifest.hpp" #include "test_support/agent/stub_provider.hpp" #include "utils/base64.hpp" From a5cad80df6897fe7ec22ba4bdbdfba5999aefec5 Mon Sep 17 00:00:00 2001 From: tmoonlight Date: Mon, 28 Sep 2026 11:09:41 +0800 Subject: [PATCH 4/6] =?UTF-8?q?refactor20260927(layers/P2-07):=20=E5=90=8C?= =?UTF-8?q?=E6=AD=A5=E5=AE=BF=E4=B8=BB=E8=B7=AF=E5=BE=84=E4=B8=8E=E9=AA=8C?= =?UTF-8?q?=E8=AF=81=E8=AE=B0=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .git-blame-ignore-revs | 4 ++++ AGENTS.md | 16 +++++++------- CLAUDE.md | 8 +++---- docs/desktop-shell/multi-workspace.md | 2 +- docs/help-source/group7.py | 2 +- docs/help-source/sources.json | 2 +- docs/subagents.md | 6 +++--- ...56\214\346\225\264\347\211\210_2026-08.md" | 2 +- .../branch-inventory.md | 6 ++++++ .../layout-map.md | 3 +++ .../verification/P2-07-session-host.md | 21 +++++++++++++++++++ 11 files changed, 53 insertions(+), 19 deletions(-) create mode 100644 openspec/changes/refactor20260927-restructure-src-layers/verification/P2-07-session-host.md diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 2fea1425b..e7c7de46a 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -22,3 +22,7 @@ bb94c967934cf3c355a143435bc2b292a917275f # P2-06 纯搬迁与 include 改写(2026-09-28) 51c37dea8be09e73b823f0799cef301352fbf2ff f095f76256a47f86a403a66fde160e8d96782039 + +# P2-07 纯搬迁与 include 改写(2026-09-28) +dfe8a5c14113d68822ff78ecaeddac965c11fb51 +88d0e6d212d726685dc664c9a5a1bf9593af5e3b diff --git a/AGENTS.md b/AGENTS.md index 301292637..26f57f1c8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -56,19 +56,19 @@ This is a terminal UI project. Avoid emoji or ambiguous-width glyphs in C++ sour 以下 C1–C14 约定来自 [refactor20260927 所有权设计](openspec/changes/refactor20260927-adopt-ownership-conventions/design.md)。新代码遵守这些约定;存量问题按该系列任务逐项整改,不借此扩大一期范围。下面的反例指向重构前的实现,搬迁时同步更新路径。 1. **C1 独占所有权**:拥有对象用 `std::unique_ptr`,包括 pimpl。裸 `new` 只允许用于私有构造工厂中的 `std::unique_ptr(new T)`,禁止裸 `delete`。只有确实共享寿命才用 `shared_ptr`,声明处说明共享方与原因。反例:[web_search/runtime.hpp](src/tool/web_search/runtime.hpp) 的 `Impl* impl_` 在 [runtime.cpp](src/tool/web_search/runtime.cpp) 中手工 new/delete。 -2. **C2 构造注入**:必填依赖用构造注入的引用成员;可选指针注明 nullable、borrowed。构造完成后不再更换依赖。反例:[SessionRegistryDeps](src/session/session_registry.hpp) 的 `tools` 可空,但 [make_entry_locked](src/session/session_registry.cpp) 会解引用它。存量依赖结构改为引用注入属于二期。 -3. **C3 借用不跨调用**:形参与 `ToolContext` 中的 `T*`/`T&` 不得保存为成员或被异步回调、线程捕获;跨调用使用持有寿命的快照。反例:[spawn_subagent_tool.cpp](src/tool/spawn_subagent_tool.cpp) 把 `child->skill_registry.get()` 存入 `child_skills`,在持有 child 的局部 `shared_ptr` 离开作用域后继续使用。 -4. **C4 共享只读快照**:共享只读数据使用 `shared_ptr`,只由一个发布点更新;不把共享可变对象的子对象地址交给其它线程。反例:[session_registry.cpp](src/session/session_registry.cpp) 的 `set_project_instructions_config(deps_.project_instructions_cfg)` 保存借用配置,而 [settings_mutations.cpp](src/config/settings_mutations.cpp) 可以整份替换配置。 -5. **C5 异步捕获**:listener、control、AgentCallbacks、工具闭包和线程入口只能捕获值、自有状态的 `shared_ptr`、`weak_ptr` 或 `LifetimeRef`,禁止裸指针、`[this]`、`[&]`。唯一例外是回调由宿主持有的 `JoiningThread` 执行,且在所依赖成员析构前 join。反例:[session_registry.cpp](src/session/session_registry.cpp) 的 `on_turn_finished = [this, id]` 与 [worker.cpp](src/daemon/worker.cpp) 的 on_spawn 对 server 的引用捕获。 -6. **C6 不形成自持环**:对象自己的队列或成员回调不得强持有该对象;捕获 `weak_ptr`,执行时 lock 并核对身份。反例:[session_registry.cpp](src/session/session_registry.cpp) 的 `enqueue_control([entry, loop, ...])` 形成 entry → loop → 队列 → lambda → entry。 +2. **C2 构造注入**:必填依赖用构造注入的引用成员;可选指针注明 nullable、borrowed。构造完成后不再更换依赖。反例:[SessionRegistryDeps](src/session_host/session_registry.hpp) 的 `tools` 可空,但 [make_entry_locked](src/session_host/session_registry.cpp) 会解引用它。存量依赖结构改为引用注入属于二期。 +3. **C3 借用不跨调用**:形参与 `ToolContext` 中的 `T*`/`T&` 不得保存为成员或被异步回调、线程捕获;跨调用使用持有寿命的快照。反例:[spawn_subagent_tool.cpp](src/session_host/tools/spawn_subagent_tool.cpp) 把 `child->skill_registry.get()` 存入 `child_skills`,在持有 child 的局部 `shared_ptr` 离开作用域后继续使用。 +4. **C4 共享只读快照**:共享只读数据使用 `shared_ptr`,只由一个发布点更新;不把共享可变对象的子对象地址交给其它线程。反例:[session_registry.cpp](src/session_host/session_registry.cpp) 的 `set_project_instructions_config(deps_.project_instructions_cfg)` 保存借用配置,而 [settings_mutations.cpp](src/config/settings_mutations.cpp) 可以整份替换配置。 +5. **C5 异步捕获**:listener、control、AgentCallbacks、工具闭包和线程入口只能捕获值、自有状态的 `shared_ptr`、`weak_ptr` 或 `LifetimeRef`,禁止裸指针、`[this]`、`[&]`。唯一例外是回调由宿主持有的 `JoiningThread` 执行,且在所依赖成员析构前 join。反例:[session_registry.cpp](src/session_host/session_registry.cpp) 的 `on_turn_finished = [this, id]` 与 [worker.cpp](src/daemon/worker.cpp) 的 on_spawn 对 server 的引用捕获。 +6. **C6 不形成自持环**:对象自己的队列或成员回调不得强持有该对象;捕获 `weak_ptr`,执行时 lock 并核对身份。反例:[session_registry.cpp](src/session_host/session_registry.cpp) 的 `enqueue_control([entry, loop, ...])` 形成 entry → loop → 队列 → lambda → entry。 7. **C7 订阅即资源**:使用 move-only 的 `ScopedSubscription`,声明在被捕获对象之后;析构时退订并等待在途投递。持有 listener 需要的锁时不得析构订阅。反例:[headless_runner.cpp](src/headless/headless_runner.cpp) 在 `subscribe` 后的 send_input 失败分支早退,绕过末尾 `unsubscribe`。 -8. **C8 线程有宿主**:禁止新增裸 `std::thread` 成员、局部变量与直接 detach。长期线程用 `JoiningThread`,短任务用 `ReapingThreadSet`;可放弃等待的阻塞工作只经 `run_abandonable` / `spawn_owned_detached`。原语实现自身的 detach 例外须封装并记录。反例:[routes_workspaces.cpp](src/web/routes/routes_workspaces.cpp) 的导入线程 detach,以及 [session_registry.hpp](src/session/session_registry.hpp) 只增不减的 `vector`。 +8. **C8 线程有宿主**:禁止新增裸 `std::thread` 成员、局部变量与直接 detach。长期线程用 `JoiningThread`,短任务用 `ReapingThreadSet`;可放弃等待的阻塞工作只经 `run_abandonable` / `spawn_owned_detached`。原语实现自身的 detach 例外须封装并记录。反例:[routes_workspaces.cpp](src/web/routes/routes_workspaces.cpp) 的导入线程 detach,以及 [session_registry.hpp](src/session_host/session_registry.hpp) 只增不减的 `vector`。 9. **C9 进程级服务**:由组合根的 RAII Scope 管理 init/shutdown;全局访问只返回 `shared_ptr` 租约,不采用 `is_initialized()` 后再 `service()` 的两步访问。反例:[lsp_tool.cpp](src/tool/lsp_tool.cpp) 的两步式 LSP 获取。存量服务租约化在二期,一期新代码先遵守。 10. **C10 关停顺序**:停入口 → 停回合生产者 → `SessionRegistry::shutdown_all()` → MCP → LSP/web_search → 其余,由成员声明顺序或显式关停序列固化。反例:[worker.cpp](src/daemon/worker.cpp) 在 registry 析构前关闭 MCP/LSP;D6 按所有权 change 单独提交。 -11. **C11 析构契约**:持有 worker 的成员必须声明在其依赖成员之后;做不到时写显式析构函数先停 worker。反例:[SessionEntry](src/session/session_registry.hpp) 的 ask_prompter 比 loop 先析构;[SubagentHost](src/tui/subagent_host.hpp) 的 registry 比其回调访问的 mu_、running_ 后析构。 +11. **C11 析构契约**:持有 worker 的成员必须声明在其依赖成员之后;做不到时写显式析构函数先停 worker。反例:[SessionEntry](src/session_host/session_registry.hpp) 的 ask_prompter 比 loop 先析构;[SubagentHost](src/tui/subagent_host.hpp) 的 registry 比其回调访问的 mu_、running_ 后析构。 12. **C12 不延迟绑定依赖**:依赖 registry 的工具应在 registry 构造后注册,闭包捕获 `weak_ptr`。反例:[headless_runner.cpp](src/headless/headless_runner.cpp) 先注册工具、再回填 `subagent_deps->registry` 与 `thread_deps->service`。一期 O-06 只修退出后的悬垂,注册顺序重构留二期。 13. **C13 句柄 RAII**:OS 与第三方句柄使用 move-only 封装,不通过 `void*` 出参交付所有权,不在多个出口手工 Close。反例:[sandbox_backend.hpp](src/sandbox/sandbox_backend.hpp) 的 `void*` 句柄接口与 [computer_use/runtime.cpp](src/computer_use/runtime.cpp) 可复制的 Handle。 -14. **C14 锁序**:LifetimeToken、AbortSignal、GoalRuntime 的内部状态锁是叶子锁,持锁时不进入 registry、AgentLoop 或业务回调;持有 `model_control_mu` 时不得获取队列门。现有风险入口是 [session_registry.cpp](src/session/session_registry.cpp) 的 `switch_model` 在 model 锁内调用 `apply_model_to_session`;迁移时不能在这条调用链内新增队列加锁。该文件的 `set_reasoning_effort` 已明确 queue → model → binding → metadata 的正确顺序,不得反转。 +14. **C14 锁序**:LifetimeToken、AbortSignal、GoalRuntime 的内部状态锁是叶子锁,持锁时不进入 registry、AgentLoop 或业务回调;持有 `model_control_mu` 时不得获取队列门。现有风险入口是 [session_registry.cpp](src/session_host/session_registry.cpp) 的 `switch_model` 在 model 锁内调用 `apply_model_to_session`;迁移时不能在这条调用链内新增队列加锁。该文件的 `set_reasoning_effort` 已明确 queue → model → binding → metadata 的正确顺序,不得反转。 所有权棘轮 R15 的度量与一期目标以所有权设计为准。新增原语必须有覆盖取消、关停、回调在途与析构顺序的测试;既有豁免与二期待办不得通过新增同类问题扩大。 diff --git a/CLAUDE.md b/CLAUDE.md index 49937b170..2c93612f5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -141,7 +141,7 @@ generates one standard-quality image and must never run on load or save. Core tools are registered for both TUI and daemon paths: `bash`, `file_read`, `file_write`, `file_edit`, `grep`, `glob`, `task_complete`, `AskUserQuestion`, skill tools, memory tools, optional `web_search`, and MCP tools. `ToolResult` can carry summaries and hunks so TUI/web resume can render useful compact rows instead of raw output folds. -`spawn_subagent` / `wait_subagent` are registered in both daemon (`worker.cpp`) and TUI (via `src/tui/subagent_host.{hpp,cpp}` — SessionRegistry/LocalSessionClient have no web dependency, so the TUI process instantiates them directly; the TUI main session lives outside the registry, so its permission mode reaches children through `SubagentToolDeps::fallback_permissions`, and `on_spawn` lets the host register tasks + subscribe child events): a sub-agent is a normal SessionRegistry session (isolated context) created with the parent's cwd and permission mode. `spawn_subagent(prompt, wait=true)` blocks until the child turn finishes and returns its final assistant reply into the parent context; `wait=false` is fire-and-forget for pipeline handoff, joined later via `wait_subagent(session_id)`. Prompts starting with `/` go through the same skill-command expansion as Web input. Sub-agents cannot spawn further sub-agents (`SessionEntry::subagent_depth`). Implementation: [src/tool/spawn_subagent_tool.cpp](src/tool/spawn_subagent_tool.cpp); deps are late-bound via shared_ptr because ToolExecutor is constructed before SessionRegistry in worker.cpp. +`spawn_subagent` / `wait_subagent` are registered in both daemon (`worker.cpp`) and TUI (via `src/tui/subagent_host.{hpp,cpp}` — SessionRegistry/LocalSessionClient have no web dependency, so the TUI process instantiates them directly; the TUI main session lives outside the registry, so its permission mode reaches children through `SubagentToolDeps::fallback_permissions`, and `on_spawn` lets the host register tasks + subscribe child events): a sub-agent is a normal SessionRegistry session (isolated context) created with the parent's cwd and permission mode. `spawn_subagent(prompt, wait=true)` blocks until the child turn finishes and returns its final assistant reply into the parent context; `wait=false` is fire-and-forget for pipeline handoff, joined later via `wait_subagent(session_id)`. Prompts starting with `/` go through the same skill-command expansion as Web input. Sub-agents cannot spawn further sub-agents (`SessionEntry::subagent_depth`). Implementation: [src/session_host/tools/spawn_subagent_tool.cpp](src/session_host/tools/spawn_subagent_tool.cpp); deps are late-bound via shared_ptr because ToolExecutor is constructed before SessionRegistry in worker.cpp. **TUI surface**: the right sidebar's "Background Tasks" section lists running sub-agents only (● title + elapsed; removed the moment the child turn ends — user decision); `/tasks [list|abort |clear]` is the operation entry (clear = same permanent-purge semantics as Web, via `SessionStorage::purge_session_files`). A child's `permission_request` is queued (`TuiState::remote_confirm_queue`) and pumped into the confirm overlay when free (origin-labelled; the answer routes back via `SubagentHost::respond_permission`). AskUserQuestion needs no bridging — children share the TUI ToolExecutor, so the TUI ask tool runs directly; it now queues on `TuiState::overlay_cv` until the confirm/ask overlay is free and sets `ask_origin_label` when the caller is a sub-agent. `on_tool_confirm` in main.cpp queues on the same cv, so concurrent overlay claims (main confirm / child ask / remote pump) serialize instead of clobbering each other. @@ -229,7 +229,7 @@ Canonical shared transcripts are protected by a lightweight writer lease under t **会话标题有两份且内存那份优先(改名回滚的坑)**:`SessionRegistry::list_active()` 报的是 `SessionManager` 内存里的 title,`session_info_to_json` 让它盖过磁盘 meta。所以任何**只写磁盘**的改名都会被内存那份盖掉:活跃会话的下一次 `update_meta()` 把旧标题写回去,列表也立刻显示旧标题 —— 用户看到的就是「重命名无效」。两条路径会走到只写磁盘:(1) `set_session_title_response` 的 workspace 归属判定失败(`no_workspace` 会话永远匹配不上任何 workspace,cwd 形态不同也匹配不上),(2) Desktop 每个 workspace 一个 daemon、只有 active 的那个服务 UI,改别的 workspace 的会话时落到的是没有这份内存的 daemon。修法两端都在:web 层在磁盘分支后按 id 同步本进程的活跃 entry;`SessionManager::update_meta` 写前调 `adopt_foreign_user_title_locked()`,除本进程正在提交的显式用户改名外都采纳磁盘上的 `user`/`user-cleared` 标题,因此已手动命名过的会话也能继续跨 daemon 改名;`session_info_to_json` 里磁盘 user 标题压过旧的内存标题。回归测试:`tests/session/session_title_test.cpp` + `tests/web/web_server_smoke_test.cpp` 的 title 用例。 -P2-07 将可复用的文本逻辑归回实际模块:标题清洗与空输入判定在 `src/session/session_title_text.{hpp,cpp}`,终端标题 OS 调用仍在 `src/platform/terminal/terminal_title`;`/init` 的共享提示词在 `src/prompt/init_prompt`,LSP 状态格式化与子命令分发在 `src/lsp/lsp_status_text`。TUI 命令层仅负责注册与交互适配,会话宿主直接依赖这些共享接口。 +P2-07 将可复用的文本逻辑归回实际模块:标题清洗与空输入判定在 `src/session/session_title_text.{hpp,cpp}`,终端标题 OS 调用仍在 `src/platform/terminal/terminal_title.hpp`;`/init` 的共享提示词在 `src/prompt/init_prompt.hpp`,LSP 状态格式化与子命令分发在 `src/lsp/lsp_status_text.hpp`。TUI 命令层仅负责注册与交互适配,会话宿主直接依赖这些共享接口。 **会话显示标题只有一个字段,前端不许从消息正文现推。** 侧栏、顶部标题栏、网格卡片、搜索面板、托盘菜单显示的都是 `sessionDisplayTitle(session)`:服务端 `title`(用户改名 / 大模型生成,generated 的 `[Error]` 标题忽略)优先,否则 `summary`。`summary` 由 `SessionStorage::summarize_user_message_text` 从最近一条可见用户消息的**显示文本**(`metadata.display_text` 优先于 `content`,`SessionStorage::visible_user_message_text`)折成单行、UTF-8 安全截到 80 字节 + "..."; `SessionManager` 的内存值与 `list_sessions` 补齐旧 meta 走同一实现。实时同步只有一条路:用户消息落盘后 AgentLoop 发 `session_updated{summary}`(`emit_session_summary_updated`,正常回合与 soft-steer 插话都发),大模型标题就绪时 SessionRegistry 发 `session_updated{title,title_source}`,`list_active` 与 `GET /messages?since=0` 快照都带这三个字段;Web 的 transcript store(`transcriptDisplayTitle`)与 Sidebar 列表对同一事件做同样的合并。两条被删掉的歧路别再加回来:(1) transcript 加载历史后用 `titleFromMessages` 把**最后一条 user 消息全文**当顶部标题 —— 会话 20260923-163126-e7e3 的首条消息是 `@session` 引用展开后的 9k 字符长文,顶部标题就是它、长度不受限,侧栏却显示 80 字节的 `summary`,而 `summary` 又是按展开后的 `content` 截的 "Referenced ACECode session context follows...";(2) 侧栏 hover 跑马灯再拉整份消息把标题「水合」成全文(`sidebarFullTitle.js`,已删)。ChatView 发送时也不再本地改写标题。回归:`tests/session/session_summary_test.cpp`、`tests/agent_loop/agent_loop_session_summary_event_test.cpp`、前端 `sessionTitleSingleSourceArchitecture.test.js` + `sessionTranscript.test.js` 的「会话显示标题」组。 @@ -561,7 +561,7 @@ SidePanel 折叠 UI:`ChatView` 把 `SidePanel` 包到 `
:<原标题>`(N=同源 sibling+1,原标题截 50 codepoint)。继承 cwd/provider/model,**不**继承 file_checkpoints。实现:`src/web/handlers/fork_handler.{hpp,cpp}`(纯函数 compute_fork_title + find_message_index_by_id) + `SessionManager::fork_session_to_new_id` | -| `GET /api/models` / `POST /api/sessions/:id/model` / `POST /api/sessions/:id/model/reload` | 模型下拉:`saved_models`;每个 session 自带独立 `SessionModelBinding`,显式切换与按需重载共用同一 install/revalidation 路径;`apply_model_to_session`(`src/provider/apply_model_to_session.{hpp,cpp}`)仅保留为兼容适配器— TUI 与 daemon 共用同一生命周期实现 | +| `GET /api/models` / `POST /api/sessions/:id/model` / `POST /api/sessions/:id/model/reload` | 模型下拉:`saved_models`;每个 session 自带独立 `SessionModelBinding`,显式切换与按需重载共用同一 install/revalidation 路径;`apply_model_to_session`(`src/session_host/apply_model_to_session.{hpp,cpp}`)仅保留为兼容适配器— TUI 与 daemon 共用同一生命周期实现 | | `POST` `/api/models` body `SavedModelDraft` / `PUT /api/models/` / `DELETE /api/models/` / `POST /api/config/default-model` body `{name}` | saved_models 增删改 + 默认设置。失败时 cfg 内存与磁盘保持原子(handler 持快照,save_config 抛异常即回滚)。响应永不携带 api_key 字段 | | `GET /api/history?cwd=&max=` / `POST /api/history` | per-cwd 输入历史,与 TUI 共享同一份 `/input_history.jsonl`,经 `InputHistoryStore::append` atomic rename | | `PUT /api/skills/:name` body `{enabled}` / `GET /api/skills/:name/body` | 启停切换 + 查看 SKILL.md;PUT 写 `cfg.skills.disabled` 数组并 `save_config` + `SkillRegistry::reload` | @@ -824,7 +824,7 @@ At startup and on every `--resume` / `/resume`, `src/provider/model_resolver.cpp If no saved model is configured, normal startup/session creation fails instead of synthesizing a fallback model. -`LlmProvider` 由每个会话的 `SessionModelBinding` 持有;TUI 与 daemon 都从 binding 取得 `shared_ptr` 快照。`AgentLoop` 在 turn 开始时捕获快照,所以 binding 发布新 Provider 后,进行中的 turn 仍由旧实例保活到结束,后续 turn 才看到新实例。显式切换、revision 驱动的 lazy reload 与 REST forced reload 共用 binding 的 install/revalidation 路径;`src/provider/apply_model_to_session.cpp`只是薄兼容适配器。Copilot 静默登录失败和非致命 meta 写盘失败均返回固定、脱敏 warning,不会回滚已发布 Provider。 +`LlmProvider` 由每个会话的 `SessionModelBinding` 持有;TUI 与 daemon 都从 binding 取得 `shared_ptr` 快照。`AgentLoop` 在 turn 开始时捕获快照,所以 binding 发布新 Provider 后,进行中的 turn 仍由旧实例保活到结束,后续 turn 才看到新实例。显式切换、revision 驱动的 lazy reload 与 REST forced reload 共用 binding 的 install/revalidation 路径;`src/session_host/apply_model_to_session.cpp`只是薄兼容适配器。Copilot 静默登录失败和非致命 meta 写盘失败均返回固定、脱敏 warning,不会回滚已发布 Provider。 ### `/model` command diff --git a/docs/desktop-shell/multi-workspace.md b/docs/desktop-shell/multi-workspace.md index f71b73fe8..681843e9c 100644 --- a/docs/desktop-shell/multi-workspace.md +++ b/docs/desktop-shell/multi-workspace.md @@ -71,7 +71,7 @@ daemon slot 已启动,并向 daemon `POST /api/workspaces` 注册 cwd,返回同 | `src/desktop/web_host.{hpp,cpp}` | webview/webview wrapper,暴露 `bind` / `eval` / `native_window` | | `src/desktop/main.cpp` | wWinMain 入口,串起所有 | | `src/utils/cwd_hash.{hpp,cpp}` | desktop 与 SessionStorage 共享的 hash 算法(FNV-1a 64bit) | -| `src/session/session_registry.{hpp,cpp}` | workspace-aware session create/resume/list | +| `src/session_host/session_registry.{hpp,cpp}` | workspace-aware session create/resume/list | | `src/web/server.{hpp,cpp}` | `/api/workspaces` 与 workspace-scoped session routes | | `web/src/components/Sidebar.jsx` | workspace 分组渲染 + workspace-scoped session 选择 | | `web/src/lib/api.js` | workspace-scoped list/create/resume helper | diff --git a/docs/help-source/group7.py b/docs/help-source/group7.py index 91686cbb0..d8a0bc039 100644 --- a/docs/help-source/group7.py +++ b/docs/help-source/group7.py @@ -32,7 +32,7 @@ '''
  1. 入口接收用户输入与上下文,并定位目标会话和工作目录。
  2. 保存用户消息,按当前模型绑定构造请求。
  3. Provider 将统一消息转换为对应服务协议,并返回文本、推理或工具调用。
  4. 工具经过权限与运行条件检查后执行,结果追加到会话。
  5. 智能体根据结果继续下一次模型调用,或生成最终回复。
  6. 终态、用量与消息通过事件更新前端,持久记录用于后续恢复。

用户界面中的“排队成功”只是输入被接受,任务完成还需要观察对应回合的终态。模型重试和上下文压缩也会产生状态变化,不能把每一次 busy 变化都视为成功完成。

'''), section("boundaries", "修改时保持的边界", '''

可复用的解析、校验和状态机放在对应共享模块,不把终端渲染对象带入后台接口。HTTP 处理应复用路由解析与错误响应辅助函数,模型变更应经过当前会话模型绑定路径。

Web 源码在 web/src,构建结果由工具生成。后台会话状态与前端临时草稿分开维护;文件修改又有独立的磁盘副作用,不能只更新界面就视作状态持久化。

新增行为需要检查 TUI、CLI、Web 和桌面是否使用了不同入口。协议变化同步维护 API 说明,关键持久字段补兼容性验证。

''') -], ["ARCHITECTURE.md", "CMakeLists.txt", "src/main.cpp", "src/agent_loop.cpp", "src/session/session_registry.cpp", "src/tool/tool_executor.hpp", "src/provider/session_model_binding.hpp"]), +], ["ARCHITECTURE.md", "CMakeLists.txt", "src/main.cpp", "src/agent_loop.cpp", "src/session_host/session_registry.cpp", "src/tool/tool_executor.hpp", "src/provider/session_model_binding.hpp"]), "daemon": page("独立 daemon 提供 HTTP 与 WebSocket 服务。根据个人后台、调试或系统服务场景选择生命周期。", [ section("lifecycle", "启动、查看与停止", diff --git a/docs/help-source/sources.json b/docs/help-source/sources.json index 92c0b1acc..a229753c3 100644 --- a/docs/help-source/sources.json +++ b/docs/help-source/sources.json @@ -468,7 +468,7 @@ "CMakeLists.txt", "src/main.cpp", "src/agent_loop.cpp", - "src/session/session_registry.cpp", + "src/session_host/session_registry.cpp", "src/tool/tool_executor.hpp", "src/provider/session_model_binding.hpp" ] diff --git a/docs/subagents.md b/docs/subagents.md index 1a9c3faa7..0777d02db 100644 --- a/docs/subagents.md +++ b/docs/subagents.md @@ -80,10 +80,10 @@ | 文件 | 职责 | |---|---| -| `src/tool/spawn_subagent_tool.{hpp,cpp}` | 两个工具本体 + `SubagentToolDeps{registry, client, config, fallback_permissions, on_spawn}`。daemon 里 deps 用 shared_ptr 延迟回填(ToolExecutor 先于 SessionRegistry 构造,见 worker.cpp) | +| `src/session_host/tools/spawn_subagent_tool.{hpp,cpp}` | 两个工具本体 + `SubagentToolDeps{registry, client, config, fallback_permissions, on_spawn}`。daemon 里 deps 用 shared_ptr 延迟回填(ToolExecutor 先于 SessionRegistry 构造,见 worker.cpp) | | `src/session/session_storage.{hpp,cpp}` | `SessionMeta.parent_session_id`(空省略序列化);`purge_session_files(project_dir, id)` 删 jsonl + meta + `/` 目录(web purge 路由与 TUI `/tasks clear` 共用) | | `src/session/session_manager.{hpp,cpp}` | `set_parent_session_id` / `current_parent_session_id`;start_session 重置、ensure_created/update_meta 落盘、resume_session 读回 | -| `src/session/session_registry.{hpp,cpp}` | `SessionEntry::{subagent_depth, parent_session_id}`;make_entry_locked 从 opts 写入 / 从 resumed_meta 恢复(恢复时强制 depth≥1);list_active 透出 parent 字段 | +| `src/session_host/session_registry.{hpp,cpp}` | `SessionEntry::{subagent_depth, parent_session_id}`;make_entry_locked 从 opts 写入 / 从 resumed_meta 恢复(恢复时强制 depth≥1);list_active 透出 parent 字段 | | `src/session/session_client.hpp` | `SessionOptions::{subagent_depth, parent_session_id}`、`SessionInfo::parent_session_id` | | `src/web/server_helpers.cpp` | `sessions_for_workspace(..., parent_filter)`:空 = 常规列表**排除**全部子会话;非空 = 只返回该父会话的子任务(active 部分跳过 workspace 过滤);`session_info_to_json` / `session_meta_to_json` 输出 `parent_session_id` | | `src/web/routes/routes_sessions.cpp` | `GET /api/sessions?parent=`;`DELETE /api/sessions/:id?purge=1`(仅子会话,主会话 400,busy 409;destroy 后 purge_session_files) | @@ -199,7 +199,7 @@ TuiState overlay,工具线程 wait ask_cv 天然带回结果)。只需两点:入 | 文件 | 覆盖 | |---|---| -| `tests/tool/spawn_subagent_tool_test.cpp` | deps 缺失 / 空 prompt / fire-and-forget / 深度拒绝 / wait 全链路 / parent 持久化(ChildRecordsParentSessionId)/ resume 恢复身份(ResumeRestoresSubagentIdentityFromMeta)/ wait_subagent / worktree 与写边界继承(ChildSharesParentWorktreeWithWriteBoundary)/ LOOP 策略继承 / ChildFailed / 主 checkout 监视(WaitReportsMainCheckoutChangesMadeWhileChildRan)| +| `tests/session_host/spawn_subagent_tool_test.cpp` | deps 缺失 / 空 prompt / fire-and-forget / 深度拒绝 / wait 全链路 / parent 持久化(ChildRecordsParentSessionId)/ resume 恢复身份(ResumeRestoresSubagentIdentityFromMeta)/ wait_subagent / worktree 与写边界继承(ChildSharesParentWorktreeWithWriteBoundary)/ LOOP 策略继承 / ChildFailed / 主 checkout 监视(WaitReportsMainCheckoutChangesMadeWhileChildRan)| | `tests/agent_loop/agent_loop_tool_lifecycle_events_test.cpp` | Yolo worktree 会话写边界(文件工具 / 继承 write_root / shell 写守卫) | | `tests/worktree/worktree_tool_test.cpp` + `worktree_meta_roundtrip_test.cpp` | 继承 worktree 拒绝 Enter/Exit;meta `inherited` 往返与省略 | | `tests/loop/loop_scheduler_test.cpp` + `loop_store_test.cpp` | `detect_workspace_touched` 真实 git;`workspace_touched` 持久化与 v3 迁移 | diff --git "a/docs/\347\253\236\345\223\201\350\260\203\347\240\224\346\212\245\345\221\212_\345\256\214\346\225\264\347\211\210_2026-08.md" "b/docs/\347\253\236\345\223\201\350\260\203\347\240\224\346\212\245\345\221\212_\345\256\214\346\225\264\347\211\210_2026-08.md" index 69448ea13..cb95a9941 100644 --- "a/docs/\347\253\236\345\223\201\350\260\203\347\240\224\346\212\245\345\221\212_\345\256\214\346\225\264\347\211\210_2026-08.md" +++ "b/docs/\347\253\236\345\223\201\350\260\203\347\240\224\346\212\245\345\221\212_\345\256\214\346\225\264\347\211\210_2026-08.md" @@ -54,7 +54,7 @@ |---|---|---| | 1 | 内置 MCP:`/mcp` 命令 + 运行时管理 + 启动协调 | `src/tool/mcp_manager.cpp`、`mcp_startup_coordination.cpp`、`builtin_commands.cpp:1990` | | 2 | 计划模式 `/plan` + 权限模式 `/mode` | `builtin_commands.cpp:1964,1968`、`src/tool/plan_mode_tool.cpp` | -| 3 | 子代理:spawn_subagent 工具 + `/tasks` 后台任务 + Web Swarm | `src/tool/spawn_subagent_tool.cpp`、`builtin_commands.cpp:2022`、`web/src/components/SwarmModeIcon.jsx` | +| 3 | 子代理:spawn_subagent 工具 + `/tasks` 后台任务 + Web Swarm | `src/session_host/tools/spawn_subagent_tool.cpp`、`builtin_commands.cpp:2022`、`web/src/components/SwarmModeIcon.jsx` | | 4 | TodoWrite 工具 | `src/tool/todo_write_tool.cpp` | | 5 | opencode 式 markdown 自定义命令(`$ARGUMENTS` 插值,注册为斜杠命令) | `src/skills/opencode_command.cpp`、`opencode_command_registry.cpp` | | 6 | OAuth:GitHub Copilot Device Flow + Grok Coding Plan(xAI) | `src/provider/auth/github_auth.{hpp,cpp}`、`xai_auth.{hpp,cpp}`、`web/routes/routes_models.cpp` | diff --git a/openspec/changes/refactor20260927-restructure-src-layers/branch-inventory.md b/openspec/changes/refactor20260927-restructure-src-layers/branch-inventory.md index 2be00f9c7..85b764b3b 100644 --- a/openspec/changes/refactor20260927-restructure-src-layers/branch-inventory.md +++ b/openspec/changes/refactor20260927-restructure-src-layers/branch-inventory.md @@ -470,3 +470,9 @@ P2-05 不保留旧路径转发头。合并本阶段主线前,先在各自的迁 本任务把 frontmatter 移到 utils,把可复用的 opencode / skill 命令展开器归到 skills,把技能斜杠命令注册归到 tui/commands,对应五个测试文件同步归位。旧路径不保留转发头。上表九个遗留 ref 继续按映射运行 `migrate_branch.py --apply-map` 或 include 规范化脚本,原 ref 的功能内容与去留仍按 P3-03 逐项处理。 hook 模型事件构造器与只读种子表属于 extract 行,不能用整文件路径替换代替职责拆分:模型事件调用方引用 `agent/hook_bridge/hook_events.hpp`,种子消费者引用 `hooks/hook_seeds.hpp`。原 `hooks/hook_payload.hpp` 只提供无模型依赖的启动事件,种子安装事务仍由 skills 的统一安装器协调。 + +## P2-07 迁移提示(2026-09-28) + +本阶段把 SessionRegistry、LocalSessionClient、ThreadService、任务建议和标题生成的编排职责归到 session_host,模型切换适配器及三个依赖宿主的工具组一并上移,相关测试同步归位。九个遗留 ref 的原文件与提交继续保留,迁移时按映射运行 include 规范化与 `migrate_branch.py --apply-map`,旧路径没有转发头。 + +拆出的纯逻辑需要按调用内容选择头文件:`prompt/init_prompt.hpp` 提供 /init 提示词与骨架,`lsp/lsp_status_text.hpp` 提供 LSP 状态文本与子命令分派,`session/session_title_text.hpp` 提供标题校验和终端标题净化。终端 OS 设置/清除操作仍在 `platform/terminal/terminal_title.hpp`;旧命令、标题生成器头不再间接暴露这些接口。`tests/commands/init_command_test.cpp` 与经 P2-03 搬到 platform 的标题净化测试分别归入 `tests/prompt/init_prompt_test.cpp`、`tests/session/session_title_text_test.cpp`。 diff --git a/openspec/changes/refactor20260927-restructure-src-layers/layout-map.md b/openspec/changes/refactor20260927-restructure-src-layers/layout-map.md index 84fcd03a6..bb497e115 100644 --- a/openspec/changes/refactor20260927-restructure-src-layers/layout-map.md +++ b/openspec/changes/refactor20260927-restructure-src-layers/layout-map.md @@ -171,6 +171,9 @@ | `tests/agent_loop/` | `tests/agent/` | P2-08 | 30 处 `#include "agent_loop.hpp"` 同步改 | | `tests/permissions_test.cpp` | `tests/permissions/permissions_test.cpp` | P2-05 | | | `tests/skill_registry_test.cpp` | `tests/skills/skill_registry_test.cpp` | P2-06 | | +| `tests/{session,provider,tool}/` 中的宿主专用测试 | `tests/session_host/` | P2-07 | session_registry、task_suggestion_service、apply_model_to_session、spawn_subagent_tool、thread_tools 五个测试文件 | +| `tests/commands/init_command_test.cpp` | `tests/prompt/init_prompt_test.cpp` | P2-07 | 只验证提示词与骨架,命令注册留在 TUI | +| `tests/utils/terminal_title_test.cpp` | `tests/session/session_title_text_test.cpp` | P2-03 / P2-07 | P2-03 中间路径为 tests/platform/terminal_title_test.cpp,P2-07 随 sanitize_title 纯逻辑归入 session | | `tests/smoke_test.cpp` | 按被测对象归入模块目录 | P2-08 | 届时确认 | | `tests/agent_loop/stub_provider.hpp` | `tests/test_support/agent/stub_provider.hpp` | P1-01 | 29 个使用方 | | `tests/channels/test_support.hpp` | `tests/test_support/channels/test_support.hpp` | P1-01 | 16 个使用方 | diff --git a/openspec/changes/refactor20260927-restructure-src-layers/verification/P2-07-session-host.md b/openspec/changes/refactor20260927-restructure-src-layers/verification/P2-07-session-host.md new file mode 100644 index 000000000..c0a808e93 --- /dev/null +++ b/openspec/changes/refactor20260927-restructure-src-layers/verification/P2-07-session-host.md @@ -0,0 +1,21 @@ +# P2-07 会话编排层迁移验证 + +任务认领由 PR #85 合入 master,基线 `fd6f96955540bf2492c2306a21fb1ff73a47e987`,前置 P2-06 已由 PR #83 验收。工作分支为 `refactor20260927/P2-07`,复用已合入、干净且无活动构建/运行进程的原 P2-04 工作区;原工作区内已有构建目录保留,本任务使用全新目录。 + +## 实现边界 + +- `35ef1d9e`:将 /init 提示词和骨架归入 prompt/init_prompt,将 LSP 状态文本与子命令分派归入 lsp/lsp_status_text,将生成标题校验、净化及终端标题净化归入 session/session_title_text。命令注册、宿主编排和终端 OS 操作分别保留在各自职责层。七组提取函数与原始文本逐项核对一致;标题请求的空白检查共用提取后的纯函数,语义不变。 +- `dfe8a5c1`:27 个文件纯 `git mv`,全部 R100,0 行增删。20 个源/头文件归到 session_host,五个宿主测试与两个纯逻辑测试同步镜像归位。 +- `88d0e6d2`:39 个 src/tests 文件的 95 处 include 路径改写,逐文件增加行数 = 删除行数 = 修改的 include 行数。补充 init_prompt 和 terminal_title 纯逻辑测试映射,保留 P2-03 中间路径别名供旧分支迁移。 +- 不在旧路径留下转发头。SessionRegistry 对文件工具恢复的依赖由后继 P2-08 的 tool/file_state_restore 拆分处理;不借本次移动改变现有线程、回调或服务租约语义。 +- 当前文档更新移动路径;帮助站点从 group7.py 重新生成 49 篇文章及辅助清单,实际差异仅 sources.json 中一条源码路径。已将两个机械提交登记到 .git-blame-ignore-revs,并向 branch-inventory.md 追加九个遗留 ref 的迁移提示。 + +## 静态检查 + +src / tests include 规范化二次运行均为 0 改动。映射 strict、已有分层阻断项、行数与所有权 strict 通过;分层违规从 83 降到 79,未增加例外。下降项为 session_registry 对 commands 的两条依赖,以及 session_manager / session_storage 对标题生成宿主头的两条依赖。apply_model_to_session 上移后不再构成 provider 对 agent_loop 的物理反向依赖;尚存的裸 agent_loop.hpp include 由 P2-08 的整体归位处理。OpenSpec strict 通过。 + +完整 `pnpm test` 与 `pnpm build` 均通过。 + +## 待完成验证 + +此文件记录实施中的状态,任务复选框仍未勾选。全新目录原生构建、File API 归属对照、完整与定向单测、四平台构建与既有失败复核完成后补充结果;未验收前不将采集工作流成功等同于单测全绿。 From 303ec2a20efc6f6e4ee6e4dbed387778f663636c Mon Sep 17 00:00:00 2001 From: tmoonlight Date: Mon, 28 Sep 2026 12:55:03 +0800 Subject: [PATCH 5/6] =?UTF-8?q?refactor20260927(layers/P2-07):=20=E7=99=BB?= =?UTF-8?q?=E8=AE=B0=E6=9C=AC=E6=9C=BA=E5=8E=9F=E7=94=9F=E9=AA=8C=E8=AF=81?= =?UTF-8?q?=E4=B8=8E=E5=9B=9B=E5=B9=B3=E5=8F=B0=E8=BF=90=E8=A1=8C=E8=BF=9B?= =?UTF-8?q?=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../verification/P2-07-session-host.md | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/openspec/changes/refactor20260927-restructure-src-layers/verification/P2-07-session-host.md b/openspec/changes/refactor20260927-restructure-src-layers/verification/P2-07-session-host.md index c0a808e93..1c970762c 100644 --- a/openspec/changes/refactor20260927-restructure-src-layers/verification/P2-07-session-host.md +++ b/openspec/changes/refactor20260927-restructure-src-layers/verification/P2-07-session-host.md @@ -16,6 +16,24 @@ src / tests include 规范化二次运行均为 0 改动。映射 strict、已 完整 `pnpm test` 与 `pnpm build` 均通过。 -## 待完成验证 +## 本机原生验证 -此文件记录实施中的状态,任务复选框仍未勾选。全新目录原生构建、File API 归属对照、完整与定向单测、四平台构建与既有失败复核完成后补充结果;未验收前不将采集工作流成功等同于单测全绿。 +固定源码 `a5cad80df6897fe7ec22ba4bdbdfba5999aefec5`,使用 MSVC 2022 x64 / Ninja / Release,全新目录 `build/p2-07-session-host`,BUILD_TESTING 与 Desktop 均开启。CLI、Desktop、acecode_unit_tests 与五个 EXCLUDE_FROM_ALL 冒烟目标均构建成功;Desktop 的 File API 依赖清单不含 acecode_testable。 + +CMake File API 为 59 个目标、3658 个元组,对照 P2-06 的 59 / 3637,原有目标、文件归属和编译参数均保持,无删除元组。21 个新增元组只对应 init_prompt、lsp_status_text 与 session_title_text 三组源/头和生成对象。 + +独立 HOME/TEMP 下完整 gtest 清单 5117,实际运行 5116,9 SKIP,0 失败,退出码 0;用例名和 SKIP 集合相对 P2-06 完全一致。本次涉及的会话宿主、子代理、线程工具、任务建议、模型切换、标题、LSP、init_prompt 与 Web 服务共 18 个测试套件、428 个用例,全部实际执行且 0 SKIP。完整测试已覆盖这些定向用例,没有另行重复运行。 + +CTest 注册条目仍为 5121。原始展示名中 42 项含参数指针地址或 `GetParam()` 对象字节,重新链接后这些展示数据不同;只移除该原始对象展示注释、归一化 `pointing to` 前的指针值后,保留测试标识与字面参数的条目多重集相同。gtest 的原始规范用例名直接逐项相同,未按模糊前缀合并或隐藏用例。 + +`AgentLoopTermination.RequestPrefixIsByteStableAcrossIterationsInATurn` 与 `SystemPromptTest.NonGptModelStateIsByteIdenticalToLegacyPrompt` 均实际执行并通过。 + +`migrate_branch.py --check` 已执行,过渡布局仍返回非零:documentation 102、layers 2341、migration_paths 60;include_normalization、map、ownership、seed 为 0。这里包含尚未执行的 P3 最终分组规则;当前迁移模式的实际分层违规为 79,不将此报告当作 P3 验收。 + +## 四平台验证进度 + +[refactor-matrix 36372667024](https://github.com/tmoonlight/acecode/actions/runs/36372667024) 固定上述源码。Deepin 已完成构建,File API 为 13 个目标、1409 个元组;对照 P2-06,原有目标/元组均保持,仅新增三组提取实现的 9 个元组。该平台不运行单测。 + +Deepin 的 provenance 源码字段仍因容器 Git 归属检查为空;额外核对已完成 job `108772039048` 的 checkout 命令、HEAD 提示和紧接 `git log -1 --format=%H` 的完整 SHA,均为固定源码,不使用空字段作为来源证明。 + +Windows / Linux / macOS 的构建和完整用例仍在进行,尚未登记为通过。任务复选框保持未勾选;三个平台的快照、用例与既有失败复核完成后再合入。 From c87fcf533d0a39d1ee9138d249a241ad59cd017e Mon Sep 17 00:00:00 2001 From: tmoonlight Date: Mon, 28 Sep 2026 12:55:06 +0800 Subject: [PATCH 6/6] =?UTF-8?q?refactor20260927(layers/P2-07):=20=E6=8C=89?= =?UTF-8?q?=20D26=20=E5=AE=8C=E6=88=90=20Windows=20=E6=9C=AC=E6=9C=BA?= =?UTF-8?q?=E6=A0=B8=E5=AF=B9=E5=B9=B6=E5=8B=BE=E9=80=89=E9=AA=8C=E6=94=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tasks.md | 2 +- .../verification/P2-07-session-host.md | 13 +++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/openspec/changes/refactor20260927-restructure-src-layers/tasks.md b/openspec/changes/refactor20260927-restructure-src-layers/tasks.md index fec594071..82c2928b7 100644 --- a/openspec/changes/refactor20260927-restructure-src-layers/tasks.md +++ b/openspec/changes/refactor20260927-restructure-src-layers/tasks.md @@ -196,7 +196,7 @@ - `tests/skill_registry_test.cpp` 移到 `tests/skills/`。 - 前置:3.1、3.4。 - 验证:hooks、skills、seeder 相关测试全部通过;`skill_commands_reload_test` 能链接并运行;lint 显示 hooks→provider、hooks→skills、memory→skills、skills↔commands 边消失。 -- [ ] 3.6 【P2-07】【主】编排层上移到 `session_host/`。〔认领: Codex-root 2026-09-28〕 +- [x] 3.6 【P2-07】【主】编排层上移到 `session_host/`。〔认领: Codex-root 2026-09-28〕〔接手: Claude-D26 2026-09-28,原 Codex 会话停在等待四平台结果阶段,按 D26 只补 Windows 本机核对〕〔验收: Claude-D26 2026-09-28,PR #86;本机全新构建 + 全量 5116 / 9 SKIP / 0 失败(Codex 记录)+ 快速档复核 0 失败;target 快照 59 目标无增删,新增元组仅三组提取文件;refactor-matrix 36372667024 四平台 success,逐平台对照留 5.4;见 verification/P2-07-session-host.md〕 - 先拆出 `prompt/init_prompt`(自 `commands/init_command` 的 `build_*`)与 `lsp/lsp_status_text`(自 `commands/lsp_command`); - 再把 session_registry、local_session_client、thread_service、task_suggestion_service、session_auto_title、session_title_generator 移过去,其中纯函数留在 `session/session_title_text`; - 按 layout-map.md §2,terminal_title 的 sanitize_title 及其 UTF-8 截断辅助归入 session/session_title_text,终端 OS 调用留在 platform;纯标题与 init_prompt 测试随实际职责迁移; diff --git a/openspec/changes/refactor20260927-restructure-src-layers/verification/P2-07-session-host.md b/openspec/changes/refactor20260927-restructure-src-layers/verification/P2-07-session-host.md index 1c970762c..31a52d92c 100644 --- a/openspec/changes/refactor20260927-restructure-src-layers/verification/P2-07-session-host.md +++ b/openspec/changes/refactor20260927-restructure-src-layers/verification/P2-07-session-host.md @@ -30,10 +30,19 @@ CTest 注册条目仍为 5121。原始展示名中 42 项含参数指针地址 `migrate_branch.py --check` 已执行,过渡布局仍返回非零:documentation 102、layers 2341、migration_paths 60;include_normalization、map、ownership、seed 为 0。这里包含尚未执行的 P3 最终分组规则;当前迁移模式的实际分层违规为 79,不将此报告当作 P3 验收。 -## 四平台验证进度 +## 四平台验证(D26 之前已触发的一次运行) [refactor-matrix 36372667024](https://github.com/tmoonlight/acecode/actions/runs/36372667024) 固定上述源码。Deepin 已完成构建,File API 为 13 个目标、1409 个元组;对照 P2-06,原有目标/元组均保持,仅新增三组提取实现的 9 个元组。该平台不运行单测。 Deepin 的 provenance 源码字段仍因容器 Git 归属检查为空;额外核对已完成 job `108772039048` 的 checkout 命令、HEAD 提示和紧接 `git log -1 --format=%H` 的完整 SHA,均为固定源码,不使用空字段作为来源证明。 -Windows / Linux / macOS 的构建和完整用例仍在进行,尚未登记为通过。任务复选框保持未勾选;三个平台的快照、用例与既有失败复核完成后再合入。 +该运行随后全部完成:windows-x64、linux-x64、macos-arm64 与 Deepin(configure + build、snapshot)五个 job 均为 success。按 D26(2026-09-28)逐任务验收只做 Windows 本机,三个测试平台的快照、用例清单与既有失败复核没有逐项对照,统一留到 tasks.md 5.4「多平台补验」。 + +## D26 接手核对(Claude,2026-09-28) + +原 Codex 会话停在等待四平台结果的阶段(用户反映验证成本过高),本节按 design.md §7.4 的 B 档在 Windows 本机复核,不再触发新的 CI: + +- 静态闸门(分支 `a5cad80d`):`normalize_includes --check` src / tests 均 0 改动、0 错误;`check_layers --enforce-parent-includes` 79 项(master 83);`validate_map --strict`、`check_file_size`、`check_ownership` 均 0;`check_doc_paths` 102 项,较 master 多 23 项,全部是 `docs/superpowers/plans|specs/2026-05-09-model-selection*`、`docs/reviews/pr-24-*` 等历史文档里的 `apply_model_to_session` / `session_registry` 旧路径,P3 M2 的文档改写会统一换算(apply_layout 预演已覆盖 54 份文档),不在本任务改写历史记录。 +- 快速档单测:用上文全新目录 `build/p2-07-session-host` 的 `acecode_unit_tests.exe`,`run_fast_tests.py --profile fast`(6 分片、隔离 HOME/TEMP、对照 P2-05 二进制的全量记录):清单 5117,执行 4515,9 SKIP,0 失败,601 条慢套件用例未运行(已如实记录),27 秒。守护测试 `AgentLoopTermination.RequestPrefixIsByteStableAcrossIterationsInATurn` 与 `SystemPromptTest.*ByteStable*` 单独补跑通过。 +- target 快照:对 master(`fd6f9695`,configure-only File API)与本分支的快照都按映射表反查换算后对照:59 个目标无增删;新增元组只有 init_prompt、lsp_status_text、session_title_text 三组源/头及其 `.obj` 在各链接目标里的引用;另有 25 对 `nlohmann_json.natvis` 差异是主检出记成相对路径、worktree 记成绝对路径的已知形态差异,1 对 `terminal_title_test.cpp` 差异来自本分支补充的映射行(master 侧映射表还没有该行),两者都不是归属变化。 +- 合入前已把 master(D26 决策、apply_layout 工具)合进本分支;src / tests / CMake 相对验证源码无差异。