zcode-pro 1.5.0__tar.gz → 1.5.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. {zcode_pro-1.5.0 → zcode_pro-1.5.1}/PKG-INFO +2 -2
  2. {zcode_pro-1.5.0 → zcode_pro-1.5.1}/README.en.md +1 -1
  3. {zcode_pro-1.5.0 → zcode_pro-1.5.1}/README.md +1 -1
  4. {zcode_pro-1.5.0 → zcode_pro-1.5.1}/dist/inject.js +113 -47
  5. {zcode_pro-1.5.0 → zcode_pro-1.5.1}/package.json +1 -1
  6. {zcode_pro-1.5.0 → zcode_pro-1.5.1}/src/host/helper.mjs +38 -7
  7. {zcode_pro-1.5.0 → zcode_pro-1.5.1}/src/host/taskIndex.mjs +11 -7
  8. {zcode_pro-1.5.0 → zcode_pro-1.5.1}/src/inject/core.js +6 -10
  9. {zcode_pro-1.5.0 → zcode_pro-1.5.1}/src/inject/features/newtab-panels.js +3 -1
  10. {zcode_pro-1.5.0 → zcode_pro-1.5.1}/src/inject/features/session-switch.js +92 -30
  11. {zcode_pro-1.5.0 → zcode_pro-1.5.1}/src/inject/features/settings-dialog.js +1 -2
  12. {zcode_pro-1.5.0 → zcode_pro-1.5.1}/src/inject/features/title-gen.js +3 -2
  13. {zcode_pro-1.5.0 → zcode_pro-1.5.1}/src/inject/features/title-lock.js +3 -2
  14. {zcode_pro-1.5.0 → zcode_pro-1.5.1}/src/inject/features/ws-running.js +55 -8
  15. {zcode_pro-1.5.0 → zcode_pro-1.5.1}/zcode_pro/__init__.py +1 -1
  16. {zcode_pro-1.5.0 → zcode_pro-1.5.1}/.gitignore +0 -0
  17. {zcode_pro-1.5.0 → zcode_pro-1.5.1}/LICENSE +0 -0
  18. {zcode_pro-1.5.0 → zcode_pro-1.5.1}/bin/zcode-pro +0 -0
  19. {zcode_pro-1.5.0 → zcode_pro-1.5.1}/bin/zcode-pro.cmd +0 -0
  20. {zcode_pro-1.5.0 → zcode_pro-1.5.1}/bin/zcode-pro.mjs +0 -0
  21. {zcode_pro-1.5.0 → zcode_pro-1.5.1}/bin/zcode-pro.vbs +0 -0
  22. {zcode_pro-1.5.0 → zcode_pro-1.5.1}/cli.mjs +0 -0
  23. {zcode_pro-1.5.0 → zcode_pro-1.5.1}/pyproject.toml +0 -0
  24. {zcode_pro-1.5.0 → zcode_pro-1.5.1}/src/host/cdp.mjs +0 -0
  25. {zcode_pro-1.5.0 → zcode_pro-1.5.1}/src/host/main.mjs +0 -0
  26. {zcode_pro-1.5.0 → zcode_pro-1.5.1}/src/host/paths.mjs +0 -0
  27. {zcode_pro-1.5.0 → zcode_pro-1.5.1}/src/host/pickFolder.mjs +0 -0
  28. {zcode_pro-1.5.0 → zcode_pro-1.5.1}/src/host/settings.mjs +0 -0
  29. {zcode_pro-1.5.0 → zcode_pro-1.5.1}/src/host/setup.mjs +0 -0
  30. {zcode_pro-1.5.0 → zcode_pro-1.5.1}/src/host/taskOrder.mjs +0 -0
  31. {zcode_pro-1.5.0 → zcode_pro-1.5.1}/src/host/version.mjs +0 -0
  32. {zcode_pro-1.5.0 → zcode_pro-1.5.1}/src/host/webdav.mjs +0 -0
  33. {zcode_pro-1.5.0 → zcode_pro-1.5.1}/src/inject/features/alias-migration.js +0 -0
  34. {zcode_pro-1.5.0 → zcode_pro-1.5.1}/src/inject/features/alias.js +0 -0
  35. {zcode_pro-1.5.0 → zcode_pro-1.5.1}/src/inject/features/composer-provider.js +0 -0
  36. {zcode_pro-1.5.0 → zcode_pro-1.5.1}/src/inject/features/copy-path.js +0 -0
  37. {zcode_pro-1.5.0 → zcode_pro-1.5.1}/src/inject/features/file-menu.js +0 -0
  38. {zcode_pro-1.5.0 → zcode_pro-1.5.1}/src/inject/features/idle-reclaim.js +0 -0
  39. {zcode_pro-1.5.0 → zcode_pro-1.5.1}/src/inject/features/image-menu.js +0 -0
  40. {zcode_pro-1.5.0 → zcode_pro-1.5.1}/src/inject/features/open-folder.js +0 -0
  41. {zcode_pro-1.5.0 → zcode_pro-1.5.1}/src/inject/features/pinned-collapse.js +0 -0
  42. {zcode_pro-1.5.0 → zcode_pro-1.5.1}/src/inject/features/plugin-card-retry.js +0 -0
  43. {zcode_pro-1.5.0 → zcode_pro-1.5.1}/src/inject/features/plugin-updates.js +0 -0
  44. {zcode_pro-1.5.0 → zcode_pro-1.5.1}/src/inject/features/project-menu.js +0 -0
  45. {zcode_pro-1.5.0 → zcode_pro-1.5.1}/src/inject/features/relocate-dialog.js +0 -0
  46. {zcode_pro-1.5.0 → zcode_pro-1.5.1}/src/inject/features/sandbox-reclaim.js +0 -0
  47. {zcode_pro-1.5.0 → zcode_pro-1.5.1}/src/inject/features/settings-entry.js +0 -0
  48. {zcode_pro-1.5.0 → zcode_pro-1.5.1}/src/inject/features/status-bar.js +0 -0
  49. {zcode_pro-1.5.0 → zcode_pro-1.5.1}/src/inject/features/styles.js +0 -0
  50. {zcode_pro-1.5.0 → zcode_pro-1.5.1}/src/inject/features/table-copy.js +0 -0
  51. {zcode_pro-1.5.0 → zcode_pro-1.5.1}/src/inject/features/task-order.js +0 -0
  52. {zcode_pro-1.5.0 → zcode_pro-1.5.1}/src/inject/features/theme.js +0 -0
  53. {zcode_pro-1.5.0 → zcode_pro-1.5.1}/src/inject/features/toolbar-icons.js +0 -0
  54. {zcode_pro-1.5.0 → zcode_pro-1.5.1}/src/inject/features/turn-usage.js +0 -0
  55. {zcode_pro-1.5.0 → zcode_pro-1.5.1}/src/inject/features/ws-rows.js +0 -0
  56. {zcode_pro-1.5.0 → zcode_pro-1.5.1}/src/inject/index.js +0 -0
  57. {zcode_pro-1.5.0 → zcode_pro-1.5.1}/src/inject/provider-fields.js +0 -0
  58. {zcode_pro-1.5.0 → zcode_pro-1.5.1}/src/inject/ui.js +0 -0
  59. {zcode_pro-1.5.0 → zcode_pro-1.5.1}/zcode_pro/__main__.py +0 -0
  60. {zcode_pro-1.5.0 → zcode_pro-1.5.1}/zcode_pro/cli.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: zcode-pro
