zen-gitsync 2.17.21 → 2.17.23

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. package/README.md +20 -12
  2. package/package.json +10 -1
  3. package/scripts/release.js +214 -82
  4. package/src/ui/public/assets/{AgentEngineSelector-D_8VedA0.js → AgentEngineSelector-DLJ8He83.js} +1 -1
  5. package/src/ui/public/assets/{AgentView-CpKIfQLq.js → AgentView-CQ18nw02.js} +1 -1
  6. package/src/ui/public/assets/{AppVersionBadge-kyk4Mmvf.js → AppVersionBadge-1pyUzRer.js} +2 -2
  7. package/src/ui/public/assets/{BranchSelector-BUPiKcdW.js → BranchSelector-DrsrZiWJ.js} +1 -1
  8. package/src/ui/public/assets/{CommitForm-CMe1gjY-.js → CommitForm-B5p-7P7m.js} +1 -1
  9. package/src/ui/public/assets/{CommonDialog-DMWtstZq.js → CommonDialog-BEm2cK1X.js} +1 -1
  10. package/src/ui/public/assets/{EditorView-mUmFBb7H.css → EditorView-C6HFYeEM.css} +1 -1
  11. package/src/ui/public/assets/EditorView-fuMrS9P7.js +1 -0
  12. package/src/ui/public/assets/{FileDiffViewer-CvN5b1ZC.js → FileDiffViewer-CYxcLw10.js} +1 -1
  13. package/src/ui/public/assets/{FlowExecutionViewer-Ynr4q8qE.js → FlowExecutionViewer-DTmlBLXQ.js} +1 -1
  14. package/src/ui/public/assets/{FlowOrchestrationWorkspace-DPDZc9uf.js → FlowOrchestrationWorkspace-Da_UmTRT.js} +1 -1
  15. package/src/ui/public/assets/{IconButton-CFJkDLVG.js → IconButton-BShiC2AP.js} +1 -1
  16. package/src/ui/public/assets/{LogList-CbfsCVsy.js → LogList-5M5JBhlB.js} +1 -1
  17. package/src/ui/public/assets/{MindmapView-D8A1V-Fd.js → MindmapView-BUV_w14M.js} +1 -1
  18. package/src/ui/public/assets/{MonitorView-Bm_vUP8Y.js → MonitorView-D9N4QH5Y.js} +1 -1
  19. package/src/ui/public/assets/{ProjectStartupButton-BBND5XcZ.js → ProjectStartupButton-DYYQ-O7P.js} +1 -1
  20. package/src/ui/public/assets/RecentDirectoriesChat-BtWa_HMY.js +1 -0
  21. package/src/ui/public/assets/RecentDirectoriesChat-vHT8jMxW.css +1 -0
  22. package/src/ui/public/assets/{RemoteManagerDialog-BVQeIr8U.js → RemoteManagerDialog-C4QIJK_F.js} +1 -1
  23. package/src/ui/public/assets/{RemoteRepoCard-vuCu_cpA.js → RemoteRepoCard-BmDh538v.js} +1 -1
  24. package/src/ui/public/assets/{SourceMapView-BZjgxQM8.js → SourceMapView-Ceamya-N.js} +1 -1
  25. package/src/ui/public/assets/{SvgIcon-D0hK-p1U.js → SvgIcon-xtz4vvc9.js} +1 -1
  26. package/src/ui/public/assets/{TemplateManager-XoPZv_i6.js → TemplateManager-DhKTZ4EM.js} +1 -1
  27. package/src/ui/public/assets/{UserInputNode-CZarmsrv.js → UserInputNode-DfeCGlEO.js} +1 -1
  28. package/src/ui/public/assets/WorkbenchView-D9l3i4Qj.css +1 -0
  29. package/src/ui/public/assets/WorkbenchView-DX2epPD9.js +6 -0
  30. package/src/ui/public/assets/{_plugin-vue_export-helper-N8Vc5eEs.js → _plugin-vue_export-helper-C_bQVzeb.js} +4 -4
  31. package/src/ui/public/assets/{agentConversations-Da7zLPfP.js → agentConversations-BUPj8vzH.js} +1 -1
  32. package/src/ui/public/assets/configStore-DJH7OlB_.js +1 -0
  33. package/src/ui/public/assets/{gitStore-grKwgyHH.js → gitStore-ByP9lpuT.js} +1 -1
  34. package/src/ui/public/assets/index-CFX8sK4w.js +29 -0
  35. package/src/ui/public/assets/{previewSandbox-D4455rKO.js → previewSandbox-DSzZ_Rky.js} +1 -1
  36. package/src/ui/public/index.html +10 -10
  37. package/src/ui/public/sounds/CREDITS.txt +32 -0
  38. package/src/ui/public/sounds/task-done.wav +0 -0
  39. package/src/ui/public/sounds/task-error.wav +0 -0
  40. package/src/ui/server/routes/config.js +4 -1
  41. package/src/ui/server/routes/workbench/index.js +15 -6
  42. package/src/ui/server/routes/workbench/jobActivity.js +274 -0
  43. package/src/ui/server/routes/workbench/jobActivity.test.js +295 -0
  44. package/src/ui/server/routes/workbench/liveJobs.js +3 -0
  45. package/src/ui/server/routes/workbench/orchestratorStore.js +53 -2
  46. package/src/ui/server/routes/workbench/orchestratorStore.progressReport.test.js +177 -0
  47. package/src/ui/server/routes/workbench/progressReport.js +54 -47
  48. package/src/ui/server/routes/workbench/progressReport.test.js +73 -42
  49. package/src/ui/server/routes/workbench/projectRegistry.js +36 -2
  50. package/src/ui/server/routes/workbench/projectRegistry.test.js +98 -0
  51. package/src/ui/server/routes/workbench/taskRunner.js +9 -0
  52. package/src/utils/selfUpdatePolicy.js +123 -0
  53. package/src/utils/selfUpdatePolicy.test.js +204 -0
  54. package/src/ui/public/assets/EditorView-C6msf9YP.js +0 -1
  55. package/src/ui/public/assets/RecentDirectoriesChat-BQbncY1L.css +0 -1
  56. package/src/ui/public/assets/RecentDirectoriesChat-D9fMFh8r.js +0 -1
  57. package/src/ui/public/assets/WorkbenchView-BLyXsdYw.css +0 -1
  58. package/src/ui/public/assets/WorkbenchView-CPur8WEj.js +0 -6
  59. package/src/ui/public/assets/configStore-BFb0I8eP.js +0 -1
  60. package/src/ui/public/assets/index-BHYAwndE.js +0 -29
package/README.md CHANGED
@@ -199,7 +199,7 @@ When the GUI is opened on a directory that is not a Git repository, the right pa
199
199
 
200
200
  The same list carries a short note underneath the cards — in the **full-screen** directory switcher dialog the path field and the **常用目录** cards take the left side, and that note lives in a right-hand column beside them. With an AI model configured it is an **AI status summary** — one paragraph written by the model from the freshly fetched states, naming the projects that need a pull, have unpushed commits or uncommitted changes, and saying so when everything is in sync. It is generated once per distinct state right after the **刷新全部** pass finishes (never mid-refresh), cached for the page, and there is a regenerate button on the right; the summary is shared between the panel and the dialog, so opening the switcher never triggers a second call. Without a model configured it falls back to a static note explaining what the badges mean.
201
201
 
202
- In the switcher dialog, that right column continues with a **g ai** follow-up box: ask which project to handle first, or how far one of them is behind, and it answers from the very same status the cards show — every turn carries the current directory states (plus the summary text) as request-scoped context, so there is no need to restate the background. Four one-click question cards sit in the empty state (what to handle first / who is behind / pull everything behind / what is uncommitted) — clicking one sends it straight away, for the same reason the box exists: you should not have to retype the background. It always runs on the built-in **g ai**, and each time the dialog opens it starts a fresh session, so the context is never a stale snapshot.
202
+ In the switcher dialog, that right column continues with a **g ai** follow-up box: ask which project to handle first, or how far one of them is behind, and it answers from the very same status the cards show — every turn carries the current directory states (plus the summary text) as request-scoped context, so there is no need to restate the background. Four one-click question cards sit in the empty state (what to handle first / who is behind / pull everything behind / what is uncommitted) — clicking one sends it straight away, for the same reason the box exists: you should not have to retype the background. It always runs on the built-in **g ai**, and each time the dialog opens it starts a fresh session, so the context is never a stale snapshot. The always-on recent-projects panel is not rebuilt on close, so its box would otherwise accumulate one session all day long — that is why a **New chat** button appears above it once there are messages: it starts a fresh session and stops whatever was still generating. The previous session is kept rather than deleted (it is already saved, and still listed in the Agent view).
203
203
 
204
204
  ---
205
205
 
@@ -349,10 +349,12 @@ A full IDE-like editor (fourth icon in the activity bar) for browsing and editin
349
349
  | File tree | Collapsible directory tree with file-type icons; **auto-refreshes every 15s** to pick up changes made outside the GUI (skipped when the tab is hidden or the search box is non-empty) |
