zen-gitsync 2.17.29 → 2.17.30

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 (135) hide show
  1. package/README.md +38 -6
  2. package/package.json +5 -2
  3. package/scripts/release.js +175 -32
  4. package/src/cli/ai/agent.js +1 -1
  5. package/src/cli/ai/extensions.js +1 -1
  6. package/src/cli/ai/mcp.js +6 -5
  7. package/src/cli/ai/mcp.test.js +14 -2
  8. package/src/cli/ai/skills.js +8 -5
  9. package/src/cli/ai/skills.test.js +14 -2
  10. package/src/paths.js +26 -7
  11. package/src/ui/public/assets/AgentEngineSelector-DMqqMLhx.js +1 -0
  12. package/src/ui/public/assets/AgentEngineSelector-DR3r5eQE.css +1 -0
  13. package/src/ui/public/assets/AgentView-B0wVzLSW.js +1 -0
  14. package/src/ui/public/assets/AgentView-D87GB-H9.css +1 -0
  15. package/src/ui/public/assets/{AppVersionBadge-DDYQqoy7.css → AppVersionBadge--5If494j.css} +1 -1
  16. package/src/ui/public/assets/AppVersionBadge-Cc1kJ0D9.js +2 -0
  17. package/src/ui/public/assets/{BranchSelector-F4ibldMN.css → BranchSelector-BTCMYwG0.css} +1 -1
  18. package/src/ui/public/assets/BranchSelector-CLWTSyiv.js +1 -0
  19. package/src/ui/public/assets/CommitForm-C-cL8_vz.css +1 -0
  20. package/src/ui/public/assets/CommitForm-xhy_saX5.js +6 -0
  21. package/src/ui/public/assets/CommonDialog-Bzi5ISqT.css +1 -0
  22. package/src/ui/public/assets/CommonDialog-DeSPG5C-.js +1 -0
  23. package/src/ui/public/assets/EditorView-BsuQfcO4.css +1 -0
  24. package/src/ui/public/assets/EditorView-DOBl6sqW.js +1 -0
  25. package/src/ui/public/assets/FileDiffViewer-B6zy6NiA.css +1 -0
  26. package/src/ui/public/assets/FileDiffViewer-YaOsJZZI.js +14 -0
  27. package/src/ui/public/assets/FlowExecutionViewer-B5kh7VxS.css +1 -0
  28. package/src/ui/public/assets/FlowExecutionViewer-BkkeKggj.js +1 -0
  29. package/src/ui/public/assets/FlowOrchestrationWorkspace-B95bYCEf.js +21 -0
  30. package/src/ui/public/assets/FlowOrchestrationWorkspace-CM2_9TW3.css +1 -0
  31. package/src/ui/public/assets/IconButton-DI4O8Y_p.js +1 -0
  32. package/src/ui/public/assets/IconButton-W2X09OKH.css +1 -0
  33. package/src/ui/public/assets/LogList-DLxxk9NX.css +1 -0
  34. package/src/ui/public/assets/LogList-DiIWwVCn.js +7 -0
  35. package/src/ui/public/assets/MindmapView-BFK6XzVU.js +1 -0
  36. package/src/ui/public/assets/MindmapView-C4xg3TwX.css +1 -0
  37. package/src/ui/public/assets/MonitorView-BWOQfPtI.css +1 -0
  38. package/src/ui/public/assets/MonitorView-Blsb4ZtW.js +1 -0
  39. package/src/ui/public/assets/ProjectStartupButton-DiN1xs-u.js +1 -0
  40. package/src/ui/public/assets/ProjectStartupButton-VoQqFee4.css +1 -0
  41. package/src/ui/public/assets/RecentDirectoriesChat-DD-qyYN0.js +1 -0
  42. package/src/ui/public/assets/RecentDirectoriesChat-o4xK1Koc.css +1 -0
  43. package/src/ui/public/assets/RemoteManagerDialog-BSOph2h2.css +1 -0
  44. package/src/ui/public/assets/RemoteManagerDialog-D_rJHy9p.js +1 -0
  45. package/src/ui/public/assets/RemoteRepoCard-B98ZUYrp.js +1 -0
  46. package/src/ui/public/assets/RemoteRepoCard-CG9WPcA_.css +1 -0
  47. package/src/ui/public/assets/SourceMapView-CO_BZJKu.css +1 -0
  48. package/src/ui/public/assets/SourceMapView-DCLg5IHN.js +3 -0
  49. package/src/ui/public/assets/{SvgIcon-CDNsk4-Q.css → SvgIcon-BQ6bR7l0.css} +1 -1
  50. package/src/ui/public/assets/SvgIcon-Dg1c0l0q.js +1 -0
  51. package/src/ui/public/assets/TemplateManager-ClQb9yY3.js +1 -0
  52. package/src/ui/public/assets/TemplateManager-DiM3vrbf.css +1 -0
  53. package/src/ui/public/assets/UserInputNode-d-oRXM_J.css +1 -0
  54. package/src/ui/public/assets/UserInputNode-oLCCf_Bn.js +1 -0
  55. package/src/ui/public/assets/WorkbenchView-BljYNp6b.css +1 -0
  56. package/src/ui/public/assets/WorkbenchView-C-yJWhrO.js +20 -0
  57. package/src/ui/public/assets/{_plugin-vue_export-helper-BKeZSroN.js → _plugin-vue_export-helper-OGa67zj-.js} +3 -3
  58. package/src/ui/public/assets/{agentConversations-ChTc-LkQ.js → agentConversations-DaJcYEPn.js} +1 -1
  59. package/src/ui/public/assets/{configStore-fJaic3kE.js → configStore-BrdhjW5U.js} +1 -1
  60. package/src/ui/public/assets/element-plus-Cfu0ZUp9.js +28 -0
  61. package/src/ui/public/assets/flow-mindmap-C_fqj8ZL.js +257 -0
  62. package/src/ui/public/assets/{gitStore-D9oF5nof.js → gitStore-BgvCpppG.js} +1 -1
  63. package/src/ui/public/assets/index-BgO3-PhT.js +29 -0
  64. package/src/ui/public/assets/{index-DVpFh9xE.css → index-BrIl1iaG.css} +1 -1
  65. package/src/ui/public/assets/office-docx-rxiawIim.js +31 -0
  66. package/src/ui/public/assets/{office-excel-CXD9VwgB.js → office-excel-IkgTJC0h.js} +1 -1
  67. package/src/ui/public/assets/office-pptx-CyxidPZ2.js +60 -0
  68. package/src/ui/public/assets/previewSandbox-NWN_GKkA.js +9 -0
  69. package/src/ui/public/assets/{previewSandbox-PYnM6Paz.css → previewSandbox-SqNQvuHv.css} +1 -1
  70. package/src/ui/public/assets/socket-io-TBtw0msS.js +1 -0
  71. package/src/ui/public/assets/{toolsStore-C89takm3.js → toolsStore-DrTCC32p.js} +1 -1
  72. package/src/ui/public/assets/{useThemeObserver-BvSA7RDh.js → useThemeObserver-qsxO0WJ7.js} +1 -1
  73. package/src/ui/public/assets/{vendor-CCrjQw5C.js → vendor-sxw_ye8l.js} +173 -180
  74. package/src/ui/public/assets/vue-flow-Cjr_ADp5.js +39 -0
  75. package/src/ui/public/index.html +24 -24
  76. package/src/ui/server/routes/workbench/agentMarketplace.js +12 -10
  77. package/src/ui/server/routes/workbench/agentMarketplace.test.js +18 -9
  78. package/src/ui/server/routes/workbench/envContext.js +16 -0
  79. package/src/ui/server/routes/workbench/envContext.test.js +17 -0
  80. package/src/ui/server/routes/workbench/index.js +84 -0
  81. package/src/ui/server/routes/workbench/jobImage.js +141 -0
  82. package/src/ui/server/routes/workbench/jobImage.test.js +153 -0
  83. package/src/utils/desktopNotify.js +217 -0
  84. package/src/utils/desktopNotify.test.js +122 -0
  85. package/src/ui/public/assets/AgentEngineSelector-DUf0TfaU.css +0 -1
  86. package/src/ui/public/assets/AgentEngineSelector-Wl7dKYKL.js +0 -2
  87. package/src/ui/public/assets/AgentView-Bebv5xk6.css +0 -1
  88. package/src/ui/public/assets/AgentView-CebQ3545.js +0 -11
  89. package/src/ui/public/assets/AppVersionBadge-jm8ejuZ-.js +0 -2
  90. package/src/ui/public/assets/BranchSelector-CcNILIbg.js +0 -1
  91. package/src/ui/public/assets/CommitForm-1ocxo8Cb.js +0 -6
  92. package/src/ui/public/assets/CommitForm-4kI2csgs.css +0 -1
  93. package/src/ui/public/assets/CommonDialog-DpFTO5cg.js +0 -1
  94. package/src/ui/public/assets/CommonDialog-w6EmBEXv.css +0 -1
  95. package/src/ui/public/assets/EditorView-CftLj8-x.css +0 -1
  96. package/src/ui/public/assets/EditorView-CtRHV1Sv.js +0 -37
  97. package/src/ui/public/assets/FileDiffViewer-DXJO-G55.js +0 -15
  98. package/src/ui/public/assets/FileDiffViewer-n5ea9qMe.css +0 -1
  99. package/src/ui/public/assets/FlowExecutionViewer-d4O5Y9Ca.js +0 -1
  100. package/src/ui/public/assets/FlowExecutionViewer-jNHI5zdm.css +0 -1
  101. package/src/ui/public/assets/FlowOrchestrationWorkspace-CKLRMeaL.js +0 -21
  102. package/src/ui/public/assets/FlowOrchestrationWorkspace-DwuSSxJ1.css +0 -1
  103. package/src/ui/public/assets/IconButton-Dl1cdP_x.js +0 -1
  104. package/src/ui/public/assets/IconButton-RlXqKOeu.css +0 -1
  105. package/src/ui/public/assets/LogList-CdDRC_CO.js +0 -7
  106. package/src/ui/public/assets/LogList-XspUhx-C.css +0 -1
  107. package/src/ui/public/assets/MindmapView-Diq2Cu4O.css +0 -1
  108. package/src/ui/public/assets/MindmapView-U84MWPHc.js +0 -2
  109. package/src/ui/public/assets/MonitorView-DwUwJXnY.css +0 -1
  110. package/src/ui/public/assets/MonitorView-spOV0cP7.js +0 -1
  111. package/src/ui/public/assets/ProjectStartupButton-BbAbfFn2.js +0 -1
  112. package/src/ui/public/assets/ProjectStartupButton-CB8IOg1E.css +0 -1
  113. package/src/ui/public/assets/RecentDirectoriesChat-BMkc7fS1.css +0 -1
  114. package/src/ui/public/assets/RecentDirectoriesChat-CxNjjgVG.js +0 -3
  115. package/src/ui/public/assets/RemoteManagerDialog-BVm2-Hn4.js +0 -4
  116. package/src/ui/public/assets/RemoteManagerDialog-D4WphMtO.css +0 -1
  117. package/src/ui/public/assets/RemoteRepoCard-UHK_hrDS.js +0 -7
  118. package/src/ui/public/assets/RemoteRepoCard-o01IrbVz.css +0 -1
  119. package/src/ui/public/assets/SourceMapView-CfDydtsu.css +0 -1
  120. package/src/ui/public/assets/SourceMapView-PHMr0wt3.js +0 -3
  121. package/src/ui/public/assets/SvgIcon-Dk7WnZLB.js +0 -1
  122. package/src/ui/public/assets/TemplateManager-B3ZNTXnP.js +0 -1
  123. package/src/ui/public/assets/TemplateManager-DO7EPfj8.css +0 -1
  124. package/src/ui/public/assets/UserInputNode-DDNSarag.css +0 -1
  125. package/src/ui/public/assets/UserInputNode-DeAD7H0z.js +0 -1
  126. package/src/ui/public/assets/WorkbenchView-DsZ2ONpW.js +0 -143
  127. package/src/ui/public/assets/WorkbenchView-kZ8DoH2l.css +0 -1
  128. package/src/ui/public/assets/element-plus-D0R8BMj4.js +0 -48
  129. package/src/ui/public/assets/flow-mindmap-Doi70WWn.js +0 -257
  130. package/src/ui/public/assets/index-CKxygqdx.js +0 -148
  131. package/src/ui/public/assets/office-docx-O0YXk5gA.js +0 -32
  132. package/src/ui/public/assets/office-pptx-CiF2OlTq.js +0 -74
  133. package/src/ui/public/assets/previewSandbox-21uStCdd.js +0 -9
  134. package/src/ui/public/assets/socket-io-BFqQXqAp.js +0 -1
  135. package/src/ui/public/assets/vue-flow-5Aq2bwoG.js +0 -39