3
- Version: 1.5.0
3
+ Version: 1.5.1
4
4
  Summary: ZCode Pro — ZCode 桌面版界面增强启动器(不修改客户端文件),核心功能:项目自定义别名
5
5
  Project-URL: Homepage, https://github.com/duanluan/zcode-pro
6
6
  Project-URL: Repository, https://github.com/duanluan/zcode-pro
@@ -38,7 +38,7 @@ ZCode 桌面版界面增强工具。不修改客户端文件,通过 ZCode Pro
38
38
  | | ~~项目自定义别名~~(已停用) | 侧边栏显示自定义名称,仅界面显示,随时恢复;ZCode ≥ 3.16.1 已内置改名(项目菜单“编辑项目”),本功能自动停用并引导把已有别名迁移过去 |
39
39
  | | ~~切换文件夹~~(已停用) | 将项目指向另一个文件夹,侧边栏、标签页与任务历史一并迁移;切换前先停止运行中的任务;历史检查点仍引用原路径;ZCode ≥ 3.16.1 官方已支持一个项目挂多个目录,本功能随之停用 |
40
40
  | | 会话拖动排序 | 拖动调整置顶、项目与分组中的会话顺序,重启后不变 |
41
- | | 会话快捷切换 | `alt+z` 在当前与上次会话间来回切换;按住 `alt` 按 `x`(上移)/ `c`(下移)在最近会话列表中选择(初始 10 条,`c` 到底再加载 5 条),`alt+e` 直接打开最近 10 条,`alt+r` 打开正在运行与运行完未查看的任务;方向键同样可选,松开 `alt` 或 `Enter` 切换(类 `alt+tab`);五个入口键可在 设置 → 快捷键 中修改 |
41
+ | | 会话快捷切换 | `alt+z` 在当前与上次会话间来回切换;按住 `alt` 按 `x`(上移)/ `c`(下移)在最近会话列表中选择(初始 10 条,`c` 到底再加载 5 条),`alt+e` 直接打开最近 10 条,`alt+r` 打开正在运行与运行完未查看的任务;方向键同样可选,`Enter` 或点击行切换;五个入口键可在 设置 → 快捷键 中修改 |
42
42
  | | 重命名会话名锁定 | 手动重命名过的会话不再被自动标题覆盖,侧栏始终显示重命名后的名字 |
43
43
  | | 已置顶分区可折叠 | 侧边栏“已置顶”标题可点击折叠/展开其任务列表 |
44
44
  | | 文件菜单增强 | 会话中文件链接的右键菜单新增“默认应用打开”“打开所在目录” |
@@ -18,7 +18,7 @@ Grouped by where each feature lives in the settings window: the category column
18
18
  | | ~~Custom project alias~~ (retired) | Show a custom name for a project in the sidebar; UI-only, revert anytime; renaming is built into ZCode ≥ 3.16.1 (project menu "Edit project") — the feature disables itself there and offers to migrate existing aliases |
19
19
  | | ~~Switch project folder~~ (retired) | Point a project at another folder; sidebar, tabs and task history follow. Stop running tasks before switching; historical checkpoints still reference the original path; retired on ZCode ≥ 3.16.1, where a project can hold multiple directories natively |
20
20
  | | Session drag ordering | Reorder sessions in Pinned, Projects and Groups; persists across refreshes |
21
- | | Session quick switch | `alt+z` toggles between the current and the last session; hold `alt` and press `x` (up) / `c` (down) to pick from recent sessions (10 shown initially, pressing `c` at the bottom loads 5 more), `alt+e` opens the last 10 directly, `alt+r` opens running / finished-but-unseen tasks; arrow keys also move the highlight, release `alt` or press `Enter` to switch (like `alt+tab`); the five trigger keys are customizable in Settings → Shortcuts |
21
+ | | Session quick switch | `alt+z` toggles between the current and the last session; hold `alt` and press `x` (up) / `c` (down) to pick from recent sessions (10 shown initially, pressing `c` at the bottom loads 5 more), `alt+e` opens the last 10 directly, `alt+r` opens running / finished-but-unseen tasks; arrow keys also move the highlight, press `Enter` or click a row to switch; the five trigger keys are customizable in Settings → Shortcuts |
22
22
  | | Keep renamed session titles | Sessions you renamed keep their custom title; auto-generated titles no longer overwrite it in the sidebar |
23
23
  | | Collapsible Pinned section | The sidebar "Pinned" header collapses/expands its task list; the state is remembered |
24
24
  | | File menu actions | Adds "Open with default app" and "Reveal in file manager" to file links in chat |
@@ -18,7 +18,7 @@ ZCode 桌面版界面增强工具。不修改客户端文件,通过 ZCode Pro
18
18
  | | ~~项目自定义别名~~(已停用) | 侧边栏显示自定义名称,仅界面显示,随时恢复;ZCode ≥ 3.16.1 已内置改名(项目菜单“编辑项目”),本功能自动停用并引导把已有别名迁移过去 |
19
19
  | | ~~切换文件夹~~(已停用) | 将项目指向另一个文件夹,侧边栏、标签页与任务历史一并迁移;切换前先停止运行中的任务;历史检查点仍引用原路径;ZCode ≥ 3.16.1 官方已支持一个项目挂多个目录,本功能随之停用 |
20
20
  | | 会话拖动排序 | 拖动调整置顶、项目与分组中的会话顺序,重启后不变 |
21
- | | 会话快捷切换 | `alt+z` 在当前与上次会话间来回切换;按住 `alt` 按 `x`(上移)/ `c`(下移)在最近会话列表中选择(初始 10 条,`c` 到底再加载 5 条),`alt+e` 直接打开最近 10 条,`alt+r` 打开正在运行与运行完未查看的任务;方向键同样可选,松开 `alt` 或 `Enter` 切换(类 `alt+tab`);五个入口键可在 设置 → 快捷键 中修改 |
21
+ | | 会话快捷切换 | `alt+z` 在当前与上次会话间来回切换;按住 `alt` 按 `x`(上移)/ `c`(下移)在最近会话列表中选择(初始 10 条,`c` 到底再加载 5 条),`alt+e` 直接打开最近 10 条,`alt+r` 打开正在运行与运行完未查看的任务;方向键同样可选,`Enter` 或点击行切换;五个入口键可在 设置 → 快捷键 中修改 |
22
22
  | | 重命名会话名锁定 | 手动重命名过的会话不再被自动标题覆盖,侧栏始终显示重命名后的名字 |
