zen-gitsync 2.17.4 → 2.17.6

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 (53) hide show
  1. package/README.md +13 -0
  2. package/package.json +2 -1
  3. package/scripts/release.js +93 -66
  4. package/src/aiCommit.js +6 -3
  5. package/src/cli/ai/agent.js +23 -8
  6. package/src/cli/ai/modelSetup.js +6 -8
  7. package/src/config.js +57 -4
  8. package/src/ui/public/assets/{AgentView-BkNdHFRW.js → AgentView-BC3F0F6m.js} +3 -3
  9. package/src/ui/public/assets/AgentView-CzUeyQfR.css +1 -0
  10. package/src/ui/public/assets/{AppVersionBadge-pJ33fsvn.js → AppVersionBadge-DV6KuLqb.js} +2 -2
  11. package/src/ui/public/assets/{BranchSelector-Ixv2uRS6.js → BranchSelector-DGXDAOkR.js} +1 -1
  12. package/src/ui/public/assets/{CommitForm-DqJvDzSw.js → CommitForm-BYBj7UUa.js} +1 -1
  13. package/src/ui/public/assets/{CommonDialog-DfT04-V7.js → CommonDialog-DWMCeujE.js} +1 -1
  14. package/src/ui/public/assets/{EditorView-Br0JBESF.js → EditorView-D-TEQZso.js} +1 -1
  15. package/src/ui/public/assets/{FileDiffViewer-C3DxCngU.js → FileDiffViewer-C4ZANajI.js} +1 -1
  16. package/src/ui/public/assets/{FlowExecutionViewer-C_FOK1_9.js → FlowExecutionViewer-DUv7mBuG.js} +1 -1
  17. package/src/ui/public/assets/{FlowOrchestrationWorkspace-wu9ieYbz.js → FlowOrchestrationWorkspace-BiSuvzL7.js} +1 -1
  18. package/src/ui/public/assets/{IconButton-CQB_swX6.js → IconButton-BltPKVbV.js} +1 -1
  19. package/src/ui/public/assets/{LogList-CSpeYuAd.js → LogList-BY1Haq9r.js} +1 -1
  20. package/src/ui/public/assets/{MindmapView-8AHGHv9g.js → MindmapView-BKsvCnYv.js} +1 -1
  21. package/src/ui/public/assets/{MonitorView-Do6DcGd9.js → MonitorView-B3_NaJRy.js} +1 -1
  22. package/src/ui/public/assets/{ProjectStartupButton-DwrukeUb.js → ProjectStartupButton-DP3eDVRp.js} +1 -1
  23. package/src/ui/public/assets/{RemoteManagerDialog-Dhdwvb2y.js → RemoteManagerDialog-Bl9b_iRv.js} +1 -1
  24. package/src/ui/public/assets/{RemoteRepoCard-DjKHYGRi.js → RemoteRepoCard-TQ3DxDnq.js} +1 -1
  25. package/src/ui/public/assets/{SourceMapView-COuEBUTw.js → SourceMapView-BF-Mzv3V.js} +1 -1
  26. package/src/ui/public/assets/{SvgIcon-C6aJB-3Q.js → SvgIcon-DxazW7dI.js} +1 -1
  27. package/src/ui/public/assets/{TemplateManager-Th1ibeIh.js → TemplateManager-BN6gWFWX.js} +1 -1
  28. package/src/ui/public/assets/{UserInputNode-B8F60XEE.js → UserInputNode-CPwvIfP6.js} +1 -1
  29. package/src/ui/public/assets/WorkbenchView-CDu61WWs.js +13 -0
  30. package/src/ui/public/assets/WorkbenchView-DEejub9Z.css +1 -0
  31. package/src/ui/public/assets/_plugin-vue_export-helper-B7A-NgBp.js +19 -0
  32. package/src/ui/public/assets/configStore-Dx84lvTX.js +1 -0
  33. package/src/ui/public/assets/{gitStore-QokQdW7C.js → gitStore-CYrdjXPU.js} +1 -1
  34. package/src/ui/public/assets/index-DNHOYMCg.js +24 -0
  35. package/src/ui/public/assets/index-Dsfmp6w8.css +1 -0
  36. package/src/ui/public/assets/{previewSandbox-CA1fFxdo.js → previewSandbox-D_j5PWX1.js} +0 -0
  37. package/src/ui/public/assets/{toolsStore-Bu8Ujw_A.js → toolsStore-BiC2mmC3.js} +1 -1
  38. package/src/ui/public/assets/{vendor-CwZs6S_i.js → vendor-92fKgtnz.js} +7 -7
  39. package/src/ui/public/assets/{vendor-_6pgW9Z2.css → vendor-B1UrVTTI.css} +1 -1
  40. package/src/ui/public/index.html +14 -14
  41. package/src/ui/server/routes/codeAnalysis.js +6 -3
  42. package/src/ui/server/routes/config.js +32 -6
  43. package/src/ui/server/routes/workbench/agentChat.js +34 -8
  44. package/src/ui/server/routes/workbench/llmClient.js +15 -8
  45. package/src/utils/aiEndpoint.js +231 -0
  46. package/src/utils/aiEndpoint.test.js +175 -0
  47. package/src/ui/public/assets/AgentView-BsN9y4Kg.css +0 -1
  48. package/src/ui/public/assets/WorkbenchView-CML-A0y6.css +0 -1
  49. package/src/ui/public/assets/WorkbenchView-jutdH9WI.js +0 -13
  50. package/src/ui/public/assets/_plugin-vue_export-helper-nUAWISuX.js +0 -19
  51. package/src/ui/public/assets/configStore-CRYvJSvr.js +0 -1
  52. package/src/ui/public/assets/index-BLO9MkBC.js +0 -24
  53. package/src/ui/public/assets/index-ClqvC4kJ.css +0 -1
package/README.md CHANGED
@@ -384,6 +384,7 @@ A dedicated view (robot icon in the activity bar) for chatting with the built-in
384
384
  | Streaming chat | SSE-based real-time streaming with thinking process, content, tool calls, and tool results rendered inline |
385
385
  | 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
386
  | 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 |
387
+ | Per-turn tool limit | A single message may trigger up to N tool calls in a row (default **200**, range 1–2000). Configurable in **Settings → AI models → Agent Runtime**; hitting the limit ends the turn and asks you to send another message. The same setting drives the CLI agent |
387
388
  | Preset questions | Quick-start buttons on the welcome screen for common tasks (view project structure, analyze code quality, write tests, check git status) |
388
389
  | Stop generation | A floating stop button appears during streaming; aborts the LLM request and any running child processes |
389
390
  | Theme sync | The chat area follows the GUI's current theme (light / dark / auto) |
@@ -454,6 +455,12 @@ to numeric input. `Esc` or `Ctrl+C` cancels the wizard cleanly.
454
455
 
455
456
  In-session commands: `/help`, `/model`, `/addmodel`, `/cd <path>`, `/image [path]`, `/think`, `/clear`, `/exit`.
456
457
 
458
+ Tool-call budget: one message may trigger up to N tool calls in a row before the turn is
459
+ force-ended with a "max tool iterations reached" notice (send another message to continue).
460
+ N defaults to **200** and is configurable in **Settings → AI models → Agent Runtime**
461
+ (`aiMaxToolIterations` in `~/.zen-gitsync/config.json`, range 1–2000) — the Web agent shares
462
+ the same value.
463
+
457
464
  Images: press `Alt+V` in the REPL to paste a clipboard image (screenshot), or attach a
458
465
  local file with `/image <path>`; images are sent as multimodal `image_url` parts with your
459
466
  next message (requires a vision-capable model). `/image` alone lists pending images,
@@ -973,6 +980,7 @@ Activity Bar 中的机器人图标视图,可直接在浏览器中与内置 AI
973
980
  | 流式对话 | 基于 SSE 的实时流式输出,包含思考过程、正文内容、工具调用和工具结果的内联渲染 |
974
981
  | 工具调用展示 | 每次工具调用(run_command、read_file、edit_file、list_files、search_text、write_file)以可折叠卡片形式展示,含参数预览和执行结果 |
975
982
  | 会话持久化 | 所有对话保存为 JSON 文件到 `~/.zen-gitsync/agent-sessions/`;CLI 智能体(`g ai`)写入同一目录,Web 端与 CLI 端会话统一管理 |
983
+ | 单轮工具调用上限 | 一条消息内智能体最多连续调用多少次工具(默认 **200**,可调范围 1–2000)。在 **设置 → AI 模型配置 → 智能体运行时** 中修改;达到上限本轮会被强制结束并提示再发一条消息继续。CLI 智能体共用同一项设置 |
976
984
  | 预设问题 | 开场界面提供快捷按钮(查看项目结构、分析代码质量、写测试、Git 状态检查)|
977
985
  | 停止生成 | 流式输出期间出现浮动停止按钮;中止 LLM 请求及正在运行的子进程 |
978
986
  | 主题同步 | 对话区域跟随 GUI 当前主题(浅色 / 深色 / 自动)|
@@ -1038,6 +1046,11 @@ $ g ai --model=2 # 使用第 2 个已配置的模型(序号或
1038
1046
 