package/README.md CHANGED
@@ -384,7 +384,8 @@ A dedicated view for running coding agents across one or many projects. Every ta
384
384
  | Progress report | The console's **Instruction** pane reports where your running tasks actually stand instead of listing what was dispatched and what finished (the board already shows that). The master agent reads each running job — elapsed time, **its latest thinking**, **what mix of tools it has been calling**, **how long it has been silent**, and the latest output — and writes a short paragraph per report: what each task is doing, how far it has got, and whether it looks stuck. The thinking line is what makes this work for tasks that never write a line of prose (they are the common case — their output stays empty from start to finish); "looks stuck" now has to come with evidence — a silence figure, or a tool mix going in circles — instead of being inferred from a high call count. Reports appear on an interval you pick (5 / 10 / 15 / 30 / 60 minutes, or off) or whenever you hit **Report now**, and the newest one is shown with the facts behind it (project, elapsed, tool mix, latest thinking, last tool call, silence) while earlier ones stay browsable from the history list. Automatic reports are generated **server-side**, so the history keeps filling up while the tab is closed and is waiting for you when you come back. Nothing running means nothing filed: automatic reports are skipped outright, and **Report now** answers with a plain "no task is running right now" rather than filing an empty entry (no empty entries, no wasted model call), while two identical reports arriving seconds apart are collapsed into one so a double click leaves no duplicate. The paragraph is written in the UI language and, when a task's output is inconclusive, it says so rather than inventing progress |
385
385
  | Dispatch default prompts | Give every dispatch a standing prompt: one **global** entry that applies to all projects, plus one **per project** that is appended after it whenever you dispatch to that project (it supplements the global one rather than replacing it). Both are edited from the gear button in the console's composer; the combined text is prepended to the instruction, a "Default prompt (global + this project)" checkbox appears next to **Run now** so a single dispatch can opt out, and the instruction log records which level was attached. The prompt is copied onto the task at dispatch time, so editing the setting later never rewrites tasks that already exist; it lands in the task's own prompt field, where you can still edit it per task |
386
386
  | Open-with menu per project | Hovering a project row in the board's project list reveals two buttons: **Open folder** (straight to your file manager) and **Open with**, a menu holding file manager / terminal / `g ui` in a new tab plus every editor and AI tool (VS Code, Codex, OpenCode, Kimi Code, ZCode, DeepSeek Harness, and Claude Code in default or fully-approved mode). Tools that are not installed are dimmed and labelled "Not installed" — clicking one opens the very same install guide the top bar uses. Every action applies to that row's project only; the board's selection is never touched |
387
- | Task editor overlay | Clicking a board card opens the editor as an overlay: a flat sidebar holding the task list and prompt presets, then the task header, preset selector, executor split-button, the execution-log / clear-execution actions, and a chat-style execution body. The description collapses into a one-line "Task description (optional)" summary until clicked, showing a "Filled" badge and the attachment count once there is content. Opening the overlay lands the conversation on its **newest** turn rather than its first: the flow keeps re-pinning to the bottom while markdown highlighting and tool-call folding are still growing it after mount, and lets go as soon as you scroll yourself |
387
+ | Task editor overlay | Clicking a board card opens the editor as an overlay: a flat sidebar holding the task list and prompt presets, then the task header, preset selector, executor split-button, the copy-execution / execution-log / clear-execution actions, and a chat-style execution body. The description collapses into a one-line "Task description (optional)" summary until clicked, showing a "Filled" badge and the attachment count once there is content. Opening the overlay lands the conversation on its **newest** turn rather than its first: the flow keeps re-pinning to the bottom while markdown highlighting and tool-call folding are still growing it after mount, and lets go as soon as you scroll yourself |
388
+ | Copy execution content | **Copy execution content** in the task header puts the whole task — every turn, not just whatever is on screen — on the clipboard as plain Markdown: a header line for the task and its project, then one section per turn (`## Round N`, with the executor, status and start time on the line under it) holding the user's prompt, the agent's thinking, its tool calls (arguments and results in fenced blocks) and the model's output. It is assembled from the run records rather than scraped from the DOM, so what you get never depends on what happens to be selected. The user side goes through the same trimming the chat bubbles use, so what you copy is what you typed — the injected environment / memory blocks and the attachment list stay behind in `job.prompt`. Turns with nothing in them are skipped without renumbering the ones after them, and a task that has never run answers with a "nothing to copy" toast instead of putting an empty string on the clipboard |
388
389
  | Executor choice | Run each task with **Claude Code**, **OpenCode** or **Codex**. The global default is set in **Settings → General → Task executor** (`config.taskExecutor`); the split-button next to the run button switches it for the next run and remembers that pick in the browser. A continued conversation always stays on the executor that started it — Claude's `--resume`, OpenCode's `--session` and Codex's thread id are not interchangeable |
389
390
  | Attachments | Up to 9 files per task (image / PDF / text / Markdown / CSV / JSON / log, ≤ 20 MB each); images over 3.5 MB are re-encoded / downscaled in the browser before upload so 4K screenshots still fit what the model and the reader can take. Their absolute paths are appended to the prompt so the agent reads them directly. Right-click an image attachment to copy it to the system clipboard (`image/png` / `jpeg` / `webp` / `gif`) |
390
391
  | Prompt presets | Reusable prompt templates with `{{task.title}}` / `{{task.desc}}` / `{{repo.path}}` / `{{branch}}` variable interpolation |
@@ -396,10 +397,11 @@ A dedicated view for running coding agents across one or many projects. Every ta
396
397
  | Live log | The "执行日志 / Execution log" panel **opens by default** and auto-scrolls, showing accumulated `stdout` + `stderr` (last 64 KB rendered client-side; the server keeps up to 100 MB per job) |