23
23
  | | 已置顶分区可折叠 | 侧边栏“已置顶”标题可点击折叠/展开其任务列表 |
24
24
  | | 文件菜单增强 | 会话中文件链接的右键菜单新增“默认应用打开”“打开所在目录” |
@@ -280,14 +280,14 @@
280
280
  userFontDesc: "\u4F1A\u8BDD\u4E2D\u63D0\u95EE\u5185\u5BB9\u7684\u5B57\u4F53\uFF1B\u9009\u201C\u9ED8\u8BA4\u201D\u65F6\u8DDF\u968F\u754C\u9762\u5B57\u4F53\u3002",
281
281
  assistantFontName: "\u56DE\u7B54\u5B57\u4F53",
282
282
  assistantFontDesc: "\u4F1A\u8BDD\u4E2D\u56DE\u7B54\u6B63\u6587\u7684\u5B57\u4F53\uFF08\u4EE3\u7801\u5757\u4ECD\u7528\u7B49\u5BBD\u5B57\u4F53\uFF09\uFF1B\u9009\u201C\u9ED8\u8BA4\u201D\u65F6\u8DDF\u968F\u754C\u9762\u5B57\u4F53\u3002",
283
- switcherHint: "{prev} / {next} \u6216 \u2191 / \u2193 \u9009\u62E9\uFF0CEnter \u6216\u677E\u5F00 alt \u5207\u6362\uFF0CEsc \u53D6\u6D88",
283
+ switcherHint: "{prev} / {next} \u6216 \u2191 / \u2193 \u9009\u62E9\uFF0CEnter \u6216\u70B9\u51FB\u884C\u5207\u6362\uFF0CEsc \u53D6\u6D88",
284
284
  switcherCurrent: "\u5F53\u524D",
285
285
  switcherEmpty: "\u6682\u65E0\u4E0A\u6B21\u4F1A\u8BDD\uFF08\u5207\u6362\u8FC7\u4F1A\u8BDD\u540E\u53EF\u7528\uFF09",
286
286
  switcherFailed: "\u672A\u80FD\u5207\u6362\u5230\u8BE5\u4F1A\u8BDD\uFF08\u53EF\u80FD\u5DF2\u5220\u9664\uFF09",
287
287
  switcherMore: "\u8FD8\u6709 {n} \u6761 \xB7 \u6309 c \u52A0\u8F7D",
288
288
  switcherRunning: "\u8FD0\u884C\u4E2D",
289
289
  switcherUnseen: "\u672A\u67E5\u770B",
290
- switcherAttentionHint: "\u8FD0\u884C\u4E2D / \u8FD0\u884C\u5B8C\u672A\u67E5\u770B\u7684\u4EFB\u52A1 \xB7 {prev} / {next} \u6216 \u2191 / \u2193 \u9009\u62E9\uFF0CEnter \u6216\u677E\u5F00 alt \u5207\u6362",
290
+ switcherAttentionHint: "\u8FD0\u884C\u4E2D / \u8FD0\u884C\u5B8C\u672A\u67E5\u770B\u7684\u4EFB\u52A1 \xB7 {prev} / {next} \u6216 \u2191 / \u2193 \u9009\u62E9\uFF0CEnter \u6216\u70B9\u51FB\u884C\u5207\u6362",
291
291
  switcherAttentionEmpty: "\u6682\u65E0\u8FD0\u884C\u4E2D\u6216\u672A\u67E5\u770B\u7684\u4EFB\u52A1",
292
292
  featureNewtabPanels: "\u65B0\u6807\u7B7E\u9875\u63D2\u4EF6\u9762\u677F\u5165\u53E3",
293
293
  featureNewtabPanelsDesc: "\u65B0\u6807\u7B7E\u9875\u52A0\u5165\u7B2C\u4E09\u65B9\u63D2\u4EF6\uFF08headroom\u3001rtk \u7B49\uFF09\u7684\u9762\u677F\u5361\u7247\uFF0C\u70B9\u51FB\u5728\u4FA7\u8FB9\u9762\u677F\u6253\u5F00\u3002",
@@ -384,8 +384,6 @@
384
384
  hkArrow: "\u5F39\u7A97\u6253\u5F00\u65F6\u79FB\u52A8\u9AD8\u4EAE\uFF08\u4E0E alt + {prev} / alt + {next} \u540C\u89C4\u5219\uFF09",
385
385
  hkEnter: "\u786E\u8BA4\u5207\u6362\u5230\u9AD8\u4EAE\u7684\u4F1A\u8BDD",
386
386
  hkEsc: "\u53D6\u6D88\u5E76\u5173\u95ED\u5F39\u7A97",
387
- hkRelease: "\u79FB\u52A8\u8FC7\u9AD8\u4EAE\u65F6\u677E\u5F00\u5373\u5207\u6362\uFF1B\u4ECE\u672A\u79FB\u52A8\u5219\u5F39\u7A97\u4FDD\u6301\u6253\u5F00\uFF0C\u7EE7\u7EED\u9009\u62E9",
388
- hkReleaseKey: "\u677E\u5F00 alt",
389
387
  hkModify: "\u4FEE\u6539",
390
388
  hkRecording: "\u6309\u4E0B\u65B0\u952E\u2026",
391
389
  hkInvalid: "\u5FEB\u6377\u952E\u53EA\u652F\u6301\u5355\u4E2A\u5B57\u6BCD\u6216\u6570\u5B57",
@@ -478,7 +476,7 @@
478
476
  headroomDesc: "Headroom \u63D2\u4EF6\uFF1A\u672C\u5730\u538B\u7F29\u4EE3\u7406\u9A7B\u7559 127.0.0.1\uFF08\u9ED8\u8BA4 8787 \u7AEF\u53E3\uFF09\uFF0C\u538B\u7F29\u53D1\u5F80\u6A21\u578B\u7684\u4E0A\u4E0B\u6587\u4EE5\u7701 token\uFF08/hr-* \u547D\u4EE4\u7F16\u8F91\u540C\u4E00\u914D\u7F6E\uFF09\u3002\u5207\u6362\u8BBE\u5907\u4F1A\u91CD\u542F\u4EE3\u7406\uFF0C\u6B63\u5728\u5904\u7406\u7684\u8BF7\u6C42\u53EF\u80FD\u4E2D\u65AD\u4E00\u6B21\u3002",
