zen-gitsync 2.17.7 → 2.17.8

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 (48) hide show
  1. package/README.md +21 -4
  2. package/package.json +1 -1
  3. package/src/cli/ai/agent.js +11 -7
  4. package/src/cli/ai/cli.integration.test.js +27 -2
  5. package/src/cli/ai/runtime.test.js +29 -0
  6. package/src/cli/ai/termui.js +29 -12
  7. package/src/cli/ai/termui.test.js +102 -2
  8. package/src/cli/ai/turn.js +1 -1
  9. package/src/ui/public/assets/AgentView-CdJaeRh0.js +5 -0
  10. package/src/ui/public/assets/AgentView-DAY4S5WN.css +1 -0
  11. package/src/ui/public/assets/{AppVersionBadge-Gs5lUl7P.js → AppVersionBadge-CiijSYfM.js} +2 -2
  12. package/src/ui/public/assets/{BranchSelector-IjaFk0S-.js → BranchSelector-BS7ajwvv.js} +1 -1
  13. package/src/ui/public/assets/{CommitForm-C-sowOXH.js → CommitForm-CEOSqXCA.js} +1 -1
  14. package/src/ui/public/assets/{CommonDialog-QyptFD1R.js → CommonDialog-CAaEi-4T.js} +1 -1
  15. package/src/ui/public/assets/{EditorView-CvvluFCY.js → EditorView-DyaFyRDD.js} +1 -1
  16. package/src/ui/public/assets/{FileDiffViewer-BXVsdCxE.js → FileDiffViewer-BvX9cQjR.js} +1 -1
  17. package/src/ui/public/assets/{FlowExecutionViewer-CRxipXru.js → FlowExecutionViewer-uXrzZjsW.js} +1 -1
  18. package/src/ui/public/assets/{FlowOrchestrationWorkspace-AE1ZHTKF.js → FlowOrchestrationWorkspace-DIBpUSt_.js} +1 -1
  19. package/src/ui/public/assets/{IconButton-BmSueF_H.js → IconButton-CnoW2on6.js} +1 -1
  20. package/src/ui/public/assets/{LogList-iaVA-_aC.js → LogList-D2espSVo.js} +1 -1
  21. package/src/ui/public/assets/{MindmapView-DcG4jpCh.js → MindmapView-CZF-rZVs.js} +1 -1
  22. package/src/ui/public/assets/{MonitorView-DO59syvl.js → MonitorView-DjLr7qOG.js} +1 -1
  23. package/src/ui/public/assets/{ProjectStartupButton-DYppceyw.js → ProjectStartupButton-DzsU-QIO.js} +1 -1
  24. package/src/ui/public/assets/{RemoteManagerDialog-CDbxG6pZ.js → RemoteManagerDialog-CuuG2ucT.js} +1 -1
  25. package/src/ui/public/assets/{RemoteRepoCard-Cko2KB8w.js → RemoteRepoCard-BGf8L0of.js} +1 -1
  26. package/src/ui/public/assets/{SourceMapView-CwWPVdz-.js → SourceMapView-CFb02m6z.js} +1 -1
  27. package/src/ui/public/assets/{SvgIcon-Cf7nOKc8.js → SvgIcon-SRAeYjnN.js} +1 -1
  28. package/src/ui/public/assets/{TemplateManager-DWPDjWxM.js → TemplateManager-gedqAXl8.js} +1 -1
  29. package/src/ui/public/assets/{UserInputNode-YOziN-5A.js → UserInputNode-BOCuMQW6.js} +1 -1
  30. package/src/ui/public/assets/{WorkbenchView-DNnEpIYy.js → WorkbenchView-CRoR21LQ.js} +4 -4
  31. package/src/ui/public/assets/{_plugin-vue_export-helper-CQjg8MXm.js → _plugin-vue_export-helper-C8OE7U9H.js} +4 -4
  32. package/src/ui/public/assets/{configStore-C7HOx5S1.js → configStore-CuMElTgV.js} +1 -1
  33. package/src/ui/public/assets/{gitStore-DIsBd6n4.js → gitStore-CpcS-MzM.js} +1 -1
  34. package/src/ui/public/assets/index-cApp6w0b.css +1 -0
  35. package/src/ui/public/assets/index-yXm9JLGv.js +24 -0
  36. package/src/ui/public/assets/{previewSandbox-TKucqLxt.js → previewSandbox-DkXYEpLP.js} +0 -0
  37. package/src/ui/public/assets/{toolsStore-BiC2mmC3.js → toolsStore-DGxlRe3Y.js} +1 -1
  38. package/src/ui/public/assets/vendor-4ojsjDYc.css +1 -0
  39. package/src/ui/public/assets/{vendor-92fKgtnz.js → vendor-dO4B7wQf.js} +28 -28
  40. package/src/ui/public/index.html +14 -14
  41. package/src/ui/server/routes/fs.js +37 -0
  42. package/src/ui/server/utils/directoryFetch.js +209 -0
  43. package/src/ui/server/utils/directoryFetch.test.js +232 -0
  44. package/src/ui/public/assets/AgentView-CZc191Xm.js +0 -4
  45. package/src/ui/public/assets/AgentView-CzUeyQfR.css +0 -1
  46. package/src/ui/public/assets/index-FOe1cHkh.css +0 -1
  47. package/src/ui/public/assets/index-MmcnT2Dn.js +0 -24
  48. package/src/ui/public/assets/vendor-B1UrVTTI.css +0 -1
package/README.md CHANGED
@@ -228,6 +228,13 @@ Create, manage, and run shell commands from the sidebar (Console view):
228
228
  - Save command **templates** for quick reuse
229
229
  - Each command has its own **enable / disable** toggle so you can stage a suite of commands without running them
230
230
 
231
+ **Scheduled commit** (pinned to the bottom of the same sidebar): auto `git add -A` + `git commit` on an interval.
232
+
233
+ - Interval in minutes / hours / days, with an optional commit right on start
234
+ - Commit message: the configured default message, a per-schedule message, or AI-generated
235
+ - Push to the remote after every successful commit (can be turned off)
236
+ - The panel echoes the **equivalent `g` CLI command** — e.g. `g -y --interval=1800 --path="<dir>"` (`--interval` is in **seconds**) — with a one-click copy button, so the same schedule can be reproduced from a terminal without the GUI
237
+
231
238
  ---
232
239
 
233
240
  ### Flow Orchestration (Visual Workflow Designer)
@@ -381,6 +388,7 @@ A dedicated view (robot icon in the activity bar) for chatting with the built-in
381
388
  | Feature | Description |
382
389
  |---|---|
383
390
  | Session list | Browse, search, rename, and delete past conversations; sessions created via `g ai` in the terminal also appear here with a **CLI** badge |
391
+ | Live session entry | Sending the first message of a new session makes it show up in the list **immediately** with a "Generating..." badge, instead of waiting for the whole turn to finish; once the reply ends and the server persists the session, the entry is replaced by the real timestamp and message count |
384
392
  | Streaming chat | SSE-based real-time streaming with thinking process, content, tool calls, and tool results rendered inline |
385
393
  | Tool call display | Each tool invocation (run_command, read_file, edit_file, list_files, search_text, write_file) is shown as a collapsible card with arguments preview and execution result |
386
394
  | 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 |
@@ -455,8 +463,8 @@ to numeric input. `Esc` or `Ctrl+C` cancels the wizard cleanly.
455
463
 
456
464
  In-session commands: `/help`, `/model`, `/addmodel`, `/cd <path>`, `/image [path]`, `/think`, `/tools`, `/stats`, `/new`, `/resume`, `/clear`, `/exit`.
457
465
 
458
- Reasoning, tool calls and answers have separate visual sections. Reasoning defaults to a short preview;
459
- `/think full` shows subsequent reasoning in full, `/think off` hides it, and `/think compact` restores the preview.
466
+ Reasoning, tool calls and answers have separate visual sections. Reasoning returned by the model is shown in full by default;
467
+ `/think full` restores full display, `/think off` hides it, and `/think compact` previews the first 12 nonblank lines. All three modes appear in the `/` menu and support completion after `/think `.
460
468
  Tool output defaults to a few head/tail lines; `/tools full` shows subsequent tool results in full and
461
469
  `/tools compact` restores compact output. These display settings do not reduce model token usage.
462
470
 
@@ -833,6 +841,13 @@ $ ZEN_ALLOWED_ORIGINS="https://zen.example.com,http://10.0.0.5:8080" g ui
833
841
  - 保存**命令模板**快速复用
834
842
  - 每条命令都有 **启用 / 禁用** 开关,可以在不立即运行的情况下预排一组命令
835
843
 
844
+ **定时提交**(固定在同一侧边栏底部):按间隔自动 `git add -A` + `git commit`。
845
+
846
+ - 间隔可选分钟 / 小时 / 天,可设置启动时立即提交一次
847
+ - 提交信息:全局默认信息、本次自定义信息,或 AI 生成
848
+ - 每次提交成功后自动推送到远程(可关闭)
849
+ - 面板底部同步显示**等效的 `g` 命令行**(如 `g -y --interval=1800 --path="<目录>"`,`--interval` 单位为**秒**),带一键复制按钮,方便在终端里复现同一套定时任务
850
+
836
851
  ---
837
852
 
838
853
  ### 可视化流程编排
@@ -988,6 +1003,7 @@ Activity Bar 中的机器人图标视图,可直接在浏览器中与内置 AI
988
1003
  | 功能 | 说明 |
989
1004
  |---|---|
990
1005
  | 会话列表 | 浏览、搜索、重命名、删除历史对话;通过 `g ai` 在终端创建的会话也会出现在这里,带 **CLI** 标记 |
1006
+ | 会话实时入列 | 新会话发出第一条消息后,左侧列表**立刻**出现这一条(带「正在生成中…」标记),不用等整轮回答跑完;回答结束、服务端落盘后自动替换成真实的时间与条数 |
991
1007
  | 流式对话 | 基于 SSE 的实时流式输出,包含思考过程、正文内容、工具调用和工具结果的内联渲染 |
992
1008
  | 工具调用展示 | 每次工具调用(run_command、read_file、edit_file、list_files、search_text、write_file)以可折叠卡片形式展示,含参数预览和执行结果 |
993
1009
  | 会话持久化 | 所有对话保存为 JSON 文件到 `~/.zen-gitsync/agent-sessions/`;CLI 智能体(`g ai`)写入同一目录,Web 端与 CLI 端会话统一管理 |
@@ -1057,8 +1073,9 @@ $ g ai --model=2 # 使用第 2 个已配置的模型(序号或
1057
1073
 
1058
1074
  会话内命令:`/help`、`/model`、`/addmodel`、`/cd <路径>`、`/image [路径]`、`/think`、`/tools`、`/stats`、`/new`、`/resume`、`/clear`、`/exit`。
1059
1075
 
1060
- 思考、工具调用和回答分区展示。默认只显示简短思考预览,`/think full` 显示后续完整思考,
1061
- `/think off` 隐藏思考,`/think compact` 恢复预览。工具结果默认保留头尾几行,
1076
+ 思考、工具调用和回答分区展示。默认完整显示模型返回的思考,`/think full` 恢复完整显示,
1077
+ `/think off` 隐藏思考,`/think compact` 切换为前 12 行非空预览。三种模式均直接列在 `/` 菜单中,输入 `/think ` 后也可补全。
1078
+ 工具结果默认保留头尾几行,
1062
1079
  `/tools full` 显示后续完整工具结果,`/tools compact` 恢复精简。这些显示设置不会减少模型的 Token 消耗。