397
398
  | Live status | Task status (pending / running / done / error / cancelled) and PID stream in real time over SSE |
398
399
  | Tool-call stream | The model's tool calls are rendered inline in the conversation flow, so you can follow what the agent actually did |
400
+ | Inline images in a reply | The agent can put a picture in its reply body: write it as a Markdown image pointing at a local file — `![caption](C:\...\docs\shot.png)` — and it is rendered right in the conversation flow. That path is rewritten to a server endpoint that only serves images from **the task's own repository** (path traversal, absolute paths outside the repo and symlinks pointing out of it are all rejected, and the response carries `nosniff` plus a sandbox CSP), and the executor is told the syntax through the injected environment block — pasting a bare path is what it does otherwise, and a bare path or one wrapped in a code fence stays plain text. Follow-up turns carry a one-line version of the same hint. A file the run cannot read renders as a broken image rather than silently disappearing; png / jpg / jpeg / gif / webp / bmp / svg up to 20 MB |
399
401
  | Finish notice | When a run finishes or fails you get a chime (a distinct tone for done vs. error; stopping on purpose stays silent) plus an in-app toast while the page is in the foreground, and a system notification when it is not — so you can be looking at another view or window and still be told. The chime has its **own switch** under the notice toggle, so you can keep the notices but mute the sound, or the other way round. Sounds are CC0 assets under `public/sounds/`; see `CREDITS.txt` there to swap in another tone |
400
402
  | Cross-view indicator | While any Workbench task is running, a pulsing dot appears on the Workbench icon in the Activity Bar so you can see job state from the Git or Editor view |
401
403
  | Execution log manager (dialog) | The "Execution logs" button in the workbench top bar opens a dialog with the list / filter / batch delete / clear / retention-policy UI (defaults: 500 records, 256 MB); the task execution view stays mounted so no work-in-progress state is dropped |
402
- | Continue chat | After a task reaches a done / error / cancelled state, a follow-up composer appears; sending a message resumes the previous session (`claude --resume <session_id>` for Claude Code, `--session` for OpenCode, `codex exec resume <thread_id>` for Codex), and each new turn stacks into the same chat-style flow. Since the resumed session already carries the previous turn's context, follow-up turns inject only a **slim refresh** of the run-environment block (current project + board totals + truth-file paths) instead of the full project list — the chat log no longer repeats the same list every round |
404
+ | Continue chat | After a task reaches a done / error / cancelled state, a follow-up composer appears; sending a message resumes the previous session (`claude --resume <session_id>` for Claude Code, `--session` for OpenCode, `codex exec resume <thread_id>` for Codex), and each new turn stacks into the same chat-style flow. Since the resumed session already carries the previous turn's context, follow-up turns inject only a **slim refresh** of the run-environment block (current project + board totals + truth-file paths) instead of the full project list. The bubbles show only what the user actually typed: the injected environment / memory blocks and the attachment list stay in the raw `job.prompt` (readable and copyable in the run log), so copying a conversation out and pasting it back no longer drags a whole round of background along |
403
405
  | Local tool detection | On startup + every 10 min the server probes 7 CLIs (`code`, `claude`, `codex`, `opencode`, `kimi`, `zcode`, `dsh`). Tools that are missing are dimmed and labelled "Not installed" — clicking one opens the install guide, and right-clicking a tool button offers an update to the latest published version |
404
406
 
405
407
  Prompt presets and tasks are persisted to `~/.zen-gitsync/prompts.json` and `~/.zen-gitsync/tasks.json` (cross-project, shared across repos); run history and the retention policy live in `jobs.json` / `jobs-config.json`, the master-agent console state in `orchestrator.json` (with its report history in `orchestrator-reports.json` — kept apart so the polled state file stays small), and task attachments under `~/.zen-gitsync/workbench-images/_task-<taskId>/`.
@@ -420,7 +422,7 @@ A dedicated view (robot icon in the activity bar) for chatting with the built-in
420
422
  | Task plan | Multi-step work gets a visible plan: the agent calls the built-in `update_plan` tool to split the task into 3-8 verifiable steps before touching anything, then updates each step's status as it goes. Steps render as a checklist with completed / in-progress / pending states and a `2/5` progress header — in the terminal as a `✓ / ▶ / ○` list, in the Web panel as a card that **stays visible even when the tool group is collapsed** (collapsing hides other tool calls, never the current plan) |
421
423
  | Recent-projects awareness | Ask "which of my projects need a pull?" and the agent calls its built-in `list_projects` tool instead of scanning the disk: it returns exactly the list behind the GUI's **Recent projects** panel (recent directories plus any directory a task was created in, with branch / ahead / behind / uncommitted counts and task progress), so the agent's answer and the UI agree. Ahead/behind reads local refs, so the agent can pass `refresh=true` to run a `git fetch` pass first when the question is about pulling |
422
424
  | Session persistence | All conversations are saved to `~/.zen-gitsync/agent-sessions/` as JSON files; the CLI agent (`g ai`) writes to the same directory so Web and CLI sessions are unified |
423
- | Skill / MCP plaza | The **Skill plaza** and **MCP plaza** tabs list skills and MCP servers from several sources, each with its description, weekly downloads / usage count and install state. Install one into the **current project** or into the **`g ai` agent** — entries already installed can be opened in the system file manager or uninstalled from the same row, and ones that still need environment variables are flagged. The installed list shows both the skill's own `name` and the on-disk id, since a repository often ships a skill whose `SKILL.md` calls itself something else. From a terminal, `g ai` lists what is installed with `/skills` (`/mcp` is an alias) |
425
+ | Skill / MCP plaza | The **Skill plaza** and **MCP plaza** tabs list skills and MCP servers from several sources, each with its description, weekly downloads / usage count and install state. Install one into the **current project** (`<project>/.zen-gitsync/ai/skills/<id>/SKILL.md` and `<project>/.zen-gitsync/ai/mcp.json`) or into the **`g ai` agent** (`~/.zen-gitsync/ai/`, applying to every project) — zen-gitsync's own directories, not another tool's. Entries already installed can be opened in the system file manager or uninstalled from the same row, and ones that still need environment variables are flagged. The installed list shows both the skill's own `name` and the on-disk id, since a repository often ships a skill whose `SKILL.md` calls itself something else. From a terminal, `g ai` lists what is installed with `/skills` (`/mcp` is an alias) |
424
426
  | SSH-first cloning | When you ask it to clone a repo (or add a remote) it uses the SSH form — `git@github.com:owner/repo.git` / `git@gitee.com:owner/repo.git` — converting an `https://` URL first, so the clone never stalls on a Git Credential Manager username/password prompt; it falls back to https only when SSH genuinely fails (`Permission denied (publickey)` / host-key verification) and says which one it used. The same preference is injected into every workbench task, whose executor is an external CLI with a system prompt this app does not own |
425
427
  | Per-turn tool limit | A single message may trigger up to N tool calls in a row (default **200**, range 1–2000). Configurable in **Settings → AI models → Agent Runtime**; hitting the limit ends the turn and asks you to send another message. The same setting drives the CLI agent |
426
428
  | Preset questions | Quick-start buttons on the welcome screen for common tasks (view project structure, analyze code quality, write tests, check git status, start the project) |
@@ -470,6 +472,20 @@ git add --renormalize .
470
472
 
471
473
  All `package.json` scripts use **npm** (`npm install`, `npm run dev`, `npm run release`, etc.). The `package-lock.json` is git-ignored, so each developer generates it locally. The CLI's own `bin` entry and most devDeps are pinned to caret ranges.
472
474
 
475
+ ### Release
476
+
477
+ `npm run release` (`scripts/release.js`) runs the whole publish in one shot: bump the patch version → `vue-tsc` type check → build the frontend → verify the package contents (`files` whitelist vs relative imports, plus a real `npm pack` manifest) → commit + tag + push → `npm publish` → `npm install -g zen-gitsync@<version>`.
478
+
479
+ The last step is the slow one: the registry can take anywhere from seconds to over 30 minutes to make a freshly published version installable, so the script polls on two readiness signals (packument has the version / tarball is fetchable) and force-installs every 4 rounds — the probes only save a doomed call, **`npm` itself is the judge**. Each failed attempt prints an `[E404]` / `[EPERM]` short code, and giving up prints the breakdown of what it kept hitting.
480
+
481
+ **You don't have to watch it.** When the run ends you get a desktop notification plus a sound (success and failure use different sounds), and the terminal / taskbar title switches to the result. All of it is best-effort and can never fail the release itself; pass `--no-notify` (or set `ZEN_NO_NOTIFY=1`) to turn it off. The three outcomes are reported separately, because "published but the global install failed" is neither success nor failure:
482
+
483
+ - **release complete** — published to npm and the global version was verified.
484
+ - **published, global not updated** — the version is on npm but the global install didn't land. Re-running the release won't help (the version number is taken); just run `npm install -g zen-gitsync@<version>`.
485
+ - **release failed** — an earlier step (type check / package self-check / git / `npm publish`) aborted the run.
486
+
487
+ Other switches: `--dry-run` (print the plan only), `--skip-push`, `--skip-self-update`, `--keep-instances`, `--poll-timeout=<seconds>`.
488
+
473
489
  ---
474
490
 
475
491
  ## CLI Commands