479
477
  headroomBinMissing: "\u672A\u627E\u5230 headroom \u7A0B\u5E8F\uFF0C\u4EE3\u7406\u65E0\u6CD5\u542F\u52A8\uFF1B\u5B89\u88C5 headroom \u540E\u4EE5\u4E0B\u8BBE\u7F6E\u81EA\u52A8\u751F\u6548",
480
478
  hrInstallBin: "\u5B89\u88C5 headroom \u672C\u4F53",
481
- hrBinInstalled: "headroom \u672C\u4F53\u5DF2\u5B89\u88C5\uFF1B\u91CD\u65B0\u6253\u5F00\u4F1A\u8BDD\u540E\u4EE3\u7406\u53EF\u7528\uFF08SessionStart \u94A9\u5B50\u81EA\u52A8\u62C9\u8D77\uFF09",
479
+ hrBinInstalled: "headroom \u672C\u4F53\u5DF2\u5B89\u88C5\uFF1B\u91CD\u65B0\u6253\u5F00\u5E94\u7528\u6216\u4F1A\u8BDD\u540E\u4EE3\u7406\u53EF\u7528\uFF08SessionStart \u94A9\u5B50\u81EA\u52A8\u62C9\u8D77\uFF09",
482
480
  headroomInstallTitle: "\u672A\u5B89\u88C5 headroom \u63D2\u4EF6",
483
481
  headroomInstallDesc: "\u4ECE zcode-plugins \u5E02\u573A\u5B89\u88C5\u540E\u5373\u53EF\u5728\u6B64\u7BA1\u7406\u538B\u7F29\u4EE3\u7406\uFF1B\u91CD\u542F\u4F1A\u8BDD\u540E\u751F\u6548\u3002",
484
482
  headroomInstall: "\u5B89\u88C5",
@@ -792,14 +790,14 @@
792
790
  userFontDesc: 'Font of question text in conversations; "default" follows the UI font.',
793
791
  assistantFontName: "Answer font",
794
792
  assistantFontDesc: 'Font of answer text in conversations (code blocks stay monospaced); "default" follows the UI font.',
795
- switcherHint: "{prev} / {next} or \u2191 / \u2193 to choose, Enter or release alt to switch, Esc to cancel",
793
+ switcherHint: "{prev} / {next} or \u2191 / \u2193 to choose, Enter or click a row to switch, Esc to cancel",
796
794
  switcherCurrent: "current",
797
795
  switcherEmpty: "No previous session yet (available after you switch sessions)",
798
796
  switcherFailed: "Failed to switch to that session (it may have been deleted)",
799
797
  switcherMore: "{n} more \xB7 press c to load",
800
798
  switcherRunning: "running",
801
799
  switcherUnseen: "unseen",
802
- switcherAttentionHint: "Running / finished but unseen tasks \xB7 {prev} / {next} or \u2191 / \u2193 to choose, Enter or release alt to switch",
800
+ switcherAttentionHint: "Running / finished but unseen tasks \xB7 {prev} / {next} or \u2191 / \u2193 to choose, Enter or click a row to switch",
803
801
  switcherAttentionEmpty: "No running or unseen tasks",
804
802
  featureNewtabPanels: "Plugin panels on the new tab page",
805
803
  featureNewtabPanelsDesc: "Adds third-party plugin panels (headroom, rtk, ...) to the new tab page; click to open in the side pane.",
@@ -896,8 +894,6 @@
896
894
  hkArrow: "Move the highlight while the popup is open (same rules as alt + {prev} / alt + {next})",
897
895
  hkEnter: "Switch to the highlighted session",
898
896
  hkEsc: "Cancel and close the popup",
899
- hkRelease: "Releasing alt switches once the highlight has moved; otherwise the popup stays open for further selection",
900
- hkReleaseKey: "release alt",
901
897
  hkModify: "Change",
902
898
  hkRecording: "press a key\u2026",
903
899
  hkInvalid: "Shortcuts accept a single letter or digit only",
@@ -990,7 +986,7 @@
990
986
  headroomDesc: "Headroom plugin: a local compression proxy on 127.0.0.1 (port 8787 by default) that compresses context sent to the model to save tokens (/hr-* commands edit the same file). Switching the device restarts the proxy, so requests in progress may be interrupted once.",
991
987
  headroomBinMissing: "The headroom program was not found, so the proxy cannot start. The settings below take effect once it is installed.",
992
988
  hrInstallBin: "Install headroom binary",
993
- hrBinInstalled: "headroom installed; reopen a session and the hook will start the proxy automatically",
989
+ hrBinInstalled: "headroom installed; reopen the app or a session and the proxy will start automatically",
994
990
  headroomInstallTitle: "headroom plugin not installed",
995
991
  headroomInstallDesc: "Install it from the zcode-plugins marketplace to manage the compression proxy here; takes effect after a session restart.",
996
992
  headroomInstall: "Install",
@@ -4923,8 +4919,7 @@
4923
4919
  }),
4924
4920
  hkCell("\u2191 / \u2193", L.hkArrow.replaceAll("{prev}", keys.switchPrev).replaceAll("{next}", keys.switchNext)),
4925
4921
  hkCell("Enter", L.hkEnter),
4926
- hkCell("Esc", L.hkEsc),
4927
- hkCell(L.hkReleaseKey, L.hkRelease)
4922
+ hkCell("Esc", L.hkEsc)
4928
4923
  );
4929
4924
  };
4930
4925
  const stopHkRecording = () => {
@@ -8044,7 +8039,7 @@
8044
8039
  }
8045
8040
  }
8046
8041
  function workspaceOf(key) {
8047
- const i = key.indexOf(":sess_");
8042
+ const i = key.lastIndexOf(":");
8048
8043
  return i > 0 ? key.slice(0, i) : key;
8049
8044
  }