350
350
  | File search | Type in the sidebar search box to filter the tree (180 ms debounce); matched substrings are highlighted in node names; `Ctrl+F` / `Cmd+F` focuses the box; `Esc` clears the query or blurs the input |
351
351
  | Multi-tab editing | Open multiple files simultaneously; tabs show unsaved (●) indicator |
352
+ | Workspace restore | The tree's expanded folders and the open tabs (their order plus which one is active) are remembered **per project** and put back on reload, and when you switch back to that project. The snapshot lives in `~/.zen-gitsync/config.json` under `ui.editorWorkspaceByProject`; only paths are stored — files are re-read from disk, so unsaved edits do not survive a reload, and files that no longer exist are skipped silently |
353
+ | Sidebar width | Drag the divider to resize the file tree pane; unlike the workspace snapshot the width is **global** (one value for every project) and is stored in `~/.zen-gitsync/config.json` under `ui.editorSidebarWidth`, restored on reload. Clamped to 140–400px |
352
354
  | Monaco editor | Syntax highlighting for JS, TS, Vue, Python, Go, JSON, CSS, and more |
353
355
  | Markdown preview | Toggle between source and rendered preview for `.md` files |
354
356
  | HTML preview / browser | `.html` / `.htm` render in a sandboxed in-app iframe; right-click one in the file tree → **Open in Browser** to hand it to the system default browser instead |
355
- | Save | `Ctrl+S` to save; optional auto-save on focus loss |
357
+ | Save | `Ctrl+S` to save; auto-save on focus loss is on by default (can be turned off in settings) |
356
358
  | Create | New file or folder inline in the file tree |
357
359
  | Rename / Delete | Rename or delete any file or folder directly from the tree |
358
360
  | Resizable sidebar | Drag the divider to adjust file tree width |
@@ -374,11 +376,13 @@ A dedicated view for running coding agents across one or many projects. Every ta
374
376
  | Task list | Create, edit, delete tasks. Rows are grouped by project (current project first), each a single line — the title, or the first characters of the description (ellipsised) when the title is empty. Tasks with neither a title nor a description count as drafts that are never saved (switching away drops them) |
375
377
  | Multi-project board | Three resizable / collapsible panes: the project list with its run monitor, the kanban board, and the master-agent console. Drag a divider to resize (project list 180–420 px, console 260–560 px), double-click it to reset the responsive default, or fold a side away — the top-bar button folds the project list, the console folds into a 32 px rail that expands on click. The run monitor has its own divider (120 px up to half the viewport height) for when several jobs run at once. All widths / heights are remembered across sessions |
376
378
  | Kanban board | **Todo / Doing / Done** columns with the done column sorted newest-first; a filter checkbox narrows the board to tasks whose last run failed, and a table view is one click away for a denser listing. A **New task** entry sits at the end of the Todo column whether or not that column already holds cards (it doubles as the empty state, and opens the same create dialog as the top-right button) |
379
+ | Card status while running | A running task is more than a blinking dot: the card itself carries its executor's brand icon (hover for the product name), the elapsed time, how many tool calls it has made (hover for the mix of types), its most recent tool call, what it is thinking right now, and its newest reply — plus a silence figure once the job has gone quiet for a while. A row with nothing in it is left out instead of being filled with "none yet", and a card that is not running has no such block at all. The facts come from the execution log, the same source the progress report reads, so they refresh with the board's 5-second poll and a task running in another `g ui` instance shows up here too; in table view the same summary sits under the task name |
377
380
  | Master-agent console | Give the master agent an instruction and it dispatches a new task, deciding the target project itself (or honouring the project you selected). Enter dispatches, Shift+Enter starts a new line. Dispatching can be paused and resumed — while paused, a dispatch creates the task without running it |
378
- | 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, tool calls, latest output — and writes a short paragraph per report: what each task is doing, how far it has got, and whether it looks stuck. 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, last tool call) 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; with nothing running they are skipped entirely (no empty entries, no model call). The paragraph is written in the UI language and, when a task's output is inconclusive, it says so rather than inventing progress |
381
+ | Card after a run | A card in Done no longer stops at its title and timestamp: it keeps a short excerpt of the last thing the agent said — the tail of the newest run's reply, flattened to one line and capped at 100 characters. This is the case it was built for: a run often ends by asking you something ("shall I push?"), so a task sitting in Done may still be waiting on your answer, and the only way to find out used to be opening the editor and reading the log. The executor's brand icon sits at the head of that excerpt — the line is what **it** said, so the icon that says which CLI said it belongs there rather than somewhere else on the card. A task that is still running keeps showing the live block instead (the two are mutually exclusive), a run that wrote no body text leaves this line out entirely rather than borrowing an earlier run's words, and the full excerpt is in the hover tooltip; the table view carries the same line under the task name. Executor icons are drawn only when the executor is actually known — a run recorded before the `agent` field existed, or one whose value this build does not recognise, gets no icon rather than a guessed brand (in the table view, icons appear on the status line for a running task and on the last-reply line for a finished one) |
382
+ | 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 |
379
383
  | 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 |
380
384
  | 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 |
381
- | 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 |
385
+ | 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 |
382
386
  | 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 |