@@ -1051,7 +1067,8 @@ Activity Bar 第四个视图,在 GUI 内直接浏览并编辑项目文件:
1051
1067
  | 派发执行器 | 派出去的任务由哪个 CLI 跑:**对话模式**在输入框下方选,**指令模式**在派发栏选 —— 两处是同一个组件、同一份选择,与执行按钮的临时切换也共用。注意「引擎」和「执行器」不是一回事:只有**内置 g ai** 能在服务端跑工具循环、才有派发能力,所以对话模式下把引擎切成外部 CLI 时,执行器选择会换成一句「当前引擎只能对话,不能派发任务 —— 切到内置 g ai 才能派活」的说明,而不是让你点了个不会生效的下拉 |
1052
1068
  | 派发默认提示词 | 给每次派发配一段常驻提示词:一条**全局**的(所有项目都附加)+ 每个项目一条(派发到该项目时追加在全局之后,是补充而不是覆盖)。两者都在控制台输入区的齿轮按钮里设置;拼好的正文放在指令**之前**,「立即执行」旁边会多出一个「默认提示词(全局 + 本项目)」勾选,单次派发可以取消勾选,指令流水里也记下这条指令附带的是哪一级。提示词在派发那一刻就抄进任务自己的提示词字段,之后改设置不会回头改写已建任务,单条任务仍可再改 |
1053
1069
  | 项目行打开方式 | 编排台项目列表里 hover 任意一行会出现两个按钮:「打开文件夹」一键进资源管理器,以及「打开方式」菜单 —— 文件管理器 / 终端 / 新标签页跑 `g ui`,以及各编辑器与 AI 工具(VS Code、Codex、OpenCode、Kimi Code、ZCode、DeepSeek Harness,加上默认权限或完全批准的 Claude Code)。没安装的工具会置灰并标「未安装」,点它弹的是顶栏那套安装引导;菜单里的动作只作用于该行项目,不会改变看板选中态 |
1054
- | 任务编辑器浮层 | 点击看板卡片时以浮层打开:左侧是扁平化的任务列表与提示词预置,右侧依次是任务头部、预置下拉、执行器 split 按钮、「执行日志 / 清空执行」动作,以及对话式执行主体。描述默认折叠成一行「任务描述(可选)」摘要,点击展开;已填写描述或挂有附件时摘要右侧显示「已填写」徽标与附件数量。打开浮层时对话流直接落在**最新一轮**而不是第一轮:正文异步高亮与工具调用折叠会在挂载后继续长高,这段时间里持续贴底,用户自己一滚就立刻撒手 |
1070
+ | 任务编辑器浮层 | 点击看板卡片时以浮层打开:左侧是扁平化的任务列表与提示词预置,右侧依次是任务头部、预置下拉、执行器 split 按钮、「复制执行内容 / 执行日志 / 清空执行」动作,以及对话式执行主体。描述默认折叠成一行「任务描述(可选)」摘要,点击展开;已填写描述或挂有附件时摘要右侧显示「已填写」徽标与附件数量。打开浮层时对话流直接落在**最新一轮**而不是第一轮:正文异步高亮与工具调用折叠会在挂载后继续长高,这段时间里持续贴底,用户自己一滚就立刻撒手 |
1071
+ | 复制执行内容 | 任务头部的「复制执行内容」把**整条任务**(所有轮次,不只是屏幕上那一段)以纯 Markdown 放进剪贴板:先是任务与项目的标题行,然后一轮一节(`## 第 N 轮`,下面一行是本轮执行器、状态与开始时间),每节依次是用户提示词、智能体思考、工具调用(参数与结果都放在围栏代码块里)与模型返回。内容是从执行记录现拼的、不是从 DOM 里抠的,所以复制到什么跟你当前选中了哪段文字无关。用户侧走的是与对话气泡同一套裁剪,复制出来的就是你真正说过的话 —— 注入的环境块 / 记忆块 / 附件清单留在 `job.prompt` 里不跟着出去。一个字都没有的轮次会被跳过,且不让后面的轮次跟着往前重编号;从没跑过的任务回答「暂无执行内容可复制」,而不是把空串写进剪贴板 |
1055
1072
  | 执行器选择 | 每个任务可用 **Claude Code** / **OpenCode** / **Codex** 执行。全局默认在 **设置 → 通用设置 → 任务执行器**(`config.taskExecutor`);执行按钮旁的 split 按钮可临时切换下一次执行用的执行器,选择记在浏览器里。续聊固定沿用最初那个执行器 —— Claude 的 `--resume`、OpenCode 的 `--session`、Codex 的 thread id 互不通用 |
1056
1073
  | 附件 | 每个任务最多挂 9 个附件(图片 / PDF / 文本 / Markdown / CSV / JSON / log,单个 ≤ 20 MB);超过 3.5 MB 的图片会先在浏览器里压缩(先按原分辨率转 WebP,压不下去再逐级降采样),4K 屏截图不用再手动裁剪;执行时绝对路径会自动追加到 prompt 末尾,智能体直接按路径读取。**右键图片附件可一键复制到系统剪贴板**(支持 png / jpeg / webp / gif) |
1057
1074
  | 提示词预置 | 可复用提示词模板,支持 `{{task.title}}` / `{{task.desc}}` / `{{repo.path}}` / `{{branch}}` 变量插值 |
@@ -1063,10 +1080,11 @@ Activity Bar 第四个视图,在 GUI 内直接浏览并编辑项目文件:
1063
1080
  | 实时日志 | 「执行日志」面板**默认展开**,方便随时回看上次执行结果;面板内自动滚到底,展示累积的 stdout / stderr(客户端渲染最近 64 KB,服务端单个 job 最多保留 100 MB 输出) |
1064
1081
  | 实时状态 | 任务状态(pending / running / done / error / cancelled)和 PID 通过 SSE 实时推送 |
1065
1082
  | 工具调用流 | 模型的工具调用直接渲染在对话流里,能看清智能体具体做了什么 |
1083
+ | 正文配图 | 智能体可以在回答正文里直接给你看图:按 Markdown 图片语法写 `![说明](C:\...\docs\shot.png)`,界面就在对话流里渲染出来。该路径会被重写成只服务**本任务仓库内**图片的后端端点(`..` 穿越、仓库外的绝对路径、指向仓库外的符号链接一律拒掉,响应带 `nosniff` 与 sandbox CSP);同时通过注入的环境上下文块把这条写法告诉执行器 —— 不告诉它,它默认只会贴一行路径(裸路径、或放进代码块的路径,仍是纯文本)。续聊轮发的是这条提示的一句话版本。读不到的图会显示成裂图而不是悄悄消失;限 png / jpg / jpeg / gif / webp / bmp / svg,单张 20 MB |
1066
1084
  | 结束提示 | 任务结束或报错时响一声提示音(完成 / 出错各一种音色,主动停止不响),同时页面在前台弹应用内提示、页面在后台/别的窗口发系统通知,切到别的视图也不会漏掉结果。提示音在总开关下**有自己的开关**——可以"要提示但静音",也可以反过来只要声音。音源是 CC0 资源(`public/sounds/`),换音色见同目录 `CREDITS.txt` |
1067
1085
  | 跨视图指示 | 任意任务运行中时,Activity Bar 上的工作台图标会显示脉动小圆点;切换到 Git 或编辑器视图也能看到运行状态 |
1068
1086
  | 执行日志管理(弹窗) | 顶部「执行日志」按钮唤起弹窗:列表 / 过滤 / 批量删除 / 清空 / 保留策略全部可在此一次性管理(默认保留 500 条、256 MB);弹窗关闭后任务执行视图常驻,避免切换时不必要的卸载 |
1069
- | 继续对话 | 任务进入终态(done / error / cancelled)后出现续聊输入框;发送续聊消息会用 `claude --resume <session_id>`(Claude Code)、`--session`(OpenCode)或 `codex exec resume <thread_id>`(Codex)续接上一轮会话,**新一轮 = 新 job**,多轮纵向堆叠成对话流。续接的会话本身就带着上一轮的上下文,所以续聊轮注入的是**精简刷新版**运行环境块(当前项目 + 看板合计 + 真相源路径),不再重发整份项目清单 —— 聊天记录里也就不会每轮复制一遍同一张表 |
1087
+ | 继续对话 | 任务进入终态(done / error / cancelled)后出现续聊输入框;发送续聊消息会用 `claude --resume <session_id>`(Claude Code)、`--session`(OpenCode)或 `codex exec resume <thread_id>`(Codex)续接上一轮会话,**新一轮 = 新 job**,多轮纵向堆叠成对话流。续接的会话本身就带着上一轮的上下文,所以续聊轮注入的是**精简刷新版**运行环境块(当前项目 + 看板合计 + 真相源路径),不再重发整份项目清单。对话气泡只显示用户真正说过的那句话 —— 注入的环境块 / 记忆块 / 附件清单仍留在 `job.prompt` 原文里(执行日志详情可看可复制),所以把对话复制出来再粘回续聊框时不会连带一整套背景一起回去 |
1070
1088
  | 本地工具检测 | 启动时 + 每 10 分钟探测 7 个 CLI(`code` / `claude` / `codex` / `opencode` / `kimi` / `zcode` / `dsh`)。未安装的工具置灰并标「未安装」,点击弹出安装引导;右键工具按钮可升级到最新已发布版本 |
