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.
- package/README.md +21 -4
- package/package.json +1 -1
- package/src/cli/ai/agent.js +11 -7
- package/src/cli/ai/cli.integration.test.js +27 -2
- package/src/cli/ai/runtime.test.js +29 -0
- package/src/cli/ai/termui.js +29 -12
- package/src/cli/ai/termui.test.js +102 -2
- package/src/cli/ai/turn.js +1 -1
- package/src/ui/public/assets/AgentView-CdJaeRh0.js +5 -0
- package/src/ui/public/assets/AgentView-DAY4S5WN.css +1 -0
- package/src/ui/public/assets/{AppVersionBadge-Gs5lUl7P.js → AppVersionBadge-CiijSYfM.js} +2 -2
- package/src/ui/public/assets/{BranchSelector-IjaFk0S-.js → BranchSelector-BS7ajwvv.js} +1 -1
- package/src/ui/public/assets/{CommitForm-C-sowOXH.js → CommitForm-CEOSqXCA.js} +1 -1
- package/src/ui/public/assets/{CommonDialog-QyptFD1R.js → CommonDialog-CAaEi-4T.js} +1 -1
- package/src/ui/public/assets/{EditorView-CvvluFCY.js → EditorView-DyaFyRDD.js} +1 -1
- package/src/ui/public/assets/{FileDiffViewer-BXVsdCxE.js → FileDiffViewer-BvX9cQjR.js} +1 -1
- package/src/ui/public/assets/{FlowExecutionViewer-CRxipXru.js → FlowExecutionViewer-uXrzZjsW.js} +1 -1
- package/src/ui/public/assets/{FlowOrchestrationWorkspace-AE1ZHTKF.js → FlowOrchestrationWorkspace-DIBpUSt_.js} +1 -1
- package/src/ui/public/assets/{IconButton-BmSueF_H.js → IconButton-CnoW2on6.js} +1 -1
- package/src/ui/public/assets/{LogList-iaVA-_aC.js → LogList-D2espSVo.js} +1 -1
- package/src/ui/public/assets/{MindmapView-DcG4jpCh.js → MindmapView-CZF-rZVs.js} +1 -1
- package/src/ui/public/assets/{MonitorView-DO59syvl.js → MonitorView-DjLr7qOG.js} +1 -1
- package/src/ui/public/assets/{ProjectStartupButton-DYppceyw.js → ProjectStartupButton-DzsU-QIO.js} +1 -1
- package/src/ui/public/assets/{RemoteManagerDialog-CDbxG6pZ.js → RemoteManagerDialog-CuuG2ucT.js} +1 -1
- package/src/ui/public/assets/{RemoteRepoCard-Cko2KB8w.js → RemoteRepoCard-BGf8L0of.js} +1 -1
- package/src/ui/public/assets/{SourceMapView-CwWPVdz-.js → SourceMapView-CFb02m6z.js} +1 -1
- package/src/ui/public/assets/{SvgIcon-Cf7nOKc8.js → SvgIcon-SRAeYjnN.js} +1 -1
- package/src/ui/public/assets/{TemplateManager-DWPDjWxM.js → TemplateManager-gedqAXl8.js} +1 -1
- package/src/ui/public/assets/{UserInputNode-YOziN-5A.js → UserInputNode-BOCuMQW6.js} +1 -1
- package/src/ui/public/assets/{WorkbenchView-DNnEpIYy.js → WorkbenchView-CRoR21LQ.js} +4 -4
- package/src/ui/public/assets/{_plugin-vue_export-helper-CQjg8MXm.js → _plugin-vue_export-helper-C8OE7U9H.js} +4 -4
- package/src/ui/public/assets/{configStore-C7HOx5S1.js → configStore-CuMElTgV.js} +1 -1
- package/src/ui/public/assets/{gitStore-DIsBd6n4.js → gitStore-CpcS-MzM.js} +1 -1
- package/src/ui/public/assets/index-cApp6w0b.css +1 -0
- package/src/ui/public/assets/index-yXm9JLGv.js +24 -0
- package/src/ui/public/assets/{previewSandbox-TKucqLxt.js → previewSandbox-DkXYEpLP.js} +0 -0
- package/src/ui/public/assets/{toolsStore-BiC2mmC3.js → toolsStore-DGxlRe3Y.js} +1 -1
- package/src/ui/public/assets/vendor-4ojsjDYc.css +1 -0
- package/src/ui/public/assets/{vendor-92fKgtnz.js → vendor-dO4B7wQf.js} +28 -28
- package/src/ui/public/index.html +14 -14
- package/src/ui/server/routes/fs.js +37 -0
- package/src/ui/server/utils/directoryFetch.js +209 -0
- package/src/ui/server/utils/directoryFetch.test.js +232 -0
- package/src/ui/public/assets/AgentView-CZc191Xm.js +0 -4
- package/src/ui/public/assets/AgentView-CzUeyQfR.css +0 -1
- package/src/ui/public/assets/index-FOe1cHkh.css +0 -1
- package/src/ui/public/assets/index-MmcnT2Dn.js +0 -24
- 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
|
|
459
|
-
`/think full`
|
|
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
|
-
|
|
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.
|
|
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",
|
package/src/cli/ai/agent.js
CHANGED
|
@@ -93,7 +93,7 @@ const STRINGS = {
|
|
|
93
93
|
bannerCwd: '目录',
|
|
94
94
|
thinkingLabel: '思考',
|
|
95
95
|
answerLabel: '回答',
|
|
96
|
-
thinkingHint: '…
|
|
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: '…
|
|
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
|
|
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
|
|
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' : '
|
|
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 ?
|
|
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: '
|
|
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
|
-
|
|
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
|
|
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
|
package/src/cli/ai/termui.js
CHANGED
|
@@ -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
|
|
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: '
|
|
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('/')
|
|
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(
|
|
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 =
|
|
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
|
-
//
|
|
531
|
-
//
|
|
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'), [])
|
package/src/cli/ai/turn.js
CHANGED
|
@@ -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 === '
|
|
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-
|
|
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.
|
|
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};
|