zen-gitsync 2.16.10 → 2.16.12
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.
- package/README.md +17 -16
- package/package.json +1 -1
- package/scripts/release.js +3 -3
- package/src/cli/ai/modelSetup.js +197 -14
- package/src/cli/ai/sessionStore.js +97 -97
- package/src/cli/ai/termui.js +92 -0
- package/src/cli/ai/termui.test.js +131 -0
- package/src/cli/customCommand.exec.test.js +52 -13
- package/src/ui/public/assets/AgentView-B_SeSMYO.js +4 -0
- package/src/ui/public/assets/AppVersionBadge-BeXH_zPw.js +2 -0
- package/src/ui/public/assets/BranchSelector-DiKVTd6_.js +1 -0
- package/src/ui/public/assets/CommitForm-bj3k-DPA.js +6 -0
- package/src/ui/public/assets/EditorView-B9Px9HM-.js +0 -0
- package/src/ui/public/assets/FlowExecutionViewer-B5j4P2AL.js +1 -0
- package/src/ui/public/assets/FlowOrchestrationWorkspace-CvpUR_GE.js +21 -0
- package/src/ui/public/assets/LogList-CRf4Qp6G.js +15 -0
- package/src/ui/public/assets/MindmapView-F9BzvQzm.js +2 -0
- package/src/ui/public/assets/MonitorView-Cp74zQIe.js +1 -0
- package/src/ui/public/assets/ProjectStartupButton-CAJ6NfgE.js +1 -0
- package/src/ui/public/assets/RemoteRepoCard-DbxZKwZQ.js +5 -0
- package/src/ui/public/assets/SourceMapView-SLERMi2h.js +3 -0
- package/src/ui/public/assets/UserInputNode-BJdehl9K.js +1 -0
- package/src/ui/public/assets/WorkbenchView-Ds2-DJ0d.js +28 -0
- package/src/ui/public/assets/element-plus-C0loJL0J.js +32 -0
- package/src/ui/public/assets/element-plus-CXfbWXaJ.css +1 -0
- package/src/ui/public/assets/{flow-mindmap-DnyDhwJv.js → flow-mindmap-CGn6wqsF.js} +1 -1
- package/src/ui/public/assets/index-BEPT2LNX.js +82 -0
- package/src/ui/public/assets/index-Fbjp0DKn.css +1 -0
- package/src/ui/public/assets/{monaco-Dje8Rg13.js → monaco-DPQ3OaiL.js} +2 -2
- package/src/ui/public/assets/socket-io-B9UuzwTN.js +1 -0
- package/src/ui/public/assets/{ts.worker-Dth06zuC.js → ts.worker-METxwbDZ.js} +1 -16
- package/src/ui/public/assets/{vendor-DVvq7qtS.css → vendor-C2ZpZPB4.css} +1 -1
- package/src/ui/public/assets/{vendor-exjCs1jx.js → vendor-pbWglt5U.js} +26 -67
- package/src/ui/public/assets/vue-flow-DRwHh4gM.js +39 -0
- package/src/ui/public/index.html +9 -15
- package/src/ui/server/index.js +4 -0
- package/src/ui/server/routes/workbench/agentChat.js +14 -3
- package/src/ui/server/routes/workbench/agentRoutes.js +218 -212
- package/src/ui/server/routes/workbench/agentSessionStore.js +173 -173
- package/src/ui/public/assets/AgentView-CioAzh-a.js +0 -4
- package/src/ui/public/assets/AppVersionBadge-CZ3UbiAu.js +0 -2
- package/src/ui/public/assets/BranchSelector-Kwcj7e0Z.js +0 -1
- package/src/ui/public/assets/CommitForm-DueeSB7T.js +0 -6
- package/src/ui/public/assets/CommonDialog-ChSKsEz2.css +0 -1
- package/src/ui/public/assets/CommonDialog-Tlk0Fa7g.js +0 -1
- package/src/ui/public/assets/EditorView-B2gm2OW1.js +0 -0
- package/src/ui/public/assets/FlowExecutionViewer-CFpiqZ2C.js +0 -1
- package/src/ui/public/assets/FlowOrchestrationWorkspace-BRUlRQwp.js +0 -21
- package/src/ui/public/assets/LogList-Do3C4BmA.js +0 -15
- package/src/ui/public/assets/MindmapView-C1pPUu8W.js +0 -2
- package/src/ui/public/assets/MonitorView-C2EyggDk.js +0 -1
- package/src/ui/public/assets/ProjectStartupButton-D7mgEsmf.js +0 -1
- package/src/ui/public/assets/RemoteRepoCard-D582mbhK.js +0 -5
- package/src/ui/public/assets/SourceMapView-gQRnN0Vj.js +0 -3
- package/src/ui/public/assets/SvgIcon-CDNsk4-Q.css +0 -1
- package/src/ui/public/assets/SvgIcon-Diox29ks.js +0 -1
- package/src/ui/public/assets/UserInputNode-CYxFuwuS.js +0 -1
- package/src/ui/public/assets/WorkbenchView-CbWSllXy.js +0 -28
- package/src/ui/public/assets/_plugin-vue_export-helper-B9cwfrdH.js +0 -15
- package/src/ui/public/assets/configStore-B8X980y1.js +0 -1
- package/src/ui/public/assets/element-plus-BId3Dnij.js +0 -25
- package/src/ui/public/assets/element-plus-D3Dn7nX0.css +0 -1
- package/src/ui/public/assets/index--onqtBRQ.css +0 -1
- package/src/ui/public/assets/index-DvrM8frM.js +0 -68
- package/src/ui/public/assets/socket-io-CGezBd--.js +0 -1
- package/src/ui/public/assets/vue-flow-Dj_X_Vr1.js +0 -39
- /package/src/ui/public/assets/{dagre-BCXBo6I9.js → dagre-CePNlbK4.js} +0 -0
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
# zen-gitsync
|
|
2
2
|
|
|
3
3
|
[English](#zen-gitsync) | [中文](#zh)
|
|
4
4
|
|
|
@@ -7,7 +7,7 @@ A Git automation platform with interactive commits, scheduled sync, custom comma
|
|
|
7
7
|
## Table of Contents
|
|
8
8
|
|
|
9
9
|
- [Installation](#installation)
|
|
10
|
-
- [What'
|
|
10
|
+
- [What's New](#v2xx--whats-new)
|
|
11
11
|
- [GUI](#gui)
|
|
12
12
|
- [Core Git Panel](#core-git-panel)
|
|
13
13
|
- [Branch Management](#branch-management)
|
|
@@ -38,7 +38,7 @@ npm install -g zen-gitsync
|
|
|
38
38
|
|
|
39
39
|
---
|
|
40
40
|
|
|
41
|
-
## v2.x.x — What'
|
|
41
|
+
## v2.x.x — What's New
|
|
42
42
|
|
|
43
43
|
- **Visual GUI** — Full graphical interface for Git operations
|
|
44
44
|
- **Branch management** — Create, switch, and track local/remote branches
|
|
@@ -62,7 +62,7 @@ npm install -g zen-gitsync
|
|
|
62
62
|
- **Accessibility (WCAG 2.1 AA)** — Dialog focus trap & restore, role-based separators, keyboard-only panel resize (`← → ↑ ↓`), screen-reader friendly commit context menu, ARIA-pressed toggle buttons, commit button `aria-busy` during in-flight commits, Git SHA hashes meet ≥ 4.5:1 contrast in both light and dark themes
|
|
63
63
|
- **Faster cold start** — `monaco-editor` / `@vue-flow` / `flow-mindmap` / `dagre` are split into independent chunks and lazy-loaded so the Git panel boots without waiting on the code editor or visual workflow designer
|
|
64
64
|
|
|
65
|
-
> Detailed per-release
|
|
65
|
+
> Detailed per-release changes can be found via `git log` or the [GitHub Releases](https://github.com/xz333221/zen-gitsync/releases) page.
|
|
66
66
|
|
|
67
67
|
---
|
|
68
68
|
|
|
@@ -410,16 +410,7 @@ git add --renormalize .
|
|
|
410
410
|
|
|
411
411
|
### Package manager
|
|
412
412
|
|
|
413
|
-
All `package.json` scripts use **npm** (`npm install`, `npm run dev`, `npm run release`, etc.). The
|
|
414
|
-
|
|
415
|
-
If you switch managers, delete the other manager's lockfile so only one is in the tree:
|
|
416
|
-
|
|
417
|
-
```bash
|
|
418
|
-
rm -f pnpm-lock.yaml # when running on npm
|
|
419
|
-
rm -f package-lock.json # when running on pnpm
|
|
420
|
-
```
|
|
421
|
-
|
|
422
|
-
`.gitignore` already lists both `pnpm-lock.yaml` and `package-lock.json`, so neither is forced into version control.
|
|
413
|
+
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.
|
|
423
414
|
|
|
424
415
|
---
|
|
425
416
|
|
|
@@ -437,6 +428,11 @@ $ g ai "fix the failing test" # one-shot task, then exit
|
|
|
437
428
|
$ g ai --model=2 # pick the 2nd configured model (index or name)
|
|
438
429
|
```
|
|
439
430
|
|
|
431
|
+
The first-run setup wizard and `/addmodel` provider/model lists support **↑↓ keys to switch
|
|
432
|
+
selection + Enter to confirm** (typing a number also jumps directly; `0` selects the trailing
|
|
433
|
+
"custom / manual input" entry). Non-TTY environments (CI, piped input) automatically fall back
|
|
434
|
+
to numeric input. `Esc` or `Ctrl+C` cancels the wizard cleanly.
|
|
435
|
+
|
|
440
436
|
In-session commands: `/help`, `/model`, `/addmodel`, `/cd <path>`, `/image [path]`, `/think`, `/clear`, `/exit`.
|
|
441
437
|
|
|
442
438
|
Images: press `Alt+V` in the REPL to paste a clipboard image (screenshot), or attach a
|
|
@@ -603,7 +599,7 @@ npm install -g zen-gitsync
|
|
|
603
599
|
- **可访问性(WCAG 2.1 AA)** — 弹窗焦点陷阱与归还、`role="separator"` 键盘可达的分隔条、纯键盘(`← → ↑ ↓`)调整面板宽度、屏幕阅读器友好的提交右键菜单、ARIA-pressed 切换按钮、提交按钮在提交过程中挂 `aria-busy`、Git 提交哈希在明/暗主题下对比度均 ≥ 4.5:1
|
|
604
600
|
- **更快的冷启动** — `monaco-editor` / `@vue-flow` / `flow-mindmap` / `dagre` 拆分为独立 chunk 并按需懒加载,Git 面板首屏不再等待代码编辑器或可视化流程编排模块
|
|
605
601
|
|
|
606
|
-
>
|
|
602
|
+
> 每个版本的详细变更可通过 `git log` 或 [GitHub Releases](https://github.com/xz333221/zen-gitsync/releases) 页面查看。
|
|
607
603
|
|
|
608
604
|
---
|
|
609
605
|
|
|
@@ -918,7 +914,7 @@ Activity Bar 中的机器人图标视图,可直接在浏览器中与内置 AI
|
|
|
918
914
|
|
|
919
915
|

|
|
920
916
|
|
|
921
|
-
> 点击顶部条右上角齿轮图标。tab 包含 **通用 / Git / 提交 / AI / 主题 / 语言 / 文件锁定 / NPM 路径**。大部分开关即时生效,无需重启 GUI。
|
|
917
|
+
> 点击顶部条右上角齿轮图标。tab 包含 **通用 / Git / 提交 / AI / 主题 / 语言 / 文件锁定 / NPM 路径**。大部分开关即时生效,无需重启 GUI。点击底栏的 **默认模型** 名称可一键定位到「AI 模型」tab。
|
|
922
918
|
|
|
923
919
|
| 设置项 | 说明 |
|
|
924
920
|
|---|---|
|
|
@@ -928,6 +924,7 @@ Activity Bar 中的机器人图标视图,可直接在浏览器中与内置 AI
|
|
|
928
924
|
| 语言 | 中文 / English |
|
|
929
925
|
| 文件锁定 | 锁定文件,使其永远不被暂存或储藏 |
|
|
930
926
|
| NPM 路径 | 配置 `package.json` 的扫描位置 |
|
|
927
|
+
| AI 模型 | 配置 OpenAI / Claude 等兼容 OpenAI 协议的 LLM 端点(API Key、baseURL、模型名),可设置默认模型,多套配置并存 |
|
|
931
928
|
|
|
932
929
|
---
|
|
933
930
|
|
|
@@ -966,6 +963,10 @@ $ g ai "修复失败的测试" # 单发模式:执行一轮后退出
|
|
|
966
963
|
$ g ai --model=2 # 使用第 2 个已配置的模型(序号或名称)
|
|
967
964
|
```
|
|
968
965
|
|
|
966
|
+
启动配置向导与 `/addmodel` 的"服务商 / 模型"列表支持 **↑↓ 键切换 + Enter 确认**(也可
|
|
967
|
+
直接输入数字跳转,`0` = 列表底部的"自定义 / 手动输入");非 TTY 环境下自动回退为数字输入。
|
|
968
|
+
`Esc` 或 `Ctrl+C` 一键取消整个向导。
|
|
969
|
+
|
|
969
970
|
会话内命令:`/help`、`/model`、`/addmodel`、`/cd <路径>`、`/image [路径]`、`/think`、`/clear`、`/exit`。
|
|
970
971
|
|
|
971
972
|
图片:在 REPL 中按 `Alt+V` 粘贴剪贴板图片(截图),或用 `/image <路径>` 附加本地图片;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zen-gitsync",
|
|
3
|
-
"version": "2.16.
|
|
3
|
+
"version": "2.16.12",
|
|
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",
|
package/scripts/release.js
CHANGED
|
@@ -50,10 +50,10 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
|
50
50
|
|
|
51
51
|
// 显式白名单:只有这些文件路径会被 git add。
|
|
52
52
|
// 禁止 `git add .` 防止把工作区脏文件 / 临时调试文件一起带上 release commit。
|
|
53
|
-
// lockfile (package-lock.json
|
|
53
|
+
// lockfile (package-lock.json) 在 .gitignore 里,
|
|
54
54
|
// 即便列在白名单也会被 git 拒绝 add,所以这里不列。
|
|
55
|
-
// 实际入 release commit 的只有 package.json(版本号)
|
|
56
|
-
const RELEASE_FILES = ['package.json'
|
|
55
|
+
// 实际入 release commit 的只有 package.json(版本号)。
|
|
56
|
+
const RELEASE_FILES = ['package.json']
|
|
57
57
|
|
|
58
58
|
function createReadlineInterface() {
|
|
59
59
|
return readline.createInterface({ input: process.stdin, output: process.stdout })
|
package/src/cli/ai/modelSetup.js
CHANGED
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
import readline from 'node:readline'
|
|
35
35
|
import chalk from 'chalk'
|
|
36
36
|
import config from '../../config.js'
|
|
37
|
-
import { startSpinner } from './termui.js'
|
|
37
|
+
import { startSpinner, renderSelectableListBody, parseKeyForSelectableList } from './termui.js'
|
|
38
38
|
|
|
39
39
|
// ──────────────────────────────────────────────
|
|
40
40
|
// 预设服务商列表
|
|
@@ -131,6 +131,7 @@ const STRINGS = {
|
|
|
131
131
|
selectProvider: '选择 AI 服务商:',
|
|
132
132
|
customOption: '自定义',
|
|
133
133
|
selectPrompt: (min, max) => `请选择 (${min}-${max}): `,
|
|
134
|
+
navHint: '↑↓ 切换,Enter 确认,数字直跳(0 = 上一行)',
|
|
134
135
|
invalidChoice: (min, max) => `无效选择,请输入 ${min}-${max} 之间的数字`,
|
|
135
136
|
endpointLabel: '接口地址',
|
|
136
137
|
endpointPrompt: (def) => `接口地址 [${def}]: `,
|
|
@@ -166,6 +167,7 @@ const STRINGS = {
|
|
|
166
167
|
selectProvider: 'Select an AI provider:',
|
|
167
168
|
customOption: 'Custom',
|
|
168
169
|
selectPrompt: (min, max) => `Choose (${min}-${max}): `,
|
|
170
|
+
navHint: '↑↓ to switch, Enter to confirm, number to jump (0 = extra)',
|
|
169
171
|
invalidChoice: (min, max) => `Invalid choice, enter a number between ${min} and ${max}`,
|
|
170
172
|
endpointLabel: 'Endpoint URL',
|
|
171
173
|
endpointPrompt: (def) => `Endpoint [${def}]: `,
|
|
@@ -413,32 +415,201 @@ async function askYesNo(asker, prompt) {
|
|
|
413
415
|
}
|
|
414
416
|
|
|
415
417
|
/**
|
|
416
|
-
*
|
|
417
|
-
*
|
|
418
|
-
*
|
|
419
|
-
*
|
|
420
|
-
*
|
|
421
|
-
*
|
|
422
|
-
*
|
|
423
|
-
*
|
|
418
|
+
* 让用户从列表中选择一项。
|
|
419
|
+
*
|
|
420
|
+
* 两条路径:
|
|
421
|
+
* - TTY(终端)且传入了 rl → 上下键切换 + Enter 提交(类似 Codex CLI 的列表选择)
|
|
422
|
+
* - 数字键 1..9 / 0 也可直跳(0 = 额外项)
|
|
423
|
+
* - Esc / Ctrl+C 视作取消整个 wizard(调用方按 { cancelled: true } 走 catch 分支)
|
|
424
|
+
* - 其他情况 → 回退到原数字输入(由 asker.ask 提示,数字 + 回车确认;非法输入不退出,
|
|
425
|
+
* 提示 t.invalidChoice 让用户重新输)
|
|
426
|
+
*
|
|
427
|
+
* 设计要点:
|
|
428
|
+
* - 列表用 renderSelectableListBody 渲染,选中行整行反白(可视化锚点固定,跟斜杠命令
|
|
429
|
+
* 提示一样的"反白起点严格对齐"思路)
|
|
430
|
+
* - 方向键会触发 readline 默认历史浏览,我们用"清空 rl.line + 调 _refreshLine"撤销
|
|
431
|
+
* (参考 agent.js 对斜杠命令提示的处理)
|
|
432
|
+
* - 按 Enter 时 rl.question 回调自然拿到 line 字符串;空字符串 → 用当前 selectedIndex,
|
|
433
|
+
* 数字 → 直接选中,非数字 → 视为无效,保持当前列表继续等待输入(不重画列表)
|
|
434
|
+
*
|
|
435
|
+
* @param {object} opts
|
|
436
|
+
* @param {import('node:readline').Interface} [opts.rl] - 用于挂 keypress + rl.question;
|
|
437
|
+
* 不传或非 TTY 时退回数字输入
|
|
438
|
+
* @param {{ask:(p:string,d?:string)=>Promise<string>}} opts.asker - 数字输入回退路径的 asker;
|
|
439
|
+
* TTY 路径不需要,但参数签名上必须传(避免 collectModelInput 内部拆开两个调用)
|
|
440
|
+
* @param {string} opts.title
|
|
441
|
+
* @param {Array<{label: string, value: any}>} opts.items
|
|
442
|
+
* @param {object} opts.t - i18n strings
|
|
443
|
+
* @param {string} [opts.extraOptionLabel] - 额外项(编号 0)
|
|
444
|
+
* @returns {Promise<{index: number, value: any}|null>}
|
|
445
|
+
* - { index, value }:选中标准项
|
|
446
|
+
* - null:选中额外项
|
|
447
|
+
* - 抛出 { cancelled: true }:用户取消整个 wizard(Ctrl+C / Esc / readline close)
|
|
424
448
|
*/
|
|
425
|
-
async function selectFromList(asker, title, items, t, extraOptionLabel) {
|
|
449
|
+
async function selectFromList({ rl, asker, title, items, t, extraOptionLabel } = {}) {
|
|
450
|
+
const safeItems = Array.isArray(items) ? items : []
|
|
451
|
+
const extraActive = !!extraOptionLabel
|
|
452
|
+
const totalItems = safeItems.length + (extraActive ? 1 : 0)
|
|
453
|
+
if (totalItems === 0) return null
|
|
454
|
+
|
|
426
455
|
console.log()
|
|
427
456
|
console.log(chalk.bold(title))
|
|
457
|
+
|
|
458
|
+
// 判断走哪条路
|
|
459
|
+
const interactive = !!(rl && rl.input && process.stdout.isTTY && process.stdin.isTTY)
|
|
460
|
+
|
|
461
|
+
if (!interactive) {
|
|
462
|
+
// 非 TTY / 没 rl:走数字输入回退(原行为,保留 CI / 管道环境的可脚本化能力)
|
|
463
|
+
return selectByDigits({ asker, items: safeItems, t, extraOptionLabel })
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// ── TTY 路径:方向键列表选择 ──
|
|
467
|
+
let selectedIndex = 0
|
|
468
|
+
let finished = false
|
|
469
|
+
|
|
470
|
+
// 渲染初始列表
|
|
471
|
+
const initialBody = renderSelectableListBody(safeItems, 0, extraOptionLabel)
|
|
472
|
+
process.stdout.write(initialBody + '\n')
|
|
473
|
+
const listRows = initialBody.split('\n').length
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* 重画列表:用 ANSI 把光标逐行上移 + 清行,然后重新打印,光标回到列表首行行首,
|
|
477
|
+
* 不影响 readline 在 listRows+1 行的 prompt 行。
|
|
478
|
+
* 用 \x1b[2K(清整行)+ \x1b[1A(上移一行)循环 N 次,等价于
|
|
479
|
+
* "清掉光标以上 N 行"但不污染更下面的内容;最后 \r 回到行首方便后续输出起点对齐。
|
|
480
|
+
*/
|
|
481
|
+
const redraw = () => {
|
|
482
|
+
for (let i = 0; i < listRows; i++) {
|
|
483
|
+
process.stdout.write('\x1b[1A\x1b[2K')
|
|
484
|
+
}
|
|
485
|
+
process.stdout.write('\r')
|
|
486
|
+
process.stdout.write(renderSelectableListBody(safeItems, selectedIndex, extraOptionLabel) + '\n')
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* 撤销 readline 内置行为对 rl.line 的副作用:
|
|
491
|
+
* - 方向键:readline 读 history 一条回填到 rl.line + _refreshLine 重画 prompt
|
|
492
|
+
* 用户本来在 prompt 行没敲任何字符,历史回填会让用户突然看到历史的脏字符。
|
|
493
|
+
* 所以清空 line buffer 让 prompt 行保持空(类似 agent.js 对斜杠命令提示的做法)。
|
|
494
|
+
* - 数字键(jump:N):用户在 prompt 行确实敲了数字,该数字已经在 rl.line 里,
|
|
495
|
+
* 不能撤销(否则用户按 "8" 看不到 "8")— 但目前的 jump 路径用 rl.line.length === 0 守门,
|
|
496
|
+
* 等于已经避免和 readline 的字符输入重叠,所以 prev/next 路径才需要清,数字路径不用清。
|
|
497
|
+
*/
|
|
498
|
+
const clearLineBuffer = () => {
|
|
499
|
+
if (!rl) return
|
|
500
|
+
rl.line = ''
|
|
501
|
+
rl.cursor = 0
|
|
502
|
+
if (typeof rl._refreshLine === 'function') {
|
|
503
|
+
try { rl._refreshLine() } catch { /* 兜底 — 某些 readline 版本没有 _refreshLine */ }
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
return new Promise((resolve, reject) => {
|
|
508
|
+
const cleanup = () => {
|
|
509
|
+
if (finished) return
|
|
510
|
+
finished = true
|
|
511
|
+
if (rl && rl.input) rl.input.removeListener('keypress', keyHandler)
|
|
512
|
+
if (rl) rl.removeListener('close', onClose)
|
|
513
|
+
}
|
|
514
|
+
const onClose = () => {
|
|
515
|
+
cleanup()
|
|
516
|
+
reject({ cancelled: true })
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const keyHandler = (_str, key) => {
|
|
520
|
+
if (finished || !key) return
|
|
521
|
+
const action = parseKeyForSelectableList(key)
|
|
522
|
+
const max = totalItems - 1
|
|
523
|
+
|
|
524
|
+
if (action === 'prev' || action === 'next') {
|
|
525
|
+
if (action === 'prev') {
|
|
526
|
+
selectedIndex = selectedIndex > 0 ? selectedIndex - 1 : max
|
|
527
|
+
} else {
|
|
528
|
+
selectedIndex = selectedIndex < max ? selectedIndex + 1 : 0
|
|
529
|
+
}
|
|
530
|
+
clearLineBuffer() // 撤销 readline 历史回填
|
|
531
|
+
redraw()
|
|
532
|
+
return
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if (typeof action === 'string' && action.startsWith('jump:')) {
|
|
536
|
+
// 只在用户当前没敲字符时接受 jump,避免和 readline 正常字符输入冲突
|
|
537
|
+
if (rl.line && rl.line.length > 0) return
|
|
538
|
+
const n = Number(action.slice(5))
|
|
539
|
+
let target = -1
|
|
540
|
+
if (n === 0 && extraActive) target = safeItems.length
|
|
541
|
+
else if (n >= 1 && n <= safeItems.length) target = n - 1
|
|
542
|
+
if (target >= 0) {
|
|
543
|
+
selectedIndex = target
|
|
544
|
+
redraw() // 数字已进 rl.line,不清;用户会看到 prompt 行同步显示这个数字
|
|
545
|
+
}
|
|
546
|
+
return
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (action === 'cancel') {
|
|
550
|
+
// Esc / Ctrl+C — 让用户取消整个 wizard
|
|
551
|
+
cleanup()
|
|
552
|
+
reject({ cancelled: true })
|
|
553
|
+
return
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// 'confirm' (Enter) 不处理,让 readline 自然触发 line 事件 → rl.question 回调拿 answer
|
|
557
|
+
// 其他字符(字母/数字但 line buffer 非空)让 readline 自然处理
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
rl.input.on('keypress', keyHandler)
|
|
561
|
+
rl.once('close', onClose)
|
|
562
|
+
|
|
563
|
+
// 提示词:列出可选范围 + 导航提示
|
|
564
|
+
const minIdx = extraActive ? 0 : 1
|
|
565
|
+
const maxIdx = safeItems.length
|
|
566
|
+
const navHint = t.navHint // "↑↓ 切换,Enter 确认,数字直跳"
|
|
567
|
+
const promptStr = `${chalk.cyan(t.selectPrompt(minIdx, maxIdx))}${chalk.dim(navHint ? ' ' + navHint : '')} `
|
|
568
|
+
|
|
569
|
+
rl.question(promptStr, (answer) => {
|
|
570
|
+
cleanup()
|
|
571
|
+
const trimmed = (answer || '').trim()
|
|
572
|
+
let idx = Number.parseInt(trimmed, 10)
|
|
573
|
+
if (Number.isFinite(idx) && idx >= minIdx && idx <= maxIdx) {
|
|
574
|
+
if (idx === 0) return resolve(null)
|
|
575
|
+
return resolve({ index: idx - 1, value: safeItems[idx - 1].value })
|
|
576
|
+
}
|
|
577
|
+
if (trimmed === '') {
|
|
578
|
+
// 空回答:用当前 selectedIndex
|
|
579
|
+
if (selectedIndex >= safeItems.length) return resolve(null)
|
|
580
|
+
return resolve({ index: selectedIndex, value: safeItems[selectedIndex].value })
|
|
581
|
+
}
|
|
582
|
+
// 非法输入:在当前列表下方打印提示,不重画列表(列表还在可见)
|
|
583
|
+
console.log(chalk.yellow(t.invalidChoice(minIdx, maxIdx)))
|
|
584
|
+
resolve('__retry__')
|
|
585
|
+
})
|
|
586
|
+
}).then(async (r) => {
|
|
587
|
+
if (r === '__retry__') {
|
|
588
|
+
// 递归重试(重新调一次)— 列表会再画一遍,实测可接受;保持 wizard 流程的简洁
|
|
589
|
+
return selectFromList({ rl, asker, title, items, t, extraOptionLabel })
|
|
590
|
+
}
|
|
591
|
+
return r
|
|
592
|
+
})
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* 数字输入回退路径(原 selectFromList 的实现)。
|
|
597
|
+
* 保留给非 TTY / 无 readline 的场景(管道、CI、测试)。
|
|
598
|
+
*/
|
|
599
|
+
async function selectByDigits({ asker, items, t, extraOptionLabel }) {
|
|
428
600
|
items.forEach((item, i) => {
|
|
429
601
|
console.log(` ${chalk.cyan(String(i + 1).padStart(2))}. ${item.label}`)
|
|
430
602
|
})
|
|
431
603
|
if (extraOptionLabel) {
|
|
432
604
|
console.log(` ${chalk.dim('0')}. ${chalk.dim(extraOptionLabel)}`)
|
|
433
605
|
}
|
|
434
|
-
|
|
435
606
|
const min = extraOptionLabel ? 0 : 1
|
|
436
607
|
const max = items.length
|
|
437
608
|
while (true) {
|
|
438
609
|
const answer = await asker.ask(t.selectPrompt(min, max))
|
|
439
610
|
const idx = Number.parseInt(answer, 10)
|
|
440
611
|
if (Number.isFinite(idx) && idx >= min && idx <= max) {
|
|
441
|
-
if (idx === 0) return null
|
|
612
|
+
if (idx === 0) return null
|
|
442
613
|
return { index: idx - 1, value: items[idx - 1].value }
|
|
443
614
|
}
|
|
444
615
|
console.log(chalk.yellow(t.invalidChoice(min, max)))
|
|
@@ -481,7 +652,13 @@ export async function collectModelInput({ locale = 'zh-CN', rl: injectedRl, fetc
|
|
|
481
652
|
label: `${p.label} ${chalk.dim(p.url)}`,
|
|
482
653
|
value: p,
|
|
483
654
|
}))
|
|
484
|
-
const providerChoice = await selectFromList(
|
|
655
|
+
const providerChoice = await selectFromList({
|
|
656
|
+
rl, asker,
|
|
657
|
+
title: t.selectProvider,
|
|
658
|
+
items: providerItems,
|
|
659
|
+
t,
|
|
660
|
+
extraOptionLabel: t.customOption,
|
|
661
|
+
})
|
|
485
662
|
|
|
486
663
|
let baseURL
|
|
487
664
|
if (providerChoice) {
|
|
@@ -528,7 +705,13 @@ export async function collectModelInput({ locale = 'zh-CN', rl: injectedRl, fetc
|
|
|
528
705
|
let model
|
|
529
706
|
if (modelList.length > 0) {
|
|
530
707
|
const modelItems = modelList.map(m => ({ label: m, value: m }))
|
|
531
|
-
const modelChoice = await selectFromList(
|
|
708
|
+
const modelChoice = await selectFromList({
|
|
709
|
+
rl, asker,
|
|
710
|
+
title: t.builtinModels,
|
|
711
|
+
items: modelItems,
|
|
712
|
+
t,
|
|
713
|
+
extraOptionLabel: t.manualInput,
|
|
714
|
+
})
|
|
532
715
|
if (modelChoice) {
|
|
533
716
|
model = modelChoice.value
|
|
534
717
|
} else {
|
|
@@ -1,97 +1,97 @@
|
|
|
1
|
-
// Copyright 2026 xz333221
|
|
2
|
-
//
|
|
3
|
-
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
-
// you may not use this file except in compliance with the License.
|
|
5
|
-
// You may obtain a copy of the License at
|
|
6
|
-
//
|
|
7
|
-
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
-
//
|
|
9
|
-
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
-
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
-
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
-
// See the License for the specific language governing permissions and
|
|
13
|
-
// limitations under the License.
|
|
14
|
-
//
|
|
15
|
-
// CLI 侧智能体会话持久化。
|
|
16
|
-
//
|
|
17
|
-
// 与 Web 端 (src/ui/server/routes/workbench/agentSessionStore.js) 共享同一数据目录
|
|
18
|
-
// (~/.zen-gitsync/agent-sessions/) 和同一 JSON 格式,这样:
|
|
19
|
-
// - CLI 对话在 Web UI 的智能体 tab 里可见(带 CLI 标记)
|
|
20
|
-
// - Web UI 可以读取/继续 CLI 创建的会话
|
|
21
|
-
//
|
|
22
|
-
// 本模块刻意不依赖服务器代码,仅用 node 内置 fs/path/os,保持 CLI 自包含。
|
|
23
|
-
|
|
24
|
-
import fsp from 'fs/promises';
|
|
25
|
-
import path from 'path';
|
|
26
|
-
import os from 'os';
|
|
27
|
-
|
|
28
|
-
const SESSIONS_DIR = path.join(os.homedir(), '.zen-gitsync', 'agent-sessions');
|
|
29
|
-
const MAX_SESSIONS = 200;
|
|
30
|
-
const KEEP_SESSIONS = 100;
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* 生成会话 ID: ag-{时间戳base36}-{随机base36}
|
|
34
|
-
*/
|
|
35
|
-
export function genSessionId() {
|
|
36
|
-
return `ag-${Date.now().toString(36).slice(-8)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* 从第一条 user 消息自动生成标题(取第一行,截断 40 字符)
|
|
41
|
-
*/
|
|
42
|
-
export function autoTitle(messages) {
|
|
43
|
-
for (const m of messages) {
|
|
44
|
-
if (m.role === 'user') {
|
|
45
|
-
let text = '';
|
|
46
|
-
if (typeof m.content === 'string') {
|
|
47
|
-
text = m.content;
|
|
48
|
-
} else if (Array.isArray(m.content)) {
|
|
49
|
-
text = m.content.filter(p => p?.type === 'text').map(p => p.text).join(' ');
|
|
50
|
-
}
|
|
51
|
-
text = text.trim();
|
|
52
|
-
if (text) {
|
|
53
|
-
const firstLine = text.split('\n')[0].trim();
|
|
54
|
-
return firstLine.length > 40 ? firstLine.slice(0, 40) + '…' : firstLine;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
return '(新会话)';
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* 写入会话(原子操作: tmp + rename)
|
|
63
|
-
*/
|
|
64
|
-
export async function writeSession(sessionId, data) {
|
|
65
|
-
await fsp.mkdir(SESSIONS_DIR, { recursive: true });
|
|
66
|
-
const file = path.join(SESSIONS_DIR, `${sessionId}.json`);
|
|
67
|
-
const tmp = `${file}.tmp.${process.pid}.${Date.now()}`;
|
|
68
|
-
await fsp.writeFile(tmp, JSON.stringify(data, null, 2), 'utf-8');
|
|
69
|
-
await fsp.rename(tmp, file);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* 读取会话
|
|
74
|
-
*/
|
|
75
|
-
export async function readSession(sessionId) {
|
|
76
|
-
const file = path.join(SESSIONS_DIR, `${sessionId}.json`);
|
|
77
|
-
return JSON.parse(await fsp.readFile(file, 'utf-8'));
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* 保留策略:超过 MAX_SESSIONS 时删最旧的,保留 KEEP_SESSIONS 个
|
|
82
|
-
*/
|
|
83
|
-
export async function enforceRetention() {
|
|
84
|
-
const files = await fsp.readdir(SESSIONS_DIR).catch(() => []);
|
|
85
|
-
const jsonFiles = files.filter(f => f.endsWith('.json'));
|
|
86
|
-
if (jsonFiles.length < MAX_SESSIONS) return;
|
|
87
|
-
const stats = await Promise.all(jsonFiles.map(async f => {
|
|
88
|
-
const full = path.join(SESSIONS_DIR, f);
|
|
89
|
-
const s = await fsp.stat(full);
|
|
90
|
-
return { file: full, mtime: s.mtimeMs };
|
|
91
|
-
}));
|
|
92
|
-
stats.sort((a, b) => b.mtime - a.mtime);
|
|
93
|
-
const toDelete = stats.slice(KEEP_SESSIONS);
|
|
94
|
-
await Promise.all(toDelete.map(s => fsp.unlink(s.file).catch(() => {})));
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export { SESSIONS_DIR };
|
|
1
|
+
// Copyright 2026 xz333221
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
// You may obtain a copy of the License at
|
|
6
|
+
//
|
|
7
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
//
|
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
//
|
|
15
|
+
// CLI 侧智能体会话持久化。
|
|
16
|
+
//
|
|
17
|
+
// 与 Web 端 (src/ui/server/routes/workbench/agentSessionStore.js) 共享同一数据目录
|
|
18
|
+
// (~/.zen-gitsync/agent-sessions/) 和同一 JSON 格式,这样:
|
|
19
|
+
// - CLI 对话在 Web UI 的智能体 tab 里可见(带 CLI 标记)
|
|
20
|
+
// - Web UI 可以读取/继续 CLI 创建的会话
|
|
21
|
+
//
|
|
22
|
+
// 本模块刻意不依赖服务器代码,仅用 node 内置 fs/path/os,保持 CLI 自包含。
|
|
23
|
+
|
|
24
|
+
import fsp from 'fs/promises';
|
|
25
|
+
import path from 'path';
|
|
26
|
+
import os from 'os';
|
|
27
|
+
|
|
28
|
+
const SESSIONS_DIR = path.join(os.homedir(), '.zen-gitsync', 'agent-sessions');
|
|
29
|
+
const MAX_SESSIONS = 200;
|
|
30
|
+
const KEEP_SESSIONS = 100;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 生成会话 ID: ag-{时间戳base36}-{随机base36}
|
|
34
|
+
*/
|
|
35
|
+
export function genSessionId() {
|
|
36
|
+
return `ag-${Date.now().toString(36).slice(-8)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 从第一条 user 消息自动生成标题(取第一行,截断 40 字符)
|
|
41
|
+
*/
|
|
42
|
+
export function autoTitle(messages) {
|
|
43
|
+
for (const m of messages) {
|
|
44
|
+
if (m.role === 'user') {
|
|
45
|
+
let text = '';
|
|
46
|
+
if (typeof m.content === 'string') {
|
|
47
|
+
text = m.content;
|
|
48
|
+
} else if (Array.isArray(m.content)) {
|
|
49
|
+
text = m.content.filter(p => p?.type === 'text').map(p => p.text).join(' ');
|
|
50
|
+
}
|
|
51
|
+
text = text.trim();
|
|
52
|
+
if (text) {
|
|
53
|
+
const firstLine = text.split('\n')[0].trim();
|
|
54
|
+
return firstLine.length > 40 ? firstLine.slice(0, 40) + '…' : firstLine;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return '(新会话)';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 写入会话(原子操作: tmp + rename)
|
|
63
|
+
*/
|
|
64
|
+
export async function writeSession(sessionId, data) {
|
|
65
|
+
await fsp.mkdir(SESSIONS_DIR, { recursive: true });
|
|
66
|
+
const file = path.join(SESSIONS_DIR, `${sessionId}.json`);
|
|
67
|
+
const tmp = `${file}.tmp.${process.pid}.${Date.now()}`;
|
|
68
|
+
await fsp.writeFile(tmp, JSON.stringify(data, null, 2), 'utf-8');
|
|
69
|
+
await fsp.rename(tmp, file);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 读取会话
|
|
74
|
+
*/
|
|
75
|
+
export async function readSession(sessionId) {
|
|
76
|
+
const file = path.join(SESSIONS_DIR, `${sessionId}.json`);
|
|
77
|
+
return JSON.parse(await fsp.readFile(file, 'utf-8'));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 保留策略:超过 MAX_SESSIONS 时删最旧的,保留 KEEP_SESSIONS 个
|
|
82
|
+
*/
|
|
83
|
+
export async function enforceRetention() {
|
|
84
|
+
const files = await fsp.readdir(SESSIONS_DIR).catch(() => []);
|
|
85
|
+
const jsonFiles = files.filter(f => f.endsWith('.json'));
|
|
86
|
+
if (jsonFiles.length < MAX_SESSIONS) return;
|
|
87
|
+
const stats = await Promise.all(jsonFiles.map(async f => {
|
|
88
|
+
const full = path.join(SESSIONS_DIR, f);
|
|
89
|
+
const s = await fsp.stat(full);
|
|
90
|
+
return { file: full, mtime: s.mtimeMs };
|
|
91
|
+
}));
|
|
92
|
+
stats.sort((a, b) => b.mtime - a.mtime);
|
|
93
|
+
const toDelete = stats.slice(KEEP_SESSIONS);
|
|
94
|
+
await Promise.all(toDelete.map(s => fsp.unlink(s.file).catch(() => {})));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export { SESSIONS_DIR };
|