1071
1089
 
1072
1090
  提示词预置与任务数据持久化到 `~/.zen-gitsync/prompts.json` 和 `~/.zen-gitsync/tasks.json`(跨项目共享);执行历史与保留策略在 `jobs.json` / `jobs-config.json`,主 Agent 控制台状态在 `orchestrator.json`(进度报告历史另存 `orchestrator-reports.json` —— 与"配置和历史分两个文件"同一个理由:被 5s 轮询的那份要小),任务附件落盘在 `~/.zen-gitsync/workbench-images/_task-<taskId>/`。
@@ -1087,7 +1105,7 @@ Activity Bar 中的机器人图标视图,可直接在浏览器中与内置 AI
1087
1105
  | 任务计划 | 多步任务有一份看得见的计划:智能体在动手之前先调内置的 `update_plan` 工具,把任务拆成 3-8 个可核对的步骤,随后逐步更新状态。步骤以清单渲染,区分完成 / 进行中 / 待办三态,标题右侧带 `2/5` 进度 —— 终端里是 `✓ / ▶ / ○` 列表,Web 面板里是一张卡片,且**工具组折叠时仍然常驻**(折叠只藏别的工具调用,绝不藏当前计划) |
1088
1106
  | 最近项目感知 | 问「我哪些项目需要 pull」时,智能体调用内置的 `list_projects` 工具,而不是自己去扫盘:返回的就是 GUI「最近项目」面板那份清单(最近目录 + 建过任务的目录,带分支 / 领先 / 落后 / 未提交数与任务进度),回答与界面对得上。领先/落后读的是本地引用,因此问到"要不要拉"时它可以带 `refresh=true` 先联网 fetch 一轮再答 |
1089
1107
  | 会话持久化 | 所有对话保存为 JSON 文件到 `~/.zen-gitsync/agent-sessions/`;CLI 智能体(`g ai`)写入同一目录,Web 端与 CLI 端会话统一管理 |
1090
- | Skill / MCP 广场 | **Skill 广场** 与 **MCP 广场** 两个 tab 列出多个来源的 Skill 与 MCP 服务,每项带说明、周下载 / 使用次数与安装状态。可安装到**当前项目**或 **`g ai` 智能体**;已安装的可在同一行「打开文件夹」定位到落盘位置,或直接卸载,还缺环境变量的会标出「还缺环境变量」。已安装清单同时显示 skill 自报的 `name` 和实际落盘的目录 id —— 仓库名和 `SKILL.md` 里自称的名字经常不是一个。终端侧 `g ai` 用 `/skills`(`/mcp` 为别名)查看已装清单 |
1108
+ | Skill / MCP 广场 | **Skill 广场** 与 **MCP 广场** 两个 tab 列出多个来源的 Skill 与 MCP 服务,每项带说明、周下载 / 使用次数与安装状态。可安装到**当前项目**(`<项目>/.zen-gitsync/ai/skills/<id>/SKILL.md` 与 `<项目>/.zen-gitsync/ai/mcp.json`)或 **`g ai` 智能体**(`~/.zen-gitsync/ai/`,对所有项目生效)—— 两处都是 zen-gitsync 自己的目录,不借别家工具的。已安装的可在同一行「打开文件夹」定位到落盘位置,或直接卸载,还缺环境变量的会标出「还缺环境变量」。已安装清单同时显示 skill 自报的 `name` 和实际落盘的目录 id —— 仓库名和 `SKILL.md` 里自称的名字经常不是一个。终端侧 `g ai` 用 `/skills`(`/mcp` 为别名)查看已装清单 |
1091
1109
  | 克隆优先 SSH | 让它克隆仓库(或加远端)时走 SSH 形式 —— `git@github.com:owner/repo.git` / `git@gitee.com:owner/repo.git`;拿到 `https://` 地址先换算,克隆不会停在 Git Credential Manager 的账号密码弹窗上。只有 SSH 真的不可用(`Permission denied (publickey)` / 主机密钥校验失败)才退回 https,并说明这次走的是哪条。同一条偏好也会注入到每个工作台任务的 prompt —— 那里执行器是外部 CLI,系统提示词不归本应用管,环境上下文块是唯一的注入口 |
1092
1110
  | 单轮工具调用上限 | 一条消息内智能体最多连续调用多少次工具(默认 **200**,可调范围 1–2000)。在 **设置 → AI 模型配置 → 智能体运行时** 中修改;达到上限本轮会被强制结束并提示再发一条消息继续。CLI 智能体共用同一项设置 |
1093
1111
  | 预设问题 | 开场界面提供快捷按钮(查看项目结构、分析代码质量、写测试、Git 状态检查、帮我启动项目)|
@@ -1133,6 +1151,20 @@ GUI 底栏版本号每个会话会向 npm 查询一次最新版本。检测到
1133
1151
  git add --renormalize .