1039
1047
  会话内命令:`/help`、`/model`、`/addmodel`、`/cd <路径>`、`/image [路径]`、`/think`、`/clear`、`/exit`。
1040
1048
 
1049
+ 工具调用预算:一条消息内智能体最多连续调用 N 次工具,触顶后本轮被强制结束并提示
1050
+ "已达单轮最大工具调用次数",再发一条消息即可继续。N 默认 **200**,可在
1051
+ **设置 → AI 模型配置 → 智能体运行时** 修改(即 `~/.zen-gitsync/config.json` 的
1052
+ `aiMaxToolIterations`,范围 1–2000),Web 端智能体共用同一项设置。
1053
+
1041
1054
  图片:在 REPL 中按 `Alt+V` 粘贴剪贴板图片(截图),或用 `/image <路径>` 附加本地图片;
1042
1055
  图片以多模态 `image_url` 部件随下一条消息发送(需视觉模型)。单独 `/image` 查看待发送图片,
1043
1056
  `/image clear` 清除。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zen-gitsync",
3
- "version": "2.17.4",
3
+ "version": "2.17.6",
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",
@@ -37,6 +37,7 @@
37
37
  "verify:wb-proj-filter": "node scripts/verify-wb-proj-filter.cjs",
38
38
  "verify:wb-panel-resize": "node scripts/verify-wb-panel-resize.cjs",
39
39
  "verify:wb-responsive": "node scripts/verify-wb-responsive.cjs",
40
+ "verify:wb-editor-persist": "node scripts/verify-workbench-editor-persist.cjs",
40
41
  "e2e": "cd src/ui/client && npx playwright test",
41
42
  "e2e:headed": "cd src/ui/client && npx playwright test --headed",
42
43
  "e2e:install": "cd src/ui/client && npx playwright install chromium",
@@ -26,10 +26,11 @@
26
26
  * npm run release -- --skip-self-update # 发布后不自动 npm install -g zen-gitsync
27
27
  * npm run release -- --skip-push # 只发布到 npm,不 push git
28
28
  * npm run release -- --dry-run # 只打印计划,不真正改 package.json / commit / publish
29
- * npm run release -- --poll-interval=20 --poll-timeout=600 # 调 registry 轮询节奏(秒)
29
+ * npm run release -- --poll-interval=20 --poll-timeout=600 # 调自更新重试节奏(秒)
30
30
  *
31
- * 发布后自更新:先轮询 registry 确认新版本可见,再 `npm install -g zen-gitsync@<版本>`。
32
- * 原因见 POLL_INTERVAL_MS 处注释 —— publish 成功不等于 registry 立即可见。
31
+ * 发布后自更新:反复尝试 `npm install -g zen-gitsync@<版本>` 并校验全局版本;
32
+ * 精确版本号解析不到(ETARGET)时改用 tarball URL 直连兜底。
33
+ * 原因见 tarballUrl() / selfUpdateGlobal() 处注释 —— publish 成功不等于 packument 立即可见。
33
34
  */
34
35
 
35
36
  import fs from 'node:fs'
@@ -67,12 +68,27 @@ function readNumberArg(name, fallback) {
67
68
  // 仍是 2.17.2,versions 里没有 2.17.3;packument 响应头
68
69
  // `Cache-Control: public, max-age=300` —— 光 CDN 层就可能滞后 5 分钟。
69
70
  // 所以"publish 完立刻 npm install -g"必然装回旧版(实测 `npm ls -g` 停在 2.17.2)。
70
- // 对策:publish 后先轮询,确认 dist-tags.latest 已翻到目标版本,再安装。
71
- // 默认 15s 一轮、上限 600s(必须 > CDN 的 max-age=300,否则稳定误判超时);
72
- // 可用 `--poll-interval=<秒>` / `--poll-timeout=<秒>` 调。
71
+ //
72
+ // 再实测(2026-09-20, v2.17.4):滞后不止 600s。那次脚本"先等 dist-tags 翻牌"一直等到
73
+ // 超时(600s 里 latest 全程是 2.17.3),超时后再 install 依旧 ETARGET —— 因为
74
+ // `npm view dist-tags` 和 `npm install pkg@ver` 读的是**同一份 packument**:
75
+ // 等它、用它,走的是同一条被缓存的路。只把超时 600s 往上加,只是把失败往后推。
76
+ //
77
+ // 兜底:tarball URL 是不可变资源,发布完成即可取,且不经过 packument 的那层缓存
78
+ // (见 tarballUrl 注释)。所以自更新不再把安装卡在 dist-tags 后面,而是
79
+ // "反复尝试安装 + 校验全局版本",精确版本号解析不到时自动改用 tarball 直连。
80
+ // 默认 15s 一轮、上限 600s;可用 `--poll-interval=<秒>` / `--poll-timeout=<秒>` 调。
73
81
  const POLL_INTERVAL_MS = readNumberArg('--poll-interval', 15) * 1000
74
82
  const POLL_TIMEOUT_MS = readNumberArg('--poll-timeout', 600) * 1000
75
83
 
84
+ // 发布物 tarball 的地址。npm publish 是先把 tarball 推进 registry、再更新 packument;
85
+ // tarball 按 URL 寻址、内容不可变,所以它比 packument 更早对外可用,也不会被
86
+ // max-age 那层缓存挂在旧内容上 —— "新版本已发布、但 packument 还是旧的"时,
87
+ // 这是唯一能立刻装上的路径。
88
+ function tarballUrl(version) {
89
+ return `${NPM_REGISTRY.replace(/\/$/, '')}/${PKG_NAME}/-/${PKG_NAME}-${version}.tgz`
90
+ }
91
+
76
92
  // 跨平台 sleep:Node 原生,避免 `sleep N || ping` 的 Windows 兜底 hack
77
93
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
78
94
 
@@ -531,86 +547,97 @@ function readGlobalInstalledVersion() {
531
547
  }
532
548
  }
533
549
 
