zen-gitsync 2.17.12 → 2.17.14

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 (132) hide show
  1. package/README.md +6 -2
  2. package/package.json +1 -1
  3. package/scripts/release.js +201 -9
  4. package/src/cli/ai/agent.js +16 -0
  5. package/src/ui/public/assets/AgentView-DLHlVWRg.js +5 -0
  6. package/src/ui/public/assets/AgentView-vvVhbIQ5.css +1 -0
  7. package/src/ui/public/assets/{AppVersionBadge-D5tJzJfa.css → AppVersionBadge-CJrDr7GV.css} +1 -1
  8. package/src/ui/public/assets/AppVersionBadge-JxpMuGwP.js +2 -0
  9. package/src/ui/public/assets/BranchSelector-B0eLkAdv.css +1 -0
  10. package/src/ui/public/assets/BranchSelector-XIN87Mkw.js +1 -0
  11. package/src/ui/public/assets/CommitForm-CQ0K8Yy2.css +1 -0
  12. package/src/ui/public/assets/CommitForm-CW53Er5m.js +6 -0
  13. package/src/ui/public/assets/CommonDialog-BiNTGpA-.js +1 -0
  14. package/src/ui/public/assets/CommonDialog-jP0E9mco.css +1 -0
  15. package/src/ui/public/assets/EditorView-D0JoJhgt.js +1 -0
  16. package/src/ui/public/assets/EditorView-DZMLvQds.css +1 -0
  17. package/src/ui/public/assets/FileDiffViewer-B9EkuKdF.css +1 -0
  18. package/src/ui/public/assets/FileDiffViewer-Yas2nO2U.js +15 -0
  19. package/src/ui/public/assets/FlowExecutionViewer-D9Vzgh91.js +1 -0
  20. package/src/ui/public/assets/FlowExecutionViewer-jNHI5zdm.css +1 -0
  21. package/src/ui/public/assets/FlowOrchestrationWorkspace-CYGp-PqS.css +1 -0
  22. package/src/ui/public/assets/FlowOrchestrationWorkspace-DQ3fg5b9.js +21 -0
  23. package/src/ui/public/assets/IconButton-DysyaC1j.js +1 -0
  24. package/src/ui/public/assets/IconButton-tjkOjzkC.css +1 -0
  25. package/src/ui/public/assets/LogList-BlJplIi1.css +1 -0
  26. package/src/ui/public/assets/LogList-QDgok-UJ.js +7 -0
  27. package/src/ui/public/assets/MindmapView-BQTqx842.css +1 -0
  28. package/src/ui/public/assets/MindmapView-CeZsQNBK.js +2 -0
  29. package/src/ui/public/assets/MonitorView-D1tgmjjS.js +1 -0
  30. package/src/ui/public/assets/MonitorView-DetlFFXq.css +1 -0
  31. package/src/ui/public/assets/ProjectStartupButton-COntA4q1.js +1 -0
  32. package/src/ui/public/assets/ProjectStartupButton-QLPltCK5.css +1 -0
  33. package/src/ui/public/assets/RemoteManagerDialog-3dihAOab.js +4 -0
  34. package/src/ui/public/assets/RemoteManagerDialog-BauAeH2w.css +1 -0
  35. package/src/ui/public/assets/RemoteRepoCard-Bn9cKt1D.js +7 -0
  36. package/src/ui/public/assets/RemoteRepoCard-CG-jARTe.css +1 -0
  37. package/src/ui/public/assets/SourceMapView-C8UdzQXF.js +3 -0
  38. package/src/ui/public/assets/SourceMapView-CN4Hkyp4.css +1 -0
  39. package/src/ui/public/assets/{SvgIcon-BQ6bR7l0.css → SvgIcon-CDNsk4-Q.css} +1 -1
  40. package/src/ui/public/assets/SvgIcon-CvtoU5yo.js +1 -0
  41. package/src/ui/public/assets/TemplateManager-DMuG4i3G.css +1 -0
  42. package/src/ui/public/assets/TemplateManager-saqC9kdQ.js +1 -0
  43. package/src/ui/public/assets/UserInputNode-BVgQkB2l.css +1 -0
  44. package/src/ui/public/assets/UserInputNode-CvEfvX3i.js +1 -0
  45. package/src/ui/public/assets/WorkbenchView-B1BJhWHi.css +1 -0
  46. package/src/ui/public/assets/WorkbenchView-B7WAu1Ay.js +56 -0
  47. package/src/ui/public/assets/_plugin-vue_export-helper-BdTxl34g.js +19 -0
  48. package/src/ui/public/assets/configStore-AQMSazCM.js +1 -0
  49. package/src/ui/public/assets/element-plus-CMSkP_w2.js +48 -0
  50. package/src/ui/public/assets/element-plus-Dc1iTOWq.css +1 -0
  51. package/src/ui/public/assets/{flow-mindmap-C_w8GJXm.js → flow-mindmap-CHxAWdLw.js} +16 -16
  52. package/src/ui/public/assets/gitStore-BgC8OY0k.js +6 -0
  53. package/src/ui/public/assets/index-Dpx9Bcqn.css +1 -0
  54. package/src/ui/public/assets/index-qAmzFO10.js +104 -0
  55. package/src/ui/public/assets/office-docx-CmBY5bpF.js +32 -0
  56. package/src/ui/public/assets/{office-excel-CDjN3Va1.js → office-excel-BEhsBUNX.js} +1 -1
  57. package/src/ui/public/assets/office-pptx-DWfi413x.js +74 -0
  58. package/src/ui/public/assets/{previewSandbox-B6Ud9g6a.css → previewSandbox-CgdwPcuW.css} +1 -1
  59. package/src/ui/public/assets/previewSandbox-CyuMMYaq.js +0 -0
  60. package/src/ui/public/assets/socket-io-BFqQXqAp.js +1 -0
  61. package/src/ui/public/assets/{useThemeObserver-DAWKXvWv.js → useThemeObserver-DhCU3SIo.js} +1 -1
  62. package/src/ui/public/assets/{vendor-DcK6TCKZ.js → vendor-CZnXb2r9.js} +46 -39
  63. package/src/ui/public/assets/vue-flow-7_n4vJqi.js +39 -0
  64. package/src/ui/public/index.html +24 -25
  65. package/src/ui/server/index.js +4 -0
  66. package/src/ui/server/routes/fs.js +3 -1
  67. package/src/ui/server/routes/git/clone.js +166 -0
  68. package/src/ui/server/routes/git/clone.test.js +253 -0
  69. package/src/ui/server/routes/gitOps.js +5 -0
  70. package/src/ui/server/routes/localRepos.js +60 -0
  71. package/src/ui/server/routes/localRepos.test.js +151 -0
  72. package/src/ui/server/routes/workbench/agentChat.js +16 -0
  73. package/src/ui/server/routes/workbench/envContext.js +16 -2
  74. package/src/ui/server/routes/workbench/envContext.test.js +14 -0
  75. package/src/ui/server/routes/workbench/remoteCliPromptParity.test.js +9 -0
  76. package/src/ui/server/utils/directoryGitState.js +49 -0
  77. package/src/ui/server/utils/directoryGitState.test.js +47 -0
  78. package/src/ui/server/utils/localRepoScan.js +314 -0
  79. package/src/ui/server/utils/localRepoScan.test.js +291 -0
  80. package/src/ui/public/assets/AgentView-BNH92bKu.js +0 -5
  81. package/src/ui/public/assets/AgentView-BV15UtEF.css +0 -1
  82. package/src/ui/public/assets/AppVersionBadge-DXWTExsY.js +0 -2
  83. package/src/ui/public/assets/BranchSelector-CJI-I8JY.js +0 -1
  84. package/src/ui/public/assets/BranchSelector-T_rJm-JY.css +0 -1
  85. package/src/ui/public/assets/CommitForm-CTYhgpkj.js +0 -6
  86. package/src/ui/public/assets/CommitForm-Oij8R6P-.css +0 -1
  87. package/src/ui/public/assets/CommonDialog-C_RjF9Mp.css +0 -1
  88. package/src/ui/public/assets/CommonDialog-DNnN70HC.js +0 -1
  89. package/src/ui/public/assets/EditorView-BgA-_8WV.css +0 -1
  90. package/src/ui/public/assets/EditorView-C2AbXelw.js +0 -1
  91. package/src/ui/public/assets/FileDiffViewer-B2_ghtbv.css +0 -1
  92. package/src/ui/public/assets/FileDiffViewer-ZD705xXx.js +0 -14
  93. package/src/ui/public/assets/FlowExecutionViewer-B5kh7VxS.css +0 -1
  94. package/src/ui/public/assets/FlowExecutionViewer-BP9w_OQb.js +0 -1
  95. package/src/ui/public/assets/FlowOrchestrationWorkspace-BAQDU9Jd.css +0 -1
  96. package/src/ui/public/assets/FlowOrchestrationWorkspace-DeSI8Mlj.js +0 -21
  97. package/src/ui/public/assets/IconButton-BxRsG5E1.js +0 -1
  98. package/src/ui/public/assets/IconButton-CTwyZrv2.css +0 -1
  99. package/src/ui/public/assets/LogList-1sIFUx4e.js +0 -7
  100. package/src/ui/public/assets/LogList-DlWwSZet.css +0 -1
  101. package/src/ui/public/assets/MindmapView-6_fbvhh3.js +0 -1
  102. package/src/ui/public/assets/MindmapView-CS2t30Ko.css +0 -1
  103. package/src/ui/public/assets/MonitorView-CURagJDe.js +0 -1
  104. package/src/ui/public/assets/MonitorView-CphDOlQu.css +0 -1
  105. package/src/ui/public/assets/ProjectStartupButton-B7zvqRj_.css +0 -1
  106. package/src/ui/public/assets/ProjectStartupButton-COyb4Fbk.js +0 -1
  107. package/src/ui/public/assets/RemoteManagerDialog-DoS9EWQr.js +0 -1
  108. package/src/ui/public/assets/RemoteManagerDialog-bpwaVAvX.css +0 -1
  109. package/src/ui/public/assets/RemoteRepoCard-BwrdI5UH.js +0 -1
  110. package/src/ui/public/assets/RemoteRepoCard-CDykPlPj.css +0 -1
  111. package/src/ui/public/assets/SourceMapView-DADzBGfb.js +0 -3
  112. package/src/ui/public/assets/SourceMapView-DjYKJZLj.css +0 -1
  113. package/src/ui/public/assets/SvgIcon-VCh_kGGV.js +0 -1
  114. package/src/ui/public/assets/TemplateManager-C0rm8xfC.css +0 -1
  115. package/src/ui/public/assets/TemplateManager-C4go4RZu.js +0 -1
  116. package/src/ui/public/assets/UserInputNode-Cl5-bI42.js +0 -1
  117. package/src/ui/public/assets/UserInputNode-DDtvv9Iq.css +0 -1
  118. package/src/ui/public/assets/WorkbenchView-B8QOp8nM.css +0 -1
  119. package/src/ui/public/assets/WorkbenchView-DtCyyW3b.js +0 -6
  120. package/src/ui/public/assets/_plugin-vue_export-helper-Dcylt9fO.js +0 -19
  121. package/src/ui/public/assets/configStore-BlqdtNUA.js +0 -1
  122. package/src/ui/public/assets/element-plus-C1PloHQu.css +0 -1
  123. package/src/ui/public/assets/element-plus-IG-bwpR5.js +0 -28
  124. package/src/ui/public/assets/gitStore-BKkZm2m3.js +0 -4
  125. package/src/ui/public/assets/index-Bq7iCtof.css +0 -1
  126. package/src/ui/public/assets/index-DGHrUfrh.js +0 -28
  127. package/src/ui/public/assets/office-docx-CkLOvV7H.js +0 -31
  128. package/src/ui/public/assets/office-pptx-CVu3fXsL.js +0 -60
  129. package/src/ui/public/assets/previewSandbox-DYMhXJ91.js +0 -0
  130. package/src/ui/public/assets/socket-io-TBtw0msS.js +0 -1
  131. package/src/ui/public/assets/toolsStore-BlRHNqXl.js +0 -1
  132. package/src/ui/public/assets/vue-flow-B3gZmkCI.js +0 -39