8050
8045
  function wsHead(row) {
@@ -8182,6 +8177,7 @@
8182
8177
  var anyActiveCache = { at: 0, value: null, busy: false };
8183
8178
  var sectionActive = { task: false, project: false, pinned: false };
8184
8179
  var activeProjectPaths = /* @__PURE__ */ new Set();
8180
+ var authActiveByWs = /* @__PURE__ */ new Map();
8185
8181
  setInterval(() => {
8186
8182
  if (!anyActiveCache.busy && Date.now() - anyActiveCache.at >= 1e4) globalAnyActive();
8187
8183
  }, 1e4);
@@ -8193,6 +8189,7 @@
8193
8189
  sectionActive.project = false;
8194
8190
  sectionActive.pinned = false;
8195
8191
  activeProjectPaths = /* @__PURE__ */ new Set();
8192
+ authActiveByWs = /* @__PURE__ */ new Map();
8196
8193
  void (async () => {
8197
8194
  let hit = null;
8198
8195
  try {
@@ -8244,12 +8241,24 @@
8244
8241
  const allIds = [...byKind.task, ...byKind.project];
8245
8242
  const st = allIds.length ? await rpc("/task-statuses?ids=" + encodeURIComponent(allIds.join(","))).catch(() => null) : null;
8246
8243
  const TERMINAL = /* @__PURE__ */ new Set(["completed", "error", "interrupted"]);
8244
+ const recOf = (id) => st && st.ok && st.available ? st.statuses[id] : void 0;
8247
8245
  const live = (id) => {
8248
- const rec = st && st.ok && st.available ? st.statuses[id] : void 0;
8246
+ const rec = recOf(id);
8249
8247
  if (rec === void 0) return true;
8250
8248
  if (!TERMINAL.has(rec.status)) return true;
8251
8249
  return Date.now() - rec.updatedAt < 3e5;
8252
8250
  };
8251
+ const liveSession = (id) => {
8252
+ const rec = recOf(id);
8253
+ if (rec === void 0) return !!domRowKeyOf(id);
8254
+ if (!TERMINAL.has(rec.status)) return true;
8255
+ return Date.now() - rec.updatedAt < 3e5;
8256
+ };
8257
+ authActiveByWs = /* @__PURE__ */ new Map();
8258
+ for (const [base, ids] of byKindByPath) {
8259
+ const liveIds = ids.filter(liveSession);
8260
+ if (liveIds.length) authActiveByWs.set(base, liveIds);
8261
+ }
8253
8262
  activeProjectPaths = /* @__PURE__ */ new Set();
8254
8263
  for (const [base, ids] of byKindByPath) {
8255
8264
  if (!projPaths.has(base)) continue;
@@ -8272,7 +8281,7 @@
8272
8281
  for (const el of list.querySelectorAll("[data-task-item-key]")) {
8273
8282
  if (el.closest("section[data-project-id]")) continue;
8274
8283
  const k = el.getAttribute("data-task-item-key") || "";
8275
- const id = k.slice(k.indexOf(":sess_") + 1);
8284
+ const id = k.slice(k.lastIndexOf(":") + 1);
8276
8285
  const base = k.slice(0, k.lastIndexOf(":")).replace(/[\/]+$/, "");
8277
8286
  const ids = byKindByPath.get(base) || [];
8278
8287
  if (ids.map(String).includes(id) && ids.some(live)) {
@@ -8423,9 +8432,23 @@
8423
8432
  current: curKey ? workspaceOf(curKey) || null : null
8424
8433
  };
8425
8434
  }
8435
+ function domRowKeyOf(id) {
8436
+ if (!id) return "";
8437
+ try {
8438
+ const el = document.querySelector(`[data-task-item-key$=":${CSS.escape(id)}"]`);
8439
+ return el && el.getAttribute("data-task-item-key") || "";
8440
+ } catch {
8441
+ return "";
8442
+ }
8443
+ }
8426
8444
  function wsRunningKeys() {
8427
8445
  const keys = /* @__PURE__ */ new Set();
8428
- for (const set of runningByWs.values()) for (const k of set) keys.add(k);
8446
+ for (const set of runningByWs.values()) {
8447
+ for (const k of set) if (!k.endsWith(":active")) keys.add(k);
8448
+ }
8449
+ for (const [base, ids] of authActiveByWs) {
8450
+ for (const id of ids) keys.add(domRowKeyOf(id) || `${base}:${id}`);
8451
+ }
8429
8452
  return keys;
8430
8453
  }
8431
8454
  async function verifyWorkspaceNotRunning(ws) {
@@ -8603,7 +8626,6 @@
8603
8626
  var visibleCount = INITIAL_COUNT;
8604
8627
  var unseen = /* @__PURE__ */ new Set();
8605
8628
  var lastRunKeys = null;
8606
- var movedSinceOpen = false;
8607
8629
  var hotkeys = { ...DEFAULT_SHORTCUTS };
8608
8630
  function loadHotkeys() {
8609
8631
  return getConfig(true).then((cfg) => {
@@ -8621,7 +8643,7 @@
8621
8643
  return (tEl ? tEl.textContent : (item.innerText || "").split("\n")[0])?.trim() || "";
8622
8644
  }
8623
8645
  function workspaceOf2(key) {
8624
- const i = key.indexOf(":sess_");
8646
+ const i = key.lastIndexOf(":");
8625
8647
  return i > 0 ? key.slice(0, i) : key;
8626
8648
  }
8627
8649
  function findCurrent() {
@@ -8672,7 +8694,8 @@
8672
8694
  }, SCAN_DEBOUNCE_MS2);
8673
8695
  }
8674
8696
  async function switchTo(key) {
8675
- const find = () => document.querySelector(`[data-task-item-key="${CSS.escape(key)}"]`);
8697
+ const id = key.slice(key.lastIndexOf(":") + 1);
8698
+ const find = () => document.querySelector(`[data-task-item-key="${CSS.escape(key)}"]`) || (id ? document.querySelector(`[data-task-item-key$=":${CSS.escape(id)}"]`) : null);
8676
8699
  let item = find();
8677
8700
  if (!item) {
8678
8701
  const ws = workspaceOf2(key);
@@ -8690,23 +8713,28 @@
8690
8713
  }
8691
8714
  function attentionList() {
8692
8715
  const run = wsRunningKeys();
8716
+ const idOf = (k) => k.slice(k.lastIndexOf(":") + 1);
8717
+ const runKeyById = new Map([...run].map((k) => [idOf(k), k]));
8693
8718
  const listed = /* @__PURE__ */ new Set();
8694
8719
  const head = [];
8695
8720
  for (const k of mru) {
8696
- if (run.has(k) || unseen.has(k)) {
8697
- head.push(k);
8698
- listed.add(k);
8721
+ const id = idOf(k);
8722
+ if (listed.has(id)) continue;
8723
+ const runForm = runKeyById.get(id);
8724
+ if (run.has(k) || unseen.has(k) || runForm) {
8725
+ head.push(runForm || k);
8726
+ listed.add(id);
8699
8727
  }
8700
8728
  }
8701
8729
  const tail = [];
8702
- for (const k of run) if (!listed.has(k)) {
8730
+ const push = (k) => {
8731
+ const id = idOf(k);
8732
+ if (listed.has(id)) return;
8733
+ listed.add(id);
8703
8734
  tail.push(k);
8704
- listed.add(k);
8705
- }
8706
- for (const k of unseen) if (!listed.has(k)) {
8707
- tail.push(k);
8708
- listed.add(k);
8709
- }
8735
+ };
8736
+ for (const k of run) push(k);
8737
+ for (const k of unseen) push(k);
8710
8738
  return [...head, ...tail];
8711
8739
  }
8712
8740
  function activeList() {
@@ -8727,9 +8755,23 @@
8727
8755
  function findTitle(key) {
8728
8756
  const item = document.querySelector(`[data-task-item-key="${CSS.escape(key)}"]`);
8729
8757
  if (item) titles.set(key, firstLineOf(item));
8730
- const id = key.slice(key.indexOf(":sess_") + 1);
8758
+ const id = key.slice(key.lastIndexOf(":") + 1);
8731
8759
  return titles.get(key) || `sess ${id.slice(0, 8)}`;
8732
8760
  }
8761
+ async function fetchMissingTitles(keys) {
8762
+ const sessIdOf3 = (k) => k.slice(k.lastIndexOf(":") + 1);
8763
+ const missing = keys.filter((k) => {
8764
+ const id = sessIdOf3(k);
8765
+ return id && !titles.get(k) && !document.querySelector(`[data-task-item-key$=":${CSS.escape(id)}"]`);
8766
+ });
8767
+ if (!missing.length) return;
8768
+ const st = await rpc("/task-statuses?ids=" + encodeURIComponent(missing.map(sessIdOf3).join(","))).catch(() => null);
8769
+ if (!st || !st.ok || !st.available || !st.statuses) return;
8770
+ for (const k of missing) {
8771
+ const rec = st.statuses[sessIdOf3(k)];
8772
+ if (rec && rec.title) titles.set(k, String(rec.title));
8773
+ }
8774
+ }
8733
8775
  function projLabelOf(key, aliases2) {
8734
8776
  const ws = workspaceOf2(key);
8735
8777
  if (!ws) return "";
@@ -8775,14 +8817,23 @@
8775
8817
  ));
8776
8818
  }
8777
8819
  }
8778
- async function openPopup(mode = "mru") {
8820
+ var openPopupInflight = null;
8821
+ function openPopup(mode = "mru") {
8779
8822
  if (popupEl) return;
8823
+ if (openPopupInflight) return openPopupInflight;
8824
+ openPopupInflight = openPopupInner(mode).finally(() => {
8825
+ openPopupInflight = null;
8826
+ });
8827
+ return openPopupInflight;
8828
+ }
8829
+ async function openPopupInner(mode) {
8780
8830
  popupMode = mode;
8781
8831
  visibleCount = INITIAL_COUNT;
8782
8832
  highlight = 0;
8783
- movedSinceOpen = false;
8784
8833
  const cfg = await getConfig().catch(() => null);
8785
8834
  aliasesCache = cfg && cfg.aliases && typeof cfg.aliases === "object" ? cfg.aliases : {};
8835
+ if (mode === "attention") await fetchMissingTitles(attentionList()).catch(() => {
8836
+ });
8786
8837
  ensureStyle();
8787
8838
  const L = t();
8788
8839
  popupEl = h(
@@ -8817,7 +8868,6 @@
8817
8868
  const len = Math.min(activeList().length, visibleCount);
8818
8869
  if (len < 2) return;
8819
8870
  highlight = highlight > 0 ? highlight - 1 : len - 1;
8820
- movedSinceOpen = true;
8821
8871
  renderPopup();
8822
8872
  scrollHighlight();
8823
8873
  }
@@ -8833,7 +8883,6 @@
8833
8883
  } else {
8834
8884
  highlight = 0;
8835
8885
  }
8836
- movedSinceOpen = true;
8837
8886
  renderPopup();
8838
8887
  scrollHighlight();
8839
8888
  }
@@ -8856,8 +8905,10 @@
8856
8905
  if (document.hidden) return;
8857
8906
  const now = wsRunningKeys();
8858
8907
  if (lastRunKeys) {
8908
+ const idOf = (k) => k.slice(k.lastIndexOf(":") + 1);
8909
+ const nowIds = new Set([...now].map(idOf));
8859
8910
  for (const k of lastRunKeys) {
8860
- if (!now.has(k) && k !== current) unseen.add(k);
8911
+ if (!nowIds.has(idOf(k)) && k !== current) unseen.add(k);
8861
8912
  }
8862
8913
  }
8863
8914
  lastRunKeys = now;
@@ -8874,6 +8925,14 @@
8874
8925
  closePopup(false);
8875
8926
  return;
8876
8927
  }
8928
+ const bareK = e.key.toLowerCase();
8929
+ if (popupEl && !e.altKey && !e.ctrlKey && !e.metaKey && !e.isComposing && (bareK === hotkeys.switchPrev || bareK === hotkeys.switchNext)) {
8930
+ e.preventDefault();
8931
+ e.stopImmediatePropagation();
8932
+ if (bareK === hotkeys.switchPrev) moveUp();
8933
+ else moveDown();
8934
+ return;
8935
+ }
8877
8936
  if (popupEl && (e.key === "ArrowUp" || e.key === "ArrowDown" || e.key === "Enter")) {
8878
8937
  e.preventDefault();
8879
8938
  e.stopImmediatePropagation();
@@ -8917,22 +8976,29 @@
8917
8976
  await openPopup("attention");
8918
8977
  return;
8919
8978
  }
8920
- if (mru.length < 2) {
8921
- showToast(t().switcherEmpty);
8922
- return;
8923
- }
8924
8979
  if (k === hotkeys.switchRecent) {
8925
- if (!e.repeat && !popupEl) await openPopup("mru");
8980
+ if (!e.repeat && !popupEl && !openPopupInflight) {
8981
+ if (!mru.length) {
8982
+ showToast(t().switcherEmpty);
8983
+ return;
8984
+ }
8985
+ await openPopup("mru");
8986
+ }
8926
8987
  return;
8927
8988
  }
8928
- if (!popupEl) await openPopup("mru");
8989
+ if (openPopupInflight) {
8990
+ await openPopupInflight;
8991
+ } else if (!popupEl) {
8992
+ if (!mru.length) {
8993
+ showToast(t().switcherEmpty);
8994
+ return;
8995
+ }
8996
+ await openPopup("mru");
8997
+ }
8929
8998
  if (k === hotkeys.switchPrev) moveUp();
8930
8999
  else moveDown();
8931
9000
  })();
8932
9001
  }, true);
8933
- document.addEventListener("keyup", (e) => {
8934
- if (e.key === "Alt" && popupEl && movedSinceOpen) closePopup(true);
8935
- }, true);
8936
9002
  window.addEventListener("blur", () => closePopup(false));
8937
9003
  }