383
387
  | 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`) |
384
388
  | Prompt presets | Reusable prompt templates with `{{task.title}}` / `{{task.desc}}` / `{{repo.path}}` / `{{branch}}` variable interpolation |
@@ -388,7 +392,7 @@ A dedicated view for running coding agents across one or many projects. Every ta
388
392
  | 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) |
389
393
  | Live status | Task status (pending / running / done / error / cancelled) and PID stream in real time over SSE |
390
394
  | Tool-call stream | The model's tool calls are rendered inline in the conversation flow, so you can follow what the agent actually did |
391
- | Finish notice | When a run finishes or fails you get 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 |
395
+ | 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. Sounds are CC0 assets under `public/sounds/`; see `CREDITS.txt` there to swap in another tone |
392
396
  | 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 |
393
397
  | 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 |
394
398
  | 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 |
@@ -433,7 +437,7 @@ A dedicated view (robot icon in the activity bar) for chatting with the built-in
433
437
  | Git global settings | `user.name` / `user.email`, auto-set upstream, pull strategy, auto-prune remote branches, line-ending handling, the default branch for `git init` |
434
438
  | Commit settings | Standardised commit form, skip hooks (`--no-verify`), Enter-to-commit, auto-close the push modal, pull before push, auto-fill the default commit message |
435
439
  | Edit config | Raw JSON editor for the config, plus a button to open the file on disk |
436
- | Editor settings | Editor behaviour such as auto-save on focus loss |
440
+ | Editor settings | Editor behaviour such as auto-save on focus loss (on by default) |
437
441
 
438
442
  ---
439
443
 
@@ -856,7 +860,7 @@ $ ZEN_ALLOWED_ORIGINS="https://zen.example.com,http://10.0.0.5:8080" g ui
856
860
 
857
861
  这份列表(以及切换目录弹窗里的 **常用目录**)在卡片下方还有一段说明;切换目录弹窗是**全屏**的,左栏放路径输入框与 **常用目录** 卡片,这段说明则落在右侧一列。配置了 AI 模型时,它是模型根据刚刷新的状态写成的 **AI 项目状态解读**:一段话说清哪些项目该 pull、哪些有未推送的提交、哪些只是工作区脏了,全都同步干净时也会明确说明。它在「刷新全部」跑完的那一刻按状态生成一次(刷新途中不会生成),整页缓存复用,右侧带重新生成按钮;面板与弹窗共用同一份解读,打开弹窗不会多问一次模型。没配模型时退回一段静态说明,讲清徽标里的数字各是什么意思。
858
862
 
859
- 切换目录弹窗里,这一列的解读底下还接着一个 **g ai** 追问框:可以直接问「先处理哪个」「某个项目落后了多少」,它答的就是卡片上这份状态 —— 每一轮都会把当前的目录状态(连同这段解读原文)作为请求级上下文带给模型,不用重新复述背景。空态下还摆着四张一键问题卡(先处理哪个 / 谁落后 / 落后的都拉一下 / 未提交的改了什么),点一下直接发出去 —— 和这个框存在的理由是同一个:不该让人把背景再敲一遍。这块固定跑内置 **g ai**,每次打开弹窗都是一次新会话,上下文因此永远是界面上这一刻的状态。
863
+ 切换目录弹窗里,这一列的解读底下还接着一个 **g ai** 追问框:可以直接问「先处理哪个」「某个项目落后了多少」,它答的就是卡片上这份状态 —— 每一轮都会把当前的目录状态(连同这段解读原文)作为请求级上下文带给模型,不用重新复述背景。空态下还摆着四张一键问题卡(先处理哪个 / 谁落后 / 落后的都拉一下 / 未提交的改了什么),点一下直接发出去 —— 和这个框存在的理由是同一个:不该让人把背景再敲一遍。这块固定跑内置 **g ai**,每次打开弹窗都是一次新会话,上下文因此永远是界面上这一刻的状态。常驻的**最近项目面板**不在「关掉重建」之列,同一块追问区会从开盘一路攒到收盘 —— 所以有消息之后框的上方会出现一个 **新建对话**:点一下开一条新会话,顺手把还在跑的那一轮停掉;旧会话不删(它已经落盘,在智能体视图的会话列表里照旧能找到)。
860
864
 
861
865
  ---
862
866
 
@@ -1006,10 +1010,12 @@ Activity Bar 第四个视图,在 GUI 内直接浏览并编辑项目文件:
1006
1010
  | 文件树 | 可折叠的目录树,附带文件类型图标;**每 15 秒自动刷新一次**,捕获 GUI 外部对文件的改动(标签页隐藏或搜索框非空时跳过) |
1007
1011
  | 文件搜索 | 在侧边栏搜索框中输入关键字过滤文件树(180ms 防抖),命中片段会在节点名中高亮;`Ctrl+F` / `Cmd+F` 聚焦搜索框,`Esc` 清空内容或失焦 |
1008
1012
  | 多标签页 | 同时打开多个文件,未保存文件显示 ● 标记 |
1013
+ | 工作区恢复 | **按项目**记住文件树展开了哪些目录、开了哪些标签(顺序 + 当前激活的那个),刷新页面或切回该项目时自动恢复;快照存在 `~/.zen-gitsync/config.json` 的 `ui.editorWorkspaceByProject`。只记路径 —— 文件按盘上最新内容重开,未保存的改动不跨会话保留,已被删除的文件静默跳过 |
1014
+ | 侧边栏宽度 | 拖拽分隔条调整文件树栏宽度;与工作区快照不同,宽度是**全局**一份(所有项目共用),存在 `~/.zen-gitsync/config.json` 的 `ui.editorSidebarWidth`,刷新后自动恢复,取值夹在 140–400px |
1009
1015
  | Monaco 编辑器 | 支持 JS、TS、Vue、Python、Go、JSON、CSS 等语法高亮 |
1010
1016
  | Markdown 预览 | `.md` 文件可切换源码与渲染预览模式 |
1011
1017
  | HTML 预览 / 浏览器打开 | `.html` / `.htm` 在应用内沙箱 iframe 里渲染;在文件树里右键 → **在浏览器中打开**,改交系统默认浏览器渲染 |
1012
- | 保存 | `Ctrl+S` 手动保存;可选失去焦点时自动保存 |
1018
+ | 保存 | `Ctrl+S` 手动保存;失去焦点时自动保存默认开启(可在设置里关闭) |
1013
1019
  | 新建 | 在文件树中内联创建文件或文件夹 |
1014
1020
  | 重命名 / 删除 | 在树中直接对文件或文件夹重命名、删除 |
1015
1021
  | 侧边栏调整 | 拖拽分隔条自由调整文件树宽度 |
@@ -1031,12 +1037,14 @@ Activity Bar 第四个视图,在 GUI 内直接浏览并编辑项目文件:
1031
1037
  | 任务列表 | 新建、编辑、删除任务;按项目分组(当前项目排在最前),每条任务只占一行 —— 有标题显示标题,没标题则显示描述前若干字符(超出省略);标题和描述都没填的任务视为草稿,切走时直接丢弃、不落盘 |
1032
1038
  | 多项目看板 | 三栏均可拖动 / 可折叠:项目列表(含执行监控)、看板、主 Agent 控制台。拖动分隔条调整宽度(项目列表 180–420 px、控制台 260 px ~ `min(900, 视口 46%)`),双击恢复响应式默认值;两侧也都能收起 —— 顶栏按钮收项目列表,控制台自己的按钮把它收成 32px 收纳条、点一下展开。项目列表下方的执行监控有独立分隔条(120 px ~ 视口高度一半),并行跑多个任务时上下拖动即可加高。所有宽高跨会话记住 |
1033
1039
  | 看板视图 | **待处理 / 进行中 / 已完成** 三列,已完成列按完成时间倒序;顶部勾选可只看「最近一次执行报错」的任务;一键切换成表格视图,信息密度更高。待处理列末尾常驻一个「新建任务」入口 —— 列里已有卡片时它照样在(列空时它还兼任空状态),点开的是和右上角按钮同一个新建弹窗 |
1040
+ | 进行中卡片 | 正在跑的任务不只是右上角一个圆点:卡片上直接带着**执行器的品牌图标**(悬停看是哪个产品)、已运行时长、调用了多少次工具(悬停看类型分布)、最近一次工具调用、最近在思考什么、最新的回复 —— 一段时间没有产出时再补一句静默多久。某一项没有内容就**整行不渲染**(不写"暂无"),没在跑的任务则完全没有这块。这些事实与右栏进度报告同源(都来自执行记录),所以跟随看板 5 秒轮询刷新,别的 `g ui` 实例里跑的任务在这里同样看得到;切到表格视图时同一行摘要压在任务名下方 |
1041
+ | 跑完卡片 | 「已完成」列的卡片不再只有标题和时间:它留下一小段**智能体最后说的话** —— 最近一轮执行的正文尾部,压平成一句话、最多 100 字。这条最有用的时候正是它被做出来的理由:一次执行常常以反问收尾(「要 push 吗?」),标着"已完成"的任务其实在等你回一句话,而以前只有点进编辑器翻日志才知道。**执行器的品牌图标就挂在这段摘录的开头** —— 这句话是"它"说的,图标说的是哪个 CLI 说的,放在这里比放在卡片别处更直接。正在跑的任务仍然走上面那块活动区(两者互斥);那次执行没写正文时这段整块不渲染,不拿上一轮的旧话顶;完整摘录在悬停提示里。表格视图的任务名下方同样有这一行。图标只在**执行器认得出来**时才画:加 `agent` 字段之前跑的记录、或值不在已知执行器里的,一律不画,而不是猜一个品牌显示(表格视图里,正在跑的任务图标在状态摘要行、跑完的在最后回复行) |
1034
1042
  | 主 Agent 控制台 | 两种工作方式,拨片切换、选择记在浏览器里:**对话** —— 直接跟内置 g ai 聊,由它按需把活派给某个项目(可多轮、可先问清再派);**指令** —— 你自己写一句话,落点由主 Agent 判断(或遵从此前选中的项目),Enter 派发、Shift+Enter 换行。两种方式落到**同一条派发链路与同一条指令流水**,差别只在"谁决定派什么"。调度可暂停 / 恢复,暂停期间派发只建任务不执行 |
1035
- | 进度报告 | 「指令」模式下这块报的是**正在跑的任务现在到哪一步了**,而不是"谁派发了、谁完成了"(那些看板本身就能看出来)。主 Agent 会读一遍每个在跑的 job —— 已运行多久、最近调了什么工具、最近一行输出 —— 写一段汇报:每个任务在做什么、走到哪一步、有没有卡住的迹象。报告按你设的间隔自动生成(5 / 10 / 15 / 30 / 60 分钟,也可以关掉),也能随时点「立即报告」;面板上展示最新那份及其依据(项目、已运行时长、最近一次工具调用),更早的从下方历史列表里点开回看。自动报告由**服务端**定时产生,所以标签页关着也照样攒历史,回来就能看到;没有任务在跑时自动报告直接跳过(不记空条目、也不白调一次模型)。汇报按界面语言生成,输出看不出进度时会直说,而不是编一段像模像样的进展 |
1043
+ | 进度报告 | 「指令」模式下这块报的是**正在跑的任务现在到哪一步了**,而不是"谁派发了、谁完成了"(那些看板本身就能看出来)。主 Agent 会读一遍每个在跑的 job —— 已运行多久、**最近在想什么**、**最近 20 次都在调哪类工具**、**已经多久没动静**、最近一行输出 —— 写一段汇报:每个任务在做什么、走到哪一步、有没有卡住的迹象。**思考那一行是关键**:一句正文都不写的任务才是常态(它们的输出从头到尾是空的),只看输出就只能写"看不出来";而"是不是卡住了"现在必须给依据(静默时长、或工具分布是不是在原地打转),不许因为调用次数多就暗示卡住。报告按你设的间隔自动生成(5 / 10 / 15 / 30 / 60 分钟,也可以关掉),也能随时点「立即报告」;面板上展示最新那份及其依据(项目、已运行时长、工具分布、最近思考、最近一次工具调用、静默时长),更早的从下方历史列表里点开回看。自动报告由**服务端**定时产生,所以标签页关着也照样攒历史,回来就能看到;**没有任务在跑就不记条目** —— 自动报告直接跳过,「立即报告」改成回你一句"当前没有正在执行的任务"(都不白调一次模型);间隔内内容一样的两份也不重复落盘,连点两下只留一条。汇报按界面语言生成,输出看不出进度时会直说,而不是编一段像模像样的进展 |
1036
1044
  | 派发执行器 | 派出去的任务由哪个 CLI 跑:**对话模式**在输入框下方选,**指令模式**在派发栏选 —— 两处是同一个组件、同一份选择,与执行按钮的临时切换也共用。注意「引擎」和「执行器」不是一回事:只有**内置 g ai** 能在服务端跑工具循环、才有派发能力,所以对话模式下把引擎切成外部 CLI 时,执行器选择会换成一句「当前引擎只能对话,不能派发任务 —— 切到内置 g ai 才能派活」的说明,而不是让你点了个不会生效的下拉 |
1037
1045
  | 派发默认提示词 | 给每次派发配一段常驻提示词:一条**全局**的(所有项目都附加)+ 每个项目一条(派发到该项目时追加在全局之后,是补充而不是覆盖)。两者都在控制台输入区的齿轮按钮里设置;拼好的正文放在指令**之前**,「立即执行」旁边会多出一个「默认提示词(全局 + 本项目)」勾选,单次派发可以取消勾选,指令流水里也记下这条指令附带的是哪一级。提示词在派发那一刻就抄进任务自己的提示词字段,之后改设置不会回头改写已建任务,单条任务仍可再改 |
1038
1046
  | 项目行打开方式 | 编排台项目列表里 hover 任意一行会出现两个按钮:「打开文件夹」一键进资源管理器,以及「打开方式」菜单 —— 文件管理器 / 终端 / 新标签页跑 `g ui`,以及各编辑器与 AI 工具(VS Code、Codex、OpenCode、Kimi Code、ZCode、DeepSeek Harness,加上默认权限或完全批准的 Claude Code)。没安装的工具会置灰并标「未安装」,点它弹的是顶栏那套安装引导;菜单里的动作只作用于该行项目,不会改变看板选中态 |
1039
- | 任务编辑器浮层 | 点击看板卡片时以浮层打开:左侧是扁平化的任务列表与提示词预置,右侧依次是任务头部、预置下拉、执行器 split 按钮、「执行日志 / 清空执行」动作,以及对话式执行主体。描述默认折叠成一行「任务描述(可选)」摘要,点击展开;已填写描述或挂有附件时摘要右侧显示「已填写」徽标与附件数量 |
1047
+ | 任务编辑器浮层 | 点击看板卡片时以浮层打开:左侧是扁平化的任务列表与提示词预置,右侧依次是任务头部、预置下拉、执行器 split 按钮、「执行日志 / 清空执行」动作,以及对话式执行主体。描述默认折叠成一行「任务描述(可选)」摘要,点击展开;已填写描述或挂有附件时摘要右侧显示「已填写」徽标与附件数量。打开浮层时对话流直接落在**最新一轮**而不是第一轮:正文异步高亮与工具调用折叠会在挂载后继续长高,这段时间里持续贴底,用户自己一滚就立刻撒手 |
1040
1048
  | 执行器选择 | 每个任务可用 **Claude Code** / **OpenCode** / **Codex** 执行。全局默认在 **设置 → 通用设置 → 任务执行器**(`config.taskExecutor`);执行按钮旁的 split 按钮可临时切换下一次执行用的执行器,选择记在浏览器里。续聊固定沿用最初那个执行器 —— Claude 的 `--resume`、OpenCode 的 `--session`、Codex 的 thread id 互不通用 |
1041
1049
  | 附件 | 每个任务最多挂 9 个附件(图片 / PDF / 文本 / Markdown / CSV / JSON / log,单个 ≤ 20 MB);超过 3.5 MB 的图片会先在浏览器里压缩(先按原分辨率转 WebP,压不下去再逐级降采样),4K 屏截图不用再手动裁剪;执行时绝对路径会自动追加到 prompt 末尾,智能体直接按路径读取。**右键图片附件可一键复制到系统剪贴板**(支持 png / jpeg / webp / gif) |
1042
1050
  | 提示词预置 | 可复用提示词模板,支持 `{{task.title}}` / `{{task.desc}}` / `{{repo.path}}` / `{{branch}}` 变量插值 |
@@ -1046,7 +1054,7 @@ Activity Bar 第四个视图,在 GUI 内直接浏览并编辑项目文件:
1046
1054
  | 实时日志 | 「执行日志」面板**默认展开**,方便随时回看上次执行结果;面板内自动滚到底,展示累积的 stdout / stderr(客户端渲染最近 64 KB,服务端单个 job 最多保留 100 MB 输出) |
1047
1055
  | 实时状态 | 任务状态(pending / running / done / error / cancelled)和 PID 通过 SSE 实时推送 |
1048
1056
  | 工具调用流 | 模型的工具调用直接渲染在对话流里,能看清智能体具体做了什么 |
1049
- | 结束提示 | 任务结束或报错时:页面在前台弹应用内提示,页面在后台/别的窗口发系统通知,切到别的视图也不会漏掉结果 |
1057
+ | 结束提示 | 任务结束或报错时响一声提示音(完成 / 出错各一种音色,主动停止不响),同时页面在前台弹应用内提示、页面在后台/别的窗口发系统通知,切到别的视图也不会漏掉结果。音源是 CC0 资源(`public/sounds/`),换音色见同目录 `CREDITS.txt` |
1050
1058
  | 跨视图指示 | 任意任务运行中时,Activity Bar 上的工作台图标会显示脉动小圆点;切换到 Git 或编辑器视图也能看到运行状态 |
1051
1059
  | 执行日志管理(弹窗) | 顶部「执行日志」按钮唤起弹窗:列表 / 过滤 / 批量删除 / 清空 / 保留策略全部可在此一次性管理(默认保留 500 条、256 MB);弹窗关闭后任务执行视图常驻,避免切换时不必要的卸载 |
1052
1060
  | 继续对话 | 任务进入终态(done / error / cancelled)后出现续聊输入框;发送续聊消息会用 `claude --resume <session_id>`(Claude Code)、`--session`(OpenCode)或 `codex exec resume <thread_id>`(Codex)续接上一轮会话,**新一轮 = 新 job**,多轮纵向堆叠成对话流 |
@@ -1091,7 +1099,7 @@ Activity Bar 中的机器人图标视图,可直接在浏览器中与内置 AI
1091
1099
  | Git 全局设置 | `user.name` / `user.email`、自动设置上游、拉取策略、自动清理远程分支、换行符处理、`git init` 默认分支 |
1092
1100
  | 提交设置 | 标准化提交、跳过钩子检查(`--no-verify`)、回车自动提交、Push 完成自动关闭、推送前拉取更新、自动填充默认提交信息 |
1093
1101
  | 编辑配置 | 直接编辑配置 JSON,并可打开系统配置文件 |
1094
- | 编辑器设置 | 编辑器行为,例如失去焦点时自动保存 |
1102
+ | 编辑器设置 | 编辑器行为,例如失去焦点时自动保存(默认开启) |
1095
1103
 
1096
1104
  ---
1097
1105
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zen-gitsync",
3
- "version": "2.17.21",
3
+ "version": "2.17.23",
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",
@@ -28,6 +28,8 @@
28
28
  "verify:data-dir-migration": "node scripts/verify-data-dir-migration.mjs",
29
29
  "verify:config-split": "node scripts/verify-config-split.mjs",
30
30
  "verify:package-files": "node --test test/package-files.test.mjs",
31
+ "verify:release:self-update": "node scripts/verify-release-self-update.mjs",
32
+ "verify:release:self-update:reverse": "node scripts/verify-release-self-update.mjs --reverse",
31
33
  "verify:wb-agent-dispatch": "node scripts/verify-workbench-agent-dispatch.mjs",
32
34
  "verify:wb-oc-modes": "node scripts/verify-wb-oc-agent-chat.cjs",
33
35
  "verify:wb-oc-modes:reverse": "node scripts/verify-wb-oc-agent-chat-reverse.cjs",
@@ -38,12 +40,19 @@
38
40
  "verify:wb-live-jobs": "node scripts/verify-wb-live-jobs.mjs",
39
41
  "verify:wb-overview": "node scripts/verify-workbench-overview.cjs",
40
42
  "verify:wb-progress-report": "node scripts/verify-wb-progress-report.cjs",
43
+ "verify:wb-card-live": "node scripts/verify-wb-card-live.cjs",
44
+ "verify:wb-card-reply": "node scripts/verify-wb-card-reply.cjs",
45
+ "verify:wb-card-executor-icon": "node scripts/verify-wb-card-executor-icon.cjs",
46
+ "verify:wb-task-sound": "node scripts/verify-wb-task-sound.cjs",
41
47
  "verify:wb-proj-branch": "node scripts/verify-workbench-proj-branch.cjs",
42
48
  "verify:wb-proj-filter": "node scripts/verify-wb-proj-filter.cjs",
43
49
  "verify:wb-panel-resize": "node scripts/verify-wb-panel-resize.cjs",
44
50
  "verify:wb-toolcalls": "node scripts/verify-workbench-toolcalls.mjs",
45
51
  "verify:wb-responsive": "node scripts/verify-wb-responsive.cjs",
46
52
  "verify:wb-editor-persist": "node scripts/verify-workbench-editor-persist.cjs",
53
+ "verify:wb-open-scroll-bottom": "node scripts/verify-workbench-open-scroll-bottom.cjs",
54
+ "verify:editor-preview-scroll": "node scripts/verify-editor-preview-scroll.cjs",
55
+ "verify:editor-sidebar-width": "node scripts/verify-editor-sidebar-width.cjs",
47
56
  "verify:wb-copy-folder-name": "node scripts/verify-copy-folder-name.cjs",
48
57
  "verify:repo-list": "node scripts/verify-repo-list.cjs",
49
58
  "e2e": "cd src/ui/client && npx playwright test",
@@ -27,14 +27,20 @@
27
27
  * npm run release -- --keep-instances # 装全局前不停掉运行中的 UI 实例(默认会停)
28
28
  * npm run release -- --skip-push # 只发布到 npm,不 push git
29
29
  * npm run release -- --dry-run # 只打印计划,不真正改 package.json / commit / publish
30
- * npm run release -- --poll-interval=20 --poll-timeout=600 # 调自更新重试节奏(秒)
30
+ * npm run release -- --poll-interval=20 # 前段轮询间隔(秒,默认 15)
31
+ * npm run release -- --poll-timeout=3600 # 窗口上限(秒,默认 1800 = 30 分钟)
32
+ * npm run release -- --poll-fast-window=600 --poll-slow-interval=60 # 600s 后退避到 60s 一跳
31
33
  * npm run release -- --install-timeout=900 # 单次 npm install 上限(秒),0 = 不限时(默认)
32
34
  *
33
- * 发布后自更新:每轮先探 tarball 能不能取,能取到才调 npm;两条路(`pkg@<版本>` /
34
- * tarball URL 直连)都试,装上后校验全局版本,失败把原因打出来。
35
+ * 发布后自更新:每轮看两个就绪信号(packument 里有没有这个版本 / tarball 能不能取),
36
+ * 任一为真就真调 npm,并两条路(`pkg@<版本>` / tarball URL 直连)都试,装上后校验全局版本;
37
+ * 探针说"取不到"时也每 4 轮强制真试一次,窗口用尽前还会补最后一次,失败把原因打出来。
35
38
  * 原因见 tarballUrl() / selfUpdateGlobal() 处注释 —— publish 成功不等于
36
39
  * packument 立即可见,也不等于 tarball 立即可取(两种先后顺序都实测出现过)。
37
40
  *
41
+ * "这一轮要不要真调 npm"的判定抽在 src/utils/selfUpdatePolicy.js(有单测);
42
+ * 动那里的阈值/分支请连带跑 `node --test src/utils/selfUpdatePolicy.test.js`。
43
+ *
38
44
  * 自更新前置步骤:先把运行中的 UI 实例停掉(见 stopRunningInstances 处注释)——
39
45
  * Windows 上全局包目录被实例占着时,npm 删旧目录会 EPERM,装不上或装出半成品。
40
46
  */
@@ -48,6 +54,13 @@ import { execSync, spawn } from 'node:child_process'
48
54
  import chalk from 'chalk'
49
55
  import readline from 'node:readline/promises'
50
56
  import { createInstanceRegistry, getRegistryPath } from '../src/ui/server/utils/instanceRegistry.js'
57
+ import {
58
+ shouldAttemptInstall,
59
+ shouldFinalAttempt,
60
+ nextPollIntervalMs,
61
+ classifyInstallError,
62
+ FORCE_INSTALL_EVERY,
63
+ } from '../src/utils/selfUpdatePolicy.js'
51
64
 
52
65
  const __filename = fileURLToPath(import.meta.url)
53
66
  const __dirname = path.dirname(__filename)
@@ -93,13 +106,38 @@ function readNumberArg(name, fallback) {
93
106
  // 于是 600s 眼看要耗尽、终端上 25 轮只有"→ npm install"没有任何原因。
94
107
  // 对策两条:① 每轮先自己探一次 tarball(isTarballFetchable,GET 而非 HEAD),
95
108
  // 取不到就跳过这一轮的 npm 调用并把状态打出来;② 失败原因压一行打出来。
109
+ //
110
+ // 又实测(2026-09-29, v2.17.21):上面①被写成了**硬闸门**(`if (probe.ok) { 真装 }`),
111
+ // 于是一旦探针连着误报,整个窗口就只剩"等待":那次 35 轮全是
112
+ // `tarball 还取不到 HTTP 404`,`npm install` **一次都没被执行**;
113
+ // 脚本刚放弃,手动 `npm install -g zen-gitsync@2.17.21` 16s 就装上了。
114
+ // 事后查那个 tarball 的 CDN 响应头:`Last-Modified` = 发布 +2s、`Age` ≈ 600s、
115
+ // `CF-Cache-Status: HIT` —— 对象在发布后 2 秒就进了缓存,探针嘴里的 404 与事实不符。
116
+ // 教训:**探针只是省一次调用的优化,不是判据**;判据只有 npm 自己。
117
+ // 对策(判定逻辑见 src/utils/selfUpdatePolicy.js):
118
+ // ① 探针降级为优化:两个就绪信号任一为真就真调 npm;
119
+ // ② 探针明确 404 时每 FORCE_INSTALL_EVERY 轮强制真试一次;
120
+ // ③ 窗口用尽时若整轮没真装过,再补最后一次真试(shouldFinalAttempt);
121
+ // ④ 失败摘要报"试了几轮 / 真装了几次 / 失败构成(E404 ×N、EPERM ×N)",并区分
122
+ // "探针误报"与"窗口不够长"两种情况(两者要采取的动作完全相反)。
123
+ //
96
124
  // 单次 npm 调用不再硬性截断(见 INSTALL_TIMEOUT_MS):曾经给 180s 是为了"快速失败、
97
125
  // 把重试交给外层循环",但实测更常见的是 180s 不够 —— 每轮装到一半被杀、下轮从零重来,
98
126
  // 进度永远清零(2026-09-24 v2.17.14 卡了 27 轮)。要收紧可用 `--install-timeout=<秒>`。
99
127
  //
100
- // 默认 15s 一轮、上限 600s;可用 `--poll-interval=<秒>` / `--poll-timeout=<秒>` 调。
128
+ // 窗口长度(v2.17.21 之后重定):原来上限写死 600s,而"registry 元数据/对象就绪"
129
+ // 这件事实测**能超过 600s**(那次 35 轮全在等,用户随后手动装才成),所以上限提到
130
+ // **1800s(30 分钟)**;但这不能变成"30 分钟里 15s 一跳跳 120 轮",于是前 600s 保持
131
+ // 15s 的灵敏度,之后退避到 60s 慢炖(见 nextPollIntervalMs)。总轮数封顶 60 左右。
132
+ // 三个口子:`--poll-fast-window`(前段长度)/ `--poll-slow-interval`(后段间隔)/ `--poll-timeout`。
133
+ // 间隔仍可用 `--poll-interval=<秒>`(前段)调。
134
+ //
135
+ // 注意窗口加长**不解耦**另一件事:轮询期间必须真的调 npm(见 selfUpdatePolicy),
136
+ // 否则窗口再长也只是更长地等 —— v2.17.21 的 35 轮就是零真试。
101
137
  const POLL_INTERVAL_MS = readNumberArg('--poll-interval', 15) * 1000
102
- const POLL_TIMEOUT_MS = readNumberArg('--poll-timeout', 600) * 1000
138
+ const POLL_TIMEOUT_MS = readNumberArg('--poll-timeout', 1800) * 1000
139
+ const POLL_FAST_WINDOW_MS = readNumberArg('--poll-fast-window', 600) * 1000
140
+ const POLL_SLOW_INTERVAL_MS = readNumberArg('--poll-slow-interval', 60) * 1000
103
141
 
104
142
  // 发布物 tarball 的地址。
105
143
  //
@@ -576,18 +614,23 @@ async function commitChanges(version) {
576
614
  }
577
615
  }
578
616
 
579
- // 查询 registry 上 dist-tags.latest。查询失败(网络抖动 / registry 波动)返回 null,
580
- // 不视为致命错误 —— 交给轮询循环重试。
581
- function readLatestDistTag() {
617
+ // 问 registry 的 packument:这个**版本**在不在里面。
618
+ // 查询失败(还没同步 / 网络抖动)返回 false —— 不是致命错误,交给轮询循环重试。
619
+ //
620
+ // 为什么问版本而不是问 `dist-tags.latest`(v2.17.6 之前的写法):
621
+ // dist-tags 与 versions 同在一份 packument 里,但 dist-tags.latest 翻牌**晚于**
622
+ // 版本本身可用 —— 只盯 latest 会把"其实已经能装"的几分钟当成"还没就绪"。
623
+ // 而这条判据问的正是 `npm install -g pkg@<ver>` 要解析的东西,预言性更强。
624
+ function readPackumentHasVersion(version) {
582
625
  try {
583
626
  const out = execSync(
584
- `npm view ${PKG_NAME} dist-tags.latest --json --registry=${NPM_REGISTRY} --prefer-online`,
627
+ `npm view ${PKG_NAME}@${version} version --json --registry=${NPM_REGISTRY} --prefer-online`,
585
628
  { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 30000 }
586
629
  )
587
630
  const parsed = JSON.parse(out.trim())
588
- return typeof parsed === 'string' ? parsed.replace(/^v/, '') : null
631
+ return (typeof parsed === 'string' ? parsed.replace(/^v/, '') : '') === version
589
632
  } catch {
590
- return null
633
+ return false
591
634
  }
592
635
  }
593
636
 
@@ -820,20 +863,28 @@ async function forceRemoveGlobalPackage() {
820
863
  // 等待阶段白等 600s,超时后再装照样 ETARGET,等于什么都没解决。
821
864
  //
822
865
  // 所以顺序反过来:先"尝试装",装不上才等。每轮:
823
- // ① 先探 tarball 能不能取(isTarballFetchable)—— 取不到就别浪费一次 npm 调用,
824
- // 并且把"还取不到 HTTP 404"如实打出来:这是"到底卡在哪"的唯一线索;
825
- // ② 取得到就按两条路试,任一条装上且校验通过即结束:
826
- // `pkg@<version>`(packument 已同步时的常规路径)/ tarball URL 直连;
866
+ // ① 取两个**就绪信号**:packument 里有没有这个版本(`npm view pkg@ver`)、
867
+ // tarball 能不能取(isTarballFetchable)。任一为真 → 真调 npm ——
868
+ // 两个信号各对应一条能绕开对方缓存的安装路径(`pkg@ver` / tarball URL 直连);
869
+ // ② 两个都不为真 → 这一轮跳过 npm 调用(省下注定失败的一次),把状态打出来。
870
+ // 但**跳过只是优化,不是判据**:探针明确 404 时每 FORCE_INSTALL_EVERY 轮强制真试,
871
+ // 窗口用尽前还会补最后一次(shouldFinalAttempt)—— v2.17.21 那次就是反例:
872
+ // 探针 35 轮全 404 → 一次 npm 都没调过 → 600s 全白等,而手动装 16s 就成;
827
873
  // ③ 失败原因压成一行打出来(summarizeInstallError)。**别再默默重试**——
828
874
  // v2.17.6 那次用户看到的就是"一直在执行安装命令"、25 轮里一句原因都没有,
829
875
  // 而真实原因是那几分钟 tarball 还没对外可取(见 tarballUrl 注释)。
830
876
  async function selfUpdateGlobal(version) {
831
877
  console.log(chalk.blue('\n=== 发布后自更新全局版本 ==='))
832
878
  console.log(chalk.gray('publish 成功 ≠ packument 立即可见、也 ≠ tarball 立即可取(两者先后顺序随机)。'))
833
- console.log(chalk.gray('不空等 dist-tags:每轮先探 tarball,能取到才调 npm,失败会打印原因。'))
879
+ console.log(chalk.gray('每轮取两个就绪信号(packument 里有没有该版本 / tarball 能不能取),任一为真就真调 npm。'))
834
880
  console.log(chalk.gray(
835
- `间隔 ${POLL_INTERVAL_MS / 1000}s,上限 ${POLL_TIMEOUT_MS / 1000}s`
836
- + '(可用 --poll-interval / --poll-timeout 调);'
881
+ `探针说"取不到"时也每 ${FORCE_INSTALL_EVERY} 轮强制真试一次,窗口用尽前还会补最后一次`
882
+ + ' —— 探针只是"省一次调用"的优化,它也会误报。'
883
+ ))
884
+ console.log(chalk.gray(
885
+ `间隔 ${POLL_INTERVAL_MS / 1000}s(前 ${POLL_FAST_WINDOW_MS / 1000}s)→`
886
+ + ` ${POLL_SLOW_INTERVAL_MS / 1000}s(之后退避),窗口上限 ${POLL_TIMEOUT_MS / 1000}s`
887
+ + '(可用 --poll-interval / --poll-fast-window / --poll-slow-interval / --poll-timeout 调);'
837
888
  + `单次安装${INSTALL_TIMEOUT_MS > 0 ? `上限 ${INSTALL_TIMEOUT_MS / 1000}s` : '不限时'}`
838
889
  + '(可用 --install-timeout=<秒> 调,0 = 不限时)。'
839
890
  ))
@@ -842,89 +893,170 @@ async function selfUpdateGlobal(version) {
842
893
  const deadline = startedAt + POLL_TIMEOUT_MS
843
894
  let attempt = 0
844
895
  let lastOutput = ''
896
+ let installAttempts = 0 // 真调过几次 npm install(和"只是探了一下"分开记)
897
+ let skippedRounds = 0 // 探针说取不到 → 整轮跳过 npm 调用的次数
898
+ let lastRoundAttempted = false
899
+ let slowPhase = false // 是否已跨过前段窗口(退避提示只打一次)
900
+ const errorTally = new Map() // 失败短码 → 次数,放弃时汇总"到底卡在哪"
845
901
  // 实例只在"第一次真要装"之前停一次,不在进循环时就停:前面的探测阶段可能还要等好几分钟
846
902
  // (tarball 还没对外可取),提前停等于白白占掉用户几分钟的 UI。
847
903
  let stopDone = false
848
904
 
905
+ // 真要装之前,先把运行中的实例停掉:接下来就是 npm 删旧目录 →
906
+ // 目录被实例占着会 EPERM(见 stopRunningInstances 处注释)。只做一次。
907
+ const ensureInstancesStopped = async () => {
908
+ if (stopDone) return
909
+ stopDone = true
910
+ if (KEEP_INSTANCES) {
911
+ console.log(chalk.yellow('--keep-instances: 不停运行中的实例,若安装报 EPERM / 文件占用属预期'))
912
+ } else {
913
+ await stopRunningInstances()
914
+ }
915
+ }
916
+
917
+ // 一轮"真装":两条 spec 依次试,任一条装上且全局版本校验通过即返回 { ok: true }。
918
+ // preferPackument=true 时先走常规精确版本;否则 tarball 优先,争取一轮命中。
919
+ const runInstallRound = async (preferPackument, label = `第 ${attempt} 轮`) => {
920
+ installAttempts += 1
921
+ await ensureInstancesStopped()
922
+ const targets = preferPackument
923
+ ? [`${PKG_NAME}@${version}`, tarballUrl(version, attempt > 1)]
924
+ : [tarballUrl(version, attempt > 1), `${PKG_NAME}@${version}`]
925
+
926
+ for (const spec of targets) {
927
+ const res = tryInstallGlobal(spec)
928
+ if (!res.ok) {
929
+ lastOutput = res.output
930
+ const code = classifyInstallError(res.output)
931
+ errorTally.set(code, (errorTally.get(code) || 0) + 1)
932
+ console.log(chalk.yellow(` ✗ [${code}] ${summarizeInstallError(res.output)}`))
933
+ // 文件占用和"registry 还没同步"是两类失败:前者要再杀一轮 + 强删旧目录,
934
+ // 后者只能等。混在一起会让前者白等满整个窗口。日志里的 [短码] 就是分类结果。
935
+ if (looksLikeFileLock(res.output)) {
936
+ console.log(chalk.yellow(' 检测到文件占用(Windows 常见):再停一轮实例 + 强制清旧全局目录'))
937
+ if (!KEEP_INSTANCES) await stopRunningInstances({ quiet: true })
938
+ await forceRemoveGlobalPackage()
939
+ }
940
+ continue
941
+ }
942
+ const installed = readGlobalInstalledVersion()
943
+ if (installed === version) {
944
+ const cost = ((Date.now() - startedAt) / 1000).toFixed(1)
945
+ console.log(chalk.green(
946
+ `全局已更新到 ${PKG_NAME}@${version}`
947
+ + `(${label},第 ${installAttempts} 次真装,耗时 ${cost}s)`
948
+ ))
949
+ return { ok: true }
950
+ }
951
+ // 退出码 0 但版本不对(极少见):按失败处理,继续下一轮
952
+ lastOutput = `安装命令退出码 0,但全局版本读到 ${installed ?? '未知'}`
953
+ console.log(chalk.yellow(` 安装命令成功,但全局版本是 ${installed ?? '未知'},继续重试`))
954
+ }
955
+ return { ok: false, output: lastOutput }
956
+ }
957
+
849
958
  for (;;) {
850
959
  attempt += 1
851
- const packumentSynced = readLatestDistTag() === version
960
+ const packumentHasVersion = readPackumentHasVersion(version)
852
961
  // 第 2 轮起给 tarball 地址加时间戳,绕开 CDN 可能挂着的"这条路径 404"负缓存
853
962
  const bust = attempt > 1
854
- const tarball = tarballUrl(version, bust)
855
963
  const probe = await isTarballFetchable(version, bust)
964
+ const plan = shouldAttemptInstall({ attempt, probe, packumentHasVersion })
965
+ lastRoundAttempted = plan.attempt
856
966
 
857
967
  console.log(chalk.gray(
858
- `第 ${attempt} 次尝试(dist-tags.latest${packumentSynced ? '已同步' : '仍是旧版本'},`
859
- + ` tarball ${probe.ok ? '已可取' : `还取不到${probe.status ? ` HTTP ${probe.status}` : ''}`})...`
968
+ `第 ${attempt} 轮(packument ${packumentHasVersion ? '已有该版本' : '还没有该版本'},`
969
+ + ` tarball ${probe.ok ? '已可取' : `还取不到${probe.status ? ` HTTP ${probe.status}` : ''}`}`
970
+ + `) → ${plan.attempt ? `真试安装(${plan.reason})` : '本轮跳过 npm 调用'}`
860
971
  ))
861
972
 
862
- if (probe.ok) {
863
- // 真要装之前,先把运行中的实例停掉:tarball 已可取,接下来就是 npm 删旧目录 →
864
- // 目录被实例占着会 EPERM(见 stopRunningInstances 处注释)。只做一次。
865
- if (!stopDone) {
866
- stopDone = true
867
- if (KEEP_INSTANCES) {
868
- console.log(chalk.yellow('--keep-instances: 不停运行中的实例,若安装报 EPERM / 文件占用属预期'))
869
- } else {
870
- await stopRunningInstances()
871
- }
872
- }
873
-
874
- // packument 已同步 → 先走常规精确版本;还没同步 → tarball 优先,争取一轮命中
875
- const targets = packumentSynced
876
- ? [`${PKG_NAME}@${version}`, tarball]
877
- : [tarball, `${PKG_NAME}@${version}`]
878
-
879
- for (const spec of targets) {
880
- const res = tryInstallGlobal(spec)
881
- if (!res.ok) {
882
- lastOutput = res.output
883
- console.log(chalk.yellow(` ✗ ${summarizeInstallError(res.output)}`))
884
- // 文件占用和"registry 还没同步"是两类失败:前者要再杀一轮 + 强删旧目录,
885
- // 后者只能等。混在一起会让前者白等满 600s。
886
- if (looksLikeFileLock(res.output)) {
887
- console.log(chalk.yellow(' 检测到文件占用(Windows 常见):再停一轮实例 + 强制清旧全局目录'))
888
- if (!KEEP_INSTANCES) await stopRunningInstances({ quiet: true })
889
- await forceRemoveGlobalPackage()
890
- }
891
- continue
892
- }
893
- const installed = readGlobalInstalledVersion()
894
- if (installed === version) {
895
- const cost = ((Date.now() - startedAt) / 1000).toFixed(1)
896
- console.log(chalk.green(
897
- `全局已更新到 ${PKG_NAME}@${version}(第 ${attempt} 次尝试,耗时 ${cost}s)`
898
- ))
899
- return
900
- }
901
- // 退出码 0 但版本不对(极少见):按失败处理,继续下一轮
902
- lastOutput = `安装命令退出码 0,但全局版本读到 ${installed ?? '未知'}`
903
- console.log(chalk.yellow(` 安装命令成功,但全局版本是 ${installed ?? '未知'},继续重试`))
973
+ if (plan.attempt) {
974
+ const res = await runInstallRound(packumentHasVersion)
975
+ if (res.ok) return
976
+ } else {
977
+ skippedRounds += 1
978
+ if (skippedRounds === FORCE_INSTALL_EVERY - 1) {
979
+ console.log(chalk.yellow(
980
+ ` · 已连续 ${skippedRounds} 轮跳过 npm 调用(探针都说 tarball 取不到)。`
981
+ + '跳过只是为了省一次注定失败的调用,**不是判据** —— 探针本身会误报'
982
+ + '(v2.17.21 实测:探针 600s 全 404,手动 npm install 只花 16s),'
983
+ + `所以每 ${FORCE_INSTALL_EVERY} 轮会强制真试一次,窗口用尽前还会补最后一次。`
984
+ ))
904
985
  }
905
986
  }
906
987
 
907
- const remain = deadline - Date.now()
908
- if (remain <= 0) {
909
- console.error(chalk.red(`已尝试 ${attempt} 次,仍未装上 ${PKG_NAME}@${version}`))
910
- if (lastOutput) console.error(chalk.gray(lastOutput))
911
- if (looksLikeFileLock(lastOutput)) {
912
- console.error(chalk.yellow(
913
- '看起来是文件被占(不是 registry 滞后):关掉所有 UI 实例 / 编辑器后重试,\n'
914
- + '必要时手动删掉全局包目录再装(注意会短暂失去全局命令)。'
915
- ))
916
- }
917
- console.error(chalk.gray(
918
- '可稍后手动重试:\n'
919
- + ` npm install -g ${PKG_NAME}@${version}\n`
920
- + '若报 ETARGET / E404(registry 元数据与 tarball 还没对齐),直连 tarball:\n'
921
- + ` npm install -g ${tarballUrl(version)}`
988
+ const elapsed = Date.now() - startedAt
989
+ // 跨过前段窗口时提示一次:窗口比原来长了一倍多,得让用户知道"还在等什么、怎么退出"
990
+ if (!slowPhase && elapsed >= POLL_FAST_WINDOW_MS) {
991
+ slowPhase = true
992
+ console.log(chalk.yellow(
993
+ `已超过原窗口上限(${POLL_FAST_WINDOW_MS / 1000}s),继续等但退避到 `
994
+ + `${POLL_SLOW_INTERVAL_MS / 1000}s 一跳,最长到 ${POLL_TIMEOUT_MS / 1000}s。\n`
995
+ + ' 不想等可以 Ctrl-C,之后手动装(发布/推送/打标签都已经完成了,只剩装全局):\n'
996
+ + ` npm install -g ${PKG_NAME}@${version}`
922
997
  ))
923
- return
924
998
  }
925
- console.log(chalk.gray(` ${Math.ceil(remain / 1000)}s 后重试`))
926
- await sleep(Math.min(POLL_INTERVAL_MS, remain))
999
+
1000
+ const remain = deadline - Date.now()
1001
+ if (remain <= 0) break
1002
+ const interval = nextPollIntervalMs({
1003
+ elapsedMs: elapsed,
1004
+ fastWindowMs: POLL_FAST_WINDOW_MS,
1005
+ fastIntervalMs: POLL_INTERVAL_MS,
1006
+ slowIntervalMs: POLL_SLOW_INTERVAL_MS,
1007
+ })
1008
+ console.log(chalk.gray(` ${Math.ceil(remain / 1000)}s 后重试(本轮间隔 ${interval / 1000}s)`))
1009
+ await sleep(Math.min(interval, remain))
1010
+ }
1011
+
1012
+ // 窗口用尽。**别就这样放弃**:整轮可能都在"跳过 npm 调用"(探针误报时就是这个形态),
1013
+ // 那就等于一次事实判据都没取到 —— 补最后一次真装。v2.17.21 那次只要补这一下就结束了。
1014
+ if (shouldFinalAttempt({ installAttempts, lastRoundAttempted })) {
1015
+ console.log(chalk.yellow(
1016
+ `\n${POLL_TIMEOUT_MS / 1000}s 窗口用尽,且最后一轮没真装过 → 补最后一次真装`
1017
+ + '(探针可能误报,事实判据只有 npm 自己)。'
1018
+ ))
1019
+ const res = await runInstallRound(readPackumentHasVersion(version), '超时前补的真装')
1020
+ if (res.ok) return
1021
+ }
1022
+
1023
+ console.error(chalk.red(
1024
+ `已试 ${attempt} 轮(其中真调 npm ${installAttempts} 次),仍未装上 ${PKG_NAME}@${version}`
1025
+ ))
1026
+ if (errorTally.size > 0) {
1027
+ // 汇总失败构成:E404 / ETARGET = registry 还没就绪(**只能等**);
1028
+ // EPERM = 文件被占(要动手杀实例 + 清目录)。两类混在一起时人肉看日志分不出来。
1029
+ const parts = [...errorTally.entries()].sort((a, b) => b[1] - a[1]).map(([k, n]) => `${k} ×${n}`)
1030
+ console.error(chalk.yellow(`失败构成:${parts.join(' / ')}`))
1031
+ }
1032
+ if (installAttempts > 0 && !errorTally.has('EPERM')) {
1033
+ // 真调过 npm、失败原因又都是"还没就绪" → 这就是"registry 比窗口还慢",不是探针的事。
1034
+ console.error(chalk.yellow(
1035
+ `真调 npm 的 ${installAttempts} 次里没有一次是文件占用,失败原因都是"还没就绪" —— `
1036
+ + `说明是 registry 确实慢,窗口(${POLL_TIMEOUT_MS / 1000}s)不够长。下次可放宽:\n`
1037
+ + ` npm run release -- --poll-timeout=${Math.max((POLL_TIMEOUT_MS / 1000) * 2, 3600)}`
1038
+ ))
1039
+ }
1040
+ if (skippedRounds > 0) {
1041
+ console.error(chalk.yellow(
1042
+ `${skippedRounds} 轮因探针报"取不到"跳过了 npm 调用。判断这次是哪种:`
1043
+ + '手动装**立刻**就成 → 探针误报(窗口其实早就够);'
1044
+ + '手动装同样报 404 / ETARGET → registry 真的慢,是窗口不够长。'
1045
+ ))
927
1046
  }
1047
+ if (lastOutput) console.error(chalk.gray(lastOutput))
1048
+ if (looksLikeFileLock(lastOutput)) {
1049
+ console.error(chalk.yellow(
1050
+ '看起来是文件被占(不是 registry 滞后):关掉所有 UI 实例 / 编辑器后重试,\n'
1051
+ + '必要时手动删掉全局包目录再装(注意会短暂失去全局命令)。'
1052
+ ))
1053
+ }
1054
+ console.error(chalk.gray(
1055
+ '可稍后手动重试:\n'
1056
+ + ` npm install -g ${PKG_NAME}@${version}\n`
1057
+ + '若报 ETARGET / E404(registry 元数据与 tarball 还没对齐),直连 tarball:\n'
1058
+ + ` npm install -g ${tarballUrl(version)}`
1059
+ ))
928
1060
  }
929
1061
 
930
1062
  // 发布到 NPM
@@ -1 +1 @@
1
- import"./rolldown-runtime-C0FnF6B9.js";import{$ as e,C as t,Cn as n,Dn as r,En as i,Er as a,Jn as o,K as s,Kn as c,Mn as l,Pn as u,Pr as d,Qn as f,Qr as p,Rn as m,S as h,Tn as g,Un as _,Xn as v,br as y,cr as b,pr as x,q as S,qr as C,w,wn as T,yn as E}from"./element-plus-Bx3j6vzG.js";import{n as D,t as O}from"./_plugin-vue_export-helper-N8Vc5eEs.js";import{t as k}from"./SvgIcon-D0hK-p1U.js";import{t as A}from"./toolsStore-B49mCaQ_.js";import{a as j,m as M}from"./index-BHYAwndE.js";import{c as N,d as P,l as F,u as I}from"./agentConversations-Da7zLPfP.js";m(),x();function L(e,t=680){let r=y(0),i=null;function a(){let t=e.value;return t?(r.value=t.clientWidth,typeof ResizeObserver>`u`||(i=new ResizeObserver(e=>{let t=e[0]?.contentRect?.width;typeof t==`number`&&(r.value=t)}),i.observe(t),!0)):!1}return o(()=>{a()||_(()=>a())}),c(()=>{i?.disconnect(),i=null}),{width:r,narrow:n(()=>r.value>0&&r.value<t)}}m(),x(),d();var R={class:`agent-engine`},z=[`disabled`,`title`,`aria-label`],B={class:`agent-engine__name`},V={class:`agent-engine__item`},H={class:`agent-engine__item-name`},U={key:3,class:`agent-engine__item-missing`},W=O(u({__name:`AgentEngineSelector`,props:{engine:{},locked:{type:Boolean}},emits:[`select`],setup(o,{emit:c}){let u=o,d=c,m=A(),_=n(()=>I(u.engine)),x=n(()=>({gai:!0,claude:m.claudeAvailable,opencode:m.opencodeAvailable,codex:m.codexAvailable})),O=y(!1),L=y(null);function W(e){if(!u.locked){if(!x.value[e]){if(!P(e))return;L.value=e,O.value=!0;return}d(`select`,e)}}return(n,c)=>(v(),r(`div`,R,[l(a(h),{trigger:`click`,onCommand:W},{dropdown:b(()=>[l(a(w),null,{default:b(()=>[(v(!0),r(E,null,f(a(N),n=>(v(),g(a(t),{key:n.id,command:n.id},{default:b(()=>[T(`span`,V,[a(P)(n.id)?(v(),g(j,{key:0,executor:n.id,class:`agent-engine__item-icon`},null,8,[`executor`])):(v(),g(k,{key:1,"icon-class":a(F),class:`agent-engine__item-icon`},null,8,[`icon-class`])),T(`span`,H,p(n.name),1),o.engine===n.id?(v(),g(a(s),{key:2,class:`agent-engine__item-check`},{default:b(()=>[l(a(e))]),_:1})):x.value[n.id]?i(``,!0):(v(),r(`span`,U,p(a(D)(`@42BB9:未安装`)),1))])]),_:2},1032,[`command`]))),128))]),_:1})]),default:b(()=>[T(`button`,{type:`button`,class:C([`agent-engine__btn`,{"is-locked":o.locked}]),disabled:o.locked,title:o.locked?a(D)(`@AGENT:本会话的引擎已锁定;要换引擎请新建会话`):a(D)(`@AGENT:新建会话使用的引擎`),"aria-label":a(D)(`@AGENT:智能体引擎`)},[a(P)(o.engine)?(v(),g(j,{key:0,executor:o.engine,class:`agent-engine__icon`},null,8,[`executor`])):(v(),g(k,{key:1,"icon-class":a(F),class:`agent-engine__icon`},null,8,[`icon-class`])),T(`span`,B,p(_.value),1),l(a(s),{class:`agent-engine__caret`},{default:b(()=>[l(a(S))]),_:1})],10,z)]),_:1}),l(M,{modelValue:O.value,"onUpdate:modelValue":c[0]||=e=>O.value=e,tool:L.value},null,8,[`modelValue`,`tool`])]))}}),[[`__scopeId`,`data-v-81dfa007`]]);export{L as n,W as t};
1
+ import"./rolldown-runtime-C0FnF6B9.js";import{$ as e,C as t,Cn as n,Dn as r,En as i,Er as a,Jn as o,K as s,Kn as c,Mn as l,Pn as u,Pr as d,Qn as f,Qr as p,Rn as m,S as h,Tn as g,Un as _,Xn as v,br as y,cr as b,pr as x,q as S,qr as C,w,wn as T,yn as E}from"./element-plus-Bx3j6vzG.js";import{n as D,t as O}from"./_plugin-vue_export-helper-C_bQVzeb.js";import{t as k}from"./SvgIcon-xtz4vvc9.js";import{t as A}from"./toolsStore-B49mCaQ_.js";import{a as j,m as M}from"./index-CFX8sK4w.js";import{c as N,d as P,l as F,u as I}from"./agentConversations-BUPj8vzH.js";m(),x();function L(e,t=680){let r=y(0),i=null;function a(){let t=e.value;return t?(r.value=t.clientWidth,typeof ResizeObserver>`u`||(i=new ResizeObserver(e=>{let t=e[0]?.contentRect?.width;typeof t==`number`&&(r.value=t)}),i.observe(t),!0)):!1}return o(()=>{a()||_(()=>a())}),c(()=>{i?.disconnect(),i=null}),{width:r,narrow:n(()=>r.value>0&&r.value<t)}}m(),x(),d();var R={class:`agent-engine`},z=[`disabled`,`title`,`aria-label`],B={class:`agent-engine__name`},V={class:`agent-engine__item`},H={class:`agent-engine__item-name`},U={key:3,class:`agent-engine__item-missing`},W=O(u({__name:`AgentEngineSelector`,props:{engine:{},locked:{type:Boolean}},emits:[`select`],setup(o,{emit:c}){let u=o,d=c,m=A(),_=n(()=>I(u.engine)),x=n(()=>({gai:!0,claude:m.claudeAvailable,opencode:m.opencodeAvailable,codex:m.codexAvailable})),O=y(!1),L=y(null);function W(e){if(!u.locked){if(!x.value[e]){if(!P(e))return;L.value=e,O.value=!0;return}d(`select`,e)}}return(n,c)=>(v(),r(`div`,R,[l(a(h),{trigger:`click`,onCommand:W},{dropdown:b(()=>[l(a(w),null,{default:b(()=>[(v(!0),r(E,null,f(a(N),n=>(v(),g(a(t),{key:n.id,command:n.id},{default:b(()=>[T(`span`,V,[a(P)(n.id)?(v(),g(j,{key:0,executor:n.id,class:`agent-engine__item-icon`},null,8,[`executor`])):(v(),g(k,{key:1,"icon-class":a(F),class:`agent-engine__item-icon`},null,8,[`icon-class`])),T(`span`,H,p(n.name),1),o.engine===n.id?(v(),g(a(s),{key:2,class:`agent-engine__item-check`},{default:b(()=>[l(a(e))]),_:1})):x.value[n.id]?i(``,!0):(v(),r(`span`,U,p(a(D)(`@42BB9:未安装`)),1))])]),_:2},1032,[`command`]))),128))]),_:1})]),default:b(()=>[T(`button`,{type:`button`,class:C([`agent-engine__btn`,{"is-locked":o.locked}]),disabled:o.locked,title:o.locked?a(D)(`@AGENT:本会话的引擎已锁定;要换引擎请新建会话`):a(D)(`@AGENT:新建会话使用的引擎`),"aria-label":a(D)(`@AGENT:智能体引擎`)},[a(P)(o.engine)?(v(),g(j,{key:0,executor:o.engine,class:`agent-engine__icon`},null,8,[`executor`])):(v(),g(k,{key:1,"icon-class":a(F),class:`agent-engine__icon`},null,8,[`icon-class`])),T(`span`,B,p(_.value),1),l(a(s),{class:`agent-engine__caret`},{default:b(()=>[l(a(S))]),_:1})],10,z)]),_:1}),l(M,{modelValue:O.value,"onUpdate:modelValue":c[0]||=e=>O.value=e,tool:L.value},null,8,[`modelValue`,`tool`])]))}}),[[`__scopeId`,`data-v-81dfa007`]]);export{L as n,W as t};