Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -998,7 +998,7 @@ jobs:
- name: Set up CMake
uses: jwlawson/actions-setup-cmake@v2
with:
cmake-version: '3.29.x'
cmake-version: '3.29.9'

- name: Build pinned DTK 5 SDK
if: matrix.build_desktop
Expand Down
28 changes: 27 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ Backup deletion verifies the pointer and preserves it when cleaning the default
**数据目录迁移的几条铁律(fix-feedback-0924 第 1 条,YTB 反馈:迁移出错后界面卡死 / 迁移必然失败)。**
(1) OS 错误文本(`ec.message()` / `e.what()`)先单独经 `migration_os_error_text` / `ensure_utf8` 转 UTF-8 再拼接 —— 不要对拼好的整串转,整串里的 UTF-8 中文路径会被按 GBK 重解成乱码;`routes_environment.cpp::json_response` 的 `dump(..., error_handler_t::replace)` 是出口兜底,不能去掉(曾经 GBK 的「系统找不到指定的路径」让 /migration 每次轮询都 500,前端永远停在「迁移中」)。前端轮询由 `migrationPollOutcome` 给出动作、组件函数式 `setJob`,连续 3 次失败就停并标 unknown。构造非法 UTF-8 测试数据时注意 GBK「一」= `D2 BB` 恰好是合法 UTF-8。
(2) `to_extended_length_path`(`\\?\`)只用于迁移与清理的文件 IO,绝不进 JSON / 指针 / 日志;SQLite 打开 ≥240 字符才换扩展形式(同进程两种形式打开同一 WAL 库会分裂 winShmNode);staging 名是 `.acecode-mig-` + 8 hex。运行期仍不是 longPathAware。
(3) `agent-browser/webview2`(Desktop 启动即创建、持续写)只能「尽力复制 + 不参与变更复检」,SQLite 仍优先 backup;`edge-app-profile/` 整个排除;已快照主库的 `-wal/-shm/-journal` 都不复制(热 journal 会把快照回滚坏)。排除 `cache/no-workspace/<id>/.acecode/tmp` 的代价:删旧目录后历史里对 scratch 的引用失效。`previous_size_bytes` 含未复制但留在旧目录的数据。
(3) Agent Browser profile(新装 `agent_browser/webview2`,旧装机沿用 `agent-browser/webview2`,`migration_is_best_effort_root` 两种都认;运行数据改下划线命名时漏过这里,新装用户 Desktop 开着就迁移失败,以后再改运行数据名字要把 `data_dir_migration.cpp` 里按目录名判定的几处一起改)Desktop 启动即创建、持续写,只能「尽力复制 + 不参与变更复检」,SQLite 仍优先 backup;`edge-app-profile/` 整个排除;已快照主库的 `-wal/-shm/-journal` 都不复制(热 journal 会把快照回滚坏)。排除 `cache/no-workspace/<id>/.acecode/tmp` 的代价:删旧目录后历史里对 scratch 的引用失效。`previous_size_bytes` 含未复制但留在旧目录的数据。
(4) 「其他实例」判定只在已被证明 PID 复用时放行(`evaluate_daemon_pid_holder`,与 daemon_pool 同口径,身份未知一律拦);每个拒绝分支都记日志(SESSIONS_BUSY 带 `busy_session_ids()`)。redirect 解析告警发生在日志初始化之前,由 worker / TUI / headless / Windows 服务四处在 `init_with_rotation` 之后调 `log_deferred_data_dir_resolution_warning()` 补记。

The Settings shell uses open groups in `globals.css`; user-provided Claude-style
Expand Down Expand Up @@ -627,6 +627,10 @@ SidePanel 折叠 UI:`ChatView` 把 `SidePanel` 包到 `<div class="ace-side-pane

**`@` 会话引用有查询词时必须走服务端检索,不能本地过滤最近列表。** `/api/session-search/sessions` 对空查询最多只给最近 50 条;`@` 菜单曾拿这 50 条在本地过滤,更早的会话永远搜不到(实测 28456 个会话里,标题完全一致的「随机选择题挑战」排第 9726 位,菜单显示「没有匹配」)。现在 `lib/sessionReference.js::searchSessionReferenceData` 带 `q` + `limit=100` 请求(内存目录过滤,2.8 万会话单次 30–46ms),同一查询词 15 秒缓存,目录预热未完成时有界轮询,查询变化 / 下拉框关闭即 abort。**不要**在 `@` 菜单按键时调 `searchSessionUserMessages`:正文搜索逐项目读 meta、开 SQLite,全量一遍几十秒,而 `@` 词同时用于文件路径;全文搜索留给 Ctrl+K。守卫:`sessionReference.test.js` + `sessionReferenceComposerArchitecture.test.js`。

**侧栏会话拖进输入框 = @ 引用。** 侧栏拖动是自绘指针拖动(不是 HTML5 拖放),输入框收不到 drop:`startSidebarSessionPointerDrag` 每次移动用 `sessionReferenceDropTargetAt` 找带 `data-session-reference-drop="true"` 的输入框根元素,命中时高亮(`is-session-reference-drop`)、停止计算排序落点,松手派发 `SESSION_REFERENCE_DROP_EVENT`(`lib/sessionReferenceDrop.js`);落点所在的 InputBar 认领后经 `RichComposer.insertSessionReferences` 在落点插入会话标签,标签由 `composerSessionTagForReference` 生成,与 @ 菜单的 `@session:` 令牌逐字节一致。多选拖动插入多个;分组行、当前会话、已引用过的会话跳过。回归:`sessionReferenceDrop.test.js`。

**侧栏多选批量操作只发一次 `POST /api/session-batch`**(置顶 / 取消置顶 / 归档 / 取消归档 / 已读 / 未读,`routes_session_batch.cpp` + `handlers/session_batch_handler.cpp`)。曾经逐个调用单项接口,置顶一项 2 个请求、归档一项还要刷新一次列表。服务端把每个置顶文件、全局置顶顺序、每个 workspace 的 attention 文件各写一次;批量置顶按请求顺序插到最前、已置顶的不动;单项失败不影响其它项。WS `mark_session_read` / `mark_session_unread` 现在委托 `mark_sessions_attention`。回归:`session_batch_handler_test.cpp`、`web_server_smoke_test.cpp` 的 `SessionBatch*` 四条、前端 `sidebarSessionBatch.test.js`。

**对话框键盘约定(Claude Code 风格,全部走 `components/Modal.jsx`)**:Esc 取消、Tab / Shift+Tab 只在对话框内循环、Enter 触发默认操作、删除类确认框打开即默认选中「删除」。判定逻辑全在 `lib/dialogKeyboard.js`(纯函数,Node 单测用鸭子类型假 DOM):默认操作按钮用 `data-ace-dialog-primary="true"` 标出;初始焦点顺序 = 已落在对话框内的焦点(React `autoFocus` 在 commit 阶段先跑,Modal 不再抢——旧实现无条件聚焦第一个可聚焦元素,曾把「添加专家」搜索框的 autoFocus 抢成头部 ×)> 第一个文本输入框(表单型)> primary(确认型)> 第一个可聚焦元素;Enter 只在焦点**不在**会自己消费 Enter 的元素(button / a[href] / textarea / contenteditable / `<form>` 内控件——表单靠隐式提交)上时点 primary,子组件自己处理 Enter 必须 `preventDefault`(Tag 输入框那样),否则会双触发;按住不放的 auto-repeat Enter 一律 `preventDefault`(Chromium 的按钮激活发生在 keypress、且派发给当时聚焦的元素,「按住 Enter 打开确认框 → 焦点落到删除 → 连带确认」就是这么来的;keydown 被阻止就不再产生 keypress),textarea / contenteditable 内放行。输入法合成中的 Esc 不关对话框。焦点环只在 `:focus-visible`(`.ace-modal-dialog :is(button, [role="button"], a[href])`),鼠标点开时脚本聚焦的默认按钮不画环、按 Tab 后才出现,与系统对话框一致。**不要**再手写 `fixed inset-0` 遮罩当对话框(循环编辑表单与 opencode 导入框已收编;`agentBrowserArchitecture.test.js` 的 floatingSurfaceOwners 清单相应减少)。自动弹出的 `WorkspaceCleanupNotice` 故意不标 primary:无人触发的对话框不能带破坏性默认操作,默认落在「保留」。守卫:`lib/dialogKeyboard.test.js` + `lib/dialogKeyboardArchitecture.test.js`(后者还断言取消类文案永远不会被标成 primary)。

排队卡片栈(`redesign-webui-queue-cards`):busy 期间提交的待发送消息**不进 transcript**,改由 `<QueueCardList>`(在 `<InputBar>` 上方)渲染成卡片堆。状态机(`lib/chatInputQueue.js`)与 `enqueueQueuedInput` / `cancelQueuedInput` / `markQueuedInput*` / `nextQueuedInput` / `completeQueuedInputForMessage` 全部不变;只是渲染分支换地方。每张卡片左侧 3px `.ace-queue-card-indicator` 色条标注状态(QUEUED 灰 / FAILED 红),右侧恒挂"取消"(close 图标),FAILED 多一个"重试"。状态↔标签映射收敛在 `lib/queueCardItem.js::buildQueueCardItem`(纯函数,Node 单测覆盖);DOM 端只是把这份结构映射到 className。`Message.jsx::UserBubble` 已剥离 `queued`/`onCancelQueued`/`onRetryQueued` props——transcript 里出现的 user 气泡一定是后端真实落库的消息。
Expand Down Expand Up @@ -762,6 +766,28 @@ JS↔C++ bridge(同进程,webview `bind`,无 HTTP):
- **前端页签只有一个来源**:`web/src/lib/agentBrowserPages.js`(App 级单写者登记表,`App.jsx` 挂载时 `installAgentBrowserPageListener()` + 全量 `reconcileAgentBrowserPageStore()`,ChatView 切会话再按会话对账)→ `previewTabs.js::syncBrowserTabsForSession` 派生页签;本 ChatView 首次见到的页面自动打开并激活(`revealedBrowserPagesRef` 按会话记)。**不要**再从 `agentBrowserActivityFromItems` 创建页签或调无参 `getAgentBrowserState()` 猜全局活动页,活动推断现在只决定彩虹边框与「Agent 切目标页时前置该页签」。旧 Desktop 不带 owner 的事件走 `claimUnownedAgentBrowserPage` 本地认领兜底。
- 回归测试:`tests/desktop/agent_browser_page_directory_test.cpp`、`tests/tool/agent_browser_tools_test.cpp` 的 owner 两条、Windows smoke 的 `SMOKE_OWNERSHIP_OK` 段、前端 `agentBrowserPages.test.js` + `previewTabs.test.js::syncBrowserTabsForSession` + `agentBrowserArchitecture.test.js` 的归属合同。设计全文见 [docs/agent-browser.md](docs/agent-browser.md)「页面归属」。

### Agent Browser 新标签页(仿 Codex New Tab,不做「工具」区)

空白页(`content_state=empty`)由 `AgentBrowserNewTab.jsx` 渲染「推荐 + 最近」,两块都空时退回原 Browser 提示。逆向依据:Codex 安装包 `app.asar` 的 `thread-side-panel-new-tab-*.js`(Tools / Recents)与 `thread-app-shell-chrome-*.js`(Suggested 读 Chromium `browsingHistory.subscribeTopSites`,前 4 个,可移除 + 撤销)。

- **推荐 = ACECode 自己记的访问**,WebView2 / WKWebView 没有历史接口。App 级 `installAgentBrowserVisitRecorder` 把 live 页面的访问报给 `POST /api/agent-browser/visits`,daemon 存 `<data_dir>/agent_browser_top_sites.json`(`handlers/agent_browser_top_sites.cpp` 纯逻辑 + `routes_agent_browser.cpp`,只给本机直连,迁移闸门期间 409)。**不要改存 localStorage**:Desktop 每次启动 daemon 端口都变,origin 跟着变。跟踪器规则:地址变化才计一次访问,刷新同址不计,标题 / 图标后到用 `new_visit:false` 补充;前端加载前已打开的页面用 `aceDesktop_agentBrowserListPages` 打底,否则每次刷新 / 切工作区都把已开页面多算一次。Desktop 的默认标题「新标签页」必须写成比较里的字面量才能过滤 —— i18n 编译插件会把模块级常量里的中文换成译文。
- **最近 = 本会话改过的文件**(`agentBrowserRecentFiles.js`,与会话变更同源),点开用文件预览替换该浏览器页签(ChatView `replaceBrowserTabWithFile`)。持久化的工具消息没有 `timestamp`,历史工具项的 `ts` 是加载时刻兜底,不能直接显示:历史项(带 `messageId` 字段,实时事件建的工具项没有)取所在回合用户消息的时间。
- 回归:`tests/web/agent_browser_top_sites_test.cpp`、`web_server_smoke_test.cpp::AgentBrowserTopSitesRoundTripAndStayLocalOnly`、前端 `agentBrowserTopSites.test.js` / `agentBrowserRecentFiles.test.js`。

### Agent Browser Cookie 存储(add-agent-browser-cookie-modes,复刻 Claude 的 Keep cookies)

三档 `cookie_mode`:`shared`(默认,全部会话共用一份并落盘)/ `per_session`(每个**根会话**一份,子代理归根会话)/ `until_quit`(只在本次运行保留)。存储键由纯函数决定(`src/apps/desktop/agent_browser_cookie_jars.{hpp,cpp}`):只有 per_session 返回 `session:<root_session_id>`,owner 为空(旧 daemon、未绑定页面)一律回 `shared`。工具栏「⋮」菜单(`AgentBrowserMoreMenu.jsx`,「将控制台日志添加到聊天」也搬进了这里)与「设置 > 工具」共用 `web/src/lib/agentBrowserCookieMode.js` 的单写者 store。用户文档:`docs/agent-browser.md`「Cookie 存储」与帮助页 agent-browser。改这块要守住的几条(多数是 2026-10-21 实测踩出来的,冒烟 `agent_browser_cookie_smoke` 逐条守着):

- **Windows 一种键一个 WebView2 Profile,共用同一个 environment。** shared **不设 ProfileName**(沿用默认 Profile,升级后登录还在,别"统一"成命名 Profile);until_quit = `acecode-until-quit` + InPrivate;session = `s-` + SHA-256(id) 前 24 位(Profile 名限 64 字符、大小写不敏感,headless 自定义 id 可能超长)。命名 Profile 的磁盘目录是 `EBWebView/WV2Profile_<名>`,不是 `EBWebView/<名>` —— 一律经 `agent_browser_session_profile_directory_name` 派生。
- **until_quit 必须挂一个隐藏的保活 controller**:InPrivate Profile 在最后一个页面关闭时就销毁,实测不保活时关掉所有页面再打开登录已丢。保活一直持有到切离该档或 Desktop 退出。macOS 对应宿主持有的单个 `nonPersistentDataStore`。
- **切档不用 `Profile8::Delete`**:被 Delete 的 Profile 同一次运行内不能同名重建,而用户可能马上切回去。切到 until_quit 时 shared 立即 `ClearBrowsingDataAll`,各会话存储只在状态文件记 `pending_wipe`(下次启动在建 environment 之前删目录,或本次运行切回 per_session 时首次建页前先清)。`Delete` / 删目录只用于永久删除会话。以 until_quit 启动时,第一个页面之前也要清一次 shared(异常退出残留)。
- **会话 cookie(无过期时间)重启必丢**,共享档也一样,是引擎行为(Claude 相同),帮助页让用户勾「记住我」。别为此去改持久化。
- **跨进程状态在 `state.json` 的 `agent_browser` 段**(`src/base/ipc/agent_browser_store_state.hpp`,经 `update_state_json` 锁):`cookie_mode` / `session_stores` / `pending_wipe` 只由 Desktop 写;`pending_deletions` / `pending_seeds`(上限 500)由 daemon / TUI 的 `host/session_host/agent_browser_store_lifecycle` 追加。不放 config.json:会被 daemon 整份配置写回覆盖。
- **删除与分叉**:永久删除会话(purge 监听)→ 记 `pending_deletions` 再在服务自己的线程上发无数据的代理操作 `drain_store_tasks` 提醒 Desktop(失败只记 debug,旧 Desktop 不认识也无害,协议版本不升);分叉(`session_fork_listeners`,`fork_session_to_new_id` 成功后触发)→ 记 `pending_seeds`,子会话**首次**建页时若父会话已有存储就复制父会话**此刻**的 cookie。监听器回调只写状态文件,不做网络 IO。
- **切档重建页面**:`agent_browser_pages_to_rebuild` 挑出存储键变化的页,同一 page id / owner / 共享标记在新存储里重建并导航回原 URL,后退历史丢失;页面目录的显示页 / Agent 目标页不动。按会话存储准备(清空 / 播种)期间页面的地址栏导航与代理命令排队,就绪后按显式 page id 重放。
- macOS 实现(`agent_browser_host_mac.mm`:固定 UUID / SHA-256 派生 UUID 的 `dataStoreForIdentifier:`、`removeDataStoreForIdentifier:`、`httpCookieStore` 逐条复制)**尚未在 Mac 上编译验证**,改动前先跑一次 mac 构建。
- 回归:`tests/desktop/agent_browser_cookie_jars_test.cpp`、`tests/ipc/agent_browser_store_state_test.cpp`、`tests/session/session_fork_listener_test.cpp`、`tests/session_host/agent_browser_store_lifecycle_test.cpp`、Windows 冒烟 `agent_browser_cookie_smoke`(EXCLUDE_FROM_ALL,输出 `SMOKE_COOKIE_MODES_OK`),前端 `agentBrowserCookieMode.test.js`、`submenuPosition.test.js`、`agentBrowserArchitecture.test.js` 的菜单合同。

### iOS 模拟器(openspec add-ios-simulator,仅 macOS)

逆向 Claude Desktop 的同类功能后自研复刻:Claude 用 FBSimulatorControl 的 Swift helper 且要求 xcode-select 指向完整 Xcode;我们的 iOS 同事常用绿色安装的 Xcode,所以 Xcode 必须自动发现 + 可配置。用户文档 [docs/ios-simulator.md](docs/ios-simulator.md),接口 [docs/daemon-api.md](docs/daemon-api.md) 第 20 节。
Expand Down
14 changes: 11 additions & 3 deletions cmake/acecode_source_paths.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,19 @@ set(ACECODE_UPGRADE_MANIFEST_SOURCE "${CMAKE_SOURCE_DIR}/src/adapters/upgrade/ma
set(ACECODE_CHANNEL_BRIDGE_SOURCE "${CMAKE_SOURCE_DIR}/src/host/channels/bridge.cpp")
set(ACECODE_WINPTY_AGENT_LOCATION_SOURCE "${CMAKE_SOURCE_DIR}/src/base/pty/winpty_agent_location.cpp")
set(ACECODE_DEEPIN_WINDOW_EFFECTS_SOURCE "${CMAKE_SOURCE_DIR}/src/apps/desktop/deepin_window_effects.cpp")
set(ACECODE_DESKTOP_MAIN_SOURCE "${CMAKE_SOURCE_DIR}/src/apps/desktop/main.cpp")
set(ACECODE_DESKTOP_MAIN_SOURCE "${CMAKE_SOURCE_DIR}/src/apps/desktop/main.cpp"
"${CMAKE_SOURCE_DIR}/src/apps/desktop/agent_browser_cookie_bridge.cpp")
set(ACECODE_MACOS_CLI_LAUNCHER_SOURCE "${CMAKE_SOURCE_DIR}/src/apps/desktop/macos_cli_launcher.cpp")
set(ACECODE_DESKTOP_SPLASH_SOURCE "${CMAKE_SOURCE_DIR}/src/apps/desktop/splash_screen.cpp")
set(ACECODE_DESKTOP_WEB_HOST_SOURCE "${CMAKE_SOURCE_DIR}/src/apps/desktop/web_host.cpp")
set(ACECODE_DESKTOP_PET_SOURCE "${CMAKE_SOURCE_DIR}/src/apps/desktop/desktop_pet.cpp")
set(ACECODE_DESKTOP_PET_MAC_SOURCE "${CMAKE_SOURCE_DIR}/src/apps/desktop/desktop_pet_mac.mm")
set(ACECODE_DESKTOP_LINUX_SOURCE "${CMAKE_SOURCE_DIR}/src/apps/desktop/linux_desktop.cpp")
set(ACECODE_AGENT_BROWSER_HOST_SOURCE "${CMAKE_SOURCE_DIR}/src/apps/desktop/agent_browser_host.cpp")
set(ACECODE_AGENT_BROWSER_HOST_MAC_SOURCE "${CMAKE_SOURCE_DIR}/src/apps/desktop/agent_browser_host_mac.mm")
set(ACECODE_AGENT_BROWSER_HOST_SOURCE "${CMAKE_SOURCE_DIR}/src/apps/desktop/agent_browser_host.cpp"
"${CMAKE_SOURCE_DIR}/src/apps/desktop/agent_browser_windows_helpers.cpp"
"${CMAKE_SOURCE_DIR}/src/apps/desktop/agent_browser_host_common.cpp")
set(ACECODE_AGENT_BROWSER_HOST_MAC_SOURCE "${CMAKE_SOURCE_DIR}/src/apps/desktop/agent_browser_host_mac.mm"
"${CMAKE_SOURCE_DIR}/src/apps/desktop/agent_browser_macos_helpers.mm")
set(ACECODE_IOS_SIMULATOR_VIEW_SOURCE "${CMAKE_SOURCE_DIR}/src/apps/desktop/ios_simulator_view.cpp")
set(ACECODE_IOS_SIMULATOR_VIEW_MAC_SOURCE "${CMAKE_SOURCE_DIR}/src/apps/desktop/ios_simulator_view_mac.mm")
set(ACECODE_IOS_SIMULATOR_VIEW_BRIDGE_SOURCE "${CMAKE_SOURCE_DIR}/src/apps/desktop/ios_simulator_view_bridge.cpp")
Expand Down Expand Up @@ -83,6 +87,10 @@ set(ACECODE_AGENT_BROWSER_HOST_SMOKE_SOURCES
${CMAKE_SOURCE_DIR}/tests/desktop/helpers/agent_browser_host_smoke.cpp
${ACECODE_AGENT_BROWSER_HOST_SOURCE}
${ACECODE_BROWSER_CDP_SOURCE})
set(ACECODE_AGENT_BROWSER_COOKIE_SMOKE_SOURCES
${CMAKE_SOURCE_DIR}/tests/desktop/helpers/agent_browser_cookie_smoke.cpp
${ACECODE_AGENT_BROWSER_HOST_SOURCE}
${ACECODE_BROWSER_CDP_SOURCE})
set(ACECODE_MICROPHONE_PERMISSION_SMOKE_SOURCES
${CMAKE_SOURCE_DIR}/tests/desktop/helpers/microphone_permission_smoke.cpp
${ACECODE_DESKTOP_WEB_HOST_SOURCE})
Expand Down
Loading
Loading