@@ -0,0 +1,314 @@
1
+ // Copyright 2026 xz333221
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+ //
15
+ // 扫出本机所有的 Git 仓库(及其 origin 地址)。
16
+ //
17
+ // 用途:远程仓库列表的「已克隆」徽标 —— 要回答"这个远程仓库本地已经有了吗"。
18
+ //
19
+ // 为什么是全盘扫,而不是只看「常用目录」:
20
+ // "本地有没有" 和 "有没有进过最近打开的目录" 是两件事。刚克隆到别的盘、
21
+ // 或者很久以前克隆但从没在这个应用里打开过,都仍然是"本地已经有了"。
22
+ // 判据只能是磁盘本身。
23
+ //
24
+ // 全盘扫的代价(2026-09-23 本机实测,C:/ + D:/ 共 8613 个目录):约 12 秒。
25
+ // 所以这里的策略是「缓存优先 + 后台刷新」:
26
+ // - 结果落盘 ~/.zen-gitsync/local-repos.json,服务重启后仍然秒回上一次的结果;
27
+ // - getLocalRepos() **永不阻塞**:先把手上这份交出去,过期了就在后台重扫;
28
+ // - 用户刚克隆完一个仓库时走 rememberRepo() 就地登记,不必等重扫(12 秒里
29
+ // 界面上的徽标是空的,那正是用户最容易以为"没生效"的时刻)。
30
+ //
31
+ // 跳过名单是**性能保护,不是安全边界**:node_modules / AppData / Windows 这些
32
+ // 目录里几乎不会有用户自己 clone 的仓库,却占了目录总数的绝大部分 —— 实测跳过
33
+ // 之后 8613 个目录就能覆盖两个盘。名单刻意保守(只列"放仓库属于极罕见"的名字),
34
+ // 所以像 softwares 这种真放项目的目录不会被跳掉。
35
+ //
36
+ // 不跟随符号链接 / junction(Dirent.isDirectory() 对它们返回 false):
37
+ // 一是避免目录环,二是 Windows 上 System32 之类的 junction 会把同一棵子树反复扫。
38
+ import fs from 'node:fs/promises'
39
+ import { accessSync } from 'node:fs'
40
+ import path from 'node:path'
41
+ import os from 'node:os'
42
+
43
+ import logger from './logger.js'
44
+ import { probeDirectoryOrigins } from './directoryGitState.js'
45
+
46
+ /** 扫描结果多久算过期。过期只是触发后台重扫,不影响本次返回 */
47
+ export const CACHE_TTL_MS = 10 * 60 * 1000
48
+
49
+ /** 遍历并发。readdir 是 IO 密集,开大一点收益明显(实测 64 路下两个盘 12 秒) */
50
+ const SCAN_CONCURRENCY = 64
51
+ /** 读 origin 的并发。每次是一个 git 子进程,不能像 readdir 那样开得猛 */
52
+ const ORIGIN_CONCURRENCY = 8
53
+ /** 深度上限:防御性,正常项目不会嵌这么深 */
54
+ const MAX_DEPTH = 16
55
+ /** 单次扫描时长上限:遇到网络盘/坏盘时不至于无限跑下去 */
56
+ const SCAN_BUDGET_MS = 3 * 60 * 1000
57
+
58
+ /**
59
+ * 不往下走的目录名(小写比较)。见文件头"性能保护"那段。
60
+ * 只写"放用户仓库属于极罕见"的名字 —— 宁可多扫,不可漏掉真仓库。
61
+ */
62
+ export const SKIP_DIR_NAMES = new Set([
63
+ // 依赖 / 构建产物(都在仓库内部,本来也不会下探,列在这里是防止仓库被拷出来当普通目录)
64
+ 'node_modules', 'bower_components', 'vendor', 'dist', 'build', 'out', 'target',
65
+ // 包管理 / 语言工具链缓存(动辄几十万文件)
66
+ '.npm', '.pnpm-store', '.yarn', '.gradle', '.m2', '.cargo', '.rustup', '.nuget',
67
+ '.conda', 'anaconda3', 'miniconda3', 'site-packages', '__pycache__', 'venv', '.venv',
68
+ '.cache', '.parcel-cache', '.turbo', '.next', '.nuxt', '.svelte-kit',
69
+ // Windows 系统 / 用户配置
70
+ 'windows', 'windows.old', 'program files', 'program files (x86)', 'programdata',
71
+ 'appdata', 'perflogs', 'recovery', 'msocache', 'system volume information',
72
+ 'intel', 'amd', 'nvidia', 'drivers',
73
+ // 明显的临时目录
74
+ 'temp', 'tmp', 'logs',
75
+ ])
76
+
77
+ /** 缓存文件路径。走环境变量是为了让单测能指到临时文件上,不碰用户真实的缓存 */
78
+ export function cacheFilePath() {
79
+ return process.env.ZEN_LOCAL_REPOS_CACHE || path.join(os.homedir(), '.zen-gitsync', 'local-repos.json')
80
+ }
81
+
82
+ /** 模块状态。repos 的键是仓库目录(原样,含 Windows 反斜杠),值是 origin 地址 */
83
+ let state = {
84
+ status: 'idle', // idle | scanning | ready
85
+ startedAt: null,
86
+ finishedAt: null,
87
+ scannedDirs: 0,
88
+ repos: {},
89
+ roots: [],
90
+ error: null,
91
+ }
92
+ let cacheLoaded = false
93
+ /** 正在跑的扫描。同一时刻只允许一个(并发触发时复用同一个 Promise) */
94
+ let inflight = null
95
+
96
+ /** 本机存在的盘符。不存在的(软驱/空光驱)accessSync 会抛,直接跳过 */
97
+ export function listDrives() {
98
+ const drives = []
99
+ for (let code = 65; code <= 90; code += 1) {
100
+ const root = `${String.fromCharCode(code)}:/`
101
+ try {
102
+ accessSync(root)
103
+ drives.push(root)
104
+ } catch {
105
+ /* 不存在 / 未就绪 */
106
+ }
107
+ }
108
+ return drives
109
+ }
110
+
111
+ /**
112
+ * 遍历目录树,收集所有 Git 仓库目录。纯遍历、不读 origin(单测主要钉这一层)。
113
+ *
114
+ * 判定:目录里存在名为 `.git` 的条目就算仓库 —— 不看它是目录还是文件,
115
+ * 因为 `git worktree` 出来的工作区里 `.git` 就是文件。
116
+ * 一旦确认是仓库就**不再往里下探**:仓库里不会再有"另一个仓库",
117
+ * 下探只会白扫 node_modules(跳过名单挡不住所有写法)。
118
+ *
119
+ * @param {{ roots?: string[], concurrency?: number, budgetMs?: number }} [options]
120
+ * @returns {Promise<{ dirs: string[], scannedDirs: number, truncated: boolean }>}
121
+ */
122
+ export async function walkGitDirs({ roots, concurrency = SCAN_CONCURRENCY, budgetMs = SCAN_BUDGET_MS } = {}) {
123
+ const startRoots = (Array.isArray(roots) && roots.length ? roots : listDrives()).filter(Boolean)
124
+ const queue = startRoots.map(dir => ({ dir, depth: 0 }))
125
+ const dirs = []
126
+ let cursor = 0
127
+ let scannedDirs = 0
128
+ let truncated = false
129
+ const deadline = Date.now() + budgetMs
130
+
131
+ // 共享游标的极简工作池:边扫边往队尾追加,谁空闲谁取。
132
+ // 不能改成"启动前把任务分发完" —— 目录树是遍历过程中才展开的。
133
+ const worker = async () => {
134
+ while (cursor < queue.length) {
135
+ if (Date.now() > deadline) {
136
+ truncated = true
137
+ return
138
+ }
139
+ const { dir, depth } = queue[cursor]
140
+ cursor += 1
141
+ scannedDirs += 1
142
+
143
+ let entries
144
+ try {
145
+ entries = await fs.readdir(dir, { withFileTypes: true })
146
+ } catch {
147
+ // 权限拒绝 / 目录刚被删 / 坏扇区:跳过这一棵,不影响其它分支
148
+ continue
149
+ }
150
+
151
+ if (entries.some(e => e.name === '.git')) {
152
+ dirs.push(dir)
153
+ continue
154
+ }
155
+ if (depth >= MAX_DEPTH) continue
156
+
157
+ for (const e of entries) {
158
+ if (!e.isDirectory()) continue // symlink / junction 在这里被挡掉
159
+ const name = e.name.toLowerCase()
160
+ if (SKIP_DIR_NAMES.has(name) || name.startsWith('$')) continue
161
+ queue.push({ dir: path.join(dir, e.name), depth: depth + 1 })
162
+ }
163
+ }
164
+ }
165
+
166
+ await Promise.all(Array.from({ length: Math.max(1, concurrency) }, worker))
167
+ return { dirs, scannedDirs, truncated }
168
+ }
169
+
170
+ /** 给调用方看的快照(不含内部字段,也不把 state 引用交出去) */
171
+ function snapshot() {
172
+ return {
173
+ scanning: state.status === 'scanning',
174
+ scannedAt: state.finishedAt,
175
+ scannedDirs: state.scannedDirs,
176
+ roots: state.roots,
177
+ repos: state.repos,
178
+ error: state.error,
179
+ }
180
+ }
181
+
182
+ async function loadCache() {
183
+ if (cacheLoaded) return
184
+ cacheLoaded = true
185
+ try {
186
+ const raw = JSON.parse(await fs.readFile(cacheFilePath(), 'utf8'))
187
+ const repos = raw && typeof raw.repos === 'object' && raw.repos ? raw.repos : {}
188
+ state = {
189
+ ...state,
190
+ status: 'ready',
191
+ repos,
192
+ finishedAt: Number(raw?.scannedAt) || null,
193
+ scannedDirs: Number(raw?.scannedDirs) || 0,
194
+ roots: Array.isArray(raw?.roots) ? raw.roots : [],
195
+ }
196
+ } catch {
197
+ // 首次运行没有缓存 / 文件损坏:保持空状态,交给 getLocalRepos 触发一次扫描
198
+ }
199
+ }
200
+
201
+ async function saveCache() {
202
+ try {
203
+ await fs.mkdir(path.dirname(cacheFilePath()), { recursive: true })
204
+ await fs.writeFile(
205
+ cacheFilePath(),
206
+ JSON.stringify({
207
+ scannedAt: state.finishedAt,
208
+ scannedDirs: state.scannedDirs,
209
+ roots: state.roots,
210
+ repos: state.repos,
211
+ }, null, 2),
212
+ 'utf8'
213
+ )
214
+ } catch (error) {
215
+ logger.warn('[local-repos] 写缓存失败:', error?.message || error)
216
+ }
217
+ }
218
+
219
+ /**
220
+ * 真扫一遍。同一时刻只有一个在跑:并发调用(两个 Tab 同时挂载、或扫描中点刷新)
221
+ * 复用同一个 Promise,不会把磁盘读两遍。
222
+ *
223
+ * @param {{ roots?: string[] }} [options] roots 只为单测留:生产走 listDrives()
224
+ */
225
+ export function refreshLocalRepos({ roots } = {}) {
226
+ if (inflight) return inflight
227
+
228
+ inflight = (async () => {
229
+ const startedAt = Date.now()
230
+ state = { ...state, status: 'scanning', startedAt, finishedAt: null, scannedDirs: 0, error: null }
231
+ try {
232
+ const useRoots = Array.isArray(roots) && roots.length ? roots : listDrives()
233
+ const { dirs, scannedDirs, truncated } = await walkGitDirs({ roots: useRoots })
234
+ // 发现仓库后统一读 origin。没有 origin 的仓库不进结果 —— 对「已克隆」没有信息量,
235
+ // 却会把响应撑大(用户 23 个仓库里有几个是本地 init 的)。
236
+ const origins = await probeDirectoryOrigins(dirs, { concurrency: ORIGIN_CONCURRENCY })
237
+ const repos = {}
238
+ for (const [dir, url] of Object.entries(origins)) {
239
+ if (url) repos[dir] = url
240
+ }
241
+
242
+ state = {
243
+ status: 'ready',
244
+ startedAt,
245
+ finishedAt: Date.now(),
246
+ scannedDirs,
247
+ repos,
248
+ roots: useRoots,
249
+ error: null,
250
+ }
251
+ await saveCache()
252
+ logger.info(
253
+ `[local-repos] 扫描完成:${scannedDirs} 个目录 / ${dirs.length} 个仓库 / ` +
254
+ `${Object.keys(repos).length} 个有 origin${truncated ? '(超时提前结束)' : ''}`
255
+ )
256
+ } catch (error) {
257
+ state = { ...state, status: 'ready', finishedAt: Date.now(), error: String(error?.message || error) }
258
+ logger.error('[local-repos] 扫描失败:', error?.message || error)
259
+ } finally {
260
+ inflight = null
261
+ }
262
+ return snapshot()
263
+ })()
264
+
265
+ return inflight
266
+ }
267
+
268
+ /**
269
+ * 取当前知道的本地仓库快照。**不阻塞**:
270
+ * - 有缓存(不管新不新鲜)→ 立刻返回手上这份;过期了顺手在后台重扫一遍;
271
+ * - 没缓存(首次运行)→ 立刻返回 { scanning: true },扫描在后台进行,
272
+ * 调用方过几秒再问一次就有了(前端就是这么做的)。
273
+ */
274
+ export async function getLocalRepos({ ttlMs = CACHE_TTL_MS, roots } = {}) {
275
+ await loadCache()
276
+ const fresh = state.finishedAt && Date.now() - state.finishedAt < ttlMs
277
+ // roots 同 refreshLocalRepos:只为单测留 —— 否则"过期触发后台重扫"这条路径
278
+ // 没法测(一测就真去遍历全盘)。
279
+ if (!fresh) void refreshLocalRepos({ roots })
280
+ return snapshot()
281
+ }
282
+
283
+ /**
284
+ * 就地登记一个刚克隆出来的仓库。
285
+ *
286
+ * 为什么不让前端等下一次全盘扫:clone 成功那一刻正是用户盯着看的时候,
287
+ * 而重扫要十几秒 —— 那十几秒里徽标是空的,看起来就像"功能没生效"。
288
+ * 克隆的目标目录是服务端自己算出来的,登记它是零成本且准确的。
289
+ */
290
+ export async function rememberRepo(dir, originUrl) {
291
+ const target = String(dir || '').trim()
292
+ const url = String(originUrl || '').trim()
293
+ if (!target || !url) return
294
+
295
+ // 必须先加载缓存:否则 memory 里只有这一个仓库,把缓存里的其它仓库盖没了
296
+ await loadCache()
297
+ state = { ...state, repos: { ...state.repos, [target]: url } }
298
+ await saveCache()
299
+ }
300
+
301
+ /** 单测用:清掉模块状态与"缓存已加载"标记 */
302
+ export function resetLocalRepoScan() {
303
+ state = {
304
+ status: 'idle',
305
+ startedAt: null,
306
+ finishedAt: null,
307
+ scannedDirs: 0,
308
+ repos: {},
309
+ roots: [],
310
+ error: null,
311
+ }
312
+ cacheLoaded = false
313
+ inflight = null
314
+ }
@@ -0,0 +1,291 @@
1
+ // Copyright 2026 xz333221
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+ //
15
+ // 全盘仓库扫描 单元测试(node:test 内置)。
16
+ //
17
+ // 手法参照 directoryGitState.test.js:临时目录造真实文件树 + 真跑 git。
18
+ // **不扫真实磁盘**:所有用例都显式传 roots 指到临时目录,唯一走 listDrives() 的
19
+ // 用例只断言"返回值形状"(本机有几个盘无所谓)。
20
+ //
21
+ // 缓存必须指到临时文件:否则会写进用户真实的 ~/.zen-gitsync/local-repos.json,
22
+ // 用真实数据覆盖掉,还会让后一个用例读到前一个的残留。见 cacheFilePath()。
23
+ import { test, after } from 'node:test';
24
+ import assert from 'node:assert/strict';
25
+ import fsSync from 'node:fs';
26
+ import fs from 'node:fs/promises';
27
+ import os from 'node:os';
28
+ import path from 'node:path';
29
+ import { execFile } from 'node:child_process';
30
+ import { promisify } from 'node:util';
31
+
32
+ const execFileAsync = promisify(execFile);
33
+
34
+ // env 必须在 import 模块前设好 —— 不为别的,是为了万一将来有人把 cacheFilePath
35
+ // 改成模块级常量时不至于悄悄写进用户家目录。
36
+ const CACHE_DIR = fsSync.mkdtempSync(path.join(os.tmpdir(), 'zen-localrepos-cache-'));
37
+ process.env.ZEN_LOCAL_REPOS_CACHE = path.join(CACHE_DIR, 'local-repos.json');
38
+
39
+ const {
40
+ listDrives,
41
+ walkGitDirs,
42
+ refreshLocalRepos,
43
+ getLocalRepos,
44
+ rememberRepo,
45
+ resetLocalRepoScan,
46
+ CACHE_TTL_MS,
47
+ } = await import('./localRepoScan.js');
48
+
49
+ const git = (args, cwd) => execFileAsync('git', args, { cwd, windowsHide: true });
50
+
51
+ /** 造一个「像仓库」的目录:有 .git 就够了,walkGitDirs 不看内容 */
52
+ async function fakeRepo(dir) {
53
+ await fs.mkdir(path.join(dir, '.git'), { recursive: true });
54
+ }
55
+
56
+ /** 造一个真仓库(带真实 git 元数据,给读 origin 的用例用) */
57
+ async function realRepo(dir, originUrl) {
58
+ await fs.mkdir(dir, { recursive: true });
59
+ await git(['init', '-q'], dir);
60
+ if (originUrl) await git(['remote', 'add', 'origin', originUrl], dir);
61
+ }
62
+
63
+ /** 临时根目录。每个用例一个,互不干扰 */
64
+ const makeRoot = () => fs.mkdtemp(path.join(os.tmpdir(), 'zen-scan-'));
65
+
66
+ /** 清缓存文件,让"首次运行"这一前提在每个用例里都成立 */
67
+ async function clearCacheFile() {
68
+ await fs.rm(process.env.ZEN_LOCAL_REPOS_CACHE, { force: true });
69
+ resetLocalRepoScan();
70
+ }
71
+
72
+ /** 轮询到条件成立(或超时),给"等后台那一趟扫描收尾"用 */
73
+ async function waitUntil(fn, { timeoutMs = 5_000 } = {}) {
74
+ const deadline = Date.now() + timeoutMs;
75
+ while (Date.now() < deadline) {
76
+ if (await fn()) return true;
77
+ await new Promise(resolve => setTimeout(resolve, 20));
78
+ }
79
+ throw new Error('waitUntil 超时');
80
+ }
81
+
82
+ after(async () => {
83
+ await fs.rm(CACHE_DIR, { recursive: true, force: true });
84
+ });
85
+
86
+ // ── listDrives ───────────────────────────────────────────────────────────
87
+
88
+ test('listDrives: 只回存在的盘符,格式是 X:/', () => {
89
+ const drives = listDrives();
90
+ assert.ok(Array.isArray(drives));
91
+ // 跑测试的机器至少有一个盘;不存在的软驱/空光驱被 accessSync 挡掉
92
+ assert.ok(drives.length > 0, '至少应有一个盘符');
93
+ for (const d of drives) assert.match(d, /^[A-Z]:\/$/);
94
+ // 同一个盘不会出现两次
95
+ assert.equal(new Set(drives).size, drives.length);
96
+ });
97
+
98
+ // ── walkGitDirs:纯遍历 ───────────────────────────────────────────────────
99
+
100
+ test('walkGitDirs: 找出各级仓库;仓库内部不再下探;跳过名单与 $-前缀不进入', async () => {
101
+ const root = await makeRoot();
102
+ try {
103
+ await fakeRepo(path.join(root, 'a-project'));
104
+ await fakeRepo(path.join(root, 'b-project'));
105
+ // 三层深度的仓库也要找到
106
+ await fakeRepo(path.join(root, 'nested', 'deep', 'c-project'));
107
+ // 普通目录不产生"仓库"
108
+ await fs.mkdir(path.join(root, 'plain', 'just-files'), { recursive: true });
109
+
110
+ // 外层是仓库 → 内层的仓库不该被当成独立仓库(否则一个 monorepo 会刷出一堆重复)
111
+ await fakeRepo(path.join(root, 'outer'));
112
+ await fakeRepo(path.join(root, 'outer', 'inner'));
113
+
114
+ // 跳过名单 / $ 前缀:目录真的存在,但一律不下探
115
+ await fakeRepo(path.join(root, 'node_modules', 'fake'));
116
+ await fakeRepo(path.join(root, '$RECYCLE.BIN', 'fake'));
117
+ await fakeRepo(path.join(root, 'temp', 'fake'));
118
+
119
+ const { dirs, scannedDirs, truncated } = await walkGitDirs({ roots: [root] });
120
+ const names = dirs.map(d => path.relative(root, d).split(path.sep).join('/')).sort();
121
+
122
+ assert.deepEqual(names, ['a-project', 'b-project', 'nested/deep/c-project', 'outer']);
123
+ assert.equal(truncated, false);
124
+ // 扫过的目录数 = 实际 readdir 过的目录数,用来给界面报进度
125
+ assert.ok(scannedDirs > 0);
126
+ } finally {
127
+ await fs.rm(root, { recursive: true, force: true });
128
+ }
129
+ });
130
+
131
+ test('walkGitDirs: .git 是文件也算仓库(git worktree 出来的工作区)', async () => {
132
+ const root = await makeRoot();
133
+ try {
134
+ const wt = path.join(root, 'wt-repo');
135
+ await fs.mkdir(wt, { recursive: true });
136
+ // worktree 里 .git 是指向主仓 gitdir 的文件,不是目录
137
+ await fs.writeFile(path.join(wt, '.git'), 'gitdir: /somewhere/.git/worktrees/wt\n');
138
+
139
+ const { dirs } = await walkGitDirs({ roots: [root] });
140
+ assert.deepEqual(dirs, [wt]);
141
+ } finally {
142
+ await fs.rm(root, { recursive: true, force: true });
143
+ }
144
+ });
145
+
146
+ test('walkGitDirs: 超预算就提前收工并标记 truncated,不抛错', async () => {
147
+ const root = await makeRoot();
148
+ try {
149
+ await fakeRepo(path.join(root, 'a-project'));
150
+ // 负预算 = 第一轮 worker 就发现已过期(死线算不出来,只能靠负值保证确定性)
151
+ const { dirs, truncated } = await walkGitDirs({ roots: [root], budgetMs: -1 });
152
+ assert.equal(truncated, true);
153
+ assert.deepEqual(dirs, []);
154
+ } finally {
155
+ await fs.rm(root, { recursive: true, force: true });
156
+ }
157
+ });
158
+
159
+ test('walkGitDirs: 根目录不存在也不抛错(空光驱 / 刚被拔掉的移动盘)', async () => {
160
+ const { dirs, truncated } = await walkGitDirs({ roots: [path.join(os.tmpdir(), 'zen-scan-ghost-xyz')] });
161
+ assert.deepEqual(dirs, []);
162
+ assert.equal(truncated, false);
163
+ });
164
+
165
+ // ── refreshLocalRepos:遍历 + 读 origin + 落盘 ─────────────────────────────
166
+
167
+ test('refreshLocalRepos: 只收有 origin 的仓库,key 是仓库目录原样', async () => {
168
+ await clearCacheFile();
169
+ const root = await makeRoot();
170
+ try {
171
+ const withOrigin = path.join(root, 'has-origin');
172
+ const noOrigin = path.join(root, 'local-only');
173
+ await realRepo(withOrigin, 'https://gitee.com/xz_web/xiangqi.git');
174
+ await realRepo(noOrigin); // 本地 init、没 remote:对「已克隆」没有信息量
175
+
176
+ const snap = await refreshLocalRepos({ roots: [root] });
177
+
178
+ assert.equal(snap.scanning, false);
179
+ assert.deepEqual(Object.keys(snap.repos), [withOrigin]);
180
+ assert.equal(snap.repos[withOrigin], 'https://gitee.com/xz_web/xiangqi.git');
181
+ assert.deepEqual(snap.roots, [root]);
182
+ assert.ok(snap.scannedAt > 0);
183
+ assert.equal(snap.error, null);
184
+ } finally {
185
+ await fs.rm(root, { recursive: true, force: true });
186
+ }
187
+ });
188
+
189
+ test('refreshLocalRepos: 同一时刻只有一个扫描在跑,并发调用复用同一个 Promise', async () => {
190
+ await clearCacheFile();
191
+ const root = await makeRoot();
192
+ try {
193
+ await realRepo(path.join(root, 'has-origin'), 'git@github.com:xz333221/zen-gitsync.git');
194
+
195
+ const [a, b] = await Promise.all([refreshLocalRepos({ roots: [root] }), refreshLocalRepos({ roots: [root] })]);
196
+ // 同一个 Promise → 同一个对象;若各扫一遍会得到两个不同的 snapshot 对象
197
+ assert.equal(a, b);
198
+ assert.equal(Object.keys(a.repos).length, 1);
199
+ } finally {
200
+ await fs.rm(root, { recursive: true, force: true });
201
+ }
202
+ });
203
+
204
+ // ── getLocalRepos:永不阻塞 ───────────────────────────────────────────────
205
+
206
+ test('getLocalRepos: 有新鲜缓存时直接回,不触发重扫', async () => {
207
+ await clearCacheFile();
208
+ const root = await makeRoot();
209
+ try {
210
+ const repo = path.join(root, 'has-origin');
211
+ await realRepo(repo, 'https://gitee.com/xz_web/xiangqi.git');
212
+ await refreshLocalRepos({ roots: [root] });
213
+
214
+ const snap = await getLocalRepos({ ttlMs: CACHE_TTL_MS });
215
+ assert.equal(snap.scanning, false);
216
+ assert.equal(snap.repos[repo], 'https://gitee.com/xz_web/xiangqi.git');
217
+ } finally {
218
+ await fs.rm(root, { recursive: true, force: true });
219
+ }
220
+ });
221
+
222
+ test('getLocalRepos: 缓存过期时仍在后台重扫 —— 本次调用立刻拿到旧快照', async () => {
223
+ await clearCacheFile();
224
+ const root = await makeRoot();
225
+ try {
226
+ const repo = path.join(root, 'has-origin');
227
+ await realRepo(repo, 'https://gitee.com/xz_web/xiangqi.git');
228
+ await refreshLocalRepos({ roots: [root] });
229
+
230
+ // ttlMs:0 → 视为过期,于是本次调用会 void 触发一次重扫。关键断言是
231
+ // **不 await 它**:返回时状态已经是 scanning,但 repos 里旧数据还在。
232
+ // 传 roots 是为了让那一趟后台重扫只走临时目录(不传就真去遍历全盘)。
233
+ const snap = await getLocalRepos({ ttlMs: 0, roots: [root] });
234
+ assert.equal(snap.scanning, true, '重扫应在后台进行,被调用方看到的就是 scanning');
235
+ assert.equal(snap.repos[repo], 'https://gitee.com/xz_web/xiangqi.git', '旧快照不能被清空');
236
+
237
+ // 等后台那一趟收尾,免得它跨到下一个用例里去写缓存
238
+ await waitUntil(async () => !(await getLocalRepos({ ttlMs: CACHE_TTL_MS })).scanning);
239
+ const after = await getLocalRepos({ ttlMs: CACHE_TTL_MS });
240
+ assert.equal(after.repos[repo], 'https://gitee.com/xz_web/xiangqi.git');
241
+ } finally {
242
+ await fs.rm(root, { recursive: true, force: true });
243
+ }
244
+ });
245
+
246
+ // ── rememberRepo:克隆后就地登记 ──────────────────────────────────────────
247
+
248
+ test('rememberRepo: 立刻进快照并落盘,不覆盖缓存里已有的其它仓库', async () => {
249
+ await clearCacheFile();
250
+ const root = await makeRoot();
251
+ try {
252
+ const existing = path.join(root, 'has-origin');
253
+ await realRepo(existing, 'https://gitee.com/xz_web/xiangqi.git');
254
+ await refreshLocalRepos({ roots: [root] });
255
+
256
+ // 模拟"用户刚 clone 到另一个目录":那个目录还没被扫到
257
+ const fresh = path.join(root, 'just-cloned');
258
+ await fs.mkdir(fresh, { recursive: true });
259
+ await rememberRepo(fresh, 'git@github.com:xz333221/zen-gitsync.git');
260
+
261
+ const snap = await getLocalRepos({ ttlMs: CACHE_TTL_MS });
262
+ assert.equal(snap.repos[fresh], 'git@github.com:xz333221/zen-gitsync.git');
263
+ assert.equal(snap.repos[existing], 'https://gitee.com/xz_web/xiangqi.git', '旧记录必须还在');
264
+
265
+ // 落盘了 → 服务重启后徽标依然是亮的(读文件而不是读内存)
266
+ const raw = JSON.parse(await fs.readFile(process.env.ZEN_LOCAL_REPOS_CACHE, 'utf8'));
267
+ assert.equal(raw.repos[fresh], 'git@github.com:xz333221/zen-gitsync.git');
268
+ assert.equal(raw.repos[existing], 'https://gitee.com/xz_web/xiangqi.git');
269
+ } finally {
270
+ await fs.rm(root, { recursive: true, force: true });
271
+ }
272
+ });
273
+
274
+ test('rememberRepo: 空目录 / 空地址直接忽略,不写坏缓存', async () => {
275
+ await clearCacheFile();
276
+ const root = await makeRoot();
277
+ try {
278
+ // 先扫一个空目录把状态置成"新鲜" —— 否则下面的 getLocalRepos 会因为
279
+ // "没有缓存"去后台触发一次全盘扫描,测试就跑到真实磁盘上去了。
280
+ await refreshLocalRepos({ roots: [root] });
281
+
282
+ await rememberRepo('', 'git@github.com:x/y.git');
283
+ await rememberRepo('D:/tmp/x', '');
284
+ await rememberRepo(' ', ' ');
285
+
286
+ const snap = await getLocalRepos({ ttlMs: CACHE_TTL_MS });
287
+ assert.deepEqual(snap.repos, {});
288
+ } finally {
289
+ await fs.rm(root, { recursive: true, force: true });
290
+ }
291
+ });
@@ -1,5 +0,0 @@
1
- import"./rolldown-runtime-C0FnF6B9.js";import{$ as e,$n as t,A as n,Bn as r,Dn as i,E as a,En as o,Er as s,Gt as c,H as l,Ht as u,In as d,K as f,Nn as p,On as m,Ot as h,Pn as g,Pr as _,Q as v,Qr as y,Tn as b,Wn as x,Yr as S,Yt as C,Zn as w,_n as T,a as E,an as ee,bn as D,br as O,cr as k,ct as te,dn as A,et as ne,i as j,j as re,kn as M,kt as N,ln as P,lr as F,mt as I,nr as L,o as ie,or as R,ot as z,pr as B,pt as ae,qr as V,vr as H,wt as oe,xn as U,zt as se}from"./element-plus-IG-bwpR5.js";import{i as W,n as G,t as K}from"./vendor-DcK6TCKZ.js";import{n as q,t as J}from"./_plugin-vue_export-helper-Dcylt9fO.js";import{t as ce}from"./configStore-BlqdtNUA.js";B();var Y=65536;function X(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:ue(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>Y&&(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||K(),name:e.function?.name||``,arguments:e.function?.arguments||``,argsPreview:le(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>Y&&(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 le(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 ue(e){let t=/^data:([^;,]+)/.exec(e);return t?t[1]:``}function de(){let e=ce(),t=O([]),n=O(!1),r=O(null),i=O([]),a=O(!1),o=O(!1),s=O(null),c=O(!1),l=null,u=0,d=0;async function f(){let r=++d,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!==d)return;if(!n.success){E.error(n.error||q(`@AGENT:加载会话列表失败`));return}t.value=Array.isArray(n.sessions)?n.sessions:[]}catch(e){if(r!==d)return;E.error(q(`@AGENT:加载会话列表失败`)+`: `+(e?.message||e))}finally{r===d&&(n.value=!1)}}function p(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 m(e){let n=t.value.find(t=>t.sessionId===e);n&&(n.isGenerating=!1)}async function h(e){o.value=!0,r.value=e;try{let t=await fetch(`/api/agent/sessions/${encodeURIComponent(e)}`).then(e=>e.json());if(!t.success){E.error(t.error||q(`@AGENT:加载会话失败`));return}i.value=X(t.session)}catch(e){E.error(q(`@AGENT:加载会话失败`)+`: `+(e?.message||e))}finally{o.value=!1}}async function g(e){try{await j.confirm(q(`@AGENT:确认删除该会话`),q(`@AGENT:删除后无法恢复`),{type:`warning`})}catch{return}try{let n=await fetch(`/api/agent/sessions/${encodeURIComponent(e)}`,{method:`DELETE`}).then(e=>e.json());if(!n.success){E.error(n.error||q(`@AGENT:删除失败`));return}t.value=t.value.filter(t=>t.sessionId!==e),r.value===e&&(r.value=null,i.value=[]),E.success(q(`@AGENT:已删除`))}catch(e){E.error(q(`@AGENT:删除失败`)+`: `+(e?.message||e))}}async function _(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){E.error(r.error||q(`@AGENT:重命名失败`));return}let i=t.value.find(t=>t.sessionId===e);i&&(i.title=n),E.success(q(`@AGENT:已重命名`))}catch(e){E.error(q(`@AGENT:重命名失败`)+`: `+(e?.message||e))}}function v(){r.value=null,i.value=[],s.value=null,t.value=t.value.filter(e=>!e.isGenerating)}async function y(t,n=[]){let o=n.filter(e=>e?.file?.type?.startsWith(`image/`));if(o.length<n.length&&E.warning(q(`@AGENT:仅支持发送图片,非图片文件已忽略`)),!t.trim()&&o.length===0||a.value)return;let d=(await Promise.allSettled(o.map(e=>Z(e.file)))).map(e=>e.status===`fulfilled`?e.value:``).filter(e=>e.startsWith(`data:image/`)),h=++u;l=new AbortController;let g=l,_={id:K(),role:`user`,content:t,status:`done`,createdAt:Date.now()};o.length>0&&(_.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(_);let v=H({id:K(),role:`assistant`,content:``,status:`pending`,createdAt:Date.now()});i.value.push(v),a.value=!0;let y=[],b=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||``,...d.length>0?{images:d}:{}}),signal:g.signal});if(h!==u)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`),c=``,l=r.value;for(;;){let{value:e,done:n}=await a.read();if(h!==u)return;if(n)break;c+=o.decode(e,{stream:!0});let d=c.split(`
2
- `);c=d.pop()??``;for(let e of d){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){l=a.sessionId,b=a.sessionId,r.value=a.sessionId;let e=String(a.title||``).trim()||t.trim().split(`
3
- `)[0].trim().slice(0,40)||q(`@AGENT:无标题`);p(a.sessionId,e)}break;case`thinking`:v.reasoning||(v.reasoning=``,v.reasoningStatus=`streaming`),v.reasoning+=String(a.delta||``),v.status=`streaming`;break;case`content`:v.status===`pending`&&(v.status=`streaming`);{let e=String(a.delta||``);v.content+=e;let t=G(v.content);t.reasoning&&(v.reasoning||(v.reasoning=``,v.reasoningStatus=`streaming`),v.reasoning=t.reasoning,v.content=t.content)}break;case`tool_call_start`:{let e=H({id:a.toolCallId||K(),name:a.name||``,argsPreview:a.argsPreview||``,arguments:a.argsPreview||``,status:`running`,result:``});y.push(e),v.toolCalls||=[],v.toolCalls.push(e),v.status===`pending`&&(v.status=`streaming`);break}case`tool_result`:{let e=y.find(e=>e.id===a.toolCallId);e&&(e.result=String(a.result||``),e.status=`done`,e.name===`ask_user`&&(s.value=null));break}case`ask_user`:s.value={interactionId:String(a.interactionId||``),question:String(a.question||``),options:Array.isArray(a.options)?a.options.map(e=>String(e)):[],allowFreeText:a.allowFreeText!==!1};break;case`done`:{let e=a.content||v.content,t=G(e);t.reasoning&&!v.reasoning&&(v.reasoning=t.reasoning),v.content=t.content,v.status=`done`,v.reasoningStatus===`streaming`&&(v.reasoningStatus=`done`);break}case`error`:v.status=`error`,v.error=String(a.error||q(`@AGENT:未知错误`)),v.reasoningStatus===`streaming`&&(v.reasoningStatus=`done`)}}i.value=[...i.value]}if(h!==u)return;v.status===`pending`&&(v.status=`done`),l&&l!==r.value&&(r.value=l),f().catch(()=>{})}catch(e){if(h!==u)return;e?.name===`AbortError`||g.signal.aborted?(v.content=(v.content||``)+`
4
-
5
- [`+q(`@AGENT:已停止`)+`]`,v.status=`done`,v.reasoningStatus===`streaming`&&(v.reasoningStatus=`done`)):(v.status=`error`,v.error=e?.message||String(e),E.error(v.error||q(`@AGENT:对话失败`)))}finally{h===u&&(a.value=!1,s.value=null,c.value=!1,l=null,b&&m(b))}}async function b(e){let t=s.value,n=r.value,i=String(e||``).trim();if(!t||!n||!i||c.value)return!1;c.value=!0;try{let e=await fetch(`/api/agent/respond`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({sessionId:n,interactionId:t.interactionId,answer:i})}).then(e=>e.json());if(!e.success)throw Error(e.error||q(`@AGENT:回答提交失败`));return s.value=null,!0}catch(e){return E.error(e?.message||q(`@AGENT:回答提交失败`)),!1}finally{c.value=!1}}function x(){l&&=(l.abort(),null)}return{sessions:t,sessionsLoading:n,currentSessionId:r,messages:i,isStreaming:a,sessionLoading:o,pendingQuestion:s,answeringQuestion:c,loadSessions:f,loadSession:h,deleteSession:g,renameSession:_,newSession:v,sendMessage:y,answerQuestion:b,stop:x}}r(),B(),A(),_();var Q={class:`marketplace-panel`},fe={class:`mp-toolbar`},pe={class:`mp-search`},me=[`placeholder`],he={class:`mp-toolbar-sub`},$={class:`mp-target-hint`},ge={class:`mp-source-filter`},_e=[`onClick`],ve={class:`mp-body`},ye={class:`mp-group-header`},be={class:`mp-group-title`},xe={key:0,class:`mp-group-count`},Se={key:1,class:`mp-group-error`},Ce=[`href`,`title`],we={key:0,class:`mp-grid`},Te={class:`mp-card-head`},Ee=[`title`],De={key:0,class:`mp-card-metric`},Oe=[`title`],ke={class:`mp-card-meta`},Ae=[`title`],je=[`title`],Me={key:2,class:`mp-tag remote`},Ne={class:`mp-card-foot`},Pe=[`href`],Fe={key:1,class:`mp-installed-chip`},Ie=[`disabled`,`onClick`],Le={key:3,class:`mp-not-installable`},Re={key:1,class:`mp-empty`},ze={key:0,class:`mp-group installed`},Be={class:`mp-group-header`},Ve={class:`mp-group-title`},He={class:`mp-group-count`},Ue={key:0,class:`mp-installed-block`},We={class:`mp-installed-target`},Ge={class:`mp-installed-list`},Ke={class:`mp-installed-name`},qe={key:0,class:`mp-meta-pkg`},Je={key:1,class:`mp-tag warn`},Ye=[`disabled`,`onClick`],Xe={key:1,class:`mp-empty all`},Ze={key:0,class:`mkt-form`},Qe={class:`mkt-form-name`},$e=[`onUpdate:modelValue`,`placeholder`],et={key:0,class:`mkt-field`},tt={class:`mkt-hint`},nt=J(d({__name:`MarketplacePanel`,props:{type:{}},setup(e){let r=e,d=ce(),_=O([]),v=O([]),x=O(!1),S=O(``),D=O(``),A=O([]),N=O(`project`),P=O(new Set),I=O(new Set),L=b(()=>d.currentDirectory||``),z=null;async function B(){if(!x.value){x.value=!0;try{let e=new URLSearchParams({type:r.type,cwd:L.value});D.value&&e.set(`q`,D.value),A.value.length&&e.set(`sources`,A.value.join(`,`));let t=await(await fetch(`/api/agent/marketplace/catalog?${e}`)).json();t?.success?(_.value=t.groups||[],v.value=t.installed||[]):E.error(t?.error||q(`@MKT:加载失败`))}catch{E.error(q(`@MKT:加载失败`))}finally{x.value=!1}}}R(S,e=>{z&&clearTimeout(z),z=setTimeout(()=>{D.value=e.trim(),B()},400)}),R(A,()=>B()),R(()=>r.type,()=>{A.value=[],D.value=``,S.value=``,B()}),R(L,()=>B()),B();let oe=b(()=>{let e=L.value?L.value.split(/[\\/]/).filter(Boolean).pop():``;return N.value===`project`?e||q(`@MKT:当前项目`):q(`@MKT:g ai 智能体`)});function se(e){return e.stars?`${W(e.stars)} ★`:e.downloads?`${W(e.downloads)} ${q(`@MKT:周下载`)}`:e.uses?`${W(e.uses)} ${q(`@MKT:次使用`)}`:``}function W(e){return e>=1e3?`${(e/1e3).toFixed(e>=1e4?0:1)}k`:String(e)}function G(e){return`${e.id}`}function K(e){return P.value.has(G(e))}let J=b(()=>{let e={project:[],global:[]};for(let t of v.value)(e[t.target]||=[]).push(t);return e}),Y=H({visible:!1,item:null,env:{},args:``});function X(e){return r.type===`mcp`&&(!!e.envKeys?.length||!!e.args?.length)}function le(e){Y.item=e,Y.env=Object.fromEntries((e.envKeys||[]).map(e=>[e,``])),Y.args=(e.args||[]).join(` `),Y.visible=!0}function Z(){let e=Y.item;if(Y.visible=!1,!e)return;let t=Object.fromEntries(Object.entries(Y.env).filter(([,e])=>e.trim()!==``)),n=Y.args.trim()?Y.args.trim().match(/(?:[^\s"]+|"[^"]*")+/g)?.map(e=>e.replace(/^"|"$/g,``))??void 0:void 0;de(e,{env:t,...n?{args:n}:{}})}async function ue(e){if(!e.installed||await nt(e)){if(X(e)){le(e);return}await de(e,{})}}async function de(e,t){let n=G(e);P.value.add(n);try{let n=await fetch(`/api/agent/marketplace/install`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({type:r.type,target:N.value,cwd:L.value,item:{...e,...t}})}),i=await n.json().catch(()=>null);if(!n.ok||!i?.success){E.error(i?.error||q(`@MKT:安装失败`));return}i.item?.warning&&E.warning(i.item.warning),i.item?.envKeys?.length?E.warning(`${q(`@MKT:还缺环境变量`)}: ${i.item.envKeys.join(`, `)}`):E.success(q(`@MKT:安装成功`)),await B()}catch{E.error(q(`@MKT:安装失败`))}finally{P.value.delete(n)}}async function nt(e){try{return await j.confirm(`${e.name} ${q(`@MKT:已安装,要重新安装吗?`)}`,q(`@MKT:重新安装`),{confirmButtonText:q(`@MKT:重装`),cancelButtonText:q(`@MKT:取消`),type:`warning`}),!0}catch{return!1}}async function rt(e){try{await j.confirm(`${q(`@MKT:确定卸载`)} ${e.name}?`,q(`@MKT:卸载`),{confirmButtonText:q(`@MKT:卸载`),cancelButtonText:q(`@MKT:取消`),type:`warning`})}catch{return}let t=`${e.target}:${e.id}`;I.value.add(t);try{let t=new URLSearchParams({target:e.target,cwd:L.value}),n=await fetch(`/api/agent/marketplace/item/${r.type}/${encodeURIComponent(e.id)}?${t}`,{method:`DELETE`}),i=await n.json().catch(()=>null);if(!n.ok||!i?.success){E.error(i?.error||q(`@MKT:卸载失败`));return}E.success(q(`@MKT:已卸载`)),await B()}catch{E.error(q(`@MKT:卸载失败`))}finally{I.value.delete(t)}}return(r,d)=>{let b=f,E=n,D=re,O=l,j=a,P=ie;return w(),M(`div`,Q,[o(`div`,fe,[o(`div`,pe,[g(b,{class:`mp-search-icon`},{default:k(()=>[g(s(c))]),_:1}),F(o(`input`,{"onUpdate:modelValue":d[0]||=e=>S.value=e,class:`mp-search-input`,type:`text`,placeholder:e.type===`skill`?s(q)(`@MKT:搜索 Skill...`):s(q)(`@MKT:搜索 MCP 服务...`)},null,8,me),[[T,S.value]])]),g(D,{modelValue:N.value,"onUpdate:modelValue":d[1]||=e=>N.value=e,size:`small`,class:`mp-target`},{default:k(()=>[g(E,{value:`project`},{default:k(()=>[p(y(s(q)(`@MKT:当前项目`)),1)]),_:1}),g(E,{value:`global`},{default:k(()=>[p(y(s(q)(`@MKT:g ai 智能体`)),1)]),_:1})]),_:1},8,[`modelValue`]),g(O,{content:s(q)(`@MKT:刷新`),placement:`top`,"show-after":300},{default:k(()=>[o(`button`,{class:V([`mp-refresh`,{spinning:x.value}]),onClick:B},[g(b,null,{default:k(()=>[g(s(u))]),_:1})],2)]),_:1},8,[`content`])]),o(`div`,he,[o(`span`,$,[p(y(s(q)(`@MKT:安装到`))+` `,1),o(`b`,null,y(oe.value),1)]),o(`div`,ge,[(w(!0),M(U,null,t(_.value,e=>(w(),M(`button`,{key:e.id,class:V([`mp-chip`,{active:A.value.includes(e.id)}]),onClick:()=>{let t=A.value.indexOf(e.id);t>=0?A.value.splice(t,1):A.value.push(e.id)}},y(e.label),11,_e))),128)),A.value.length?(w(),M(`button`,{key:0,class:`mp-chip clear`,onClick:d[2]||=e=>A.value=[]},y(s(q)(`@MKT:全部来源`)),1)):m(``,!0)])]),F((w(),M(`div`,ve,[(w(!0),M(U,null,t(_.value,e=>(w(),M(`section`,{key:e.id,class:`mp-group`},[o(`header`,ye,[o(`h3`,be,y(e.label),1),e.status===`ok`?(w(),M(`span`,xe,y(e.items.length),1)):(w(),M(`span`,Se,[g(b,null,{default:k(()=>[g(s(ee))]),_:1}),p(` `+y(e.error||s(q)(`@MKT:来源不可用`)),1)])),e.homepage?(w(),M(`a`,{key:2,class:`mp-group-link`,href:e.homepage,target:`_blank`,rel:`noopener noreferrer`,title:e.homepage},[g(b,null,{default:k(()=>[g(s(h))]),_:1})],8,Ce)):m(``,!0)]),e.status===`ok`&&e.items.length?(w(),M(`div`,we,[(w(!0),M(U,null,t(e.items,e=>(w(),M(`article`,{key:G(e),class:`mp-card`},[o(`div`,Te,[o(`span`,{class:`mp-card-name`,title:e.name},y(e.name),9,Ee),se(e)?(w(),M(`span`,De,[g(b,null,{default:k(()=>[e.stars?(w(),i(s(C),{key:0})):(w(),i(s(ae),{key:1}))]),_:2},1024),p(` `+y(se(e)),1)])):m(``,!0)]),o(`p`,{class:`mp-card-desc`,title:e.description},y(e.description||s(q)(`@MKT:暂无描述`)),9,Oe),o(`div`,ke,[e.package?(w(),M(`code`,{key:0,class:`mp-meta-pkg`,title:e.package},y(e.package),9,Ae)):e.repository?(w(),M(`span`,{key:1,class:`mp-meta-repo`,title:e.repository+(e.subpath?` · `+e.subpath:``)},[p(y(e.repository),1),e.subpath?(w(),M(U,{key:0},[p(` · `+y(e.subpath),1)],64)):m(``,!0)],8,je)):m(``,!0),e.transport===`remote`?(w(),M(`span`,Me,y(s(q)(`@MKT:远程服务`)),1)):m(``,!0),(w(!0),M(U,null,t((e.tags||[]).slice(0,2),e=>(w(),M(`span`,{key:e,class:`mp-tag`},y(e),1))),128))]),o(`div`,Ne,[e.upstream?(w(),M(`a`,{key:0,class:`mp-card-link`,href:e.upstream,target:`_blank`,rel:`noopener noreferrer`},y(s(q)(`@MKT:查看来源`)),9,Pe)):m(``,!0),d[6]||=o(`span`,{class:`mp-foot-spacer`},null,-1),e.installed?(w(),M(`span`,Fe,[g(b,null,{default:k(()=>[g(s(ne))]),_:1}),p(y(s(q)(`@MKT:已安装`)),1)])):e.installable===!1?(w(),M(`span`,Le,y(s(q)(`@MKT:暂不支持一键安装`)),1)):(w(),M(`button`,{key:2,class:`mp-install-btn`,disabled:K(e),onClick:t=>ue(e)},y(K(e)?s(q)(`@MKT:安装中...`):s(q)(`@MKT:安装`)),9,Ie))])]))),128))])):e.status===`ok`?(w(),M(`div`,Re,y(s(q)(`@MKT:该来源暂无结果`)),1)):m(``,!0)]))),128)),v.value.length?(w(),M(`section`,ze,[o(`header`,Be,[o(`h3`,Ve,y(s(q)(`@MKT:已安装`)),1),o(`span`,He,y(v.value.length),1)]),(w(),M(U,null,t([`project`,`global`],e=>o(`div`,{key:e},[J.value[e]?.length?(w(),M(`div`,Ue,[o(`div`,We,y(e===`project`?s(q)(`@MKT:当前项目`):s(q)(`@MKT:g ai 智能体`)),1),o(`div`,Ge,[(w(!0),M(U,null,t(J.value[e],t=>(w(),M(`div`,{key:e+t.id,class:`mp-installed-row`},[o(`span`,Ke,y(t.name),1),t.package?(w(),M(`code`,qe,y(t.package),1)):m(``,!0),t.requiredEnv?.length?(w(),M(`span`,Je,y(s(q)(`@MKT:待配置`))+`: `+y(t.requiredEnv.join(`, `)),1)):m(``,!0),d[7]||=o(`span`,{class:`mp-foot-spacer`},null,-1),o(`button`,{class:`mp-remove-btn`,disabled:I.value.has(e+t.id),onClick:e=>rt(t)},[g(b,null,{default:k(()=>[g(s(te))]),_:1}),p(` `+y(I.value.has(e+t.id)?s(q)(`@MKT:卸载中...`):s(q)(`@MKT:卸载`)),1)],8,Ye)]))),128))])])):m(``,!0)])),64))])):m(``,!0),!x.value&&!_.value.length?(w(),M(`div`,Xe,y(s(q)(`@MKT:加载失败`)),1)):m(``,!0)])),[[P,x.value]]),g(j,{modelValue:Y.visible,"onUpdate:modelValue":d[5]||=e=>Y.visible=e,title:s(q)(`@MKT:配置 MCP`),width:`460px`,"close-on-click-modal":!1,"append-to-body":``},{footer:k(()=>[o(`button`,{class:`mkt-cancel`,onClick:d[4]||=e=>Y.visible=!1},y(s(q)(`@MKT:取消`)),1),o(`button`,{class:`mkt-confirm`,onClick:Z},y(s(q)(`@MKT:安装`)),1)]),default:k(()=>[Y.item?(w(),M(`div`,Ze,[o(`p`,Qe,y(Y.item.name),1),(w(!0),M(U,null,t(Object.keys(Y.env),e=>(w(),M(`div`,{key:e,class:`mkt-field`},[o(`label`,null,y(e),1),F(o(`input`,{"onUpdate:modelValue":t=>Y.env[e]=t,type:`text`,autocomplete:`off`,placeholder:s(q)(`@MKT:环境变量值`),spellcheck:`false`},null,8,$e),[[T,Y.env[e]]])]))),128)),Y.item.args?.length?(w(),M(`div`,et,[o(`label`,null,y(s(q)(`@MKT:启动参数`)),1),F(o(`input`,{"onUpdate:modelValue":d[3]||=e=>Y.args=e,type:`text`,autocomplete:`off`,spellcheck:`false`},null,512),[[T,Y.args]]),o(`p`,tt,y(s(q)(`@MKT:参数提示`)),1)])):m(``,!0)])):m(``,!0)]),_:1},8,[`modelValue`,`title`])])}}}),[[`__scopeId`,`data-v-07abd42c`]]);r(),B(),_(),A();var rt={class:`agent-view`},it={class:`sidebar-header`},at={class:`sidebar-search`},ot=[`placeholder`],st={class:`session-list`},ct={key:0,class:`empty-state`},lt={class:`empty-text`},ut={key:0,class:`empty-hint`},dt=[`onClick`],ft={class:`session-item-main`},pt={class:`session-item-title`},mt={class:`session-item-meta`},ht={key:0,class:`meta-generating`},gt={class:`meta-time`},_t={class:`meta-count`},vt={key:2,class:`meta-source cli`},yt=[`onClick`],bt=[`onClick`],xt={class:`agent-chat-area`},St={class:`agent-tabs`,role:`tablist`},Ct=[`aria-selected`,`onClick`],wt={class:`agent-tab-body`},Tt={key:0,class:`agent-chat-pane`},Et={key:0,class:`chat-loading`},Dt={key:0,class:`ask-user-panel`,"aria-live":`polite`},Ot={class:`ask-user-title`},kt={class:`ask-user-question`},At={key:0,class:`ask-user-options`},jt=[`disabled`,`onClick`],Mt=[`disabled`,`placeholder`,`aria-label`],Nt=[`disabled`],Pt={key:0,class:`stop-button-bar`},Ft=J(d({__name:`AgentView`,setup(n){let r=ce(),a=O(`chat`),u=b(()=>a.value===`mcp`?`mcp`:`skill`),d=b(()=>[{id:`chat`,label:q(`@AGENT:对话`),icon:v},{id:`skill`,label:q(`@AGENT:Skill 广场`),icon:oe},{id:`mcp`,label:q(`@AGENT:MCP 广场`),icon:z}]),{sessions:h,sessionsLoading:_,currentSessionId:C,messages:ee,isStreaming:A,sessionLoading:re,loadSessions:B,loadSession:ae,deleteSession:H,renameSession:G,newSession:K,sendMessage:J,pendingQuestion:Y,answeringQuestion:X,answerQuestion:le,stop:Z}=de(),ue=b(()=>{let e=r.theme;return e===`dark`||e===`auto`&&window.matchMedia(`(prefers-color-scheme: dark)`).matches?`dark`:`light`}),Q=O(``),fe=b(()=>{if(!Q.value.trim())return h.value;let e=Q.value.toLowerCase();return h.value.filter(t=>(t.title||``).toLowerCase().includes(e)||(t.model||``).toLowerCase().includes(e))}),pe=b(()=>[{id:`p1`,label:q(`@AGENT:查看项目结构`),prompt:q(`@AGENT:prompt_p1`)},{id:`p2`,label:q(`@AGENT:分析代码质量`),prompt:q(`@AGENT:prompt_p2`)},{id:`p3`,label:q(`@AGENT:帮我写测试`),prompt:q(`@AGENT:prompt_p3`)},{id:`p4`,label:q(`@AGENT:Git 状态检查`),prompt:q(`@AGENT:prompt_p4`)},{id:`p5`,label:q(`@AGENT:帮我启动项目`),prompt:q(`@AGENT:prompt_p5`)}]);async function me(e){await J(e.text,e.files),await x(),ve()}async function he(e){await J(e.prompt),await x(),ve()}let $=O(``);async function ge(e=$.value){await le(e)&&($.value=``)}let _e=O(null);function ve(e=!0){_e.value?.scrollToBottom(e)}R(()=>ee.value.length,()=>{x(()=>ve(!1))});function ye(e){if(A.value){E.warning(q(`@AGENT:请先停止当前生成`));return}ae(e)}function be(){if(A.value){E.warning(q(`@AGENT:请先停止当前生成`));return}K()}function xe(e,t){if(t.stopPropagation(),A.value&&C.value===e){E.warning(q(`@AGENT:请先停止当前生成`));return}H(e)}async function Se(e,t,n){n.stopPropagation();try{let{value:n}=await j.prompt(q(`@AGENT:请输入新的会话标题`),q(`@AGENT:重命名会话`),{inputValue:t,inputPattern:/.+/,inputErrorMessage:q(`@AGENT:标题不能为空`)});n&&n!==t&&await G(e,n)}catch{}}function Ce(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?q(`@AGENT:今天`):`${e}${q(`@AGENT:天前`)}`}return`${t.getFullYear()}-${i(t.getMonth()+1)}-${i(t.getDate())}`}catch{return e}}let we=O(280),Te=O(!1);function Ee(e){Te.value=!0;let t=e.clientX,n=we.value,r=e=>{let r=e.clientX-t,i=Math.max(200,Math.min(500,n+r));we.value=i},i=()=>{Te.value=!1,document.removeEventListener(`mousemove`,r),document.removeEventListener(`mouseup`,i)};document.addEventListener(`mousemove`,r),document.addEventListener(`mouseup`,i)}return R(()=>[r.currentDirectory,A.value],async([e,t],n)=>{let r=!n||e!==n[0],i=!!n&&n[1]===!0&&!t;(r||i)&&await B(),C.value&&!A.value&&!h.value.some(e=>e.sessionId===C.value)&&K()},{immediate:!0}),(n,r)=>{let h=ie;return w(),M(`div`,rt,[a.value===`chat`?(w(),M(U,{key:0},[o(`aside`,{class:`agent-sidebar`,style:S({width:we.value+`px`})},[o(`div`,it,[o(`button`,{class:`new-session-btn`,onClick:be},[g(s(f),null,{default:k(()=>[g(s(se))]),_:1}),o(`span`,null,y(s(q)(`@AGENT:新建会话`)),1)])]),o(`div`,at,[g(s(f),{class:`search-icon`},{default:k(()=>[g(s(c))]),_:1}),F(o(`input`,{"onUpdate:modelValue":r[0]||=e=>Q.value=e,placeholder:s(q)(`@AGENT:搜索会话...`),class:`search-input`,type:`text`},null,8,ot),[[T,Q.value]])]),F((w(),M(`div`,st,[fe.value.length===0&&!s(_)?(w(),M(`div`,ct,[g(s(f),{size:28,color:`var(--text-tertiary)`},{default:k(()=>[g(s(e))]),_:1}),o(`div`,lt,y(Q.value?s(q)(`@AGENT:未找到匹配的会话`):s(q)(`@AGENT:暂无会话`)),1),Q.value?m(``,!0):(w(),M(`div`,ut,y(s(q)(`@AGENT:点击上方按钮开始对话`)),1))])):m(``,!0),(w(!0),M(U,null,t(fe.value,e=>(w(),M(`div`,{key:e.sessionId,class:V([`session-item`,{active:s(C)===e.sessionId}]),onClick:t=>ye(e.sessionId)},[o(`div`,ft,[o(`div`,pt,y(e.title||s(q)(`@AGENT:无标题`)),1),o(`div`,mt,[e.isGenerating?(w(),M(`span`,ht,[g(s(f),{class:`is-loading`},{default:k(()=>[g(s(N))]),_:1}),p(` `+y(s(q)(`@AGENT:正在生成中...`)),1)])):(w(),M(U,{key:1},[o(`span`,gt,y(Ce(e.updatedAt)),1),r[5]||=o(`span`,{class:`meta-dot`},`·`,-1),o(`span`,_t,y(e.messageCount)+` `+y(s(q)(`@AGENT:条`)),1)],64)),e.source===`cli`?(w(),M(`span`,vt,`CLI`)):m(``,!0)])]),o(`div`,{class:`session-item-actions`,onClick:r[1]||=D(()=>{},[`stop`])},[g(s(l),{content:s(q)(`@AGENT:重命名`),placement:`top`,"show-after":500},{default:k(()=>[o(`button`,{class:`item-action-btn`,onClick:t=>Se(e.sessionId,e.title,t)},[g(s(f),null,{default:k(()=>[g(s(I))]),_:1})],8,yt)]),_:2},1032,[`content`]),g(s(l),{content:s(q)(`@AGENT:删除`),placement:`top`,"show-after":500},{default:k(()=>[o(`button`,{class:`item-action-btn danger`,onClick:t=>xe(e.sessionId,t)},[g(s(f),null,{default:k(()=>[g(s(te))]),_:1})],8,bt)]),_:2},1032,[`content`])])],10,dt))),128))])),[[h,s(_)]])],4),o(`div`,{class:V([`sidebar-resizer`,{active:Te.value}]),onMousedown:Ee},null,34)],64)):m(``,!0),o(`main`,xt,[o(`nav`,St,[(w(!0),M(U,null,t(d.value,e=>(w(),M(`button`,{key:e.id,type:`button`,class:V([`agent-tab`,{active:a.value===e.id}]),role:`tab`,"aria-selected":a.value===e.id,onClick:t=>a.value=e.id},[g(s(f),null,{default:k(()=>[(w(),i(L(e.icon)))]),_:2},1024),o(`span`,null,y(e.label),1)],10,Ct))),128))]),o(`div`,wt,[a.value===`chat`?(w(),M(`div`,Tt,[s(re)?(w(),M(`div`,Et,[g(s(f),{class:`is-loading`,size:32},{default:k(()=>[g(s(N))]),_:1}),o(`span`,null,y(s(q)(`@AGENT:加载会话中...`)),1)])):(w(),i(s(W),{key:1,ref_key:`chatContainerRef`,ref:_e,messages:s(ee),"preset-questions":pe.value,"welcome-title":s(q)(`@AGENT:智能体助手`),"welcome-description":s(q)(`@AGENT:我可以帮你阅读代码、执行命令、修改文件。选择下方话题或直接输入你的问题。`),"assistant-name":`g ai`,theme:ue.value,disabled:s(A),"upload-config":{accept:`image/*`},placeholder:s(A)?s(q)(`@AGENT:正在生成中...`):s(q)(`@AGENT:输入消息,Enter 发送`),onSend:me,onSelect:he},null,8,[`messages`,`preset-questions`,`welcome-title`,`welcome-description`,`theme`,`disabled`,`placeholder`])),g(P,{name:`fade`},{default:k(()=>[s(Y)?(w(),M(`section`,Dt,[o(`div`,Ot,[g(s(f),null,{default:k(()=>[g(s(e))]),_:1}),o(`span`,null,y(s(q)(`@AGENT:等待你的回答`)),1)]),o(`div`,kt,y(s(Y).question),1),s(Y).options.length?(w(),M(`div`,At,[(w(!0),M(U,null,t(s(Y).options,e=>(w(),M(`button`,{key:e,type:`button`,class:`ask-user-option`,disabled:s(X),onClick:t=>ge(e)},[g(s(f),null,{default:k(()=>[g(s(ne))]),_:1}),o(`span`,null,y(e),1)],8,jt))),128))])):m(``,!0),s(Y).allowFreeText||s(Y).options.length===0?(w(),M(`form`,{key:1,class:`ask-user-form`,onSubmit:r[3]||=D(e=>ge(),[`prevent`])},[F(o(`input`,{"onUpdate:modelValue":r[2]||=e=>$.value=e,type:`text`,disabled:s(X),placeholder:s(q)(`@AGENT:输入回答`),"aria-label":s(q)(`@AGENT:输入回答`)},null,8,Mt),[[T,$.value]]),o(`button`,{type:`submit`,disabled:s(X)||!$.value.trim()},[g(s(f),null,{default:k(()=>[g(s(ne))]),_:1}),o(`span`,null,y(s(q)(`@AGENT:提交回答`)),1)],8,Nt)],32)):m(``,!0)])):m(``,!0)]),_:1}),g(P,{name:`fade`},{default:k(()=>[s(A)?(w(),M(`div`,Pt,[o(`button`,{class:`stop-button`,onClick:r[4]||=(...e)=>s(Z)&&s(Z)(...e)},[g(s(f),null,{default:k(()=>[g(s(N),{class:`is-loading`})]),_:1}),o(`span`,null,y(s(q)(`@AGENT:停止生成`)),1)])])):m(``,!0)]),_:1})])):(w(),i(nt,{key:1,type:u.value},null,8,[`type`]))])])])}}}),[[`__scopeId`,`data-v-17ac5478`]]);export{Ft as default};