1134
1152
  ```
1135
1153
 
1154
+ ### 发布到 npm
1155
+
1156
+ `npm run release`(`scripts/release.js`)一条命令跑完整个发版流程:patch 版本号 +1 → `vue-tsc` 类型检查 → 构建前端 → 发布物自检(`files` 白名单 vs 相对 import,外加一次真实 `npm pack` 清单)→ 提交 + 打标签 + 推送 → `npm publish` → `npm install -g zen-gitsync@<版本>`。
1157
+
1158
+ 最后一步最慢:registry 让刚发布的版本变得可安装,实测从几秒到 30 分钟以上都有,所以脚本用两个就绪信号(packument 里有没有该版本 / tarball 能否取到)轮询,并每 4 轮强制真装一次 —— 探针只负责省下一次注定失败的调用,**判据只有 npm 自己**。每次失败打 `[E404]` / `[EPERM]` 短码,放弃时汇总失败构成。
1159
+
1160
+ **不用盯着它。** 流程结束时你会收到一条系统通知 + 提示音(成功与失败是两种不同的音),终端 / 任务栏标题也会变成结果。这些都是尽力而为,**绝不会**影响发布本身的成败;加 `--no-notify`(或设 `ZEN_NO_NOTIFY=1`)可关掉。三种结局分开报,因为"包发出去了但全局没装上"既不是成功也不是失败:
1161
+
1162
+ - **发布完成** —— 已发布到 npm,且全局版本已校验通过。
1163
+ - **已发布但全局没更新** —— 版本已经在 npm 上,只是没装到全局。重发没有意义(版本号已被占用),按提示手动装一次即可。
1164
+ - **发布失败** —— 更早的一步(类型检查 / 发布物自检 / git / `npm publish`)把流程中断了。
1165
+
1166
+ 其它开关:`--dry-run`(只打印计划)、`--skip-push`、`--skip-self-update`、`--keep-instances`、`--poll-timeout=<秒>`。
1167
+
1136
1168
  ---
1137
1169
 
1138
1170
  ## 命令行
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zen-gitsync",
3
- "version": "2.17.29",
3
+ "version": "2.17.30",
4
4
  "description": "Auto commit, scheduled sync, and visual GUI for Git. Run `g` in any repo for one-key commit & push, AI commit messages, scheduled background sync, and a drag-and-drop workflow builder.",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -40,6 +40,8 @@
40
40
  "verify:wb-dispatch": "node scripts/verify-workbench-dispatch-attach.cjs",
41
41
  "verify:wb-env-context": "node scripts/verify-workbench-env-context.mjs",
42
42
  "verify:wb-live-jobs": "node scripts/verify-wb-live-jobs.mjs",
43
+ "verify:wb-bubble-prompt": "node scripts/verify-wb-bubble-prompt.cjs",
44
+ "verify:wb-copy-execution": "node scripts/verify-wb-copy-execution.cjs",
43
45
  "verify:wb-overview": "node scripts/verify-workbench-overview.cjs",
44
46
  "verify:wb-progress-report": "node scripts/verify-wb-progress-report.cjs",
45
47
  "verify:wb-card-live": "node scripts/verify-wb-card-live.cjs",
@@ -96,7 +98,8 @@
96
98
  "npm:ls:g": "npm ls -g",
97
99
  "npm:2fa:get": "npm profile get",
98
100
  "npm:2fa:disable": "npm profile disable-2fa",
99
- "verify:wb-select-popup": "node scripts/verify-native-select-popup.cjs"
101
+ "verify:wb-select-popup": "node scripts/verify-native-select-popup.cjs",
102
+ "verify:wb-inline-images": "node scripts/verify-wb-inline-images.mjs"
100
103
  },
101
104
  "files": [
102
105
  "src/ui/public/**",
@@ -31,6 +31,7 @@
31
31
  * npm run release -- --poll-timeout=3600 # 窗口上限(秒,默认 1800 = 30 分钟)
32
32
  * npm run release -- --poll-fast-window=600 --poll-slow-interval=60 # 600s 后退避到 60s 一跳
33
33
  * npm run release -- --install-timeout=900 # 单次 npm install 上限(秒),0 = 不限时(默认)
34
+ * npm run release -- --no-notify # 结束时不再弹系统通知 / 响提示音
34
35
  *
35
36
  * 发布后自更新:每轮看两个就绪信号(packument 里有没有这个版本 / tarball 能不能取),
36
37
  * 任一为真就真调 npm,并两条路(`pkg@<版本>` / tarball URL 直连)都试,装上后校验全局版本;
@@ -38,6 +39,11 @@
38
39
  * 原因见 tarballUrl() / selfUpdateGlobal() 处注释 —— publish 成功不等于
39
40
  * packument 立即可见,也不等于 tarball 立即可取(两种先后顺序都实测出现过)。
40
41
  *
42
+ * 结束时**离开终端也能收到**提醒(见 announceRelease):系统通知 + 提示音 + 终端标题。
43
+ * 装全局那步实测能磨十几分钟,用户不可能一直盯着;终端里再醒目的一行字,对已经走去
44
+ * 干别的事的人等于没打。三种结局(成功 / 已发布但全局没装上 / 失败)各有各的文案与音,
45
+ * 用 `--no-notify`(或环境变量 ZEN_NO_NOTIFY=1)可关掉。
46
+ *
41
47
  * "这一轮要不要真调 npm"的判定抽在 src/utils/selfUpdatePolicy.js(有单测);
42
48
  * 动那里的阈值/分支请连带跑 `node --test src/utils/selfUpdatePolicy.test.js`。
43
49
  *
@@ -61,6 +67,7 @@ import {
61
67
  classifyInstallError,
62
68
  FORCE_INSTALL_EVERY,
63
69
  } from '../src/utils/selfUpdatePolicy.js'
70
+ import { notifyDesktop, setTerminalTitle } from '../src/utils/desktopNotify.js'
64
71
 
65
72
  const __filename = fileURLToPath(import.meta.url)
66
73
  const __dirname = path.dirname(__filename)
@@ -71,6 +78,9 @@ const DRY_RUN = argv.includes('--dry-run')
71
78
  const SKIP_SELF_UPDATE = argv.includes('--skip-self-update')
72
79
  const SKIP_PUSH = argv.includes('--skip-push')
73
80
  const KEEP_INSTANCES = argv.includes('--keep-instances')
81
+ // 关掉结束时的桌面提醒(系统通知 / 提示音 / 终端标题)。环境变量那份是给
82
+ // "通过 npm script 起、不方便追加参数"的场景留的口子(CI / 别的工具代跑)。
83
+ const NO_NOTIFY = argv.includes('--no-notify') || process.env.ZEN_NO_NOTIFY === '1'
74
84
 
75
85
  const NPM_REGISTRY = 'https://registry.npmjs.org/'
76
86
  const PKG_NAME = 'zen-gitsync'
@@ -365,8 +375,7 @@ async function checkEnvironment() {
365
375
  console.log(chalk.yellow('发布已取消'))
366
376
  process.exit(0)
367
377
  }
368
- console.error(chalk.red('环境检查失败:'), err.message || err)
369
- process.exit(1)
378
+ abortRelease('环境检查失败:', err.message || err)
370
379
  }
371
380
  }
372
381
 
@@ -386,8 +395,7 @@ async function runTypeCheck() {
386
395
  execSync('npx vue-tsc -b --noEmit', { cwd: frontendDir, stdio: 'inherit' })
387
396
  console.log(chalk.green('vue-tsc 类型检查通过'))
388
397
  } catch (err) {
389
- console.error(chalk.red('vue-tsc 类型检查失败,请修复以上错误后重新发布'))
390
- process.exit(1)
398
+ abortRelease('vue-tsc 类型检查失败,请修复以上错误后重新发布')
391
399
  }
392
400
  }
393
401
 
@@ -442,6 +450,8 @@ async function buildFrontend() {
442
450
  console.error(chalk.red('前端项目构建失败:'), err.message || err)
443
451
  const shouldContinue = await askContinue(chalk.yellow('前端构建失败,是否继续发布? (Y/n): '))
444
452
  if (!shouldContinue) {
453
+ // 这里**故意不提醒**:用户刚亲手回答了上面的提示,人就在键盘前(与 abortRelease 那些
454
+ // "进程自己撞墙"的出口不同,那些才需要把人叫回来)。
445
455
  console.log(chalk.red('发布流程已取消'))
446
456
  process.exit(1)
447
457
  }
@@ -467,11 +477,10 @@ async function verifyPackageContents() {
467
477
  try {
468
478
  execSync(`"${process.execPath}" --test test/package-files.test.mjs`, { cwd: rootDir, stdio: 'inherit' })
469
479
  } catch {
470
- console.error(chalk.red(
480
+ abortRelease(
471
481
  '发布物自检失败:package.json#files 覆盖不全(上面哪几条 not ok 就补哪几个文件)。\n'
472
482
  + '继续发布会打出缺文件的包 —— 用户装上就是 ERR_MODULE_NOT_FOUND。'
473
- ))
474
- process.exit(1)
483
+ )
475
484
  }
476
485
  console.log(chalk.green('① files 覆盖所有被 import 的本地模块'))
477
486
 
@@ -482,15 +491,13 @@ async function verifyPackageContents() {
482
491
  // npm 可能在前/后混入提示行,这里取第一个 '[' 到最后一个 ']' 之间
483
492
  packJson = JSON.parse(out.slice(out.indexOf('['), out.lastIndexOf(']') + 1))
484
493
  } catch (err) {
485
- console.error(chalk.red('npm pack --dry-run 失败:'), err.message || err)
486
- process.exit(1)
494
+ abortRelease('npm pack --dry-run 失败:', err.message || err)
487
495
  }
488
496
 
489
497
  const pkg = JSON.parse(fs.readFileSync(path.join(rootDir, 'package.json'), 'utf8'))
490
498
  const packed = new Set((packJson[0]?.files || []).map((f) => f.path))
491
499
  if (packed.size === 0) {
492
- console.error(chalk.red('npm pack 打出了空包,绝对有问题'))
493
- process.exit(1)
500
+ abortRelease('npm pack 打出了空包,绝对有问题')
494
501
  }
495
502
 
496
503
  const notPacked = (pkg.files || []).filter((entry) => {
@@ -502,11 +509,10 @@ async function verifyPackageContents() {
502
509
  })
503
510
 
504
511
  if (notPacked.length) {
505
- console.error(chalk.red(
512
+ abortRelease(
506
513
  `npm pack 里有 ${notPacked.length} 条 files 白名单没真正进包(大概率是 .npmignore / .gitignore 排除了):\n`
507
514
  + notPacked.map((e) => ` - ${e}`).join('\n')
508
- ))
509
- process.exit(1)
515
+ )
510
516
  }
511
517
  console.log(chalk.green(`② 真实打包 ${packed.size} 个文件,白名单逐条命中`))
512
518
  }
@@ -534,9 +540,9 @@ function stageReleaseFiles() {
534
540
  .filter(Boolean)
535
541
  const unexpected = stagedOut.filter((f) => !RELEASE_FILES.includes(f))
536
542
  if (unexpected.length) {
537
- console.error(chalk.red('staged 范围超出白名单,中止提交:'))
538
- for (const f of unexpected) console.error(' - ' + f)
539
- process.exit(1)
543
+ abortRelease(
544
+ 'staged 范围超出白名单,中止提交:\n' + unexpected.map((f) => ` - ${f}`).join('\n')
545
+ )
540
546
  }
541
547
  }
542
548
 
@@ -609,8 +615,7 @@ async function commitChanges(version) {
609
615
  }
610
616
  }
611
617
  } catch (err) {
612
- console.error(chalk.red('Git 提交失败:'), err.message || err)
613
- process.exit(1)
618
+ abortRelease('Git 提交失败:', err.message || err)
614
619
  }
615
620
  }
616
621
 
@@ -885,6 +890,11 @@ async function selfUpdateGlobal(version) {
885
890
  + `单次安装${INSTALL_TIMEOUT_MS > 0 ? `上限 ${INSTALL_TIMEOUT_MS / 1000}s` : '不限时'}`
886
891
  + '(可用 --install-timeout=<秒> 调,0 = 不限时)。'
887
892
  ))
893
+ console.log(chalk.gray(
894
+ '这一步可能要等几分钟到半小时,**不用盯着** —— 装好或彻底失败时会弹一条系统通知'
895
+ + ' + 响一声(成功失败两种音),终端标题也会变成结果。'
896
+ + (NO_NOTIFY ? '(本次带了 --no-notify,提醒已关掉)' : '(想关掉用 --no-notify)')
897
+ ))
888
898
 
889
899
  const startedAt = Date.now()
890
900
  const deadline = startedAt + POLL_TIMEOUT_MS
@@ -970,7 +980,7 @@ async function selfUpdateGlobal(version) {
970
980
 
971
981
  if (plan.attempt) {
972
982
  const res = await runInstallRound(packumentHasVersion)
973
- if (res.ok) return
983
+ if (res.ok) return { ok: true }
974
984
  } else {
975
985
  skippedRounds += 1
976
986
  if (skippedRounds === FORCE_INSTALL_EVERY - 1) {
@@ -1015,7 +1025,7 @@ async function selfUpdateGlobal(version) {
1015
1025
  + '(探针可能误报,事实判据只有 npm 自己)。'
1016
1026
  ))
1017
1027
  const res = await runInstallRound(readPackumentHasVersion(version), '超时前补的真装')
1018
- if (res.ok) return
1028
+ if (res.ok) return { ok: true }
1019
1029
  }
1020
1030
 
1021
1031
  console.error(chalk.red(
@@ -1055,9 +1065,22 @@ async function selfUpdateGlobal(version) {
1055
1065
  + '若报 ETARGET / E404(registry 元数据与 tarball 还没对齐),直连 tarball:\n'
1056
1066
  + ` npm install -g ${tarballUrl(version)}`
1057
1067
  ))
1068
+
1069
+ // 把结论交回调用方:这一阶段失败**不会**抛异常(包已经发布成功了,只是没装到全局),
1070
+ // 所以 main() 那边光看有没有异常是分不出"全成了"和"只成了一半"的 ——
1071
+ // 曾经这里什么都不返回,于是自更新失败时终端照样打出 "🎉 发布完成!"。
1072
+ const tally = [...errorTally.entries()].sort((a, b) => b[1] - a[1]).map(([k, n]) => `${k} ×${n}`)
1073
+ return {
1074
+ ok: false,
1075
+ reason: tally.length
1076
+ ? `${POLL_TIMEOUT_MS / 1000}s 内真调 npm ${installAttempts} 次仍未装上(${tally.join(' / ')})`
1077
+ : `${POLL_TIMEOUT_MS / 1000}s 窗口内没装上(npm 没给出失败原因)`,
1078
+ }
1058
1079
  }
1059
1080
 
1060
- // 发布到 NPM
1081
+ // 发布到 NPM。返回 `{ published, selfUpdate, reason }` —— 自更新失败时 `published`
1082
+ // 仍是 true(包已经在 npm 上了),`selfUpdate: 'failed'` + `reason` 才是给 main() 拿去做
1083
+ // "已发布但全局没更新"提醒用的。`selfUpdate` 取值:`'ok' | 'failed' | 'skipped'`。
1061
1084
  async function publishToNpm(version) {
1062
1085
  console.log(chalk.blue('\n=== 发布到 NPM ==='))
1063
1086
 
@@ -1070,24 +1093,117 @@ async function publishToNpm(version) {
1070
1093
  console.log(chalk.yellow(
1071
1094
  '[dry-run] 安装前先停掉实例注册表里的所有运行实例(见 stopRunningInstances)'
1072
1095
  ))
1073
- return
1096
+ return { published: false, selfUpdate: 'skipped', reason: '' }
1074
1097
  }
1075
1098
  execSync(`npm publish --registry=${NPM_REGISTRY}`, { stdio: 'inherit' })
1076
1099
  console.log(chalk.green('已成功发布到 NPM'))
1077
1100
 
1078
1101
  if (SKIP_SELF_UPDATE) {
1079
1102
  console.log(chalk.yellow('--skip-self-update: 不自动 `npm install -g zen-gitsync`'))
1080
- return
1103
+ return { published: true, selfUpdate: 'skipped', reason: '--skip-self-update' }
1081
1104
  }
1082
1105
 
1083
1106
  // 发版后可选:把刚发布的版本装到全局(默认开启,可用 --skip-self-update 关掉)
1084
- await selfUpdateGlobal(version)
1107
+ const selfUpdate = await selfUpdateGlobal(version)
1108
+ return {
1109
+ published: true,
1110
+ selfUpdate: selfUpdate.ok ? 'ok' : 'failed',
1111
+ reason: selfUpdate.reason ?? '',
1112
+ }
1085
1113
  } catch (err) {
1086
- console.error(chalk.red('发布到 NPM 失败:'), err.message || err)
1087
- process.exit(1)
1114
+ abortRelease('发布到 NPM 失败:', err.message || err)
1088
1115
  }
1089
1116
  }
1090
1117
 
1118
+ // ===========================================================================
1119
+ // 结束提醒(系统通知 + 提示音 + 终端标题)
1120
+ // ===========================================================================
1121
+ //
1122
+ // 为什么值得专门做:装全局那一步实测能磨十几分钟(registry 元数据/对象就绪慢,见文件头
1123
+ // 注释),而且整条流程常常是"开着就走人"的。终端里再醒目的一行字,对已经走去干别的事的
1124
+ // 人等于没打 —— 提醒必须走**终端之外**的信道(实现见 src/utils/desktopNotify.js)。
1125
+ //
1126
+ // 覆盖所有提前结束的出口:类型检查 / 发布物自检 / git / npm publish 失败原本都是直接
1127
+ // `process.exit(1)`,进程当场就没了,提醒根本没机会发 —— 而这些恰恰是最该被通知的结局
1128
+ // (用户走开 15 分钟,失败发生在第 40 秒)。所以它们统一改走 abortRelease()。
1129
+
1130
+ // 发布时间与版本号挂在模块作用域:abortRelease() 可能在 updateVersion() 之前就被调用
1131
+ // (类型检查失败就是这么早),那时版本号还不存在,文案里得能优雅地少一段。
1132
+ let releaseStartedAt = 0
1133
+ let currentVersion = null
1134
+
1135
+ // 秒数换成人话:"耗时 698.7s" 是给日志看的,"11 分 38 秒" 才是给通知看的
1136
+ function humanDuration(ms) {
1137
+ const total = Math.max(0, Math.round(ms / 1000))
1138
+ if (total < 60) return `${total} 秒`
1139
+ return `${Math.floor(total / 60)} 分 ${total % 60} 秒`
1140
+ }
1141
+
1142
+ // 通知正文压成一行并截断:toast 塞不下长文本,而报错原文常常是多行 + 一长串 URL
1143
+ function oneLine(text, max = 160) {
1144
+ const s = String(text ?? '').replace(/\s+/g, ' ').trim()
1145
+ return s.length > max ? `${s.slice(0, max - 1)}…` : s
1146
+ }
1147
+
1148
+ // 各结局的文案。集中在一处,是因为"通知里说什么"和"终端里说什么"必须是同一件事 ——
1149
+ // 尤其 partial:包已经发出去了,但全局没装上,报"发布完成"是误导,报"发布失败"也是误导。
1150
+ function releaseOutcome({ status, version, durationMs, reason }) {
1151
+ const spent = humanDuration(durationMs)
1152
+ if (status === 'ok') {
1153
+ return {
1154
+ ok: true,
1155
+ title: `发布完成 · zen-gitsync v${version}`,
1156
+ message: `已发布到 npm,全局版本已更新为 v${version}。耗时 ${spent}。`,
1157
+ }
1158
+ }
1159
+ if (status === 'ok-skipped') {
1160
+ return {
1161
+ ok: true,
1162
+ title: `发布完成 · zen-gitsync v${version}`,
1163
+ message: `已发布到 npm。本次带 --skip-self-update,全局版本没动。耗时 ${spent}。`,
1164
+ }
1165
+ }
1166
+ if (status === 'partial') {
1167
+ return {
1168
+ ok: false,
1169
+ title: `已发布但全局没更新 · zen-gitsync v${version}`,
1170
+ message: `npm 上已有 v${version},全局仍是旧版。${oneLine(reason)} `
1171
+ + `手动装:npm install -g ${PKG_NAME}@${version}`,
1172
+ }
1173
+ }
1174
+ return {
1175
+ ok: false,
1176
+ title: `发布失败 · zen-gitsync${version ? ` v${version}` : ''}`,
1177
+ message: oneLine(reason || '见终端输出'),
1178
+ }
1179
+ }
1180
+
1181
+ // 发提醒 + 把结果写上终端标题。DRY_RUN 一律不提醒:什么都没发生,响一声只会让人以为发成功了。
1182
+ function announceRelease(outcome) {
1183
+ if (DRY_RUN) return
1184
+ setTerminalTitle(outcome.title)
1185
+ if (NO_NOTIFY) {
1186
+ console.log(chalk.gray('--no-notify:跳过系统通知 / 提示音(终端标题已更新)'))
1187
+ return
1188
+ }
1189
+ notifyDesktop({ title: outcome.title, message: outcome.message, ok: outcome.ok })
1190
+ console.log(chalk.gray('已发送结束提醒(系统通知 + 提示音;发不出去也不影响发布结果)'))
1191
+ }
1192
+
1193
+ // 发布提前结束的统一出口:打日志 → 发提醒 → 退出 1。
1194
+ // 原本这些地方各自写 `console.error(...)` + `process.exit(1)`,提醒没机会发(见上面注释)。
1195
+ function abortRelease(message, detail) {
1196
+ if (detail === undefined) console.error(chalk.red(message))
1197
+ else console.error(chalk.red(message), detail)
1198
+ announceRelease(releaseOutcome({
1199
+ status: 'fail',
1200
+ version: currentVersion,
1201
+ durationMs: Date.now() - releaseStartedAt,
1202
+ reason: detail ?? message,
1203
+ }))
1204
+ process.exit(1)
1205
+ }
1206
+
1091
1207
  // 主流程
1092
1208
  async function main() {
1093
1209
  console.log(chalk.cyan(`\n🚀 开始发布流程${DRY_RUN ? '(DRY RUN)' : ''}...\n`))
@@ -1099,15 +1215,21 @@ async function main() {
1099
1215
  if (SKIP_SELF_UPDATE) console.log(chalk.yellow('--skip-self-update: 发版后不自动 npm install -g'))
1100
1216
  if (KEEP_INSTANCES) console.log(chalk.yellow('--keep-instances: 装全局前不停运行中的实例'))
1101
1217
  if (SKIP_PUSH) console.log(chalk.yellow('--skip-push: 不 push git'))
1218
+ if (NO_NOTIFY) console.log(chalk.yellow('--no-notify: 结束时不再弹系统通知 / 响提示音'))
1219
+
1220
+ releaseStartedAt = Date.now()
1221
+ // 先把"发布中"写上终端标题:整条流程常要十几分钟,人不在时任务栏那行字是唯一能看出
1222
+ // "还在跑"的地方;结束时会被 announceRelease 换成结果。
1223
+ if (!DRY_RUN) setTerminalTitle('发布中 · zen-gitsync')
1102
1224
 
1103
1225
  try {
1104
1226
  await checkEnvironment()
1105
1227
  await runTypeCheck()
1106
- const newVersion = updateVersion()
1228
+ currentVersion = updateVersion()
1107
1229
  await buildFrontend()
1108
1230
  await verifyPackageContents()
1109
- await commitChanges(newVersion)
1110
- await publishToNpm(newVersion)
1231
+ await commitChanges(currentVersion)
1232
+ const { selfUpdate, reason } = await publishToNpm(currentVersion)
1111
1233
 
1112
1234
  if (stoppedInstanceCount > 0) {
1113
1235
  console.log(chalk.yellow(
@@ -1116,10 +1238,31 @@ async function main() {
1116
1238
  ))
1117
1239
  }
1118
1240
 
1241
+ // 自更新失败**不抛异常**(包已经发上去了,只是没装到全局),所以这里必须显式分支 ——
1242
+ // 否则终端会照样打 "🎉 发布完成!",而那是对用户的误导。
1243
+ if (selfUpdate === 'failed') {
1244
+ console.log(chalk.yellow(`\n⚠️ v${currentVersion} 已经发布到 npm,但全局版本没装上。`))
1245
+ console.log(chalk.yellow(' 重发没有意义(版本已经占掉了),按上面的命令手动装一次即可。'))
1246
+ announceRelease(releaseOutcome({
1247
+ status: 'partial',
1248
+ version: currentVersion,
1249
+ durationMs: Date.now() - releaseStartedAt,
1250
+ reason,
1251
+ }))
1252
+ return
1253
+ }
1254
+
1119
1255
  console.log(chalk.green('\n🎉 发布完成!'))
1256
+ if (selfUpdate === 'skipped' && !DRY_RUN) {
1257
+ console.log(chalk.gray('(全局版本没动,见上面的 --skip-self-update)'))
1258
+ }
1259
+ announceRelease(releaseOutcome({
1260
+ status: selfUpdate === 'skipped' ? 'ok-skipped' : 'ok',
1261
+ version: currentVersion,
1262
+ durationMs: Date.now() - releaseStartedAt,
1263
+ }))
1120
1264
  } catch (err) {
1121
- console.error(chalk.red('\n❌ 发布失败:'), err.message || err)
1122
- process.exit(1)
1265
+ abortRelease('\n❌ 发布失败:', err.message || err)
1123
1266
  }
1124
1267
  }
1125
1268
 
@@ -682,7 +682,7 @@ export async function handleSlashCommand(state, input, t) {
682
682
  const st = await fsp.stat(target)
683
683
  if (!st.isDirectory()) throw new Error('not a dir')
684
684
  state.ctx.cwd = target
685
- // 项目级 skill 与 .mcp.json 跟着工作目录走:先重载扩展(会关掉旧目录起的 MCP 子进程),
685
+ // 项目级 skill 与项目级 mcp.json 跟着工作目录走:先重载扩展(会关掉旧目录起的 MCP 子进程),
686
686
  // 再重建首条 system 消息 —— 顺序反了会把旧目录的 skill 清单写进新目录的提示词。
687
687
  await state.extensions?.reload({ cwd: target, locale: state.locale, onWarn: printWarn })
688
688
  state.messages[0] = { role: 'system', content: await buildProjectPrompt({ cwd: target, locale: state.locale, shellDesc: state.shellDesc, extra: extensionSuffix(state) }) }
@@ -93,7 +93,7 @@ export class AgentExtensions {
93
93
  }
94
94
 
95
95
  /**
96
- * 切换工作目录:重读项目级 skill 与 .mcp.json,先关掉旧目录起的子进程。
96
+ * 切换工作目录:重读项目级 skill 与项目级 mcp.json,先关掉旧目录起的子进程。
97
97
  * 全局级配置不受影响,但一起重载更简单也更好推理。
98
98
  *
99
99
  * @param {{ cwd: string, locale?: string, onWarn?: (message: string) => void,
package/src/cli/ai/mcp.js CHANGED
@@ -20,9 +20,10 @@
20
20
  // 自己写≈200 行,换取零依赖 + 完全可控的超时与降级行为。
21
21
  //
22
22
  // 配置来源(两级,项目级覆盖全局同名 server):
23
- // 1) ~/.zen-gitsync/ai/mcp.json 「g ai 智能体」全局安装(对所有项目生效)
24
- // 2) <cwd>/.mcp.json 项目级安装
25
- // 形状沿用 Claude 的 mcpServers,便于与生态里其他工具共享配置:
23
+ // 1) ~/.zen-gitsync/ai/mcp.json 「g ai 智能体」全局安装(对所有项目生效)
24
+ // 2) <cwd>/.zen-gitsync/ai/mcp.json 项目级安装
25
+ // 形状沿用生态通行的 mcpServers。项目级路径常量与广场安装端共用 src/paths.js 那一份;
26
+ // 项目级**不再读** <cwd>/.mcp.json(那是 Claude Code 一系的约定路径,2026-09-30 迁出)。
26
27
  // { "mcpServers": { "github": { "command": "npx", "args": ["-y", "@..."], "env": {} } } }
27
28
  //
28
29
  // ⚠️ Windows 上 spawn 的两个坑(已实测,不要"优化"掉):
@@ -38,7 +39,7 @@ import { spawn } from 'node:child_process';
38
39
  import { promises as fs, statSync } from 'node:fs';
39
40
  import path from 'node:path';
40
41
  import { trackChild } from '../cleanup.js';
41
- import { AI_MCP_FILE } from '../../paths.js';
42
+ import { AI_MCP_FILE, projectMcpFile } from '../../paths.js';
42
43
 
43
44
  const IS_WIN = process.platform === 'win32';
44
45
 
@@ -171,7 +172,7 @@ async function readServersFile(file) {
171
172
  */
172
173
  export async function loadMcpServers({ cwd, globalFile = AI_MCP_FILE, projectFile } = {}) {
173
174
  const globalServers = await readServersFile(globalFile);
174
- const projectPath = projectFile || (cwd ? path.join(path.resolve(cwd), '.mcp.json') : null);
175
+ const projectPath = projectFile || (cwd ? projectMcpFile(cwd) : null);
175
176
  const projectServers = projectPath ? await readServersFile(projectPath) : {};
176
177
  return {
177
178
  servers: { ...globalServers, ...projectServers },
@@ -94,7 +94,10 @@ test('loadMcpServers: 项目级覆盖全局同名 server,坏的 JSON 只当没
94
94
  broken: { args: ['no-command'] },
95
95
  },
96
96
  }), 'utf8');
97
- await fs.writeFile(path.join(project, '.mcp.json'), JSON.stringify({
97
+ // 项目级落在 .zen-gitsync/ai/mcp.json —— 写死字面量,常量漂了要在这里炸
98
+ const projectFile = path.join(project, '.zen-gitsync', 'ai', 'mcp.json');
99
+ await fs.mkdir(path.dirname(projectFile), { recursive: true });
100
+ await fs.writeFile(projectFile, JSON.stringify({
98
101
  mcpServers: { shared: { command: 'npx', args: ['project'] } },
99
102
  }), 'utf8');
100
103
 
@@ -103,11 +106,20 @@ test('loadMcpServers: 项目级覆盖全局同名 server,坏的 JSON 只当没
103
106
  assert.ok(servers.onlyGlobal);
104
107
  assert.equal(servers.broken, undefined, '没有 command 的条目应被丢弃');
105
108
  assert.equal(sources.length, 2);
109
+ assert.equal(sources[1].file, projectFile, '项目级来源路径必须就是那个文件');
106
110
 
107
111
  // 坏文件:整个文件当空配置,不抛错
108
- await fs.writeFile(path.join(project, '.mcp.json'), '{ this is not json', 'utf8');
112
+ await fs.writeFile(projectFile, '{ this is not json', 'utf8');
109
113
  const broken = await loadMcpServers({ cwd: project });
110
114
  assert.deepEqual(broken.servers.shared.args, ['global'], '坏的项目配置应回落到全局配置');
115
+
116
+ // 反向自证:老位置 <cwd>/.mcp.json 不再被读
117
+ const legacyProject = await fs.mkdtemp(path.join(os.tmpdir(), 'zen-mcp-legacy-'));
118
+ await fs.writeFile(path.join(legacyProject, '.mcp.json'), JSON.stringify({
119
+ mcpServers: { fromClaude: { command: 'npx', args: ['legacy'] } },
120
+ }), 'utf8');
121
+ const legacy = await loadMcpServers({ cwd: legacyProject });
122
+ assert.equal(legacy.servers.fromClaude, undefined, '项目根 .mcp.json 是别家工具的路径,不该再被读');
111
123
  });
112
124
 
113
125
  // ── 真实 stdio 往返 ───────────────────────────────────────────