1063
1080
 
1064
1081
  每轮结束显示完成时间、总耗时、首响应(含思考首字)、正文等待、模型与工具耗时,以及本轮所有模型调用的
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zen-gitsync",
3
- "version": "2.17.7",
3
+ "version": "2.17.8",
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",
@@ -93,7 +93,7 @@ const STRINGS = {
93
93
  bannerCwd: '目录',
94
94
  thinkingLabel: '思考',
95
95
  answerLabel: '回答',
96
- thinkingHint: '… 预览已收起,/think full 显示后续完整思考',
96
+ thinkingHint: '… 已显示前 12 行;输入 /think full 开启后续完整思考',
97
97
  emptyResponse: '模型没有返回回答,请重试或切换模型。',
98
98
  chars: '字符',
99
99
  imagePasting: '正在读取剪贴板图片…',
@@ -142,7 +142,7 @@ const STRINGS = {
142
142
  bannerCwd: 'CWD',
143
143
  thinkingLabel: 'Thinking',
144
144
  answerLabel: 'Answer',
145
- thinkingHint: '… preview hidden; /think full for future reasoning',
145
+ thinkingHint: '… first 12 lines shown; enter /think full for future reasoning',
146
146
  emptyResponse: 'The model returned no answer. Retry or switch models.',
147
147
  chars: 'chars',
148
148
  imagePasting: 'Reading clipboard image…',
@@ -346,7 +346,9 @@ function printSlashHelp(t, locale) {
346
346
  ' /addmodel 添加新的模型配置(交互式向导)',
347
347
  ' /cd <路径> 切换智能体工作目录',
348
348
  ' /image [路径] 查看待发送图片;/image <路径> 附加本地图片;/image clear 清除',
349
- ' /think [模式] compact 预览 / full 完整 / off 隐藏思考',
349
+ ' /think full 完整显示后续思考(默认,模型返回的内容)',
350
+ ' /think compact 预览前 12 行思考',
351
+ ' /think off 隐藏思考;/think 切换完整与隐藏',
350
352
  ' /tools [模式] compact 精简 / full 完整工具输出',
351
353
  ' /stats 查看上一轮与会话累计用量、耗时',
352
354
  ' /new 开启新对话',
@@ -364,7 +366,9 @@ function printSlashHelp(t, locale) {
364
366
  ' /addmodel Add a new model (interactive wizard)',
365
367
  ' /cd <path> Change agent working directory',
366
368
  ' /image [path] List pending images; attach a file; /image clear to reset',
367
- ' /think [mode] compact / full / off reasoning display',
369
+ ' /think full Show future thinking returned by the model in full (default)',
370
+ ' /think compact Preview the first 12 thinking lines',
371
+ ' /think off Hide thinking; /think toggles full / hidden',
368
372
  ' /tools [mode] compact / full tool output',
369
373
  ' /stats Last turn timing and session token usage',
370
374
  ' /new Start a new conversation',
@@ -479,14 +483,14 @@ export async function handleSlashCommand(state, input, t) {
479
483
  return 'ok'
480
484
  }
481
485
  if (cmd === '/think') {
482
- const next = arg || (state.showThinking ? 'off' : 'compact')
486
+ const next = arg || (state.showThinking ? 'off' : 'full')
483
487
  if (!['off', 'compact', 'full'].includes(next)) {
484
488
  printWarn('/think compact | full | off')
485
489
  return 'ok'
486
490
  }
487
491
  state.showThinking = next !== 'off'
488
492
  state.thinkingMode = next
489
- printOk(zh ? `思考显示:${({ off: '隐藏', compact: '预览', full: '完整' })[next]}` : `Thinking: ${next}`)
493
+ printOk(zh ? `后续请求的思考显示:${({ off: '隐藏', compact: '预览前 12 行', full: '完整' })[next]}` : `Thinking for future requests: ${next}`)
490
494
  return 'ok'
491
495
  }
492
496
  if (cmd === '/tools') {
@@ -682,7 +686,7 @@ export async function runAiAgent(argv = []) {
682
686
  cancelRequested: false,
683
687
  busy: false,
684
688
  showThinking: true, // /think 切换:是否回显模型的思考过程
685
- thinkingMode: 'compact',
689
+ thinkingMode: 'full',
686
690
  fullTools: false,
687
691
  lastTurnStats: null,
688
692
  sessionStats: null,
@@ -27,7 +27,11 @@ async function fixture(t) {
27
27
  res.writeHead(200, { 'Content-Type': 'text/event-stream' })
28
28
  const send = data => res.write(`data: ${JSON.stringify(data)}\n\n`)
29
29
  if (requests.length === 1) {
30
- send({ choices: [{ delta: { reasoning_content: 'Inspect the requested file.\n' } }] })
30
+ for (let i = 1; i <= 15; i++) {
31
+ for (const chunk of [`Inspection step ${i}: `, 'check the requested file.\n\n']) {
32
+ send({ choices: [{ delta: { reasoning_content: chunk } }] })
33
+ }
34
+ }
31
35
  send({ choices: [{ delta: { tool_calls: [{ index: 0, id: 'read1', type: 'function', function: { name: 'read_file', arguments: '{"path":"demo.txt"}' } }] }, finish_reason: 'tool_calls' }] })
32
36
  send({ choices: [], usage: { prompt_tokens: 100, completion_tokens: 20, total_tokens: 120 } })
33
37
  } else {
@@ -78,6 +82,8 @@ test('CLI uses switched directory, saves metrics and loads local project instruc
78
82
  assert.match(output, /first token/)
79
83
  assert.match(output, /first answer/)
80
84
  assert.match(output, /Token 180/)
85
+ assert.match(output, /Inspection step 15:/)
86
+ assert.doesNotMatch(output, /first 12 lines shown/)
81
87
  assert.equal(f.requests.length, 2)
82
88
  assert.match(f.requests[0].messages[0].content, /Fixture project rule/)
83
89
  assert.ok(f.requests[1].messages.some(m => m.role === 'tool' && m.content.includes('fixture text')))
@@ -91,9 +97,28 @@ test('CLI uses switched directory, saves metrics and loads local project instruc
91
97
  assert.equal(saved.messages.at(-1).content, 'Fixture answer.\n')
92
98
  })
93
99
 
94
- test('one-shot CLI prints the same usage footer', { timeout: 15000 }, async t => {
100
+ test('one-shot CLI shows full thinking by default and prints the usage footer', { timeout: 15000 }, async t => {
95
101
  const f = await fixture(t)
96
102
  const output = await run(f, ['inspect'])
97
103
  assert.match(output, /Token 180/)
98
104
  assert.match(output, /One-shot done/)
105
+ assert.match(output, /Inspection step 15:/)
106
+ assert.doesNotMatch(output, /first 12 lines shown/)
107
+ })
108
+
109
+ test('CLI help lists thinking modes and /think full shows reasoning beyond the preview', { timeout: 15000 }, async t => {
110
+ const f = await fixture(t)
111
+ let stage = 0
112
+ const output = await run(f, [], (stdout, child) => {
113
+ if (stage === 0 && stdout.includes('Zen GitSync')) { stage++; child.stdin.write('/help\n') }
114
+ else if (stage === 1 && stdout.includes('/think compact')) { stage++; child.stdin.write('/think full\n') }
115
+ else if (stage === 2 && stdout.includes('Thinking for future requests: full')) { stage++; child.stdin.write('Read demo.txt\n') }
116
+ else if (stage === 3 && stdout.includes('Token 180')) { stage++; child.stdin.write('/exit\n') }
117
+ })
118
+ assert.equal(stage, 4)
119
+ assert.match(output, /\/think full/)
120
+ assert.match(output, /\/think off/)
121
+ assert.match(output, /Inspection step 15: check the requested file\./)
122
+ assert.doesNotMatch(output, /first 12 lines shown/)
123
+ assert.equal(f.requests.length, 2, 'display commands must not call the model')
99
124
  })
@@ -4,6 +4,7 @@ import { streamChatOnce } from './transport.js'
4
4
  import { normalizeUsage, addUsage } from './telemetry.js'
5
5
  import { runAgentTurn } from './turn.js'
6
6
  import { buildRequestMessages, repairToolHistory } from './context.js'
7
+ import { createAssistantWriter, stripAnsi } from './termui.js'
7
8
 
8
9
  const model = { model: 'test', baseURL: 'https://example.invalid/v1', apiKey: 'test' }
9
10
  const event = data => `data: ${JSON.stringify(data)}\n\n`
@@ -83,6 +84,34 @@ const quiet = { startSpinner: () => ({ stop: noop }), createAssistantWriter: ()
83
84
  printToolHeader: noop, printToolResult: noop, printWarn: noop, printError: noop, printTurnSummary: noop }
84
85
  const state = () => ({ messages: [{ role: 'system', content: 'rules' }], ctx: { cwd: '.' }, abortController: new AbortController(), maxToolIterations: 4 })
85
86
 
87
+ test('turn shows 12 nonblank preview rows, supports full/off, and keeps all reasoning in history', async () => {
88
+ const reasoning = Array.from({ length: 15 }, (_, i) => `Inspection step ${i + 1}.`).join('\n\n')
89
+ for (const mode of ['compact', 'full', 'off']) {
90
+ const s = { ...state(), thinkingMode: mode, showThinking: mode !== 'off' }
91
+ let output = ''
92
+ const stats = await runAgentTurn(s, 'task', strings, [], {
93
+ ui: { ...quiet, createAssistantWriter: options => createAssistantWriter({ ...options, write: text => { output += text } }) },
94
+ chat: async ({ onToken }) => {
95
+ for (const chunk of reasoning) onToken({ thinking: chunk })
96
+ onToken({ content: 'Final answer.' })
97
+ return { content: 'Final answer.', reasoning, toolCalls: [] }
98
+ },
99
+ })
100
+ assert.equal(stats.status, 'completed')
101
+ assert.equal(s.messages.at(-1).reasoning_content, reasoning)
102
+ const text = stripAnsi(output)
103
+ assert.match(text, /Final answer/)
104
+ if (mode === 'compact') {
105
+ assert.match(text, /Inspection step 12\./)
106
+ assert.doesNotMatch(text, /Inspection step 13\./)
107
+ assert.match(text, /\/think full/)
108
+ } else if (mode === 'full') {
109
+ assert.match(text, /Inspection step 15\./)
110
+ assert.doesNotMatch(text, /\/think full/)
111
+ } else assert.doesNotMatch(text, /Inspection step/)
112
+ }
113
+ })
114
+
86
115
  test('turn aggregates all model calls, persists progress and records response timings', async () => {
87
116
  const s = state()
88
117
  let requests = 0, saves = 0
@@ -37,6 +37,7 @@ import { boxenAdaptive } from '../ui.js'
37
37
  // 常量
38
38
  // ──────────────────────────────────────────────
39
39
  export const DISPLAY_RESULT_LIMIT = 600 // 工具结果回显截断长度(完整结果仍进上下文)
40
+ export const THINKING_PREVIEW_LINES = 12
40
41
 
41
42
  // 回显截断的"最小省略量":只超出一两个字符时截断反而碍事(实测:git status
42
43
  // 输出 614 字符被截,省略 14 字符还把路径从中间切断),不值得就不截
@@ -59,12 +60,19 @@ export function stripAnsi(s) {
59
60
  return stripVTControlCharacters(String(s))
60
61
  }
61
62
 
63
+ // Normalize before grapheme segmentation: Intl.Segmenter treats CRLF as one
64
+ // cluster, so checking only for '\n' would leave unindented physical newlines
65
+ // inside a row and incorrectly carry its width into the next output line.
66
+ function normalizeTerminalNewlines(text) {
67
+ return String(text).replace(/\r\n?/g, '\n')
68
+ }
69
+
62
70
  /** Wrap terminal cells, preserving colour and CJK/emoji widths. */
63
71
  export function wrapTerminalText(text, width = 100) {
64
72
  const rows = []
65
73
  let row = '', cells = 0, lastBreak = -1, breakCells = 0
66
74
  const segmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' })
67
- for (const part of String(text).split(/(\x1b\[[0-9;]*m)/g)) {
75
+ for (const part of normalizeTerminalNewlines(text).split(/(\x1b\[[0-9;]*m)/g)) {
68
76
  if (/^\x1b\[/.test(part)) { row += part; continue }
69
77
  for (const { segment } of segmenter.segment(part.replace(/\t/g, ' '))) {
70
78
  if (segment === '\n') { rows.push(row); row = ''; cells = 0; lastBreak = -1; continue }
@@ -143,7 +151,10 @@ export const SLASH_COMMANDS = [
143
151
  { cmd: '/addmodel', descZh: '添加模型配置(向导)', descEn: 'Add a model (wizard)' },
144
152
  { cmd: '/cd', descZh: '切换工作目录', descEn: 'Change working directory' },
145
153
  { cmd: '/image', descZh: '附加 / 查看图片', descEn: 'Attach / list images' },
146
- { cmd: '/think', descZh: '开关思考过程显示', descEn: 'Toggle thinking display' },
154
+ { cmd: '/think', descZh: '切换思考显示:完整 / 隐藏', descEn: 'Toggle full / hidden thinking' },
155
+ { cmd: '/think full', descZh: '完整显示后续思考(默认)', descEn: 'Show future thinking in full (default)' },
156
+ { cmd: '/think compact', descZh: '预览前 12 行思考', descEn: 'Preview the first 12 thinking lines' },
157
+ { cmd: '/think off', descZh: '隐藏思考(不影响模型推理)', descEn: 'Hide thinking (model still reasons)' },
147
158
  { cmd: '/tools', descZh: '切换工具结果:精简 / 完整', descEn: 'Toggle compact / full tool output' },
148
159
  { cmd: '/stats', descZh: '查看耗时与 Token 用量', descEn: 'Show timing and token usage' },
149
160
  { cmd: '/new', descZh: '开启新对话', descEn: 'Start a new chat' },
@@ -155,7 +166,7 @@ export const SLASH_COMMANDS = [
155
166
 
156
167
  /**
157
168
  * 按当前输入过滤斜杠命令(纯函数,便于单测)。
158
- * - 仅当输入以 / 开头、且尚未输入空格(还在敲命令名)时才提示
169
+ * - 输入以 / 开头时提示,支持 /think f 等已知子命令的参数补全
159
170
  * - 前缀匹配,大小写不敏感
160
171
  * - 精确等于某命令且无后续参数时仍然展示该命令(便于确认拼写)
161
172
  * @returns {{cmd:string, desc:string}[]}
@@ -163,8 +174,8 @@ export const SLASH_COMMANDS = [
163
174
  export function filterSlashCommands(input, locale) {
164
175
  const zh = !String(locale || '').startsWith('en')
165
176
  const line = String(input || '')
166
- if (!line.startsWith('/') || /\s/.test(line)) return []
167
- const q = line.toLowerCase()
177
+ if (!line.startsWith('/')) return []
178
+ const q = line.toLowerCase().replace(/\s+/g, ' ')
168
179
  return SLASH_COMMANDS
169
180
  .filter((c) => c.cmd.startsWith(q))
170
181
  .map((c) => ({ cmd: c.cmd, desc: zh ? c.descZh : c.descEn }))
@@ -184,9 +195,10 @@ export function renderSlashHintBody(matches, selectedIndex = -1, locale = 'zh-CN
184
195
  : -1
185
196
  const zh = !String(locale || '').startsWith('en')
186
197
  const lines = []
198
+ const commandWidth = Math.max(12, ...matches.map(m => String(m.cmd).length))
187
199
  for (let i = 0; i < matches.length; i++) {
188
200
  const m = matches[i]
189
- const command = String(m.cmd).padEnd(12)
201
+ const command = String(m.cmd).padEnd(commandWidth)
190
202
  if (i === sel) {
191
203
  lines.push(`${chalk.cyanBright('›')} ${chalk.cyan(command)} ${chalk.white(m.desc)}`)
192
204
  } else {
@@ -369,9 +381,9 @@ export function drawInputBottom(write = (s) => process.stdout.write(s)) {
369
381
  // 首个 token 到达前转动;到达后 stop() 清行,接着流式输出。
370
382
  // 非 TTY 时 ora 自动退化为只打印一次文本。
371
383
 
372
- export function startSpinner(text) {
384
+ export function startSpinner(text, { spinnerFactory = ora } = {}) {
373
385
  const started = performance.now()
374
- const spinner = ora({
386
+ const spinner = spinnerFactory({
375
387
  // 琥珀色加粗,与思考内容同色系,dim 太浅看不清
376
388
  text: chalk.hex('#e8a33d').bold(text),
377
389
  spinner: 'dots',
@@ -389,8 +401,13 @@ export function startSpinner(text) {
389
401
  spinner.text = chalk.hex('#e8a33d')(text) + chalk.gray(` · ${formatDuration(performance.now() - started)}`)
390
402
  }, 100) : null
391
403
  timer?.unref()
404
+ let stopped = false
392
405
  return {
393
406
  stop() {
407
+ // ora.stop() moves the cursor even after it has stopped. Repeated calls
408
+ // during streaming would overwrite the text already on the current row.
409
+ if (stopped) return
410
+ stopped = true
394
411
  clearInterval(timer)
395
412
  spinner.stop()
396
413
  },
@@ -527,10 +544,10 @@ export function createAssistantWriter({
527
544
  write('\n' + THINK_ICON + ' ' + chalk.hex('#e8a33d').bold(thinkingHeader) + '\n')
528
545
  thinkAtLineStart = true
529
546
  }
530
- // 按换行切段,整段着色(避免逐字符 escape 刷屏);每逢行首补一层缩进,
531
- // 使多行思考整体右移成独立子块。流式 token 可能不以换行结尾,
532
- // 故用 thinkAtLineStart 记住跨调用的行首状态。
547
+ // 保留跨分片的行宽和缩进;预览合并空白行,让行数用于实际内容。
548
+ // 完整模式仍保留模型返回的空行。
533
549
  for (const char of text) {
550
+ if (Number.isFinite(thinkingLimit) && char === '\n' && thinkAtLineStart) continue
534
551
  if (thinkingRows >= thinkingLimit) {
535
552
  clipThinking()
536
553
  break
@@ -692,7 +709,7 @@ export function printToolHeader(name, summary, write = (s) => process.stdout.wri
692
709
  * @param {number} [durationMs] - 执行耗时(毫秒),有值时在结果末尾追加 ⏱ 计时行
693
710
  */
694
711
  export function printToolResult(result, write = (s) => process.stdout.write(s), durationMs, { full = false, locale = 'zh-CN', width = Math.min(100, termWidth() - 6) } = {}) {
695
- const text = stripAnsi(result)
712
+ const text = normalizeTerminalNewlines(stripAnsi(result))
696
713
  // 先把 "$ <command>\n" 这一行回显从展示里剥掉(退出码仍在第二行里识别)
697
714
  const visible = text.replace(/^\$[^\n]*\n/, '').replace(/^\(exit \d+\)\n?/, '')
698
715
  const exitMatch = text.match(/^\$[^\n]*\n\(exit (\d+)\)/)
@@ -18,9 +18,11 @@
18
18
  import { test } from 'node:test'
19
19
  import assert from 'node:assert/strict'
20
20
  import chalk from 'chalk'
21
+ import ora from 'ora'
22
+ import stringWidth from 'string-width'
21
23
  import {
22
24
  stripAnsi, truncateDisplay, summarizeToolArgs,
23
- createAssistantWriter, printToolHeader, printToolResult,
25
+ createAssistantWriter, printToolHeader, printToolResult, startSpinner,
24
26
  formatDuration, wrapTerminalText, renderTurnSummary,
25
27
  filterSlashCommands, renderSlashHintBody, parseKeyForSlashHint, SLASH_COMMANDS,
26
28
  renderSelectableListBody, parseKeyForSelectableList,
@@ -87,6 +89,35 @@ test('summarizeToolArgs: 未知工具回退 JSON,缺参数不炸', () => {
87
89
  })
88
90
 
89
91
  // ── createAssistantWriter ──
92
+ test('TTY spinner stops once without moving the cursor over later streamed text', t => {
93
+ const c = collect()
94
+ const cursorOperations = []
95
+ const stream = {
96
+ isTTY: true, columns: 100,
97
+ write: c.write,
98
+ cursorTo: column => cursorOperations.push(['column', column]),
99
+ moveCursor: (x, y) => cursorOperations.push(['move', x, y]),
100
+ clearLine: direction => cursorOperations.push(['clear', direction]),
101
+ }
102
+ const spinner = startSpinner('waiting', {
103
+ spinnerFactory: options => ora({ ...options, stream, isEnabled: true, hideCursor: false }),
104
+ })
105
+ t.after(() => spinner.stop())
106
+ spinner.stop()
107
+ const stoppedOperations = cursorOperations.length
108
+ assert.ok(stoppedOperations > 0, 'must exercise the real TTY cursor operations')
109
+ const writer = createAssistantWriter({ write: c.write })
110
+ for (const chunk of ['先检查 ', 'nginx ', '配置文件。\n', '再确认 ', '项目目录。']) {
111
+ spinner.stop()
112
+ writer.writeThinking(chunk)
113
+ }
114
+ spinner.stop()
115
+ writer.finish()
116
+ assert.equal(cursorOperations.length, stoppedOperations, 'later chunks must not reset the terminal cursor')
117
+ assert.match(c.text(), /先检查 nginx 配置文件。\n/)
118
+ assert.match(c.text(), /再确认 项目目录。/)
119
+ })
120
+
90
121
  test('writer: 思考段头部只打印一次,内容直写', () => {
91
122
  const c = collect()
92
123
  const w = createAssistantWriter({ thinkingHeader: '✻ 思考', write: c.write })
@@ -219,6 +250,35 @@ test('printToolResult: 全部行统一 │ 槽线(无 └─ 拐角)', () => {
219
250
  assert.ok(!lines.some(l => l.includes('└─')), '不应出现 └─ 拐角字符')
220
251
  })
221
252
 
253
+ test('Windows command output renders like LF output in compact and full modes', () => {
254
+ const names = ['.claude', '.git', '.gitattributes', '.github', '.gitignore', '.npmignore',
255
+ 'AGENTS.md', 'docs', 'index.js', 'package.json', 'README.md', '源码笔记.md']
256
+ for (const full of [false, true]) {
257
+ const lf = collect(), windows = collect()
258
+ printToolResult(['$ dir /a /b', '(exit 0)', ...names].join('\n'), lf.write, 38, { full, width: 60 })
259
+ printToolResult(['$ dir /a /b', '(exit 0)', ...names].join('\r\n'), windows.write, 38, { full, width: 60 })
260
+ assert.equal(windows.text(), lf.text(), 'line endings must not change layout or the omitted row count')
261
+ assert.doesNotMatch(windows.text(), /\r/)
262
+ const rows = windows.lines().filter(Boolean)
263
+ assert.ok(rows.slice(0, -1).every(row => row.startsWith(' │ ')))
264
+ assert.match(rows.at(-1), /^ └ 完成 · 38ms · exit 0$/)
265
+ if (full) assert.deepEqual(rows.slice(0, -1), names.map(name => ' │ ' + name))
266
+ else assert.match(windows.text(), /回显省略 7 行/)
267
+ }
268
+ })
269
+
270
+ test('wrapped Windows paths keep every continuation row to the right of the gutter', () => {
271
+ const c = collect()
272
+ const paths = ['C:\\workspace\\项目目录\\src\\components\\DirectorySelector.vue',
273
+ 'C:\\workspace\\项目目录\\docs\\终端显示🙂.md']
274
+ printToolResult(paths.join('\r\n'), c.write, undefined, { full: true, width: 24 })
275
+ const rows = c.lines().filter(Boolean)
276
+ assert.ok(rows.every(row => row.startsWith(' │ ')))
277
+ assert.ok(rows.every(row => stringWidth(row) <= 28), 'include the gutter when checking physical width')
278
+ assert.equal(rows.map(row => row.slice(4)).join(''), paths.join(''), 'preserve every path character')
279
+ assert.doesNotMatch(c.text(), /\r/)
280
+ })
281
+
222
282
  test('printToolResult: 超长结果被截断并含省略标记', () => {
223
283
  const c = collect()
224
284
  const long = Array.from({ length: 60 }, (_, i) => `line${i} ` + 'y'.repeat(40)).join('\n')
@@ -297,6 +357,28 @@ test('thinking previews stop after their row budget and still render the answer'
297
357
  assert.match(c.text(), /answer/)
298
358
  })
299
359
 
360
+ test('compact thinking uses its row budget for content even across blank chunks', () => {
361
+ const c = collect()
362
+ const writer = createAssistantWriter({ write: c.write, thinkingLimit: 2 })
363
+ for (const chunk of ['\n\n', 'first', '\n\n', '\nsecond\n', 'third']) writer.writeThinking(chunk)
364
+ writer.finish()
365
+ assert.match(c.text(), /first\n/)
366
+ assert.match(c.text(), /second\n/)
367
+ assert.doesNotMatch(c.text(), /third/)
368
+ assert.equal((c.text().match(/\/think full/g) || []).length, 1)
369
+ })
370
+
371
+ test('full thinking preserves paragraph breaks and text after the preview budget', () => {
372
+ const c = collect()
373
+ const writer = createAssistantWriter({ write: c.write })
374
+ const content = Array.from({ length: 20 }, (_, i) => `line ${i}`).join('\n\n')
375
+ for (const character of content) writer.writeThinking(character)
376
+ writer.finish()
377
+ assert.match(c.text(), /line 0\n\n/)
378
+ assert.match(c.text(), /line 19/)
379
+ assert.doesNotMatch(c.text(), /\/think full/)
380
+ })
381
+
300
382
  test('terminal wrapping respects wide glyphs and preserves text', () => {
301
383
  const text = '中文🙂中文abcdef'
302
384
  const rows = wrapTerminalText(chalk.cyan(text), 6)
@@ -304,6 +386,11 @@ test('terminal wrapping respects wide glyphs and preserves text', () => {
304
386
  assert.ok(rows.length >= 3)
305
387
  })
306
388
 
389
+ test('terminal wrapping recognizes CRLF and bare carriage returns as line breaks', () => {
390
+ const text = chalk.cyan('first\r\n第二行🙂\nthird\rfourth\r\n')
391
+ assert.deepEqual(wrapTerminalText(text, 20).map(stripAnsi), ['first', '第二行🙂', 'third', 'fourth', ''])
392
+ })
393
+
307
394
  test('turn summaries distinguish missing and partial usage, and show cancellation', () => {
308
395
  const stats = { status: 'cancelled', totalMs: 2000, llmMs: 1800, toolsMs: 100,
309
396
  firstTokenMs: 200, firstAnswerMs: 500, requests: 2, toolCalls: 1, usageRequests: 0, usage: null }
@@ -334,11 +421,24 @@ test('filterSlashCommands: 前缀过滤,大小写不敏感', () => {
334
421
  assert.deepEqual(filterSlashCommands('/exit', 'zh').map(m => m.cmd), ['/exit'])
335
422
  })
336
423
 
337
- test('filterSlashCommands: 已输入空格(进入参数)时不再提示', () => {
424
+ test('filterSlashCommands: 自由输入的参数不再提示', () => {
338
425
  assert.deepEqual(filterSlashCommands('/model 2', 'zh'), [])
339
426
  assert.deepEqual(filterSlashCommands('/cd ..', 'zh'), [])
340
427
  })
341
428
 
429
+ test('thinking modes are discoverable from slash menu and complete after a space', () => {
430
+ const modes = ['/think full', '/think compact', '/think off']
431
+ const all = filterSlashCommands('/', 'zh').map(m => m.cmd)
432
+ assert.ok(modes.every(mode => all.includes(mode)))
433
+ assert.deepEqual(filterSlashCommands('/think ', 'zh').map(m => m.cmd), modes)
434
+ assert.deepEqual(filterSlashCommands('/think f', 'zh').map(m => m.cmd), ['/think full'])
435
+ assert.deepEqual(filterSlashCommands('/THINK C', 'en').map(m => m.cmd), ['/think compact'])
436
+ assert.deepEqual(filterSlashCommands('/think full ', 'zh'), [], 'completed command must be ready to submit')
437
+ const lines = stripAnsi(renderSlashHintBody(filterSlashCommands('/think ', 'zh'))).split('\n')
438
+ assert.match(lines[0], /\/think full\s+完整/)
439
+ assert.match(lines[1], /\/think compact\s+预览前 12 行/)
440
+ })
441
+
342
442
  test('filterSlashCommands: 非 slash 输入 / 无匹配返回空', () => {
343
443
  assert.deepEqual(filterSlashCommands('hello', 'zh'), [])
344
444
  assert.deepEqual(filterSlashCommands('', 'zh'), [])
@@ -38,7 +38,7 @@ export async function runAgentTurn(state, userText, t, images = [], dependencies
38
38
  showThinking: state.showThinking !== false,
39
39
  thinkingHeader: t.thinkingLabel,
40
40
  answerHeader: t.answerLabel,
41
- thinkingLimit: state.thinkingMode === 'full' ? Infinity : 4,
41
+ thinkingLimit: state.thinkingMode === 'compact' ? terminal.THINKING_PREVIEW_LINES : Infinity,
42
42
  thinkingHint: t.thinkingHint,
43
43
  })
44
44
  const filter = createThinkFilter()
@@ -0,0 +1,5 @@
1
+ import"./rolldown-runtime-C0FnF6B9.js";import{$n as e,At as t,Bn as n,Dn as r,En as i,Er as a,G as o,Gt as s,In as c,Nn as l,On as u,Pn as d,Pr as f,Qr as p,Tn as m,Wn as h,Y as g,Yr as _,Zn as v,_n as ee,a as y,bn as te,br as b,cn as ne,cr as x,gt as re,i as S,kn as C,lr as w,o as ie,or as T,pr as E,qr as D,tt as ae,un as O,ut as oe,vr as k,xn as A,zt as se}from"./element-plus-KTmRV-so.js";import{i as ce,n as j,t as M}from"./vendor-dO4B7wQf.js";import{n as N,t as P}from"./_plugin-vue_export-helper-C8OE7U9H.js";import{t as F}from"./configStore-CuMElTgV.js";E();var I=65536;function L(e){if(!e||!Array.isArray(e.messages))return[];let t=[],n=e.messages.length;for(let r=0;r<n;r++){let i=e.messages[r];if(i.role!==`system`){if(i.role===`user`){let e=``,n;if(typeof i.content==`string`)e=i.content;else if(Array.isArray(i.content)){e=i.content.filter(e=>e?.type===`text`).map(e=>e.text||``).join(` `);let t=i.content.filter(e=>e?.type===`image_url`&&typeof e.image_url?.url==`string`).map((e,t)=>({id:`att-${r}-${t}`,name:`image`,size:0,type:B(e.image_url?.url||``)||`image/*`,preview:e.image_url?.url}));t.length>0&&(n=t)}(e||n)&&t.push({id:`msg-${r}`,role:`user`,content:e,attachments:n,status:`done`,createdAt:Date.now()})}else if(i.role===`assistant`){let a=``;typeof i.content==`string`&&(a=i.content),a.length>I&&(a=`…(前文已截断)\n${a.slice(-65536)}`);let o=[];if(i.tool_calls&&i.tool_calls.length>0)for(let e of i.tool_calls)o.push({id:e.id||e.function?.name||M(),name:e.function?.name||``,arguments:e.function?.arguments||``,argsPreview:R(e.function?.name||``,e.function?.arguments||``),status:`done`,result:``});let s=r+1;for(;s<n&&e.messages[s].role===`tool`;){let t=e.messages[s],n=t.tool_call_id||t.name||``,i=o.find(e=>e.id===n);if(i){let e=t.content;typeof e!=`string`&&(e=`(non-text result)`),e.length>I&&(e=`…(前文已截断)\n${e.slice(-65536)}`),i.result=e}r=s,s++}let c=!!a,l=o.length>0;(c||l)&&t.push({id:`msg-${r}`,role:`assistant`,content:a||``,toolCalls:l?o:void 0,status:`done`,createdAt:Date.now()})}}}return t}function R(e,t){try{let n=JSON.parse(t);switch(e){case`run_command`:return String(n.command||``).slice(0,200);case`read_file`:case`write_file`:case`edit_file`:return String(n.path||``);case`list_files`:return String(n.path||`.`);case`search_text`:return String(n.pattern||``);default:return JSON.stringify(n).slice(0,200)}}catch{return t.slice(0,200)}}function z(e){return new Promise((t,n)=>{let r=new FileReader;r.onload=()=>t(String(r.result||``)),r.onerror=()=>n(r.error||Error(`FileReader failed`)),r.readAsDataURL(e)})}function B(e){let t=/^data:([^;,]+)/.exec(e);return t?t[1]:``}function V(){let e=F(),t=b([]),n=b(!1),r=b(null),i=b([]),a=b(!1),o=b(!1),s=null,c=0,l=0;async function u(){let r=++l,i=e.currentDirectory||``;if(!i){t.value=[],n.value=!1;return}n.value=!0;try{let e=`/api/agent/sessions?cwd=${encodeURIComponent(i)}`,n=await fetch(e).then(e=>e.json());if(r!==l)return;if(!n.success){y.error(n.error||N(`@AGENT:加载会话列表失败`));return}t.value=Array.isArray(n.sessions)?n.sessions:[]}catch(e){if(r!==l)return;y.error(N(`@AGENT:加载会话列表失败`)+`: `+(e?.message||e))}finally{r===l&&(n.value=!1)}}function d(n,r){let i=new Date().toISOString(),a=t.value.find(e=>e.sessionId===n);if(a){a.isGenerating=!0,a.updatedAt=i;return}t.value=[{sessionId:n,title:r,source:`web`,cwd:e.currentDirectory||``,model:``,createdAt:i,updatedAt:i,messageCount:1,size:0,isGenerating:!0},...t.value]}function f(e){let n=t.value.find(t=>t.sessionId===e);n&&(n.isGenerating=!1)}async function p(e){o.value=!0,r.value=e;try{let t=await fetch(`/api/agent/sessions/${encodeURIComponent(e)}`).then(e=>e.json());if(!t.success){y.error(t.error||N(`@AGENT:加载会话失败`));return}i.value=L(t.session)}catch(e){y.error(N(`@AGENT:加载会话失败`)+`: `+(e?.message||e))}finally{o.value=!1}}async function m(e){try{await S.confirm(N(`@AGENT:确认删除该会话`),N(`@AGENT:删除后无法恢复`),{type:`warning`})}catch{return}try{let n=await fetch(`/api/agent/sessions/${encodeURIComponent(e)}`,{method:`DELETE`}).then(e=>e.json());if(!n.success){y.error(n.error||N(`@AGENT:删除失败`));return}t.value=t.value.filter(t=>t.sessionId!==e),r.value===e&&(r.value=null,i.value=[]),y.success(N(`@AGENT:已删除`))}catch(e){y.error(N(`@AGENT:删除失败`)+`: `+(e?.message||e))}}async function h(e,n){try{let r=await fetch(`/api/agent/sessions/${encodeURIComponent(e)}`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify({title:n})}).then(e=>e.json());if(!r.success){y.error(r.error||N(`@AGENT:重命名失败`));return}let i=t.value.find(t=>t.sessionId===e);i&&(i.title=n),y.success(N(`@AGENT:已重命名`))}catch(e){y.error(N(`@AGENT:重命名失败`)+`: `+(e?.message||e))}}function g(){r.value=null,i.value=[],t.value=t.value.filter(e=>!e.isGenerating)}async function _(t,n=[]){let o=n.filter(e=>e?.file?.type?.startsWith(`image/`));if(o.length<n.length&&y.warning(N(`@AGENT:仅支持发送图片,非图片文件已忽略`)),!t.trim()&&o.length===0||a.value)return;let l=(await Promise.allSettled(o.map(e=>z(e.file)))).map(e=>e.status===`fulfilled`?e.value:``).filter(e=>e.startsWith(`data:image/`)),p=++c;s=new AbortController;let m=s,h={id:M(),role:`user`,content:t,status:`done`,createdAt:Date.now()};o.length>0&&(h.attachments=o.map(e=>({id:e.id,name:e.file.name||`image`,size:e.file.size||0,type:e.file.type||``,preview:e.preview}))),i.value.push(h);let g=k({id:M(),role:`assistant`,content:``,status:`pending`,createdAt:Date.now()});i.value.push(g),a.value=!0;let _=[],v=null;try{let n=await fetch(`/api/agent/chat`,{method:`POST`,headers:{"Content-Type":`application/json`,Accept:`text/event-stream`},body:JSON.stringify({sessionId:r.value||``,userMessage:t,cwd:e.currentDirectory||``,...l.length>0?{images:l}:{}}),signal:m.signal});if(p!==c)return;if(!n.ok||!n.body){let e=await n.text().catch(()=>``);throw Error(e||`HTTP ${n.status}`)}let a=n.body.getReader(),o=new TextDecoder(`utf-8`),s=``,f=r.value;for(;;){let{value:e,done:n}=await a.read();if(p!==c)return;if(n)break;s+=o.decode(e,{stream:!0});let l=s.split(`
2
+ `);s=l.pop()??``;for(let e of l){let n=e.trim();if(!n.startsWith(`data:`))continue;let i=n.slice(5).trim();if(!i)continue;let a;try{a=JSON.parse(i)}catch{continue}switch(a.type){case`meta`:if(a.sessionId){f=a.sessionId,v=a.sessionId,r.value=a.sessionId;let e=String(a.title||``).trim()||t.trim().split(`
3
+ `)[0].trim().slice(0,40)||N(`@AGENT:无标题`);d(a.sessionId,e)}break;case`thinking`:g.reasoning||(g.reasoning=``,g.reasoningStatus=`streaming`),g.reasoning+=String(a.delta||``),g.status=`streaming`;break;case`content`:g.status===`pending`&&(g.status=`streaming`);{let e=String(a.delta||``);g.content+=e;let t=j(g.content);t.reasoning&&(g.reasoning||(g.reasoning=``,g.reasoningStatus=`streaming`),g.reasoning=t.reasoning,g.content=t.content)}break;case`tool_call_start`:{let e=k({id:a.toolCallId||M(),name:a.name||``,argsPreview:a.argsPreview||``,arguments:a.argsPreview||``,status:`running`,result:``});_.push(e),g.toolCalls||=[],g.toolCalls.push(e),g.status===`pending`&&(g.status=`streaming`);break}case`tool_result`:{let e=_.find(e=>e.id===a.toolCallId);e&&(e.result=String(a.result||``),e.status=`done`);break}case`done`:{let e=a.content||g.content,t=j(e);t.reasoning&&!g.reasoning&&(g.reasoning=t.reasoning),g.content=t.content,g.status=`done`,g.reasoningStatus===`streaming`&&(g.reasoningStatus=`done`);break}case`error`:g.status=`error`,g.error=String(a.error||N(`@AGENT:未知错误`)),g.reasoningStatus===`streaming`&&(g.reasoningStatus=`done`)}}i.value=[...i.value]}if(p!==c)return;g.status===`pending`&&(g.status=`done`),f&&f!==r.value&&(r.value=f),u().catch(()=>{})}catch(e){if(p!==c)return;e?.name===`AbortError`||m.signal.aborted?(g.content=(g.content||``)+`
4
+
5
+ [`+N(`@AGENT:已停止`)+`]`,g.status=`done`,g.reasoningStatus===`streaming`&&(g.reasoningStatus=`done`)):(g.status=`error`,g.error=e?.message||String(e),y.error(g.error||N(`@AGENT:对话失败`)))}finally{p===c&&(a.value=!1,s=null,v&&f(v))}}function v(){s&&=(s.abort(),null)}return{sessions:t,sessionsLoading:n,currentSessionId:r,messages:i,isStreaming:a,sessionLoading:o,loadSessions:u,loadSession:p,deleteSession:m,renameSession:h,newSession:g,sendMessage:_,stop:v}}n(),E(),f(),O();var H={class:`agent-view`},U={class:`sidebar-header`},W={class:`sidebar-search`},G=[`placeholder`],le={class:`session-list`},ue={key:0,class:`empty-state`},de={class:`empty-text`},fe={key:0,class:`empty-hint`},pe=[`onClick`],me={class:`session-item-main`},he={class:`session-item-title`},K={class:`session-item-meta`},ge={key:0,class:`meta-generating`},_e={class:`meta-time`},ve={class:`meta-count`},ye={key:2,class:`meta-source cli`},be=[`onClick`],xe=[`onClick`],Se={class:`agent-chat-area`},Ce={key:0,class:`chat-loading`},we={key:0,class:`stop-button-bar`},q=P(c({__name:`AgentView`,setup(n){let c=F(),{sessions:f,sessionsLoading:E,currentSessionId:O,messages:k,isStreaming:j,sessionLoading:M,loadSessions:P,loadSession:I,deleteSession:L,renameSession:R,newSession:z,sendMessage:B,stop:q}=V(),Te=m(()=>{let e=c.theme;return e===`dark`||e===`auto`&&window.matchMedia(`(prefers-color-scheme: dark)`).matches?`dark`:`light`}),J=b(``),Y=m(()=>{if(!J.value.trim())return f.value;let e=J.value.toLowerCase();return f.value.filter(t=>(t.title||``).toLowerCase().includes(e)||(t.model||``).toLowerCase().includes(e))}),Ee=m(()=>[{id:`p1`,label:N(`@AGENT:查看项目结构`),prompt:N(`@AGENT:prompt_p1`)},{id:`p2`,label:N(`@AGENT:分析代码质量`),prompt:N(`@AGENT:prompt_p2`)},{id:`p3`,label:N(`@AGENT:帮我写测试`),prompt:N(`@AGENT:prompt_p3`)},{id:`p4`,label:N(`@AGENT:Git 状态检查`),prompt:N(`@AGENT:prompt_p4`)},{id:`p5`,label:N(`@AGENT:帮我启动项目`),prompt:N(`@AGENT:prompt_p5`)}]);async function De(e){await B(e.text,e.files),await h(),Z()}async function Oe(e){await B(e.prompt),await h(),Z()}let X=b(null);function Z(e=!0){X.value?.scrollToBottom(e)}T(()=>k.value.length,()=>{h(()=>Z(!1))});function ke(e){if(j.value){y.warning(N(`@AGENT:请先停止当前生成`));return}I(e)}function Ae(){if(j.value){y.warning(N(`@AGENT:请先停止当前生成`));return}z()}function je(e,t){if(t.stopPropagation(),j.value&&O.value===e){y.warning(N(`@AGENT:请先停止当前生成`));return}L(e)}async function Me(e,t,n){n.stopPropagation();try{let{value:n}=await S.prompt(N(`@AGENT:请输入新的会话标题`),N(`@AGENT:重命名会话`),{inputValue:t,inputPattern:/.+/,inputErrorMessage:N(`@AGENT:标题不能为空`)});n&&n!==t&&await R(e,n)}catch{}}function Ne(e){if(!e)return``;try{let t=new Date(e);if(Number.isNaN(t.getTime()))return e;let n=new Date,r=n.getTime()-t.getTime(),i=e=>String(e).padStart(2,`0`);if(t.toDateString()===n.toDateString())return`${i(t.getHours())}:${i(t.getMinutes())}`;if(r<6048e5){let e=Math.floor(r/864e5);return e===0?N(`@AGENT:今天`):`${e}${N(`@AGENT:天前`)}`}return`${t.getFullYear()}-${i(t.getMonth()+1)}-${i(t.getDate())}`}catch{return e}}let Q=b(280),$=b(!1);function Pe(e){$.value=!0;let t=e.clientX,n=Q.value,r=e=>{let r=e.clientX-t,i=Math.max(200,Math.min(500,n+r));Q.value=i},i=()=>{$.value=!1,document.removeEventListener(`mousemove`,r),document.removeEventListener(`mouseup`,i)};document.addEventListener(`mousemove`,r),document.addEventListener(`mouseup`,i)}return T(()=>[c.currentDirectory,j.value],async([e,t],n)=>{let r=!n||e!==n[0],i=!!n&&n[1]===!0&&!t;(r||i)&&await P(),O.value&&!j.value&&!f.value.some(e=>e.sessionId===O.value)&&z()},{immediate:!0}),(n,c)=>{let f=ie;return v(),C(`div`,H,[i(`aside`,{class:`agent-sidebar`,style:_({width:Q.value+`px`})},[i(`div`,U,[i(`button`,{class:`new-session-btn`,onClick:Ae},[d(a(g),null,{default:x(()=>[d(a(se))]),_:1}),i(`span`,null,p(a(N)(`@AGENT:新建会话`)),1)])]),i(`div`,W,[d(a(g),{class:`search-icon`},{default:x(()=>[d(a(s))]),_:1}),w(i(`input`,{"onUpdate:modelValue":c[0]||=e=>J.value=e,placeholder:a(N)(`@AGENT:搜索会话...`),class:`search-input`,type:`text`},null,8,G),[[ee,J.value]])]),w((v(),C(`div`,le,[Y.value.length===0&&!a(E)?(v(),C(`div`,ue,[d(a(g),{size:28,color:`var(--text-tertiary)`},{default:x(()=>[d(a(ae))]),_:1}),i(`div`,de,p(J.value?a(N)(`@AGENT:未找到匹配的会话`):a(N)(`@AGENT:暂无会话`)),1),J.value?u(``,!0):(v(),C(`div`,fe,p(a(N)(`@AGENT:点击上方按钮开始对话`)),1))])):u(``,!0),(v(!0),C(A,null,e(Y.value,e=>(v(),C(`div`,{key:e.sessionId,class:D([`session-item`,{active:a(O)===e.sessionId}]),onClick:t=>ke(e.sessionId)},[i(`div`,me,[i(`div`,he,p(e.title||a(N)(`@AGENT:无标题`)),1),i(`div`,K,[e.isGenerating?(v(),C(`span`,ge,[d(a(g),{class:`is-loading`},{default:x(()=>[d(a(t))]),_:1}),l(` `+p(a(N)(`@AGENT:正在生成中...`)),1)])):(v(),C(A,{key:1},[i(`span`,_e,p(Ne(e.updatedAt)),1),c[3]||=i(`span`,{class:`meta-dot`},`·`,-1),i(`span`,ve,p(e.messageCount)+` `+p(a(N)(`@AGENT:条`)),1)],64)),e.source===`cli`?(v(),C(`span`,ye,`CLI`)):u(``,!0)])]),i(`div`,{class:`session-item-actions`,onClick:c[1]||=te(()=>{},[`stop`])},[d(a(o),{content:a(N)(`@AGENT:重命名`),placement:`top`,"show-after":500},{default:x(()=>[i(`button`,{class:`item-action-btn`,onClick:t=>Me(e.sessionId,e.title,t)},[d(a(g),null,{default:x(()=>[d(a(re))]),_:1})],8,be)]),_:2},1032,[`content`]),d(a(o),{content:a(N)(`@AGENT:删除`),placement:`top`,"show-after":500},{default:x(()=>[i(`button`,{class:`item-action-btn danger`,onClick:t=>je(e.sessionId,t)},[d(a(g),null,{default:x(()=>[d(a(oe))]),_:1})],8,xe)]),_:2},1032,[`content`])])],10,pe))),128))])),[[f,a(E)]])],4),i(`div`,{class:D([`sidebar-resizer`,{active:$.value}]),onMousedown:Pe},null,34),i(`main`,Se,[a(M)?(v(),C(`div`,Ce,[d(a(g),{class:`is-loading`,size:32},{default:x(()=>[d(a(t))]),_:1}),i(`span`,null,p(a(N)(`@AGENT:加载会话中...`)),1)])):(v(),r(a(ce),{key:1,ref_key:`chatContainerRef`,ref:X,messages:a(k),"preset-questions":Ee.value,"welcome-title":a(N)(`@AGENT:智能体助手`),"welcome-description":a(N)(`@AGENT:我可以帮你阅读代码、执行命令、修改文件。选择下方话题或直接输入你的问题。`),"assistant-name":`g ai`,theme:Te.value,disabled:a(j),"upload-config":{accept:`image/*`},placeholder:a(j)?a(N)(`@AGENT:正在生成中...`):a(N)(`@AGENT:输入消息,Enter 发送`),onSend:De,onSelect:Oe},null,8,[`messages`,`preset-questions`,`welcome-title`,`welcome-description`,`theme`,`disabled`,`placeholder`])),d(ne,{name:`fade`},{default:x(()=>[a(j)?(v(),C(`div`,we,[i(`button`,{class:`stop-button`,onClick:c[2]||=(...e)=>a(q)&&a(q)(...e)},[d(a(g),null,{default:x(()=>[d(a(t),{class:`is-loading`})]),_:1}),i(`span`,null,p(a(N)(`@AGENT:停止生成`)),1)])])):u(``,!0)]),_:1})])])}}}),[[`__scopeId`,`data-v-36e5117b`]]);export{q as default};
@@ -0,0 +1 @@
1
+ .agent-view[data-v-36e5117b]{background:var(--bg-container);height:100%;min-height:0;display:flex}.agent-sidebar[data-v-36e5117b]{background:var(--bg-panel);border-right:1px solid var(--border-color);flex-direction:column;flex-shrink:0;display:flex;overflow:hidden}.sidebar-header[data-v-36e5117b]{flex-shrink:0;padding:10px 12px}.new-session-btn[data-v-36e5117b]{border:1px dashed var(--border-color);border-radius:var(--radius-md);width:100%;color:var(--text-secondary);cursor:pointer;background:0 0;justify-content:center;align-items:center;gap:6px;padding:8px 12px;font-family:inherit;font-size:13px;transition:all .15s;display:flex}.new-session-btn[data-v-36e5117b]:hover{border-color:var(--color-primary);color:var(--color-primary);background:color-mix(in srgb, var(--color-primary) 6%, transparent)}.sidebar-search[data-v-36e5117b]{flex-shrink:0;align-items:center;gap:6px;padding:0 12px 8px;display:flex}.sidebar-search .search-icon[data-v-36e5117b]{color:var(--text-tertiary);flex-shrink:0}.sidebar-search .search-input[data-v-36e5117b]{color:var(--text-primary);background:0 0;border:none;outline:none;flex:1;padding:4px 0;font-family:inherit;font-size:13px}.sidebar-search .search-input[data-v-36e5117b]::placeholder{color:var(--text-tertiary)}.session-list[data-v-36e5117b]{flex:1;min-height:0;padding:0 6px 8px;overflow-y:auto}.empty-state[data-v-36e5117b]{text-align:center;color:var(--text-tertiary);flex-direction:column;justify-content:center;align-items:center;padding:48px 16px;display:flex}.empty-state .empty-text[data-v-36e5117b]{color:var(--text-secondary);margin-top:10px;font-size:14px}.empty-state .empty-hint[data-v-36e5117b]{margin-top:4px;font-size:12px}.session-item[data-v-36e5117b]{border-radius:var(--radius-sm);cursor:pointer;align-items:flex-start;gap:4px;padding:8px 10px;transition:background .12s;display:flex;position:relative}.session-item[data-v-36e5117b]:hover{background:var(--bg-hover)}.session-item:hover .session-item-actions[data-v-36e5117b]{opacity:1}.session-item.active[data-v-36e5117b]{background:color-mix(in srgb, var(--color-primary) 10%, transparent)}.session-item.active .session-item-title[data-v-36e5117b]{color:var(--color-primary)}.session-item.active[data-v-36e5117b]:before{content:"";background:var(--color-primary);border-radius:0 2px 2px 0;width:3px;height:20px;position:absolute;top:50%;left:0;transform:translateY(-50%)}.session-item-main[data-v-36e5117b]{flex:1;min-width:0}.session-item-title[data-v-36e5117b]{color:var(--text-primary);white-space:nowrap;text-overflow:ellipsis;margin-bottom:2px;font-size:13px;font-weight:500;overflow:hidden}.session-item-meta[data-v-36e5117b]{color:var(--text-tertiary);align-items:center;gap:4px;font-size:11px;display:flex}.session-item-meta .meta-dot[data-v-36e5117b]{opacity:.5}.session-item-meta .meta-generating[data-v-36e5117b]{color:var(--color-primary);align-items:center;gap:3px;display:inline-flex}.session-item-meta .meta-source[data-v-36e5117b]{border-radius:3px;padding:0 4px;font-size:10px;font-weight:600}.session-item-meta .meta-source.cli[data-v-36e5117b]{background:color-mix(in srgb, var(--color-warning) 20%, transparent);color:var(--color-warning)}.session-item-actions[data-v-36e5117b]{opacity:0;flex-shrink:0;gap:2px;transition:opacity .12s;display:flex}.item-action-btn[data-v-36e5117b]{width:24px;height:24px;color:var(--text-tertiary);cursor:pointer;border-radius:var(--radius-xs);background:0 0;border:none;justify-content:center;align-items:center;padding:0;transition:all .12s;display:flex}.item-action-btn[data-v-36e5117b]:hover{background:var(--bg-hover);color:var(--text-secondary)}.item-action-btn.danger[data-v-36e5117b]:hover{color:var(--color-danger);background:color-mix(in srgb, var(--color-danger) 10%, transparent)}.sidebar-resizer[data-v-36e5117b]{cursor:col-resize;background:0 0;flex-shrink:0;width:4px;transition:background .15s}.sidebar-resizer[data-v-36e5117b]:hover,.sidebar-resizer.active[data-v-36e5117b]{background:var(--color-primary);opacity:.3}.agent-chat-area[data-v-36e5117b]{flex-direction:column;flex:1;min-width:0;display:flex;position:relative;overflow:hidden}.chat-loading[data-v-36e5117b]{color:var(--text-tertiary);flex-direction:column;flex:1;justify-content:center;align-items:center;gap:12px;font-size:14px;display:flex}.stop-button-bar[data-v-36e5117b]{z-index:10;position:absolute;bottom:80px;left:50%;transform:translate(-50%)}.stop-button[data-v-36e5117b]{border:1px solid var(--color-danger);background:var(--bg-container);color:var(--color-danger);cursor:pointer;border-radius:20px;align-items:center;gap:6px;padding:6px 16px;font-family:inherit;font-size:13px;transition:all .15s;display:flex;box-shadow:0 2px 8px #0000001f}.stop-button[data-v-36e5117b]:hover{background:var(--color-danger);color:#fff}.fade-enter-active[data-v-36e5117b],.fade-leave-active[data-v-36e5117b]{transition:opacity .2s}.fade-enter-from[data-v-36e5117b],.fade-leave-to[data-v-36e5117b]{opacity:0}[data-theme=dark]{background:var(--bg-panel)}
@@ -1,2 +1,2 @@
1
- import"./rolldown-runtime-C0FnF6B9.js";import{At as e,B as t,Bn as n,D as r,Dn as i,En as a,Er as o,G as s,In as c,Nn as l,On as u,Pn as d,Pr as f,Qr as p,Tn as m,Wn as h,Y as g,Yn as _,Zn as v,a as y,br as b,cr as x,i as S,it as C,kn as w,or as T,pr as E,qn as D,qr as O,rt as k}from"./element-plus-KTmRV-so.js";import{n as A,t as j}from"./_plugin-vue_export-helper-CQjg8MXm.js";async function M(e,t){let n=e.body.getReader(),r=new TextDecoder(`utf-8`),i=``;for(;;){let{done:e,value:a}=await n.read();if(e)break;i+=r.decode(a,{stream:!0});let o=i.split(`
2
- `);i=o.pop()??``;for(let e of o)e.trim()&&t(e)}i.trim()&&t(i)}async function N(){let e=await fetch(`/api/app-version`);return e.ok?e.json():{success:!1,current:``,latest:null,hasUpdate:!1,error:`HTTP ${e.status}`}}async function P(e){let t=await fetch(`/api/app-upgrade`,{method:`POST`});if(!t.ok){let n=`HTTP ${t.status}`;try{let e=await t.json();e?.error&&(n=e.error)}catch{}e({type:`error`,message:n}),e({type:`done`,code:t.status});return}if(!t.body){e({type:`error`,message:`响应无 body`}),e({type:`done`,code:-1});return}await M(t,t=>{try{e(JSON.parse(t))}catch{}})}async function F(){let e=await fetch(`/api/app-restart`,{method:`POST`});if(!e.ok){let t=`HTTP ${e.status}`;try{let n=await e.json();n?.error&&(t=n.error)}catch{}throw Error(t)}if(!e.body)throw Error(`响应无 body`);return new Promise((t,n)=>{let r=!1,i=e=>{r||(r=!0,e())};M(e,e=>{try{let r=JSON.parse(e);r.type===`ready`&&typeof r.port==`number`&&r.port>0?i(()=>t(r.port)):r.type===`timeout`?i(()=>n(Error(`restart.timeout`))):r.type===`error`&&i(()=>n(Error(r.message||`restart.failed`)))}catch{}}).then(()=>{i(()=>n(Error(`restart.stream-closed`)))}).catch(()=>{})})}n(),E(),f();var I={key:3},L={key:4},R={key:5},z={key:0,class:`upgrade-hint`},B={key:0,class:`upgrade-countdown`},V={key:1,class:`upgrade-manual`},H={class:`upgrade-manual__label`},U=`npm install -g zen-gitsync --registry https://registry.npmjs.org/`,W=j(c({__name:`UpgradeDialog`,props:{modelValue:{type:Boolean},logs:{},status:{},countdown:{default:0}},emits:[`update:modelValue`,`retry`,`restart`,`cancel`],setup(n,{emit:s}){let c=n,f=s,m=b(null);T(()=>c.logs,async()=>{await h(),m.value&&(m.value.scrollTop=m.value.scrollHeight)});function _(){c.status!==`running`&&(c.status===`success`&&c.countdown>0&&f(`cancel`),f(`update:modelValue`,!1))}function S(){f(`retry`)}function E(){f(`restart`)}function D(){f(`cancel`),f(`update:modelValue`,!1)}async function j(){try{await navigator.clipboard.writeText(U),y.success(A(`@F13B4:已复制升级命令`))}catch{y.warning(A(`@F13B4:复制失败,请手动复制`))}}return(s,c)=>{let h=g,y=t,b=r;return v(),i(b,{"model-value":n.modelValue,"onUpdate:modelValue":c[0]||=e=>f(`update:modelValue`,e),title:o(A)(`@F13B4:升级`),width:`680px`,"close-on-click-modal":!1,"close-on-press-escape":!1,"show-close":n.status!==`running`,"append-to-body":``,onClose:_},{footer:x(()=>[n.status===`failed`?(v(),i(y,{key:0,type:`primary`,onClick:S},{default:x(()=>[l(p(o(A)(`@F13B4:重试`)),1)]),_:1})):u(``,!0),n.status===`failed`?(v(),i(y,{key:1,onClick:j},{default:x(()=>[l(p(o(A)(`@F13B4:复制升级命令`)),1)]),_:1})):u(``,!0),n.status===`success`?(v(),i(y,{key:2,type:`primary`,onClick:E},{default:x(()=>[l(p(o(A)(`@F13B4:立即重启并刷新`)),1)]),_:1})):u(``,!0),n.status===`success`?(v(),i(y,{key:3,onClick:D},{default:x(()=>[l(p(o(A)(`@F13B4:稍后手动重启`)),1)]),_:1})):u(``,!0),n.status===`running`?(v(),i(y,{key:4,disabled:``},{default:x(()=>[l(p(o(A)(`@F13B4:升级中`))+`... `,1)]),_:1})):u(``,!0)]),default:x(()=>[a(`div`,{class:O([`upgrade-status`,`is-${n.status}`])},[n.status===`running`?(v(),i(h,{key:0,class:`is-loading`},{default:x(()=>[d(o(e))]),_:1})):n.status===`success`?(v(),i(h,{key:1,color:`#67c23a`},{default:x(()=>[d(o(k))]),_:1})):(v(),i(h,{key:2,color:`#f56c6c`},{default:x(()=>[d(o(C))]),_:1})),n.status===`running`?(v(),w(`span`,I,p(o(A)(`@F13B4:升级中`)),1)):n.status===`success`?(v(),w(`span`,L,p(o(A)(`@F13B4:升级完成`)),1)):(v(),w(`span`,R,p(o(A)(`@F13B4:升级失败`)),1))],2),n.status===`success`?(v(),w(`p`,z,[l(p(o(A)(`@F13B4:新版本已全局安装,需要重启服务才能生效`))+` `,1),n.countdown>0?(v(),w(`span`,B,p(o(A)(`@F13B4:{seconds} 秒后自动刷新`,{seconds:n.countdown})),1)):u(``,!0)])):u(``,!0),n.status===`failed`?(v(),w(`p`,V,[a(`span`,H,p(o(A)(`@F13B4:手动升级命令`)),1),a(`code`,{class:`upgrade-manual__cmd`},p(U))])):u(``,!0),a(`pre`,{ref_key:`logEl`,ref:m,class:`upgrade-log`},p(n.logs||o(A)(`@F13B4:等待日志输出`)),513)]),_:1},8,[`model-value`,`title`,`show-close`])}}}),[[`__scopeId`,`data-v-40c190d7`]]);n(),E(),f();var G={class:`app-version-badge`},K=[`aria-label`],q=[`aria-label`],J=[`title`,`aria-label`],Y={class:`upgrade-btn__label`},X={class:`upgrade-btn__version`},Z=`https://github.com/xz333221/zen-gitsync/releases`,Q=5,$=`app-version-check`,ee=36e5,te=j(c({__name:`AppVersionBadge`,setup(e){let t=`2.17.7`,n=b(t),r=m(()=>n.value||t),i=b(null),c=b(!1),l=b(``),f=b(`running`),h=b(0),g=null,C=b(!1);function T(){try{let e=sessionStorage.getItem($);if(!e)return null;let{at:t,data:n}=JSON.parse(e);return Date.now()-t>ee?null:n}catch{return null}}function E(e){try{sessionStorage.setItem($,JSON.stringify({at:Date.now(),data:e}))}catch{}}async function O(e=!1){let t=T();t&&(i.value=t);try{let t=await N();i.value=t,E(t),t.current&&(n.value=t.current),!e&&t.success&&t.hasUpdate&&y({type:`success`,message:A(`@F13B4:发现新版本 {version}`,{version:t.latest??``}),duration:4e3})}catch(t){e||y({type:`warning`,message:A(`@F13B4:检查更新失败 {error}`,{error:t?.message||``})})}}async function k(){if(i.value?.hasUpdate){try{await S.confirm(A(`@F13B4:确定要从 {current} 升级到 {latest} 吗?`,{current:i.value.current,latest:i.value.latest}),A(`@F13B4:升级`),{confirmButtonText:A(`@F13B4:确定升级`),cancelButtonText:A(`@F13B4:取消`),type:`info`})}catch{return}l.value=``,f.value=`running`,c.value=!0,await P(e=>{if(e.type===`done`){if(f.value=e.code===0?`success`:`failed`,e.code===0){try{sessionStorage.removeItem($)}catch{}i.value?.latest&&(n.value=i.value.latest),j()}}else e.message&&(l.value+=e.message)})}}function j(){M(),h.value=Q,g=setInterval(()=>{--h.value,h.value<=0&&(g&&=(clearInterval(g),null),L())},1e3)}function M(){g&&=(clearInterval(g),null),h.value=0}function I(){M()}async function L(){if(M(),!C.value){C.value=!0;try{let e=await F(),{pathname:t,search:n,hash:r}=window.location;window.location.href=`http://localhost:${e}${t}${n}${r}`}catch(e){let t=e?.message||``,n={"restart.timeout":`@F13B4:重启超时`,"restart.stream-closed":`@F13B4:重启失败,请刷新页面重试`,"restart.failed":`@F13B4:重启失败`}[t];y({type:`warning`,message:n?A(n):A(`@F13B4:重启失败 {error}`,{error:t})}),C.value=!1}}}return D(()=>{M()}),_(()=>{O(!0)}),(e,t)=>{let n=s;return v(),w(`div`,G,[d(n,{content:o(A)(`@F13B4:在 GitHub 上查看源码`),placement:`top`,effect:`dark`,"show-after":300},{default:x(()=>[a(`a`,{href:`https://github.com/xz333221/zen-gitsync`,target:`_blank`,rel:`noopener noreferrer`,class:`badge-icon-link`,"aria-label":o(A)(`@F13B4:在 GitHub 上查看源码`)},[...t[1]||=[a(`svg`,{viewBox:`0 0 24 24`,width:`15`,height:`15`,"aria-hidden":`true`,fill:`currentColor`,xmlns:`http://www.w3.org/2000/svg`},[a(`path`,{d:`M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0 1 12 6.844a9.59 9.59 0 0 1 2.504.337c1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.02 10.02 0 0 0 22 12.017C22 6.484 17.522 2 12 2z`})],-1)]],8,K)]),_:1},8,[`content`]),d(n,{content:o(A)(`@F13B4:在 npm 上查看包`),placement:`top`,effect:`dark`,"show-after":300},{default:x(()=>[a(`a`,{href:`https://www.npmjs.com/package/zen-gitsync`,target:`_blank`,rel:`noopener noreferrer`,class:`badge-icon-link badge-icon-link--npm`,"aria-label":o(A)(`@F13B4:在 npm 上查看包`)},[...t[2]||=[a(`svg`,{viewBox:`0 0 24 24`,width:`15`,height:`15`,"aria-hidden":`true`,fill:`currentColor`,xmlns:`http://www.w3.org/2000/svg`},[a(`path`,{d:`M2 2v20h20V2H2zm17.167 17.167H12.5v-10h-3.333v10H4.833V4.833h14.334v14.334z`})],-1)]],8,q)]),_:1},8,[`content`]),d(n,{content:o(A)(`@F13B4:查看更新`),placement:`top`,effect:`dark`,"show-after":300},{default:x(()=>[a(`a`,{href:Z,target:`_blank`,rel:`noopener noreferrer`,class:`version-link`},`v`+p(r.value),1)]),_:1},8,[`content`]),i.value?.hasUpdate?(v(),w(`button`,{key:0,type:`button`,class:`upgrade-btn`,title:o(A)(`@F13B4:发现新版本 {version},点击升级`,{version:i.value.latest??``}),"aria-label":o(A)(`@F13B4:发现新版本 {version},点击升级`,{version:i.value.latest??``}),onClick:k},[t[3]||=a(`span`,{class:`upgrade-btn__dot`,"aria-hidden":`true`},null,-1),a(`span`,Y,p(o(A)(`@F13B4:升级`)),1),a(`span`,X,`v`+p(i.value.latest),1)],8,J)):u(``,!0),d(W,{modelValue:c.value,"onUpdate:modelValue":t[0]||=e=>c.value=e,logs:l.value,status:f.value,countdown:h.value,onRetry:k,onRestart:L,onCancel:I},null,8,[`modelValue`,`logs`,`status`,`countdown`])])}}}),[[`__scopeId`,`data-v-bc7c0d5c`]]);export{te as default};
1
+ import"./rolldown-runtime-C0FnF6B9.js";import{At as e,B as t,Bn as n,D as r,Dn as i,En as a,Er as o,G as s,In as c,Nn as l,On as u,Pn as d,Pr as f,Qr as p,Tn as m,Wn as h,Y as g,Yn as _,Zn as v,a as y,br as b,cr as x,i as S,it as C,kn as w,or as T,pr as E,qn as D,qr as O,rt as k}from"./element-plus-KTmRV-so.js";import{n as A,t as j}from"./_plugin-vue_export-helper-C8OE7U9H.js";async function M(e,t){let n=e.body.getReader(),r=new TextDecoder(`utf-8`),i=``;for(;;){let{done:e,value:a}=await n.read();if(e)break;i+=r.decode(a,{stream:!0});let o=i.split(`
2
+ `);i=o.pop()??``;for(let e of o)e.trim()&&t(e)}i.trim()&&t(i)}async function N(){let e=await fetch(`/api/app-version`);return e.ok?e.json():{success:!1,current:``,latest:null,hasUpdate:!1,error:`HTTP ${e.status}`}}async function P(e){let t=await fetch(`/api/app-upgrade`,{method:`POST`});if(!t.ok){let n=`HTTP ${t.status}`;try{let e=await t.json();e?.error&&(n=e.error)}catch{}e({type:`error`,message:n}),e({type:`done`,code:t.status});return}if(!t.body){e({type:`error`,message:`响应无 body`}),e({type:`done`,code:-1});return}await M(t,t=>{try{e(JSON.parse(t))}catch{}})}async function F(){let e=await fetch(`/api/app-restart`,{method:`POST`});if(!e.ok){let t=`HTTP ${e.status}`;try{let n=await e.json();n?.error&&(t=n.error)}catch{}throw Error(t)}if(!e.body)throw Error(`响应无 body`);return new Promise((t,n)=>{let r=!1,i=e=>{r||(r=!0,e())};M(e,e=>{try{let r=JSON.parse(e);r.type===`ready`&&typeof r.port==`number`&&r.port>0?i(()=>t(r.port)):r.type===`timeout`?i(()=>n(Error(`restart.timeout`))):r.type===`error`&&i(()=>n(Error(r.message||`restart.failed`)))}catch{}}).then(()=>{i(()=>n(Error(`restart.stream-closed`)))}).catch(()=>{})})}n(),E(),f();var I={key:3},L={key:4},R={key:5},z={key:0,class:`upgrade-hint`},B={key:0,class:`upgrade-countdown`},V={key:1,class:`upgrade-manual`},H={class:`upgrade-manual__label`},U=`npm install -g zen-gitsync --registry https://registry.npmjs.org/`,W=j(c({__name:`UpgradeDialog`,props:{modelValue:{type:Boolean},logs:{},status:{},countdown:{default:0}},emits:[`update:modelValue`,`retry`,`restart`,`cancel`],setup(n,{emit:s}){let c=n,f=s,m=b(null);T(()=>c.logs,async()=>{await h(),m.value&&(m.value.scrollTop=m.value.scrollHeight)});function _(){c.status!==`running`&&(c.status===`success`&&c.countdown>0&&f(`cancel`),f(`update:modelValue`,!1))}function S(){f(`retry`)}function E(){f(`restart`)}function D(){f(`cancel`),f(`update:modelValue`,!1)}async function j(){try{await navigator.clipboard.writeText(U),y.success(A(`@F13B4:已复制升级命令`))}catch{y.warning(A(`@F13B4:复制失败,请手动复制`))}}return(s,c)=>{let h=g,y=t,b=r;return v(),i(b,{"model-value":n.modelValue,"onUpdate:modelValue":c[0]||=e=>f(`update:modelValue`,e),title:o(A)(`@F13B4:升级`),width:`680px`,"close-on-click-modal":!1,"close-on-press-escape":!1,"show-close":n.status!==`running`,"append-to-body":``,onClose:_},{footer:x(()=>[n.status===`failed`?(v(),i(y,{key:0,type:`primary`,onClick:S},{default:x(()=>[l(p(o(A)(`@F13B4:重试`)),1)]),_:1})):u(``,!0),n.status===`failed`?(v(),i(y,{key:1,onClick:j},{default:x(()=>[l(p(o(A)(`@F13B4:复制升级命令`)),1)]),_:1})):u(``,!0),n.status===`success`?(v(),i(y,{key:2,type:`primary`,onClick:E},{default:x(()=>[l(p(o(A)(`@F13B4:立即重启并刷新`)),1)]),_:1})):u(``,!0),n.status===`success`?(v(),i(y,{key:3,onClick:D},{default:x(()=>[l(p(o(A)(`@F13B4:稍后手动重启`)),1)]),_:1})):u(``,!0),n.status===`running`?(v(),i(y,{key:4,disabled:``},{default:x(()=>[l(p(o(A)(`@F13B4:升级中`))+`... `,1)]),_:1})):u(``,!0)]),default:x(()=>[a(`div`,{class:O([`upgrade-status`,`is-${n.status}`])},[n.status===`running`?(v(),i(h,{key:0,class:`is-loading`},{default:x(()=>[d(o(e))]),_:1})):n.status===`success`?(v(),i(h,{key:1,color:`#67c23a`},{default:x(()=>[d(o(k))]),_:1})):(v(),i(h,{key:2,color:`#f56c6c`},{default:x(()=>[d(o(C))]),_:1})),n.status===`running`?(v(),w(`span`,I,p(o(A)(`@F13B4:升级中`)),1)):n.status===`success`?(v(),w(`span`,L,p(o(A)(`@F13B4:升级完成`)),1)):(v(),w(`span`,R,p(o(A)(`@F13B4:升级失败`)),1))],2),n.status===`success`?(v(),w(`p`,z,[l(p(o(A)(`@F13B4:新版本已全局安装,需要重启服务才能生效`))+` `,1),n.countdown>0?(v(),w(`span`,B,p(o(A)(`@F13B4:{seconds} 秒后自动刷新`,{seconds:n.countdown})),1)):u(``,!0)])):u(``,!0),n.status===`failed`?(v(),w(`p`,V,[a(`span`,H,p(o(A)(`@F13B4:手动升级命令`)),1),a(`code`,{class:`upgrade-manual__cmd`},p(U))])):u(``,!0),a(`pre`,{ref_key:`logEl`,ref:m,class:`upgrade-log`},p(n.logs||o(A)(`@F13B4:等待日志输出`)),513)]),_:1},8,[`model-value`,`title`,`show-close`])}}}),[[`__scopeId`,`data-v-40c190d7`]]);n(),E(),f();var G={class:`app-version-badge`},K=[`aria-label`],q=[`aria-label`],J=[`title`,`aria-label`],Y={class:`upgrade-btn__label`},X={class:`upgrade-btn__version`},Z=`https://github.com/xz333221/zen-gitsync/releases`,Q=5,$=`app-version-check`,ee=36e5,te=j(c({__name:`AppVersionBadge`,setup(e){let t=`2.17.8`,n=b(t),r=m(()=>n.value||t),i=b(null),c=b(!1),l=b(``),f=b(`running`),h=b(0),g=null,C=b(!1);function T(){try{let e=sessionStorage.getItem($);if(!e)return null;let{at:t,data:n}=JSON.parse(e);return Date.now()-t>ee?null:n}catch{return null}}function E(e){try{sessionStorage.setItem($,JSON.stringify({at:Date.now(),data:e}))}catch{}}async function O(e=!1){let t=T();t&&(i.value=t);try{let t=await N();i.value=t,E(t),t.current&&(n.value=t.current),!e&&t.success&&t.hasUpdate&&y({type:`success`,message:A(`@F13B4:发现新版本 {version}`,{version:t.latest??``}),duration:4e3})}catch(t){e||y({type:`warning`,message:A(`@F13B4:检查更新失败 {error}`,{error:t?.message||``})})}}async function k(){if(i.value?.hasUpdate){try{await S.confirm(A(`@F13B4:确定要从 {current} 升级到 {latest} 吗?`,{current:i.value.current,latest:i.value.latest}),A(`@F13B4:升级`),{confirmButtonText:A(`@F13B4:确定升级`),cancelButtonText:A(`@F13B4:取消`),type:`info`})}catch{return}l.value=``,f.value=`running`,c.value=!0,await P(e=>{if(e.type===`done`){if(f.value=e.code===0?`success`:`failed`,e.code===0){try{sessionStorage.removeItem($)}catch{}i.value?.latest&&(n.value=i.value.latest),j()}}else e.message&&(l.value+=e.message)})}}function j(){M(),h.value=Q,g=setInterval(()=>{--h.value,h.value<=0&&(g&&=(clearInterval(g),null),L())},1e3)}function M(){g&&=(clearInterval(g),null),h.value=0}function I(){M()}async function L(){if(M(),!C.value){C.value=!0;try{let e=await F(),{pathname:t,search:n,hash:r}=window.location;window.location.href=`http://localhost:${e}${t}${n}${r}`}catch(e){let t=e?.message||``,n={"restart.timeout":`@F13B4:重启超时`,"restart.stream-closed":`@F13B4:重启失败,请刷新页面重试`,"restart.failed":`@F13B4:重启失败`}[t];y({type:`warning`,message:n?A(n):A(`@F13B4:重启失败 {error}`,{error:t})}),C.value=!1}}}return D(()=>{M()}),_(()=>{O(!0)}),(e,t)=>{let n=s;return v(),w(`div`,G,[d(n,{content:o(A)(`@F13B4:在 GitHub 上查看源码`),placement:`top`,effect:`dark`,"show-after":300},{default:x(()=>[a(`a`,{href:`https://github.com/xz333221/zen-gitsync`,target:`_blank`,rel:`noopener noreferrer`,class:`badge-icon-link`,"aria-label":o(A)(`@F13B4:在 GitHub 上查看源码`)},[...t[1]||=[a(`svg`,{viewBox:`0 0 24 24`,width:`15`,height:`15`,"aria-hidden":`true`,fill:`currentColor`,xmlns:`http://www.w3.org/2000/svg`},[a(`path`,{d:`M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0 1 12 6.844a9.59 9.59 0 0 1 2.504.337c1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.02 10.02 0 0 0 22 12.017C22 6.484 17.522 2 12 2z`})],-1)]],8,K)]),_:1},8,[`content`]),d(n,{content:o(A)(`@F13B4:在 npm 上查看包`),placement:`top`,effect:`dark`,"show-after":300},{default:x(()=>[a(`a`,{href:`https://www.npmjs.com/package/zen-gitsync`,target:`_blank`,rel:`noopener noreferrer`,class:`badge-icon-link badge-icon-link--npm`,"aria-label":o(A)(`@F13B4:在 npm 上查看包`)},[...t[2]||=[a(`svg`,{viewBox:`0 0 24 24`,width:`15`,height:`15`,"aria-hidden":`true`,fill:`currentColor`,xmlns:`http://www.w3.org/2000/svg`},[a(`path`,{d:`M2 2v20h20V2H2zm17.167 17.167H12.5v-10h-3.333v10H4.833V4.833h14.334v14.334z`})],-1)]],8,q)]),_:1},8,[`content`]),d(n,{content:o(A)(`@F13B4:查看更新`),placement:`top`,effect:`dark`,"show-after":300},{default:x(()=>[a(`a`,{href:Z,target:`_blank`,rel:`noopener noreferrer`,class:`version-link`},`v`+p(r.value),1)]),_:1},8,[`content`]),i.value?.hasUpdate?(v(),w(`button`,{key:0,type:`button`,class:`upgrade-btn`,title:o(A)(`@F13B4:发现新版本 {version},点击升级`,{version:i.value.latest??``}),"aria-label":o(A)(`@F13B4:发现新版本 {version},点击升级`,{version:i.value.latest??``}),onClick:k},[t[3]||=a(`span`,{class:`upgrade-btn__dot`,"aria-hidden":`true`},null,-1),a(`span`,Y,p(o(A)(`@F13B4:升级`)),1),a(`span`,X,`v`+p(i.value.latest),1)],8,J)):u(``,!0),d(W,{modelValue:c.value,"onUpdate:modelValue":t[0]||=e=>c.value=e,logs:l.value,status:f.value,countdown:h.value,onRetry:k,onRestart:L,onCancel:I},null,8,[`modelValue`,`logs`,`status`,`countdown`])])}}}),[[`__scopeId`,`data-v-bc7c0d5c`]]);export{te as default};