534
- // 轮询 registry 直到目标版本可见。
535
- // 返回 true = 确认可见,false = 超时(调用方据此决定是否继续装)。
536
- async function waitForVersionVisible(version) {
537
- console.log(chalk.blue('\n=== 等待 NPM registry 同步 ==='))
538
- console.log(chalk.gray('publish 返回成功 ≠ 立即可见:registry 后台处理 + CDN 缓存(packument 的 max-age=300)。'))
539
- console.log(chalk.gray('实测 v2.17.3 发布后约 3 分钟才可见;这期间 install 会解析到旧版本。'))
550
+ // 执行一次全局安装。退出码 0 才返回 ok。
551
+ // 输出先兜住不打印:重试期间每失败一次就刷一屏 `npm error` 太吵,
552
+ // 只有整轮尝试全部失败时,调用方才回放最后一份输出。
553
+ function tryInstallGlobal(spec) {
554
+ console.log(chalk.gray(` → npm install -g ${spec}`))
555
+ try {
556
+ execSync(`npm install -g ${spec} --registry=${NPM_REGISTRY} --prefer-online`, {
557
+ stdio: ['ignore', 'pipe', 'pipe'],
558
+ maxBuffer: 8 * 1024 * 1024,
559
+ timeout: 300000,
560
+ })
561
+ return { ok: true }
562
+ } catch (err) {
563
+ const out = `${err.stdout ?? ''}${err.stderr ?? ''}`.trim()
564
+ return { ok: false, output: out || String(err.message || err) }
565
+ }
566
+ }
567
+
568
+ // 自更新全局版本:反复尝试安装,直到全局版本校验通过或超时。
569
+ //
570
+ // 为什么不是"先等 dist-tags 翻牌,再安装"(v2.17.4 之前的老写法):
571
+ // `npm view dist-tags.latest` 和 `npm install -g pkg@ver` 读的是同一份 packument。
572
+ // packument 被 CDN 缓存(max-age=300,实测还能更久)时两边一起滞后 ——
573
+ // 等待阶段白等 600s,超时后再装照样 ETARGET,等于什么都没解决。
574
+ //
575
+ // 所以顺序反过来:先"尝试装",装不上才等。每轮按两条路试,任一条装上且校验通过就结束:
576
+ // ① `pkg@<version>` —— 常规路径,packument 已同步时一次命中;
577
+ // ② tarball URL 直连 —— packument 还是旧的时候的唯一出路(见 tarballUrl 注释)。
578
+ async function selfUpdateGlobal(version) {
579
+ console.log(chalk.blue('\n=== 发布后自更新全局版本 ==='))
580
+ console.log(chalk.gray('publish 成功 ≠ packument 立即可见(registry 处理 + CDN 缓存)。'))
581
+ console.log(chalk.gray('不空等 dist-tags,直接反复尝试安装;精确版本解析不到就 tarball 直连兜底。'))
540
582
  console.log(chalk.gray(
541
- `轮询 dist-tags.latest,间隔 ${POLL_INTERVAL_MS / 1000}s,上限 ${POLL_TIMEOUT_MS / 1000}s;`
542
- + '确认可见后才安装,避免全局装回旧版本。'
583
+ `间隔 ${POLL_INTERVAL_MS / 1000}s,上限 ${POLL_TIMEOUT_MS / 1000}s`
584
+ + '(可用 --poll-interval / --poll-timeout 调)。'
543
585
  ))
544
586
 
545
587
  const startedAt = Date.now()
546
588
  const deadline = startedAt + POLL_TIMEOUT_MS
589
+ const tarball = tarballUrl(version)
547
590
  let attempt = 0
591
+ let lastOutput = ''
548
592
 
549
593
  for (;;) {
550
594
  attempt += 1
551
- const latest = readLatestDistTag()
595
+ const packumentSynced = readLatestDistTag() === version
596
+ // packument 已同步 → 先走常规精确版本;还没同步 → tarball 优先,争取一轮命中
597
+ const targets = packumentSynced
598
+ ? [`${PKG_NAME}@${version}`, tarball]
599
+ : [tarball, `${PKG_NAME}@${version}`]
600
+
601
+ console.log(chalk.gray(
602
+ `第 ${attempt} 次尝试(dist-tags.latest${packumentSynced ? '已同步' : '仍是旧版本'})...`
603
+ ))
552
604
 
553
- if (latest === version) {
554
- const cost = ((Date.now() - startedAt) / 1000).toFixed(1)
555
- console.log(chalk.green(`registry 已可见 ${PKG_NAME}@${version}(第 ${attempt} 次检查,耗时 ${cost}s)`))
556
- return true
605
+ for (const spec of targets) {
606
+ const res = tryInstallGlobal(spec)
607
+ if (!res.ok) {
608
+ lastOutput = res.output
609
+ continue
610
+ }
611
+ const installed = readGlobalInstalledVersion()
612
+ if (installed === version) {
613
+ const cost = ((Date.now() - startedAt) / 1000).toFixed(1)
614
+ console.log(chalk.green(
615
+ `全局已更新到 ${PKG_NAME}@${version}(第 ${attempt} 次尝试,耗时 ${cost}s)`
616
+ ))
617
+ return
618
+ }
619
+ // 退出码 0 但版本不对(极少见):按失败处理,继续下一轮
620
+ lastOutput = `安装命令退出码 0,但全局版本读到 ${installed ?? '未知'}`
621
+ console.log(chalk.yellow(` 安装命令成功,但全局版本是 ${installed ?? '未知'},继续重试`))
557
622
  }
558
623
 
559
624
  const remain = deadline - Date.now()
560
625
  if (remain <= 0) {
561
- console.log(chalk.yellow(
562
- `已等待 ${POLL_TIMEOUT_MS / 1000}s 仍未看到 ${version},`
563
- + `最近一次读到 latest=${latest ?? '查询失败'}`
626
+ console.error(chalk.red(`已尝试 ${attempt} 次,仍未装上 ${PKG_NAME}@${version}`))
627
+ if (lastOutput) console.error(chalk.gray(lastOutput))
628
+ console.error(chalk.gray(
629
+ '可稍后手动重试:\n'
630
+ + ` npm install -g ${PKG_NAME}@${version}\n`
631
+ + '若仍报 ETARGET(packument 缓存滞后),直连 tarball:\n'
632
+ + ` npm install -g ${tarball}`
564
633
  ))
565
- return false
634
+ return
566
635
  }
567
-
568
- console.log(chalk.gray(
569
- `第 ${attempt} 次:latest=${latest ?? '查询失败(网络或 registry 波动)'},`
570
- + `期望 ${version},${Math.ceil(remain / 1000)}s 后重试`
571
- ))
636
+ console.log(chalk.gray(` ${Math.ceil(remain / 1000)}s 后重试`))
572
637
  await sleep(Math.min(POLL_INTERVAL_MS, remain))
573
638
  }
574
639
  }
575
640
 
576
- // 装"精确版本"而不是 latest:即便 dist-tags 的 CDN 缓存滞后,
577
- // 精确版本号也只会命中刚发布的那份 tarball。
578
- function installGlobal(version) {
579
- console.log(chalk.gray(`执行 npm install -g ${PKG_NAME}@${version}...`))
580
- execSync(
581
- `npm install -g ${PKG_NAME}@${version} --registry=${NPM_REGISTRY} --prefer-online`,
582
- { stdio: 'inherit' }
583
- )
584
- }
585
-
586
- // 等待可见 → 全局安装 → 校验装到的版本(校验不看退出码,退出码 0 不代表版本对)
587
- async function selfUpdateGlobal(version) {
588
- const visible = await waitForVersionVisible(version)
589
-
590
- try {
591
- installGlobal(version)
592
- } catch (err) {
593
- console.error(chalk.red('全局安装失败:'), err.message || err)
594
- console.error(chalk.gray(`可稍后手动重试: npm install -g ${PKG_NAME}@${version}`))
595
- return
596
- }
597
-
598
- const installed = readGlobalInstalledVersion()
599
- if (installed === version) {
600
- console.log(chalk.green(`全局已更新到 ${PKG_NAME}@${version}`))
601
- if (!visible) {
602
- console.log(chalk.gray('(轮询阶段 npm view 未查到该版本,但安装已拿到正确版本)'))
603
- }
604
- return
605
- }
606
-
607
- console.error(chalk.red(
608
- `全局版本校验不一致:期望 ${version},实际 ${installed ?? '未知'}`
609
- + (visible ? '' : '(且轮询阶段未在 registry 上看到该版本)')
610
- ))
611
- console.error(chalk.gray(`等 registry 同步完成后手动重试: npm install -g ${PKG_NAME}@${version}`))
612
- }
613
-
614
641
  // 发布到 NPM
615
642
  async function publishToNpm(version) {
616
643
  console.log(chalk.blue('\n=== 发布到 NPM ==='))
package/src/aiCommit.js CHANGED
@@ -19,6 +19,7 @@ import chalk from 'chalk';
19
19
  import ora from 'ora';
20
20
  import config from './config.js';
21
21
  import { execGitCommand } from './utils/index.js';
22
+ import { buildAiChatRequest } from './utils/aiEndpoint.js';
22
23
 
23
24
  // ──────────────────────────────────────────────
24
25
  // 跳过的产物/资源/lock 文件(与 GUI config.js SKIP_FILE_PATTERNS 一致)
@@ -276,9 +277,11 @@ ${diffText || '(no staged content, please infer from the file list)'}`;
276
277
  // 4. 调用 LLM
277
278
  spinner.start('AI 正在生成提交信息...');
278
279
  const { default: fetch } = await import('node-fetch').catch(() => ({ default: globalThis.fetch }));
279
- const url = `${defaultModel.baseURL.replace(/\/$/, '')}/chat/completions`;
280
- const headers = { 'Content-Type': 'application/json' };
281
- if (defaultModel.apiKey) headers['Authorization'] = `Bearer ${defaultModel.apiKey}`;
280
+ const { url, headers } = buildAiChatRequest({
281
+ baseURL: defaultModel.baseURL,
282
+ model: defaultModel.model,
283
+ apiKey: defaultModel.apiKey
284
+ });
282
285
  const body = JSON.stringify({
283
286
  model: defaultModel.model,
284
287
  messages: [{ role: 'user', content: prompt }],
@@ -58,6 +58,7 @@ import {
58
58
  import { readClipboardImage, checkImageFile, imageToDataUrl, formatBytes } from './images.js'
59
59
  import { runModelSetup, collectModelInput, buildModelConfig, selectFromList } from './modelSetup.js'
60
60
  import { genSessionId, autoTitle, writeSession, enforceRetention, listSessions } from './sessionStore.js'
61
+ import { buildAiChatRequest, describeAiHttpError } from '../../utils/aiEndpoint.js'
61
62
 
62
63
  // truncateDisplay 已迁移到 termui.js;这里 re-export 保持既有测试/外部引用不断
63
64
  export { truncateDisplay } from './termui.js'
@@ -65,7 +66,7 @@ export { truncateDisplay } from './termui.js'
65
66
  // ──────────────────────────────────────────────
66
67
  // 常量
67
68
  // ──────────────────────────────────────────────
68
- const MAX_TOOL_ITERATIONS = 40 // 单轮用户输入允许的最大工具调用循环数(防失控)
69
+ const DEFAULT_MAX_TOOL_ITERATIONS = 200 // 单轮工具调用循环数兜底值(防失控);实际值取配置里的 aiMaxToolIterations
69
70
  const MAX_HISTORY_MESSAGES = 40 // 历史消息上限(超出后从最旧的整段对话裁剪)
70
71
  const LLM_TIMEOUT_MS = 300000 // 单次 LLM 请求超时(5 分钟,长推理模型够用)
71
72
 
@@ -285,10 +286,14 @@ ${isWin ? `- This is Windows cmd.exe. The following Unix commands do NOT exist h
285
286
  // LLM 流式调用(OpenAI 兼容 + function calling)
286
287
  // 返回 { content, toolCalls, aborted }
287
288
  // ──────────────────────────────────────────────
288
- async function streamChatOnce({ model, messages, signal, onToken }) {
289
- const url = `${String(model.baseURL || '').replace(/\/$/, '')}/chat/completions`
290
- const headers = { 'Content-Type': 'application/json' }
291
- if (model.apiKey) headers['Authorization'] = `Bearer ${model.apiKey}`
289
+ async function streamChatOnce({ model, messages, signal, onToken, sessionId }) {
290
+ // sessionId = 会话 ID:OpenCode 网关靠它做路由与提示缓存,同一条会话的所有请求要复用
291
+ const { url, headers } = buildAiChatRequest({
292
+ baseURL: model.baseURL,
293
+ model: model.model,
294
+ apiKey: model.apiKey,
295
+ sessionId,
296
+ })
292
297
 
293
298
  const body = JSON.stringify({
294
299
  model: model.model,
@@ -314,7 +319,8 @@ async function streamChatOnce({ model, messages, signal, onToken }) {
314
319
  const resp = await fetch(url, { method: 'POST', headers, body, signal: controller.signal })
315
320
  if (!resp.ok || !resp.body) {
316
321
  const errText = await resp.text().catch(() => '')
317
- const snippet = errText.length > 300 ? errText.slice(0, 300) + '…' : errText
322
+ // 抽 error.message:provider 的说明都在里面,整坨 JSON 打到终端没法看
323
+ const snippet = describeAiHttpError(errText, resp.status)
318
324
  // 400 且提到 tools/functions:大概率是模型不支持 function calling
319
325
  if (resp.status === 400 && /tool|function/i.test(snippet)) {
320
326
  throw new Error(`HTTP 400: 当前模型可能不支持 function calling(${snippet})。请在 g ui 中换用支持工具调用的模型(如 deepseek / qwen / gpt 系列)。`)
@@ -472,7 +478,11 @@ async function runAgentTurn(state, userText, t, images = []) {
472
478
  // 旧消息里的图片降级为占位文字,防止 base64 随对话轮次累积撑爆上下文
473
479
  stripStaleImages(state.messages, state.locale)
474
480
 
475
- for (let iter = 0; iter < MAX_TOOL_ITERATIONS; iter++) {
481
+ const maxIterations = Number.isFinite(state.maxToolIterations) && state.maxToolIterations > 0
482
+ ? state.maxToolIterations
483
+ : DEFAULT_MAX_TOOL_ITERATIONS
484
+
485
+ for (let iter = 0; iter < maxIterations; iter++) {
476
486
  trimHistory(state.messages)
477
487
 
478
488
  // 首个 token 到达前转 spinner,到达后停掉并让位给流式渲染
@@ -508,6 +518,8 @@ async function runAgentTurn(state, userText, t, images = []) {
508
518
  model: state.model,
509
519
  messages: state.messages,
510
520
  signal: state.abortController?.signal,
521
+ // 同一会话(含工具调用产生的每一轮请求)复用同一个会话 ID
522
+ sessionId: state.sessionId,
511
523
  onToken: ({ thinking, content }) => {
512
524
  if (thinking) renderSeg({ thinking })
513
525
  if (content) for (const seg of thinkFilter.feed(content)) renderSeg(seg)
@@ -580,7 +592,7 @@ async function runAgentTurn(state, userText, t, images = []) {
580
592
  // 工具结果全部入历史后继续循环,让模型基于结果决定下一步
581
593
  }
582
594
 
583
- printWarn(t.toolIterLimit(MAX_TOOL_ITERATIONS))
595
+ printWarn(t.toolIterLimit(maxIterations))
584
596
  }
585
597
 
586
598
  // JSON 解析失败时的参数回显:单行截断,不进 summarizeToolArgs(它没有结构化参数可用)
@@ -897,6 +909,9 @@ export async function runAiAgent(argv = []) {
897
909
  model,
898
910
  locale,
899
911
  shellDesc,
912
+ // 单轮工具调用上限:来自全局配置 aiMaxToolIterations(loadConfig 已规范化),
913
+ // 读不到时由 runAgentTurn 兜底成 DEFAULT_MAX_TOOL_ITERATIONS
914
+ maxToolIterations: cfg.aiMaxToolIterations,
900
915
  currentChild: null,
901
916
  abortController: null,
902
917
  cancelRequested: false,
@@ -35,6 +35,7 @@ import readline from 'node:readline'
35
35
  import chalk from 'chalk'
36
36
  import config from '../../config.js'
37
37
  import { startSpinner, renderSelectableListBody, parseKeyForSelectableList } from './termui.js'
38
+ import { buildAiChatRequest, buildAiApiRequest } from '../../utils/aiEndpoint.js'
38
39
 
39
40
  // ──────────────────────────────────────────────
40
41
  // 预设服务商列表
@@ -279,14 +280,13 @@ export async function fetchModelsFromApi({ baseURL, apiKey, timeoutMs = 5000, fe
279
280
  const fetch = fetchFn || globalThis.fetch
280
281
  if (!fetch) return []
281
282
 
282
- const url = `${String(baseURL || '').replace(/\/$/, '')}/models`
283
- const headers = {}
284
- if (apiKey) headers['Authorization'] = `Bearer ${apiKey}`
285
-
286
283
  const controller = new AbortController()
287
284
  const timer = setTimeout(() => controller.abort(), timeoutMs)
288
285
 
289
286
  try {
287
+ // 拉模型列表也要带客户端身份/会话头:OpenCode 网关对没有身份的请求照样拦。
288
+ // 放在 try 里保证"本函数不抛错"的契约不被 baseURL 为空破坏。
289
+ const { url, headers } = buildAiApiRequest({ baseURL, path: '/models', apiKey })
290
290
  const res = await fetch(url, {
291
291
  method: 'GET',
292
292
  headers,
@@ -327,10 +327,6 @@ export async function testModelConnection({ baseURL, model, apiKey, timeoutMs =
327
327
  const fetch = fetchFn || globalThis.fetch
328
328
  if (!fetch) throw new Error('fetch is not available')
329
329
 
330
- const url = `${String(baseURL || '').replace(/\/$/, '')}/chat/completions`
331
- const headers = { 'Content-Type': 'application/json' }
332
- if (apiKey) headers['Authorization'] = `Bearer ${apiKey}`
333
-
334
330
  const body = JSON.stringify({
335
331
  model,
336
332
  messages: [{ role: 'user', content: 'hi' }],
@@ -341,6 +337,8 @@ export async function testModelConnection({ baseURL, model, apiKey, timeoutMs =
341
337
  const controller = new AbortController()
342
338
  const timer = setTimeout(() => controller.abort(), timeoutMs)
343
339
  try {
340
+ // 统一构造:身份/会话头 + OpenCode 网关的协议族校验(非 chat 模型直接给可读报错)
341
+ const { url, headers } = buildAiChatRequest({ baseURL, model, apiKey })
344
342
  const res = await fetch(url, {
345
343
  method: 'POST',
346
344
  headers,
package/src/config.js CHANGED
@@ -50,6 +50,29 @@ function resolveConfigPath() {
50
50
  return _configPathReady;
51
51
  }
52
52
 
53
+ // AI 智能体单轮工具调用上限的可调范围。
54
+ // 下限 1 是为了保留"防失控"语义 —— 0 会让智能体一动手就停,没人是这个意图;
55
+ // 上限 2000 是防手改配置写成天文数字后,一轮对话把 API 额度烧光。
56
+ const AI_MAX_TOOL_ITERATIONS_MIN = 1;
57
+ const AI_MAX_TOOL_ITERATIONS_MAX = 2000;
58
+
59
+ /**
60
+ * 规范化单轮最大工具调用次数。
61
+ *
62
+ * 非法值(非数字 / NaN / 越界)一律**夹取**到合法区间而不是回退默认值 ——
63
+ * 用户手改成 5000 的意图明显是"想更大",夹到 2000 比悄悄退回 200 更贴近意图。
64
+ * 完全无法解析(undefined / 'abc')才返回 null,交给调用方取默认值。
65
+ */
66
+ function normalizeAiMaxToolIterations(value) {
67
+ if (value === undefined || value === null || value === '') return null;
68
+ const n = Number(value);
69
+ if (!Number.isFinite(n)) return null;
70
+ const int = Math.floor(n);
71
+ if (int < AI_MAX_TOOL_ITERATIONS_MIN) return AI_MAX_TOOL_ITERATIONS_MIN;
72
+ if (int > AI_MAX_TOOL_ITERATIONS_MAX) return AI_MAX_TOOL_ITERATIONS_MAX;
73
+ return int;
74
+ }
75
+
53
76
  // 默认配置
54
77
  const defaultConfig = {
55
78
  defaultCommitMessage: "submit",
@@ -90,6 +113,11 @@ const defaultConfig = {
90
113
  locale: 'zh-CN', // 语言: zh-CN | en-US
91
114
  // AI 模型配置
92
115
  models: [],
116
+ // AI 智能体单轮最大工具调用次数(防失控)。CLI `g ai` 与 Web 智能体共用,全局生效。
117
+ // 2026-09-20: 40 → 200。40 轮在"读多个文件 → 逐个验证 → 再改"这类任务里很容易触顶,
118
+ // 触顶后本轮被强制结束,用户必须再发一条消息才能接着跑,体感像被截断。
119
+ // 想调小/调大改这个值即可(GUI: 设置 → AI 模型配置)。
120
+ aiMaxToolIterations: 200,
93
121
  // UI 状态(跨项目共享,存到顶层 ui 对象)
94
122
  // 之前散落在 localStorage,因随机端口启动而失效,迁到文件持久化
95
123
  ui: {
@@ -396,7 +424,12 @@ async function loadConfig() {
396
424
  }
397
425
  // 兼容旧版(全局扁平结构)
398
426
  if (raw && !raw.projects) {
399
- return { ...defaultConfig, ...raw };
427
+ return {
428
+ ...defaultConfig,
429
+ ...raw,
430
+ aiMaxToolIterations: normalizeAiMaxToolIterations(raw.aiMaxToolIterations)
431
+ ?? defaultConfig.aiMaxToolIterations
432
+ };
400
433
  }
401
434
 
402
435
  // 新版结构:{ projects: { [key]: projectConfig }, theme?, locale?, ui?, recentDirectories? }
@@ -409,7 +442,10 @@ async function loadConfig() {
409
442
  theme: raw?.theme ?? defaultConfig.theme,
410
443
  locale: raw?.locale ?? defaultConfig.locale,
411
444
  models: raw?.models ?? defaultConfig.models,
412
- ui: raw?.ui ?? defaultConfig.ui
445
+ ui: raw?.ui ?? defaultConfig.ui,
446
+ // 同 models:全局配置,始终取顶层,防止被项目配置里的旧值覆盖
447
+ aiMaxToolIterations: normalizeAiMaxToolIterations(raw?.aiMaxToolIterations)
448
+ ?? defaultConfig.aiMaxToolIterations
413
449
  };
414
450
  }
415
451
 
@@ -457,7 +493,7 @@ async function saveConfig(config) {
457
493
 
458
494
  // 分离全局设置和项目设置
459
495
  // models / ui 也是全局配置(跨项目共享),和 theme/locale 一样存到顶层
460
- const { theme, locale, models, ui, ...projectConfig } = config;
496
+ const { theme, locale, models, ui, aiMaxToolIterations, ...projectConfig } = config;
461
497
 
462
498
  // 保存全局设置到根级别
463
499
  if (theme !== undefined) {
@@ -472,6 +508,11 @@ async function saveConfig(config) {
472
508
  if (ui !== undefined) {
473
509
  raw.ui = ui;
474
510
  }
511
+ // 工具调用上限同属全局设置:非法值不落盘(保留磁盘上的旧值),避免把手改坏的值固化
512
+ const normalizedIterations = normalizeAiMaxToolIterations(aiMaxToolIterations);
513
+ if (normalizedIterations !== null) {
514
+ raw.aiMaxToolIterations = normalizedIterations;
515
+ }
475
516
 
476
517
  // 写入当前项目配置(在 defaultConfig 基础上合并,但不清空顶层其它键)
477
518
  const existingProjectConfig = (raw.projects[key] && typeof raw.projects[key] === 'object') ? raw.projects[key] : {};
@@ -662,7 +703,19 @@ export default {
662
703
  // 显式删除某个项目的全部分文件。写路径刻意不做删除(防"只加载了一个项目就写回"
663
704
  // 连带清空其它项目),清理失效项目只能走这里。
664
705
  deleteProjectConfig: deleteProjectConfigAndInvalidate,
706
+ // AI 智能体单轮工具调用上限的规范化/区间(GUI 保存前也要用,见 /api/config/save-ai-settings)
707
+ normalizeAiMaxToolIterations,
708
+ AI_MAX_TOOL_ITERATIONS_MIN,
709
+ AI_MAX_TOOL_ITERATIONS_MAX,
665
710
  };
666
711
 
667
712
  // 命名导出 — 用于测试与外部复用
668
- export { ConfigWriteError, normalizeProjectPath, invalidateCurrentProjectKey, invalidateRawConfigCache };
713
+ export {
714
+ ConfigWriteError,
715
+ normalizeProjectPath,
716
+ invalidateCurrentProjectKey,
717
+ invalidateRawConfigCache,
718
+ normalizeAiMaxToolIterations,
719
+ AI_MAX_TOOL_ITERATIONS_MIN,
720
+ AI_MAX_TOOL_ITERATIONS_MAX,
721
+ };
@@ -1,4 +1,4 @@
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,On as l,Pn as u,Pr as d,Qr as f,Tn as p,Wn as m,Y as h,Yr as g,Zn as _,_n as ee,a as v,bn as te,br as y,cn as ne,cr as b,gt as x,i as S,kn as C,lr as w,o as re,or as T,pr as E,qr as D,tt as ie,un as O,ut as ae,vr as k,xn as oe,zt as se}from"./element-plus-KTmRV-so.js";import{a as ce,r as A,t as j}from"./vendor-CwZs6S_i.js";import{n as M,t as N}from"./_plugin-vue_export-helper-nUAWISuX.js";import{t as P}from"./configStore-CRYvJSvr.js";E();var F=65536;function I(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:z(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>F&&(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||A(),name:e.function?.name||``,arguments:e.function?.arguments||``,argsPreview:L(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>F&&(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 L(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 R(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 z(e){let t=/^data:([^;,]+)/.exec(e);return t?t[1]:``}function B(){let e=P(),t=y([]),n=y(!1),r=y(null),i=y([]),a=y(!1),o=y(!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){v.error(n.error||M(`@AGENT:加载会话列表失败`));return}t.value=Array.isArray(n.sessions)?n.sessions:[]}catch(e){if(r!==l)return;v.error(M(`@AGENT:加载会话列表失败`)+`: `+(e?.message||e))}finally{r===l&&(n.value=!1)}}async function d(e){o.value=!0,r.value=e;try{let t=await fetch(`/api/agent/sessions/${encodeURIComponent(e)}`).then(e=>e.json());if(!t.success){v.error(t.error||M(`@AGENT:加载会话失败`));return}i.value=I(t.session)}catch(e){v.error(M(`@AGENT:加载会话失败`)+`: `+(e?.message||e))}finally{o.value=!1}}async function f(e){try{await S.confirm(M(`@AGENT:确认删除该会话`),M(`@AGENT:删除后无法恢复`),{type:`warning`})}catch{return}try{let n=await fetch(`/api/agent/sessions/${encodeURIComponent(e)}`,{method:`DELETE`}).then(e=>e.json());if(!n.success){v.error(n.error||M(`@AGENT:删除失败`));return}t.value=t.value.filter(t=>t.sessionId!==e),r.value===e&&(r.value=null,i.value=[]),v.success(M(`@AGENT:已删除`))}catch(e){v.error(M(`@AGENT:删除失败`)+`: `+(e?.message||e))}}async function p(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){v.error(r.error||M(`@AGENT:重命名失败`));return}let i=t.value.find(t=>t.sessionId===e);i&&(i.title=n),v.success(M(`@AGENT:已重命名`))}catch(e){v.error(M(`@AGENT:重命名失败`)+`: `+(e?.message||e))}}function m(){r.value=null,i.value=[]}async function h(t,n=[]){let o=n.filter(e=>e?.file?.type?.startsWith(`image/`));if(o.length<n.length&&v.warning(M(`@AGENT:仅支持发送图片,非图片文件已忽略`)),!t.trim()&&o.length===0||a.value)return;let l=(await Promise.allSettled(o.map(e=>R(e.file)))).map(e=>e.status===`fulfilled`?e.value:``).filter(e=>e.startsWith(`data:image/`)),d=++c;s=new AbortController;let f=s,p={id:A(),role:`user`,content:t,status:`done`,createdAt:Date.now()};o.length>0&&(p.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(p);let m=k({id:A(),role:`assistant`,content:``,status:`pending`,createdAt:Date.now()});i.value.push(m),a.value=!0;let h=[];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:f.signal});if(d!==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=``,p=r.value;for(;;){let{value:e,done:t}=await a.read();if(d!==c)return;if(t)break;s+=o.decode(e,{stream:!0});let n=s.split(`
2
- `);s=n.pop()??``;for(let e of n){let t=e.trim();if(!t.startsWith(`data:`))continue;let n=t.slice(5).trim();if(!n)continue;let i;try{i=JSON.parse(n)}catch{continue}switch(i.type){case`meta`:i.sessionId&&(p=i.sessionId,r.value=i.sessionId);break;case`thinking`:m.reasoning||(m.reasoning=``,m.reasoningStatus=`streaming`),m.reasoning+=String(i.delta||``),m.status=`streaming`;break;case`content`:m.status===`pending`&&(m.status=`streaming`);{let e=String(i.delta||``);m.content+=e;let t=j(m.content);t.reasoning&&(m.reasoning||(m.reasoning=``,m.reasoningStatus=`streaming`),m.reasoning=t.reasoning,m.content=t.content)}break;case`tool_call_start`:{let e=k({id:i.toolCallId||A(),name:i.name||``,argsPreview:i.argsPreview||``,arguments:i.argsPreview||``,status:`running`,result:``});h.push(e),m.toolCalls||=[],m.toolCalls.push(e),m.status===`pending`&&(m.status=`streaming`);break}case`tool_result`:{let e=h.find(e=>e.id===i.toolCallId);e&&(e.result=String(i.result||``),e.status=`done`);break}case`done`:{let e=i.content||m.content,t=j(e);t.reasoning&&!m.reasoning&&(m.reasoning=t.reasoning),m.content=t.content,m.status=`done`,m.reasoningStatus===`streaming`&&(m.reasoningStatus=`done`);break}case`error`:m.status=`error`,m.error=String(i.error||M(`@AGENT:未知错误`)),m.reasoningStatus===`streaming`&&(m.reasoningStatus=`done`)}}i.value=[...i.value]}if(d!==c)return;m.status===`pending`&&(m.status=`done`),p&&p!==r.value&&(r.value=p),u().catch(()=>{})}catch(e){if(d!==c)return;e?.name===`AbortError`||f.signal.aborted?(m.content=(m.content||``)+`
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,On as l,Pn as u,Pr as d,Qr as f,Tn as p,Wn as m,Y as h,Yr as g,Zn as _,_n as ee,a as v,bn as te,br as y,cn as ne,cr as b,gt as re,i as x,kn as S,lr as C,o as ie,or as w,pr as T,qr as E,tt as D,un as O,ut as ae,vr as k,xn as oe,zt as se}from"./element-plus-KTmRV-so.js";import{a as A,n as j,r as ce}from"./vendor-92fKgtnz.js";import{n as M,t as N}from"./_plugin-vue_export-helper-B7A-NgBp.js";import{t as P}from"./configStore-Dx84lvTX.js";T();var F=65536;function I(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:z(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>F&&(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||j(),name:e.function?.name||``,arguments:e.function?.arguments||``,argsPreview:L(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>F&&(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 L(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 R(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 z(e){let t=/^data:([^;,]+)/.exec(e);return t?t[1]:``}function B(){let e=P(),t=y([]),n=y(!1),r=y(null),i=y([]),a=y(!1),o=y(!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){v.error(n.error||M(`@AGENT:加载会话列表失败`));return}t.value=Array.isArray(n.sessions)?n.sessions:[]}catch(e){if(r!==l)return;v.error(M(`@AGENT:加载会话列表失败`)+`: `+(e?.message||e))}finally{r===l&&(n.value=!1)}}async function d(e){o.value=!0,r.value=e;try{let t=await fetch(`/api/agent/sessions/${encodeURIComponent(e)}`).then(e=>e.json());if(!t.success){v.error(t.error||M(`@AGENT:加载会话失败`));return}i.value=I(t.session)}catch(e){v.error(M(`@AGENT:加载会话失败`)+`: `+(e?.message||e))}finally{o.value=!1}}async function f(e){try{await x.confirm(M(`@AGENT:确认删除该会话`),M(`@AGENT:删除后无法恢复`),{type:`warning`})}catch{return}try{let n=await fetch(`/api/agent/sessions/${encodeURIComponent(e)}`,{method:`DELETE`}).then(e=>e.json());if(!n.success){v.error(n.error||M(`@AGENT:删除失败`));return}t.value=t.value.filter(t=>t.sessionId!==e),r.value===e&&(r.value=null,i.value=[]),v.success(M(`@AGENT:已删除`))}catch(e){v.error(M(`@AGENT:删除失败`)+`: `+(e?.message||e))}}async function p(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){v.error(r.error||M(`@AGENT:重命名失败`));return}let i=t.value.find(t=>t.sessionId===e);i&&(i.title=n),v.success(M(`@AGENT:已重命名`))}catch(e){v.error(M(`@AGENT:重命名失败`)+`: `+(e?.message||e))}}function m(){r.value=null,i.value=[]}async function h(t,n=[]){let o=n.filter(e=>e?.file?.type?.startsWith(`image/`));if(o.length<n.length&&v.warning(M(`@AGENT:仅支持发送图片,非图片文件已忽略`)),!t.trim()&&o.length===0||a.value)return;let l=(await Promise.allSettled(o.map(e=>R(e.file)))).map(e=>e.status===`fulfilled`?e.value:``).filter(e=>e.startsWith(`data:image/`)),d=++c;s=new AbortController;let f=s,p={id:j(),role:`user`,content:t,status:`done`,createdAt:Date.now()};o.length>0&&(p.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(p);let m=k({id:j(),role:`assistant`,content:``,status:`pending`,createdAt:Date.now()});i.value.push(m),a.value=!0;let h=[];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:f.signal});if(d!==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=``,p=r.value;for(;;){let{value:e,done:t}=await a.read();if(d!==c)return;if(t)break;s+=o.decode(e,{stream:!0});let n=s.split(`
2
+ `);s=n.pop()??``;for(let e of n){let t=e.trim();if(!t.startsWith(`data:`))continue;let n=t.slice(5).trim();if(!n)continue;let i;try{i=JSON.parse(n)}catch{continue}switch(i.type){case`meta`:i.sessionId&&(p=i.sessionId,r.value=i.sessionId);break;case`thinking`:m.reasoning||(m.reasoning=``,m.reasoningStatus=`streaming`),m.reasoning+=String(i.delta||``),m.status=`streaming`;break;case`content`:m.status===`pending`&&(m.status=`streaming`);{let e=String(i.delta||``);m.content+=e;let t=A(m.content);t.reasoning&&(m.reasoning||(m.reasoning=``,m.reasoningStatus=`streaming`),m.reasoning=t.reasoning,m.content=t.content)}break;case`tool_call_start`:{let e=k({id:i.toolCallId||j(),name:i.name||``,argsPreview:i.argsPreview||``,arguments:i.argsPreview||``,status:`running`,result:``});h.push(e),m.toolCalls||=[],m.toolCalls.push(e),m.status===`pending`&&(m.status=`streaming`);break}case`tool_result`:{let e=h.find(e=>e.id===i.toolCallId);e&&(e.result=String(i.result||``),e.status=`done`);break}case`done`:{let e=i.content||m.content,t=A(e);t.reasoning&&!m.reasoning&&(m.reasoning=t.reasoning),m.content=t.content,m.status=`done`,m.reasoningStatus===`streaming`&&(m.reasoningStatus=`done`);break}case`error`:m.status=`error`,m.error=String(i.error||M(`@AGENT:未知错误`)),m.reasoningStatus===`streaming`&&(m.reasoningStatus=`done`)}}i.value=[...i.value]}if(d!==c)return;m.status===`pending`&&(m.status=`done`),p&&p!==r.value&&(r.value=p),u().catch(()=>{})}catch(e){if(d!==c)return;e?.name===`AbortError`||f.signal.aborted?(m.content=(m.content||``)+`
3
3
 
4
- [`+M(`@AGENT:已停止`)+`]`,m.status=`done`,m.reasoningStatus===`streaming`&&(m.reasoningStatus=`done`)):(m.status=`error`,m.error=e?.message||String(e),v.error(m.error||M(`@AGENT:对话失败`)))}finally{d===c&&(a.value=!1,s=null)}}function g(){s&&=(s.abort(),null)}return{sessions:t,sessionsLoading:n,currentSessionId:r,messages:i,isStreaming:a,sessionLoading:o,loadSessions:u,loadSession:d,deleteSession:f,renameSession:p,newSession:m,sendMessage:h,stop:g}}n(),E(),d(),O();var V={class:`agent-view`},H={class:`sidebar-header`},U={class:`sidebar-search`},W=[`placeholder`],G={class:`session-list`},le={key:0,class:`empty-state`},K={class:`empty-text`},ue={key:0,class:`empty-hint`},de=[`onClick`],fe={class:`session-item-main`},pe={class:`session-item-title`},me={class:`session-item-meta`},he={class:`meta-time`},ge={class:`meta-count`},_e={key:0,class:`meta-source cli`},ve=[`onClick`],ye=[`onClick`],be={class:`agent-chat-area`},xe={key:0,class:`chat-loading`},Se={key:0,class:`stop-button-bar`},q=N(c({__name:`AgentView`,setup(n){let c=P(),{sessions:d,sessionsLoading:E,currentSessionId:O,messages:k,isStreaming:A,sessionLoading:j,loadSessions:N,loadSession:F,deleteSession:I,renameSession:L,newSession:R,sendMessage:z,stop:q}=B(),Ce=p(()=>{let e=c.theme;return e===`dark`||e===`auto`&&window.matchMedia(`(prefers-color-scheme: dark)`).matches?`dark`:`light`}),J=y(``),Y=p(()=>{if(!J.value.trim())return d.value;let e=J.value.toLowerCase();return d.value.filter(t=>(t.title||``).toLowerCase().includes(e)||(t.model||``).toLowerCase().includes(e))}),we=p(()=>[{id:`p1`,label:M(`@AGENT:查看项目结构`),prompt:M(`@AGENT:prompt_p1`)},{id:`p2`,label:M(`@AGENT:分析代码质量`),prompt:M(`@AGENT:prompt_p2`)},{id:`p3`,label:M(`@AGENT:帮我写测试`),prompt:M(`@AGENT:prompt_p3`)},{id:`p4`,label:M(`@AGENT:Git 状态检查`),prompt:M(`@AGENT:prompt_p4`)}]);async function Te(e){await z(e.text,e.files),await m(),Z()}async function Ee(e){await z(e.prompt),await m(),Z()}let X=y(null);function Z(e=!0){X.value?.scrollToBottom(e)}T(()=>k.value.length,()=>{m(()=>Z(!1))});function De(e){if(A.value){v.warning(M(`@AGENT:请先停止当前生成`));return}F(e)}function Oe(){if(A.value){v.warning(M(`@AGENT:请先停止当前生成`));return}R()}function ke(e,t){if(t.stopPropagation(),A.value&&O.value===e){v.warning(M(`@AGENT:请先停止当前生成`));return}I(e)}async function Ae(e,t,n){n.stopPropagation();try{let{value:n}=await S.prompt(M(`@AGENT:请输入新的会话标题`),M(`@AGENT:重命名会话`),{inputValue:t,inputPattern:/.+/,inputErrorMessage:M(`@AGENT:标题不能为空`)});n&&n!==t&&await L(e,n)}catch{}}function je(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?M(`@AGENT:今天`):`${e}${M(`@AGENT:天前`)}`}return`${t.getFullYear()}-${i(t.getMonth()+1)}-${i(t.getDate())}`}catch{return e}}let Q=y(280),$=y(!1);function Me(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,A.value],async([e,t],n)=>{let r=!n||e!==n[0],i=!!n&&n[1]===!0&&!t;(r||i)&&await N(),O.value&&!A.value&&!d.value.some(e=>e.sessionId===O.value)&&R()},{immediate:!0}),(n,c)=>{let d=re;return _(),C(`div`,V,[i(`aside`,{class:`agent-sidebar`,style:g({width:Q.value+`px`})},[i(`div`,H,[i(`button`,{class:`new-session-btn`,onClick:Oe},[u(a(h),null,{default:b(()=>[u(a(se))]),_:1}),i(`span`,null,f(a(M)(`@AGENT:新建会话`)),1)])]),i(`div`,U,[u(a(h),{class:`search-icon`},{default:b(()=>[u(a(s))]),_:1}),w(i(`input`,{"onUpdate:modelValue":c[0]||=e=>J.value=e,placeholder:a(M)(`@AGENT:搜索会话...`),class:`search-input`,type:`text`},null,8,W),[[ee,J.value]])]),w((_(),C(`div`,G,[Y.value.length===0&&!a(E)?(_(),C(`div`,le,[u(a(h),{size:28,color:`var(--text-tertiary)`},{default:b(()=>[u(a(ie))]),_:1}),i(`div`,K,f(J.value?a(M)(`@AGENT:未找到匹配的会话`):a(M)(`@AGENT:暂无会话`)),1),J.value?l(``,!0):(_(),C(`div`,ue,f(a(M)(`@AGENT:点击上方按钮开始对话`)),1))])):l(``,!0),(_(!0),C(oe,null,e(Y.value,e=>(_(),C(`div`,{key:e.sessionId,class:D([`session-item`,{active:a(O)===e.sessionId}]),onClick:t=>De(e.sessionId)},[i(`div`,fe,[i(`div`,pe,f(e.title||a(M)(`@AGENT:无标题`)),1),i(`div`,me,[i(`span`,he,f(je(e.updatedAt)),1),c[3]||=i(`span`,{class:`meta-dot`},`·`,-1),i(`span`,ge,f(e.messageCount)+` `+f(a(M)(`@AGENT:条`)),1),e.source===`cli`?(_(),C(`span`,_e,`CLI`)):l(``,!0)])]),i(`div`,{class:`session-item-actions`,onClick:c[1]||=te(()=>{},[`stop`])},[u(a(o),{content:a(M)(`@AGENT:重命名`),placement:`top`,"show-after":500},{default:b(()=>[i(`button`,{class:`item-action-btn`,onClick:t=>Ae(e.sessionId,e.title,t)},[u(a(h),null,{default:b(()=>[u(a(x))]),_:1})],8,ve)]),_:2},1032,[`content`]),u(a(o),{content:a(M)(`@AGENT:删除`),placement:`top`,"show-after":500},{default:b(()=>[i(`button`,{class:`item-action-btn danger`,onClick:t=>ke(e.sessionId,t)},[u(a(h),null,{default:b(()=>[u(a(ae))]),_:1})],8,ye)]),_:2},1032,[`content`])])],10,de))),128))])),[[d,a(E)]])],4),i(`div`,{class:D([`sidebar-resizer`,{active:$.value}]),onMousedown:Me},null,34),i(`main`,be,[a(j)?(_(),C(`div`,xe,[u(a(h),{class:`is-loading`,size:32},{default:b(()=>[u(a(t))]),_:1}),i(`span`,null,f(a(M)(`@AGENT:加载会话中...`)),1)])):(_(),r(a(ce),{key:1,ref_key:`chatContainerRef`,ref:X,messages:a(k),"preset-questions":we.value,"welcome-title":a(M)(`@AGENT:智能体助手`),"welcome-description":a(M)(`@AGENT:我可以帮你阅读代码、执行命令、修改文件。选择下方话题或直接输入你的问题。`),"assistant-name":`g ai`,theme:Ce.value,disabled:a(A),"upload-config":{accept:`image/*`},placeholder:a(A)?a(M)(`@AGENT:正在生成中...`):a(M)(`@AGENT:输入消息,Enter 发送`),onSend:Te,onSelect:Ee},null,8,[`messages`,`preset-questions`,`welcome-title`,`welcome-description`,`theme`,`disabled`,`placeholder`])),u(ne,{name:`fade`},{default:b(()=>[a(A)?(_(),C(`div`,Se,[i(`button`,{class:`stop-button`,onClick:c[2]||=(...e)=>a(q)&&a(q)(...e)},[u(a(h),null,{default:b(()=>[u(a(t),{class:`is-loading`})]),_:1}),i(`span`,null,f(a(M)(`@AGENT:停止生成`)),1)])])):l(``,!0)]),_:1})])])}}}),[[`__scopeId`,`data-v-773baa74`]]);export{q as default};
4
+ [`+M(`@AGENT:已停止`)+`]`,m.status=`done`,m.reasoningStatus===`streaming`&&(m.reasoningStatus=`done`)):(m.status=`error`,m.error=e?.message||String(e),v.error(m.error||M(`@AGENT:对话失败`)))}finally{d===c&&(a.value=!1,s=null)}}function g(){s&&=(s.abort(),null)}return{sessions:t,sessionsLoading:n,currentSessionId:r,messages:i,isStreaming:a,sessionLoading:o,loadSessions:u,loadSession:d,deleteSession:f,renameSession:p,newSession:m,sendMessage:h,stop:g}}n(),T(),d(),O();var V={class:`agent-view`},H={class:`sidebar-header`},U={class:`sidebar-search`},W=[`placeholder`],G={class:`session-list`},le={key:0,class:`empty-state`},ue={class:`empty-text`},de={key:0,class:`empty-hint`},fe=[`onClick`],pe={class:`session-item-main`},K={class:`session-item-title`},me={class:`session-item-meta`},he={class:`meta-time`},ge={class:`meta-count`},_e={key:0,class:`meta-source cli`},ve=[`onClick`],ye=[`onClick`],be={class:`agent-chat-area`},xe={key:0,class:`chat-loading`},Se={key:0,class:`stop-button-bar`},q=N(c({__name:`AgentView`,setup(n){let c=P(),{sessions:d,sessionsLoading:T,currentSessionId:O,messages:k,isStreaming:A,sessionLoading:j,loadSessions:N,loadSession:F,deleteSession:I,renameSession:L,newSession:R,sendMessage:z,stop:q}=B(),Ce=p(()=>{let e=c.theme;return e===`dark`||e===`auto`&&window.matchMedia(`(prefers-color-scheme: dark)`).matches?`dark`:`light`}),J=y(``),Y=p(()=>{if(!J.value.trim())return d.value;let e=J.value.toLowerCase();return d.value.filter(t=>(t.title||``).toLowerCase().includes(e)||(t.model||``).toLowerCase().includes(e))}),we=p(()=>[{id:`p1`,label:M(`@AGENT:查看项目结构`),prompt:M(`@AGENT:prompt_p1`)},{id:`p2`,label:M(`@AGENT:分析代码质量`),prompt:M(`@AGENT:prompt_p2`)},{id:`p3`,label:M(`@AGENT:帮我写测试`),prompt:M(`@AGENT:prompt_p3`)},{id:`p4`,label:M(`@AGENT:Git 状态检查`),prompt:M(`@AGENT:prompt_p4`)},{id:`p5`,label:M(`@AGENT:帮我启动项目`),prompt:M(`@AGENT:prompt_p5`)}]);async function Te(e){await z(e.text,e.files),await m(),Z()}async function Ee(e){await z(e.prompt),await m(),Z()}let X=y(null);function Z(e=!0){X.value?.scrollToBottom(e)}w(()=>k.value.length,()=>{m(()=>Z(!1))});function De(e){if(A.value){v.warning(M(`@AGENT:请先停止当前生成`));return}F(e)}function Oe(){if(A.value){v.warning(M(`@AGENT:请先停止当前生成`));return}R()}function ke(e,t){if(t.stopPropagation(),A.value&&O.value===e){v.warning(M(`@AGENT:请先停止当前生成`));return}I(e)}async function Ae(e,t,n){n.stopPropagation();try{let{value:n}=await x.prompt(M(`@AGENT:请输入新的会话标题`),M(`@AGENT:重命名会话`),{inputValue:t,inputPattern:/.+/,inputErrorMessage:M(`@AGENT:标题不能为空`)});n&&n!==t&&await L(e,n)}catch{}}function je(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?M(`@AGENT:今天`):`${e}${M(`@AGENT:天前`)}`}return`${t.getFullYear()}-${i(t.getMonth()+1)}-${i(t.getDate())}`}catch{return e}}let Q=y(280),$=y(!1);function Me(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 w(()=>[c.currentDirectory,A.value],async([e,t],n)=>{let r=!n||e!==n[0],i=!!n&&n[1]===!0&&!t;(r||i)&&await N(),O.value&&!A.value&&!d.value.some(e=>e.sessionId===O.value)&&R()},{immediate:!0}),(n,c)=>{let d=ie;return _(),S(`div`,V,[i(`aside`,{class:`agent-sidebar`,style:g({width:Q.value+`px`})},[i(`div`,H,[i(`button`,{class:`new-session-btn`,onClick:Oe},[u(a(h),null,{default:b(()=>[u(a(se))]),_:1}),i(`span`,null,f(a(M)(`@AGENT:新建会话`)),1)])]),i(`div`,U,[u(a(h),{class:`search-icon`},{default:b(()=>[u(a(s))]),_:1}),C(i(`input`,{"onUpdate:modelValue":c[0]||=e=>J.value=e,placeholder:a(M)(`@AGENT:搜索会话...`),class:`search-input`,type:`text`},null,8,W),[[ee,J.value]])]),C((_(),S(`div`,G,[Y.value.length===0&&!a(T)?(_(),S(`div`,le,[u(a(h),{size:28,color:`var(--text-tertiary)`},{default:b(()=>[u(a(D))]),_:1}),i(`div`,ue,f(J.value?a(M)(`@AGENT:未找到匹配的会话`):a(M)(`@AGENT:暂无会话`)),1),J.value?l(``,!0):(_(),S(`div`,de,f(a(M)(`@AGENT:点击上方按钮开始对话`)),1))])):l(``,!0),(_(!0),S(oe,null,e(Y.value,e=>(_(),S(`div`,{key:e.sessionId,class:E([`session-item`,{active:a(O)===e.sessionId}]),onClick:t=>De(e.sessionId)},[i(`div`,pe,[i(`div`,K,f(e.title||a(M)(`@AGENT:无标题`)),1),i(`div`,me,[i(`span`,he,f(je(e.updatedAt)),1),c[3]||=i(`span`,{class:`meta-dot`},`·`,-1),i(`span`,ge,f(e.messageCount)+` `+f(a(M)(`@AGENT:条`)),1),e.source===`cli`?(_(),S(`span`,_e,`CLI`)):l(``,!0)])]),i(`div`,{class:`session-item-actions`,onClick:c[1]||=te(()=>{},[`stop`])},[u(a(o),{content:a(M)(`@AGENT:重命名`),placement:`top`,"show-after":500},{default:b(()=>[i(`button`,{class:`item-action-btn`,onClick:t=>Ae(e.sessionId,e.title,t)},[u(a(h),null,{default:b(()=>[u(a(re))]),_:1})],8,ve)]),_:2},1032,[`content`]),u(a(o),{content:a(M)(`@AGENT:删除`),placement:`top`,"show-after":500},{default:b(()=>[i(`button`,{class:`item-action-btn danger`,onClick:t=>ke(e.sessionId,t)},[u(a(h),null,{default:b(()=>[u(a(ae))]),_:1})],8,ye)]),_:2},1032,[`content`])])],10,fe))),128))])),[[d,a(T)]])],4),i(`div`,{class:E([`sidebar-resizer`,{active:$.value}]),onMousedown:Me},null,34),i(`main`,be,[a(j)?(_(),S(`div`,xe,[u(a(h),{class:`is-loading`,size:32},{default:b(()=>[u(a(t))]),_:1}),i(`span`,null,f(a(M)(`@AGENT:加载会话中...`)),1)])):(_(),r(a(ce),{key:1,ref_key:`chatContainerRef`,ref:X,messages:a(k),"preset-questions":we.value,"welcome-title":a(M)(`@AGENT:智能体助手`),"welcome-description":a(M)(`@AGENT:我可以帮你阅读代码、执行命令、修改文件。选择下方话题或直接输入你的问题。`),"assistant-name":`g ai`,theme:Ce.value,disabled:a(A),"upload-config":{accept:`image/*`},placeholder:a(A)?a(M)(`@AGENT:正在生成中...`):a(M)(`@AGENT:输入消息,Enter 发送`),onSend:Te,onSelect:Ee},null,8,[`messages`,`preset-questions`,`welcome-title`,`welcome-description`,`theme`,`disabled`,`placeholder`])),u(ne,{name:`fade`},{default:b(()=>[a(A)?(_(),S(`div`,Se,[i(`button`,{class:`stop-button`,onClick:c[2]||=(...e)=>a(q)&&a(q)(...e)},[u(a(h),null,{default:b(()=>[u(a(t),{class:`is-loading`})]),_:1}),i(`span`,null,f(a(M)(`@AGENT:停止生成`)),1)])])):l(``,!0)]),_:1})])])}}}),[[`__scopeId`,`data-v-a1a388e1`]]);export{q as default};
@@ -0,0 +1 @@
1
+ .agent-view[data-v-a1a388e1]{background:var(--bg-container);height:100%;min-height:0;display:flex}.agent-sidebar[data-v-a1a388e1]{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-a1a388e1]{flex-shrink:0;padding:10px 12px}.new-session-btn[data-v-a1a388e1]{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-a1a388e1]:hover{border-color:var(--color-primary);color:var(--color-primary);background:color-mix(in srgb, var(--color-primary) 6%, transparent)}.sidebar-search[data-v-a1a388e1]{flex-shrink:0;align-items:center;gap:6px;padding:0 12px 8px;display:flex}.sidebar-search .search-icon[data-v-a1a388e1]{color:var(--text-tertiary);flex-shrink:0}.sidebar-search .search-input[data-v-a1a388e1]{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-a1a388e1]::placeholder{color:var(--text-tertiary)}.session-list[data-v-a1a388e1]{flex:1;min-height:0;padding:0 6px 8px;overflow-y:auto}.empty-state[data-v-a1a388e1]{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-a1a388e1]{color:var(--text-secondary);margin-top:10px;font-size:14px}.empty-state .empty-hint[data-v-a1a388e1]{margin-top:4px;font-size:12px}.session-item[data-v-a1a388e1]{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-a1a388e1]:hover{background:var(--bg-hover)}.session-item:hover .session-item-actions[data-v-a1a388e1]{opacity:1}.session-item.active[data-v-a1a388e1]{background:color-mix(in srgb, var(--color-primary) 10%, transparent)}.session-item.active .session-item-title[data-v-a1a388e1]{color:var(--color-primary)}.session-item.active[data-v-a1a388e1]: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-a1a388e1]{flex:1;min-width:0}.session-item-title[data-v-a1a388e1]{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-a1a388e1]{color:var(--text-tertiary);align-items:center;gap:4px;font-size:11px;display:flex}.session-item-meta .meta-dot[data-v-a1a388e1]{opacity:.5}.session-item-meta .meta-source[data-v-a1a388e1]{border-radius:3px;padding:0 4px;font-size:10px;font-weight:600}.session-item-meta .meta-source.cli[data-v-a1a388e1]{background:color-mix(in srgb, var(--color-warning) 20%, transparent);color:var(--color-warning)}.session-item-actions[data-v-a1a388e1]{opacity:0;flex-shrink:0;gap:2px;transition:opacity .12s;display:flex}.item-action-btn[data-v-a1a388e1]{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-a1a388e1]:hover{background:var(--bg-hover);color:var(--text-secondary)}.item-action-btn.danger[data-v-a1a388e1]:hover{color:var(--color-danger);background:color-mix(in srgb, var(--color-danger) 10%, transparent)}.sidebar-resizer[data-v-a1a388e1]{cursor:col-resize;background:0 0;flex-shrink:0;width:4px;transition:background .15s}.sidebar-resizer[data-v-a1a388e1]:hover,.sidebar-resizer.active[data-v-a1a388e1]{background:var(--color-primary);opacity:.3}.agent-chat-area[data-v-a1a388e1]{flex-direction:column;flex:1;min-width:0;display:flex;position:relative;overflow:hidden}.chat-loading[data-v-a1a388e1]{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-a1a388e1]{z-index:10;position:absolute;bottom:80px;left:50%;transform:translate(-50%)}.stop-button[data-v-a1a388e1]{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-a1a388e1]:hover{background:var(--color-danger);color:#fff}.fade-enter-active[data-v-a1a388e1],.fade-leave-active[data-v-a1a388e1]{transition:opacity .2s}.fade-enter-from[data-v-a1a388e1],.fade-leave-to[data-v-a1a388e1]{opacity:0}[data-theme=dark]{background:var(--bg-panel)}