8938
9004
 
@@ -8997,7 +9063,7 @@
8997
9063
  const cur = rows.find((e) => (e.className + "").includes("bg-selected")) || rows[rows.length - 1];
8998
9064
  const key = cur ? cur.getAttribute("data-task-item-key") : "";
8999
9065
  if (!key) return null;
9000
- const i = key.indexOf(":sess_");
9066
+ const i = key.lastIndexOf(":");
9001
9067
  return i > 0 ? { workspacePath: key.slice(0, i), sessionId: key.slice(i + 1) } : null;
9002
9068
  }
9003
9069
  async function thirdPartySurfaces(bridge, workspacePath) {
@@ -9562,7 +9628,7 @@
9562
9628
  var fetching = null;
9563
9629
  var pendingKeys = /* @__PURE__ */ new Set();
9564
9630
  function sessIdOf(key) {
9565
- const i = key.indexOf(":sess_");
9631
+ const i = key.lastIndexOf(":");
9566
9632
  return i > 0 ? key.slice(i + 1) : key;
9567
9633
  }
9568
9634
  function titleElsOf(item) {
@@ -9823,7 +9889,7 @@
9823
9889
  var attempted = /* @__PURE__ */ new Set();
9824
9890
  var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
9825
9891
  function sessIdOf2(key) {
9826
- const i = key.indexOf(":sess_");
9892
+ const i = key.lastIndexOf(":");
9827
9893
  return i > 0 ? key.slice(i + 1) : "";
9828
9894
  }
9829
9895
  function currentKey() {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zcode-pro",
3
- "version": "1.5.0",
3
+ "version": "1.5.1",
4
4
  "description": "ZCode Pro — 为 ZCode 桌面版增加界面增强(不修改客户端文件,通过 ZCode Pro 快捷方式启动即可),核心功能:项目自定义别名",
5
5
  "type": "module",
6
6
  "bin": {
@@ -11,13 +11,13 @@ import { basename, delimiter, dirname, isAbsolute, join, resolve, sep } from 'no
11
11
  import { homedir, tmpdir, userInfo } from 'node:os';
12
12
  import { fileURLToPath, pathToFileURL } from 'node:url';
13
13
  import { readSettings, writeSettingsAtomic, remapSettingsPaths, isProjectOpenInTabs } from './settings.mjs';
14
- import { taskIndexPath, probeTaskIndexWritable, remapTaskIndexPaths, taskIndexDriverAvailable, listOverriddenTitles, listTaskWorkspaceMap, setTaskTitleIfNotOverridden, setCustomSessionTitle, isTaskTitleOverridden, reconcileCustomTitles, getTaskProviderModel, listVirtualProjects, countTasks } from './taskIndex.mjs';
14
+ import { taskIndexPath, probeTaskIndexWritable, remapTaskIndexPaths, taskIndexDriverAvailable, listOverriddenTitles, listTaskWorkspaceMap, listTaskStatuses, setTaskTitleIfNotOverridden, setCustomSessionTitle, isTaskTitleOverridden, reconcileCustomTitles, getTaskProviderModel, listVirtualProjects, countTasks } from './taskIndex.mjs';
15
15
  import { pickFolderSystem } from './pickFolder.mjs';
16
16
  import { reorderWorkspaceTasks, reorderGroupMembers } from './taskOrder.mjs';
17
17
  import { webdavRequest, webdavPutWithMkcol, webdavHttpText, parseWebdavListing, findCurl } from './webdav.mjs';
18
18
  import { resolveZcodeExecutable } from './paths.mjs';
19
19
 
20
- const VERSION = '1.5.0';
20
+ const VERSION = '1.5.1';
21
21
 
22
22
  // 全局提示词固定在用户主目录:官方加载器按 HOME/USERPROFILE 拼 .zcode/AGENTS.md,
23
23
  // 不读 ZCODE_DATA_BASE_DIR(数据根迁走时全局指令仍在原位)。
@@ -707,6 +707,17 @@ export function startHelper({ port, token, dataRoot, state, agentsFile = default
707
707
  : { ok: true, available: false, projects: [], deletedRoots: [] });
708
708
  return;
709
709
  }
710
+ if (req.method === 'GET' && url.pathname === '/task-statuses') {
711
+ // 批量会话终态 + 标题:注入侧剔除 syncer 卡滞的误报、alt+r 弹窗显示
712
+ // 折叠项目会话标题都用它。读不到时 available=false,调用方按未确认处理
713
+ const ids = (url.searchParams.get('ids') || '')
714
+ .split(',').map((s) => s.trim()).filter(Boolean).slice(0, 200);
715
+ const statuses = ids.length ? await listTaskStatuses(dataRoot, ids) : {};
716
+ json(res, 200, statuses !== null
717
+ ? { ok: true, available: true, statuses }
718
+ : { ok: true, available: false, statuses: {} });
719
+ return;
720
+ }
710
721
  if (req.method === 'GET' && url.pathname === '/projects') {
711
722
  const settings = readSettings(settingsFile);
712
723
  const projects = (settings.recentProjects || []).map((p) => {
@@ -837,7 +848,9 @@ export function startHelper({ port, token, dataRoot, state, agentsFile = default
837
848
  json(res, 400, { ok: false, code: 'title-gen-disabled', error: '智能起名未开启' });
838
849
  return;
839
850
  }
840
- const sessId = typeof body?.sessId === 'string' && /^sess_[A-Za-z0-9._-]+$/.test(body.sessId) ? body.sessId : '';
851
+ // 会话 id 有 sess_ 与 session_ 两种前缀(新版应用为 session_),两种都认,
852
+ // 否则新形式会话的标题永远不会写入数据库
853
+ const sessId = typeof body?.sessId === 'string' && /^(?:sess|session)_[A-Za-z0-9._-]+$/.test(body.sessId) ? body.sessId : '';
841
854
  let title;
842
855
  try {
843
856
  title = await generateTitleWith(cfg, text, dataRoot, sessId);
@@ -1512,7 +1525,8 @@ export function startHelper({ port, token, dataRoot, state, agentsFile = default
1512
1525
  renameSync(vTmp, vFile);
1513
1526
  }
1514
1527
  } catch { /* 无配置或不可写不影响启动 */ }
1515
- server.listen(port, '127.0.0.1', () => resolveStarted(server));
1528
+ // 服务就绪即安排 headroom 代理兜底(内部延迟 5 秒执行,避开启动高峰)
1529
+ server.listen(port, '127.0.0.1', () => { ensureHeadroomAtStartup(); resolveStarted(server); });
1516
1530
  };
1517
1531
  tryListen();
1518
1532
  });
@@ -1703,12 +1717,13 @@ function mergeTitleGenConfig(saved) {
1703
1717
  // 登录账号的套餐已在供应商列表里,跟随会话即覆盖原语义
1704
1718
  const provider = typeof s.provider === 'string' ? s.provider.trim().slice(0, 200) : d.provider;
1705
1719
  // customWritten:起名功能写过的 { sessId → { title, at } },自动校正只认这些
1706
- // 记录(见 reconcileCustomTitles)。只收合法会话 id 与短字符串,超 7 天/500 条的丢弃
1720
+ // 记录(见 reconcileCustomTitles)。只收合法会话 id(sess_/session_ 两种前缀,
1721
+ // 新版应用为 session_)与短字符串,超 7 天/500 条的丢弃
1707
1722
  const written = {};
1708
1723
  const cutoff = Date.now() - 7 * 24 * 3600 * 1000;
1709
1724
  let kept = 0;
1710
1725
  for (const [k, v] of Object.entries((s.customWritten && typeof s.customWritten === 'object' && !Array.isArray(s.customWritten)) ? s.customWritten : {})) {
1711
- if (kept >= 500 || !/^sess_[A-Za-z0-9._-]+$/.test(k)) continue;
1726
+ if (kept >= 500 || !/^(?:sess|session)_[A-Za-z0-9._-]+$/.test(k)) continue;
1712
1727
  if (!v || typeof v !== 'object' || typeof v.title !== 'string' || v.title.length > 200) continue;
1713
1728
  if (!Number.isFinite(v.at) || v.at < cutoff) continue;
1714
1729
  written[k] = { title: v.title, at: v.at };
@@ -2457,7 +2472,9 @@ export async function classifyIdleBusyWindows(procs, paths, lookupTaskWs) {
2457
2472
  if (child.ppid !== cli.pid) continue;
2458
2473
  if (isInfraCmdline(child.cmdline)) continue;
2459
2474
  if (!child.cmdline) { unattributed = true; continue; }
2460
- const sess = child.cmdline.match(/sess_[A-Za-z0-9-]+/);
2475
+ // 会话 id 有 sess_ 与 session_ 两种前缀(新版应用为 session_);匹配不到
2476
+ // 会走 unattributed 全量判忙——新形式命令挂着时空闲回收会整体停摆
2477
+ const sess = child.cmdline.match(/(?:sess|session)_[A-Za-z0-9-]+/);
2461
2478
  if (sess) await markByTask(sess[0]);
2462
2479
  else unattributed = true; // 用户进程但无会话线索:归属不到具体项目
2463
2480
  }
@@ -3664,6 +3681,20 @@ async function runHeadroomHook(args, timeoutMs = 30000) {
3664
3681
  });
3665
3682
  }
3666
3683
 
3684
+ // 启动兜底:整机重启后首次打开应用时恢复的是旧会话,触发 SessionStart(resume),
3685
+ // 旧版 headroom 插件的钩子只匹配 startup,代理不会自动启动(要等用户新建会话或
3686
+ // 手动重启)。helper 就绪后延迟几秒补一次 ensure:动作幂等(已在跑秒回),新版
3687
+ // 插件侧还带跨进程锁;失败一律静默——没装 headroom 插件/本体的环境不能打扰启动。
3688
+ let headroomStartupEnsureFired = false;
3689
+ function ensureHeadroomAtStartup() {
3690
+ if (headroomStartupEnsureFired) return;
3691
+ headroomStartupEnsureFired = true;
3692
+ const timer = setTimeout(() => {
3693
+ runHeadroomHook(['ensure'], 20000).catch(() => { /* 兜底失败不影响启动 */ });
3694
+ }, 5000);
3695
+ timer.unref?.();
3696
+ }
3697
+
3667
3698
  // 解析 ensure-proxy(.sh/.mjs) status 输出为结构化字段(字段名以脚本 hr_status 的输出为准;
3668
3699
  // headroom 插件 1.3.0+ 多实例模式由 hr-multi 输出同名字段,另附 instance: 行)。
3669
3700
  // 导出以便测试脚本直接驱动。
@@ -241,8 +241,9 @@ export async function listVirtualProjects(dataRoot) {
241
241
  }
242
242
  }
243
243
 
244
- // 批量查会话终态:task_id → task_status。分区头运行判定用它剔除官方 syncer
245
- // 卡滞的报告(会话已完成但内存态仍报活跃时,任务行 loader 会一直卡转)。
244
+ // 批量查会话终态:task_id → { status, updatedAt, title }。分区头运行判定用它
245
+ // 剔除官方 syncer 卡滞的报告(会话已完成但内存态仍报活跃时,任务行 loader 会
246
+ // 一直卡转);title 供 alt+r 弹窗显示折叠项目(行不在场)会话的标题。
246
247
  // 读不到(库缺失/无驱动)返回 null,调用方按未确认处理
247
248
  export async function listTaskStatuses(dataRoot, ids) {
248
249
  const out = {};
@@ -252,17 +253,19 @@ export async function listTaskStatuses(dataRoot, ids) {
252
253
  if (!driver) return null;
253
254
  const esc = (v) => "'" + String(v).replace(/'/g, "''") + "'";
254
255
  const inList = ids.map((id) => esc(id)).join(', ');
255
- const sql = `SELECT task_id, task_status, updated_at FROM tasks WHERE task_id IN (${inList})`;
256
+ const sql = `SELECT task_id, task_status, updated_at, title FROM tasks WHERE task_id IN (${inList})`;
256
257
  try {
257
258
  let rows;
258
259
  if (driver.kind === 'node') {
259
260
  const db = new driver.DatabaseSync(dbFile, { readOnly: true });
260
261
  try { rows = db.prepare(sql).all(); } finally { db.close(); }
261
262
  } else {
262
- rows = runCli(dbFile, sql + ';').stdout.split('\n').filter(Boolean)
263
- .map((l) => { const [id, st, at] = l.split('|'); return { task_id: id, task_status: st, updated_at: at }; });
263
+ // 标题可含任意字符(含分隔符),CLI 路径走 JSON 聚合,不做分隔符切分
264
+ const agg = runCli(dbFile,
265
+ `SELECT coalesce(json_group_array(json_object('task_id', task_id, 'task_status', task_status, 'updated_at', updated_at, 'title', title)), '[]') FROM tasks WHERE task_id IN (${inList});`);
266
+ rows = JSON.parse(agg.stdout.trim() || '[]');
264
267
  }
265
- for (const r of rows) out[String(r.task_id)] = { status: String(r.task_status || ''), updatedAt: Number(r.updated_at) || 0 };
268
+ for (const r of rows) out[String(r.task_id)] = { status: String(r.task_status || ''), updatedAt: Number(r.updated_at) || 0, title: String(r.title || '') };
266
269
  return out;
267
270
  } catch {
268
271
  return null;
@@ -535,7 +538,8 @@ export async function reconcileCustomTitles(dataRoot, writtenMap) {
535
538
  };
536
539
 
537
540
  for (const [sessId, rec] of Object.entries(writtenMap)) {
538
- if (!/^sess_[A-Za-z0-9._-]+$/.test(sessId) || !rec || typeof rec.title !== 'string') continue;
541
+ // 会话 id 有 sess_ 与 session_ 两种前缀(新版应用为 session_),两种都认
542
+ if (!/^(?:sess|session)_[A-Za-z0-9._-]+$/.test(sessId) || !rec || typeof rec.title !== 'string') continue;
539
543
  let cli;
540
544
  try { cli = await readCli(sessId); } catch { continue; }
541
545
  // CLI 已不是我们所写的 custom 标题:别人改过(手动改名/官方),不碰