zen-gitsync 2.18.1 → 2.18.2

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 (50) hide show
  1. package/README.md +1409 -1405
  2. package/package.json +2 -1
  3. package/src/cli/ai/agent.js +3 -0
  4. package/src/cli/ai/runtime.test.js +91 -2
  5. package/src/cli/ai/telemetry.js +3 -1
  6. package/src/cli/ai/termui.js +5 -1
  7. package/src/cli/ai/transport.js +215 -76
  8. package/src/cli/ai/turn.js +16 -2
  9. package/src/config.js +67 -0
  10. package/src/ui/public/assets/AgentView-2wrBHHhj.js +11 -0
  11. package/src/ui/public/assets/{AgentView-D04HxmFC.css → AgentView-BAd5T84b.css} +1 -1
  12. package/src/ui/public/assets/{AppVersionBadge-lW8vNngV.js → AppVersionBadge-CHDxEjbF.js} +2 -2
  13. package/src/ui/public/assets/{BranchSelector-BgbW1JiG.js → BranchSelector-D1g-2NKG.js} +1 -1
  14. package/src/ui/public/assets/{CommitForm-BNZ3C3Vq.js → CommitForm-FhN_BoYS.js} +1 -1
  15. package/src/ui/public/assets/{CommonDialog-CyBkT3AE.js → CommonDialog-BjcPO7gz.js} +1 -1
  16. package/src/ui/public/assets/{CopySessionButton-CwWaki0s.js → CopySessionButton-CPtwbVLu.js} +1 -1
  17. package/src/ui/public/assets/EditorView-BUKUax94.css +1 -0
  18. package/src/ui/public/assets/EditorView-DxmvG7N8.js +37 -0
  19. package/src/ui/public/assets/{FlowExecutionViewer-mdvuwP5d.js → FlowExecutionViewer-BcBdCL0t.js} +1 -1
  20. package/src/ui/public/assets/{FlowOrchestrationWorkspace-fqy0N4Xc.js → FlowOrchestrationWorkspace-CkSA1gaf.js} +1 -1
  21. package/src/ui/public/assets/{LogList-DvxNsohr.js → LogList-BojHLJ5Q.js} +4 -4
  22. package/src/ui/public/assets/{MindmapView-CDyg616o.js → MindmapView-BA7u4vhh.js} +1 -1
  23. package/src/ui/public/assets/{MonitorView-DzcTq-y3.js → MonitorView-DE6ePMZT.js} +1 -1
  24. package/src/ui/public/assets/{ProjectStartupButton-htka4EwQ.js → ProjectStartupButton-Dkx6YnGs.js} +1 -1
  25. package/src/ui/public/assets/{RecentDirectoriesChat-CarXD1-V.css → RecentDirectoriesChat-DBGoUlkn.css} +1 -1
  26. package/src/ui/public/assets/RecentDirectoriesChat-_yzf6Nu5.js +3 -0
  27. package/src/ui/public/assets/{RemoteManagerDialog-BFR7Jm5R.js → RemoteManagerDialog-Be3-3xKl.js} +1 -1
  28. package/src/ui/public/assets/{RemoteRepoCard-BEWOqwy-.js → RemoteRepoCard-DrL1qR0e.js} +1 -1
  29. package/src/ui/public/assets/{SourceMapView-BCLPm_9Z.js → SourceMapView-DmY7xaHI.js} +1 -1
  30. package/src/ui/public/assets/{SvgIcon-BBImoDdy.js → SvgIcon-DPkwl68W.js} +1 -1
  31. package/src/ui/public/assets/{UserInputNode-Cx5Ux_Zk.js → UserInputNode-uoTNgf0M.js} +1 -1
  32. package/src/ui/public/assets/{WorkbenchView-DEJHVCL1.css → WorkbenchView-3IW0OcIy.css} +1 -1
  33. package/src/ui/public/assets/{WorkbenchView-Dh-BXSrZ.js → WorkbenchView-B63oMMUD.js} +73 -73
  34. package/src/ui/public/assets/{_plugin-vue_export-helper-T5G-Ork1.js → _plugin-vue_export-helper-7_ovdUNe.js} +4 -4
  35. package/src/ui/public/assets/agentConversations-BWMhXq51.js +8 -0
  36. package/src/ui/public/assets/configStore-2tn284Z4.js +1 -0
  37. package/src/ui/public/assets/{index-DTe2ec7L.js → index-DlIQOSyv.js} +84 -84
  38. package/src/ui/public/assets/index-e9FElcZF.css +1 -0
  39. package/src/ui/public/index.html +6 -6
  40. package/src/ui/server/routes/config.js +14 -0
  41. package/src/ui/server/routes/workbench/agentChat.js +50 -15
  42. package/src/ui/server/routes/workbench/agentChatShared.test.js +105 -1
  43. package/src/ui/server/routes/workbench/agentRoutes.js +27 -4
  44. package/src/ui/public/assets/AgentView-lS8yZ1IO.js +0 -11
  45. package/src/ui/public/assets/EditorView-DLH-iHX0.css +0 -1
  46. package/src/ui/public/assets/EditorView-qhqZbWUW.js +0 -37
  47. package/src/ui/public/assets/RecentDirectoriesChat-BqKZOUER.js +0 -3
  48. package/src/ui/public/assets/agentConversations-S0q49dQw.js +0 -8
  49. package/src/ui/public/assets/configStore-BLuyXnKF.js +0 -1
  50. package/src/ui/public/assets/index-Ja0NzLzp.css +0 -1
@@ -10,18 +10,18 @@
10
10
  <meta name="theme-color" content="#141820" media="(prefers-color-scheme: dark)" />
11
11
  <title>Zen GitSync</title>
12
12
  <!-- 字体改为自托管(@font-face 见 src/main.css),不再依赖 Google Fonts 外链 -->
13
- <script type="module" crossorigin src="/assets/index-DTe2ec7L.js"></script>
13
+ <script type="module" crossorigin src="/assets/index-DlIQOSyv.js"></script>
14
14
  <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-C0FnF6B9.js">
15
15
  <link rel="modulepreload" crossorigin href="/assets/dagre-B_5cyX5p.js">
16
16
  <link rel="modulepreload" crossorigin href="/assets/element-plus-Bh2bgoEC.js">
17
17
  <link rel="modulepreload" crossorigin href="/assets/vendor-DUF7rMpx.js">
18
- <link rel="modulepreload" crossorigin href="/assets/_plugin-vue_export-helper-T5G-Ork1.js">
19
- <link rel="modulepreload" crossorigin href="/assets/SvgIcon-BBImoDdy.js">
18
+ <link rel="modulepreload" crossorigin href="/assets/_plugin-vue_export-helper-7_ovdUNe.js">
19
+ <link rel="modulepreload" crossorigin href="/assets/SvgIcon-DPkwl68W.js">
20
20
  <link rel="modulepreload" crossorigin href="/assets/socket-io-BFqQXqAp.js">
21
- <link rel="modulepreload" crossorigin href="/assets/configStore-BLuyXnKF.js">
21
+ <link rel="modulepreload" crossorigin href="/assets/configStore-2tn284Z4.js">
22
22
  <link rel="modulepreload" crossorigin href="/assets/monaco-CyhY9wTy.js">
23
23
  <link rel="modulepreload" crossorigin href="/assets/flow-mindmap-Dn1qZxCo.js">
24
- <link rel="modulepreload" crossorigin href="/assets/CommonDialog-CyBkT3AE.js">
24
+ <link rel="modulepreload" crossorigin href="/assets/CommonDialog-BjcPO7gz.js">
25
25
  <link rel="modulepreload" crossorigin href="/assets/vue-flow-DThTr3gR.js">
26
26
  <link rel="stylesheet" crossorigin href="/assets/element-plus-D-6gla1z.css">
27
27
  <link rel="stylesheet" crossorigin href="/assets/vendor-DpIR4Sxg.css">
@@ -30,7 +30,7 @@
30
30
  <link rel="stylesheet" crossorigin href="/assets/flow-mindmap-DqCKZUlr.css">
31
31
  <link rel="stylesheet" crossorigin href="/assets/CommonDialog-w6EmBEXv.css">
32
32
  <link rel="stylesheet" crossorigin href="/assets/vue-flow-CavUTEzp.css">
33
- <link rel="stylesheet" crossorigin href="/assets/index-Ja0NzLzp.css">
33
+ <link rel="stylesheet" crossorigin href="/assets/index-e9FElcZF.css">
34
34
  </head>
35
35
  <body>
36
36
  <div id="app"></div>
@@ -1124,6 +1124,20 @@ export function registerConfigRoutes({
1124
1124
  }
1125
1125
  updates.aiMaxRequestTokens = normalized
1126
1126
  }
1127
+ // 智能体视图「预设提示词」(欢迎页快捷卡片):{id?, label, prompt} 数组。
1128
+ // 空数组是**合法值**(= 恢复内置默认),所以判空用 !== undefined 而不是 length。
1129
+ // 结构坏的值整体拒绝(400),不清洗 —— 与 normalizeAgentPresetPrompts 语义一致;
1130
+ // 响应把归一化后的最终值带回(含补 id/截断),前端以回执为准。
1131
+ if (body.agentPresetPrompts !== undefined) {
1132
+ const normalized = configManager.normalizeAgentPresetPrompts(body.agentPresetPrompts)
1133
+ if (normalized === null) {
1134
+ return res.status(400).json({
1135
+ success: false,
1136
+ error: 'agentPresetPrompts 非法: 需要 [{label, prompt}] 数组(每项都不能为空)'
1137
+ })
1138
+ }
1139
+ updates.agentPresetPrompts = normalized
1140
+ }
1127
1141
  if (Object.keys(updates).length === 0) {
1128
1142
  return res.status(400).json({ success: false, error: '缺少可保存的参数' })
1129
1143
  }
@@ -33,6 +33,14 @@
33
33
  // - { type: 'tool_output', toolCallId, chunk } — 命令执行中的增量输出(仅展示)
34
34
  // - { type: 'tool_result', toolCallId, name, result }
35
35
  // - { type: 'ask_user', interactionId, question, options, allowFreeText, multiple }
36
+ // - { type: 'attempt', attempt } — 一次 LLM 请求**开始**(工具循环里每轮一条)。
37
+ // 发它是为了让前端记一个"断点":这个位置上正文/思考已经有多长。
38
+ // 自动重试时前端按这个断点回退,只丢掉失败那次尝试吐的字,
39
+ // 本轮前面几轮(工具循环)说过的话留着。
40
+ // **重试不重发它**:重试要丢的正是这一轮请求吐的全部内容,断点还是同一个。
41
+ // - { type: 'retry', attempt, maxRetries, delayMs, reason }
42
+ // 传输层正在自动重试(最多 10 次,见 cli/ai/transport.js)。前端收到就回退到断点、
43
+ // 清掉错误态;不处理的话界面上会出现"半截答案 + 重来一遍"拼在一起的两段话。
36
44
  // - { type: 'done', content } — 本轮最终完成
37
45
  // - { type: 'error', error }
38
46
  //
@@ -312,9 +320,15 @@ ${isWin ? `- This is Windows. The following Unix commands do NOT exist here:
312
320
  // - requestBudget: { maxChars, maxMessages, maxUserChars } 每轮请求的上下文预算
313
321
  // (解析自全局配置 aiMaxRequestChars,见 cli/ai/context.js 的 resolveRequestBudget)。
314
322
  // 不传时 prepareRequestMessages 走默认值,行为与改造前一致。
323
+ // - resume: true 时**不再压入 user 消息**,直接拿着会话现有的尾巴继续跑。
324
+ // 用于"重试上一轮":失败那轮留下的 user + 工具结果都还在,再发一遍 user
325
+ // 会让模型看到两条同样的请求 + 已经跑完的工具结果。
326
+ // - streamFn: LLM 传输函数,默认就是共享的 streamChatOnce。**只给测试留的接缝**
327
+ // (单测要整体关掉自动重试,真等退避会让一个用例跑成分钟级),
328
+ // 生产调用点一律不传 —— 传了就等于换了一份传输实现。
315
329
  //
316
330
  // 返回: { aborted: boolean }
317
- export async function runAgentTurn({ session, model, userMessage, images = [], cwd, locale, openFilePath, attachments = [], dirStatusBlock = '', signal, send, onChild, askUser, listProjects, dispatchTask, getContextBlock, requestBudget = null }) {
331
+ export async function runAgentTurn({ session, model, userMessage, images = [], cwd, locale, openFilePath, attachments = [], dirStatusBlock = '', signal, send, onChild, askUser, listProjects, dispatchTask, getContextBlock, requestBudget = null, resume = false, streamFn = streamChatOnce }) {
318
332
  const ctx = { cwd, locale, onChild, askUser, listProjects, dispatchTask };
319
333
 
320
334
  // 确保 session.messages 存在
@@ -328,16 +342,26 @@ export async function runAgentTurn({ session, model, userMessage, images = [], c
328
342
  });
329
343
  }
330
344
 
331
- // 追加 user 消息:有图片时组装 OpenAI 多模态 content 数组(与 CLI agent.js 一致),否则保持纯字符串
332
- const imageParts = (Array.isArray(images) ? images : [])
333
- .filter(u => typeof u === 'string' && u.startsWith('data:image/'))
334
- .map(u => ({ type: 'image_url', image_url: { url: u } }));
335
- session.messages.push({
336
- role: 'user',
337
- content: imageParts.length > 0
338
- ? [{ type: 'text', text: userMessage || ' ' }, ...imageParts]
339
- : userMessage
340
- });
345
+ // 追加 user 消息:有图片时组装 OpenAI 多模态 content 数组(与 CLI agent.js 一致),否则保持纯字符串。
346
+ // resume 是"接着跑上一轮",会话尾巴上那条 user(或那批工具结果)就是要发的东西,不能重复压。
347
+ if (resume) {
348
+ // 尾巴上是一条**没有工具调用的 assistant**,说明上一轮其实跑完了(用户点的是操作栏那颗
349
+ // 「重新生成」,不是错误气泡的「重试」)。留着它的话,模型会顺着自己刚写完的答案往下说,
350
+ // 而且会话里会出现两条连着的 assistant —— 重开这条会话能看见两份答案。
351
+ // 摘掉它,语义就干净了:拿同一个问题再问一次。
352
+ const tail = session.messages[session.messages.length - 1];
353
+ if (tail?.role === 'assistant' && !(tail.tool_calls?.length > 0)) session.messages.pop();
354
+ } else {
355
+ const imageParts = (Array.isArray(images) ? images : [])
356
+ .filter(u => typeof u === 'string' && u.startsWith('data:image/'))
357
+ .map(u => ({ type: 'image_url', image_url: { url: u } }));
358
+ session.messages.push({
359
+ role: 'user',
360
+ content: imageParts.length > 0
361
+ ? [{ type: 'text', text: userMessage || ' ' }, ...imageParts]
362
+ : userMessage
363
+ });
364
+ }
341
365
 
342
366
  // 工作区状态快照:整轮只取一次(TTL 在生成器内部,见 aiContext/index.js),
343
367
  // 不放进循环 —— 否则每执行一次工具调用都要重拼一遍,而这轮对话里它不会变。
@@ -378,8 +402,11 @@ export async function runAgentTurn({ session, model, userMessage, images = [], c
378
402
  send({ type: 'context', usage });
379
403
 
380
404
  let result;
405
+ // 断点事件:告诉前端"从这里开始是这一次请求要写的内容"。
406
+ // 前端收到自动重试时按它回退 —— 只丢失败那次吐的字,本轮前面几轮说过的话留着。
407
+ send({ type: 'attempt', attempt: iter + 1 });
381
408
  try {
382
- result = await streamChatOnce({
409
+ result = await streamFn({
383
410
  model,
384
411
  messages,
385
412
  signal,
@@ -389,11 +416,19 @@ export async function runAgentTurn({ session, model, userMessage, images = [], c
389
416
  if (thinking) send({ type: 'thinking', delta: thinking });
390
417
  if (content) send({ type: 'content', delta: content });
391
418
  },
419
+ // 传输层自动重试(长静默 / 网关 5xx / 流中断,见 cli/ai/transport.js):
420
+ // 先告诉前端回退到断点,下一次尝试的增量才不会和上一截拼在一起。
421
+ onRetry: ({ attempt, maxRetries, delayMs, error }) => {
422
+ send({ type: 'retry', attempt, maxRetries, delayMs, reason: error?.message || '' });
423
+ },
392
424
  });
393
425
  } catch (err) {
394
- // 请求失败时撤掉本轮塞入的 user 消息(如果末尾仍是 user)
395
- const last = session.messages[session.messages.length - 1];
396
- if (last?.role === 'user') session.messages.pop();
426
+ // 请求失败**不回滚**刚压入的 user 消息(2026-10-09 改)。
427
+ // 原先是"末尾仍是 user 就弹掉",为的是让用户重发同一条时历史里不留两份。
428
+ // 现在有正经的「重试」了(前端那颗按钮 → resume 续跑本轮),而续跑的全部意义
429
+ // 就是"接着会话现有的尾巴跑":尾巴被弹掉的话,续跑会退化成**重跑上一轮**。
430
+ // 留着它还有个更直接的好处:用户发出去的问题不再凭空消失(界面上一直看得见),
431
+ // 带图的那些附件也还在请求副本里,重试不用靠前端重建。
397
432
  send({ type: 'error', error: `LLM 请求失败: ${err.message}` });
398
433
  return { aborted: false };
399
434
  }
@@ -57,8 +57,13 @@ const longSession = () => {
57
57
 
58
58
  // 跑一轮真实的 runAgentTurn,桩掉 globalThis.fetch —— 抓住发出去的请求体与产生的事件
59
59
  // (respond 收到 fetch 的 init,方便测试按 signal 模拟"读到一半被中止")
60
- async function runWeb({ session, userMessage, respond, locale = 'zh-CN', signal, onEvent }) {
60
+ //
61
+ // streamFn:传输层现在默认会自动重试(最多 10 次、指数退避,见 cli/ai/transport.js)。
62
+ // 这一组用例关心的是"失败时怎么收场",不是"重试几次" —— 真等退避会让一个用例跑到分钟级。
63
+ // 所以统一注入 maxRetries: 0 的那一份;重试自身的口径在 cli/ai/runtime.test.js 里逐条钉。
64
+ async function runWeb({ session, userMessage, respond, locale = 'zh-CN', signal, onEvent, resume = false, streamFn }) {
61
65
  const { runAgentTurn } = await import('./agentChat.js')
66
+ const { streamChatOnce } = await import('../../../../cli/ai/transport.js')
62
67
  const events = []
63
68
  let sent
64
69
  const original = globalThis.fetch
@@ -68,6 +73,8 @@ async function runWeb({ session, userMessage, respond, locale = 'zh-CN', signal,
68
73
  session, model, userMessage, locale, cwd: process.cwd(),
69
74
  signal: signal ?? new AbortController().signal,
70
75
  send: e => { events.push(e); onEvent?.(e) },
76
+ resume,
77
+ streamFn: streamFn ?? (opts => streamChatOnce({ ...opts, maxRetries: 0 })),
71
78
  })
72
79
  return { sent, events, result }
73
80
  } finally {
@@ -300,3 +307,100 @@ test('tool_call_start 每种工具都带完整 arguments,argsPreview 仍是截
300
307
  const assistantWithPlan = session.messages.find(m => m.role === 'assistant' && m.tool_calls?.some(t => t.function.name === 'update_plan'))
301
308
  assert.equal(assistantWithPlan.tool_calls[0].function.arguments, planArgs)
302
309
  })
310
+
311
+ // ── 重试与续跑(2026-10-09)────────────────────────────────────
312
+ // 「重试」要能真的把上一轮跑下去,靠的是服务端两条口径:
313
+ // 1. 失败时**不撤**那条 user 消息 —— 撤了的话 resume 会退化成"重跑上一轮"
314
+ // 2. resume 时**不再压** user 消息 —— 压了就变成"同一条请求出现两次"
315
+ // 两条各自都有明确的失效症状,所以各钉一条。
316
+
317
+ test('失败时不再撤掉本轮压入的 user 消息(否则重试会退化成重跑上一轮)', async () => {
318
+ const session = newSession('ag-sandbox-keep-user', withSystem())
319
+ const boom = () => new Response(JSON.stringify({ error: { message: 'bad request: model not found' } }),
320
+ { status: 404, headers: { 'content-type': 'application/json' } })
321
+
322
+ const { events } = await runWeb({ session, userMessage: '这条不能消失', respond: boom })
323
+
324
+ assert.ok(events.some(e => e.type === 'error'))
325
+ const last = session.messages[session.messages.length - 1]
326
+ assert.equal(last.role, 'user')
327
+ assert.equal(last.content, '这条不能消失')
328
+ })
329
+
330
+ test('resume 不再压 user 消息,接着会话现有的尾巴跑', async () => {
331
+ const session = newSession('ag-sandbox-resume', withSystem())
332
+ const usersBefore = session.messages.filter(m => m.role === 'user').length
333
+
334
+ const { sent } = await runWeb({ session, userMessage: '', resume: true, respond: okStream })
335
+
336
+ assert.equal(sent.messages.length, 2, 'resume 不该往请求里加消息')
337
+ assert.deepEqual(sent.messages.map(m => m.content), ['rules', 'hi'])
338
+ // 会话记录里也不能多出一条 user(多出来的那条只会是本轮 assistant 的回答)
339
+ assert.equal(session.messages.filter(m => m.role === 'user').length, usersBefore)
340
+ assert.equal(session.messages[session.messages.length - 1].role, 'assistant')
341
+ })
342
+
343
+ test('每次 LLM 请求前发一条 attempt(前端靠它记重试断点),自动重试时发 retry', async () => {
344
+ const { streamChatOnce } = await import('../../../../cli/ai/transport.js')
345
+ let calls = 0
346
+ // 第一次吐半截就断流(可重试),第二次正常返回 —— 真链路走 streamChatOnce,
347
+ // 只把"真等退避"换掉,好让这条用例秒级跑完
348
+ const flaky = opts => streamChatOnce({
349
+ ...opts,
350
+ sleepFn: async () => {},
351
+ fetchFn: async () => {
352
+ calls++
353
+ if (calls === 1) return sse([event({ choices: [{ delta: { content: '半截' } }] })])
354
+ return okStream()
355
+ },
356
+ })
357
+ const session = newSession('ag-sandbox-retry', withSystem())
358
+
359
+ const { events } = await runWeb({ session, userMessage: '开始', streamFn: flaky })
360
+
361
+ assert.equal(calls, 2, '第一次断流后必须再试一次')
362
+ // attempt 是**每次工具循环的请求开始前**发的一条(不是每次尝试):
363
+ // 自动重试沿用的还是同一个断点 —— 它要丢的正是这一轮请求吐的全部内容。
364
+ assert.equal(events.filter(e => e.type === 'attempt').length, 1)
365
+ const retry = events.find(e => e.type === 'retry')
366
+ assert.ok(retry, '自动重试必须通知前端,否则界面上会留下上一截的字')
367
+ assert.equal(retry.attempt, 1)
368
+ assert.equal(retry.maxRetries, 10)
369
+ assert.match(retry.reason, /中断/)
370
+ // 断点必须排在正文之前:前端是按"收到 attempt 时气泡里已有的长度"回退的
371
+ const order = events.map(e => e.type)
372
+ assert.ok(order.indexOf('attempt') < order.indexOf('content'), 'attempt 要在正文之前')
373
+ assert.ok(order.indexOf('content') < order.indexOf('retry'), 'retry 落在被丢弃的那段正文之后')
374
+ })
375
+
376
+ test('对跑完的一轮点「重新生成」:把尾巴那条 assistant 摘掉再问一次', async () => {
377
+ // 会话尾巴是一条没有工具调用的 assistant = 上一轮已经答完了。
378
+ // 留着它的话模型会顺着自己的答案往下说,重开会话还会看到两条连着的 assistant。
379
+ const session = newSession('ag-sandbox-regen', [
380
+ { role: 'system', content: 'rules' },
381
+ { role: 'user', content: 'hi' },
382
+ { role: 'assistant', content: '旧答案' },
383
+ ])
384
+
385
+ const { sent } = await runWeb({ session, userMessage: '', resume: true, respond: okStream })
386
+
387
+ assert.deepEqual(sent.messages.map(m => m.content), ['rules', 'hi'], '旧答案必须从请求里消失')
388
+ assert.equal(session.messages.filter(m => m.role === 'assistant').length, 1, '只剩这一次新生成的')
389
+ assert.equal(session.messages[session.messages.length - 1].content, 'ok')
390
+ })
391
+
392
+ test('对中途失败的一轮点「重试」:工具结果留在尾巴上,一条都不摘', async () => {
393
+ // 失败发生在工具循环中途时,尾巴是 assistant(tool_calls) + 一批 tool 结果 ——
394
+ // 这些是这一轮真干过的事,摘掉就等于让模型把工具再跑一遍。
395
+ const session = newSession('ag-sandbox-resume-tools', [
396
+ { role: 'system', content: 'rules' },
397
+ { role: 'user', content: 'hi' },
398
+ { role: 'assistant', content: null, tool_calls: [call(0)] },
399
+ { role: 'tool', tool_call_id: 'call0', content: 'file contents' },
400
+ ])
401
+
402
+ const { sent } = await runWeb({ session, userMessage: '', resume: true, respond: okStream })
403
+
404
+ assert.deepEqual(sent.messages.map(m => m.role), ['system', 'user', 'assistant', 'tool'])
405
+ assert.equal(sent.messages[3].content, 'file contents', '已经跑完的工具结果必须原样带上')
406
+ })
@@ -314,6 +314,11 @@ export function registerAgentRoutes({ app, getCurrentProjectPath, configManager,
314
314
  const userMessage = String(req.body?.userMessage || '').trim();
315
315
  const sessionIdInput = String(req.body?.sessionId || '').trim();
316
316
  const locale = String(req.body?.locale || req.headers['accept-language'] || 'zh').startsWith('en') ? 'en' : 'zh';
317
+ // 重试上一轮:不压新的 user 消息,直接接着会话现有的尾巴(那条 user / 那批工具结果)跑。
318
+ // 为什么需要它:失败可能发生在**工具循环的第二轮请求**上,那时会话里已经有
319
+ // user + assistant(tool_calls) + 一批 tool 结果 —— 把 user 重发一遍,模型会看到
320
+ // 两条一模一样的请求,还会把已经跑完的工具再跑一遍(见 agentChat.js 的 resume 参数)。
321
+ const resume = req.body?.resume === true;
317
322
 
318
323
  // 图片附件(base64 dataURL 数组):前端已限制只能选图片,这里再做一层白名单校验
319
324
  const images = (Array.isArray(req.body?.images) ? req.body.images : [])
@@ -326,9 +331,13 @@ export function registerAgentRoutes({ app, getCurrentProjectPath, configManager,
326
331
  .slice(0, MAX_ATTACHMENTS)
327
332
  .map(a => ({ name: a?.name, dataUrl: a?.dataUrl }));
328
333
 
329
- if (!userMessage && images.length === 0 && rawAttachments.length === 0) {
334
+ if (!resume && !userMessage && images.length === 0 && rawAttachments.length === 0) {
330
335
  return res.status(400).json({ success: false, error: '消息内容不能为空' });
331
336
  }
337
+ // 续跑必须有会话可续(没有 sessionId 就没有"上一轮"这回事)
338
+ if (resume && !sessionIdInput) {
339
+ return res.status(400).json({ success: false, error: '重试需要指定会话' });
340
+ }
332
341
 
333
342
  // 文件空间对话:客户端把"当前打开的文档"带上来,服务端只在请求副本里注入上下文(不落库)
334
343
  const openFilePath = String(req.body?.openFilePath || '').trim().slice(0, 512);
@@ -464,6 +473,14 @@ export function registerAgentRoutes({ app, getCurrentProjectPath, configManager,
464
473
  // deepseek-v4.1-flash)。为外部引擎要求"先在设置里配模型"是错的:
465
474
  // 明明能跑,却先被一道无关的校验拦住。
466
475
  const useExternal = isExternalEngine(session.engine);
476
+ // 外部 CLI 引擎没有"续跑半轮"这回事:每一轮都是新 spawn 一个进程,会话状态在它自己那边
477
+ //(engineSessionId),服务端手上没有可回灌的中途状态。与其装作能重试、把整段 prompt
478
+ // 再喂一遍(模型会把活重干一次),不如明确告诉用户走"重新发一条"。
479
+ if (resume && useExternal) {
480
+ send({ type: 'error', error: `${engineLabel(session.engine)} 引擎暂不支持重试上一轮,请重新发送这条消息。`, code: 'ENGINE_NO_RETRY' });
481
+ finished = true;
482
+ return res.end();
483
+ }
467
484
  let model = null;
468
485
  let requestBudget = null;
469
486
  if (!useExternal) {
@@ -542,6 +559,10 @@ export function registerAgentRoutes({ app, getCurrentProjectPath, configManager,
542
559
  // 把别人跑完的那一轮覆盖掉(见 cli/ai/sessionStore.js 的 recordTurnTiming)。
543
560
  const turnIndex = (Array.isArray(session.messages) ? session.messages : [])
544
561
  .filter(m => m?.role === 'user').length;
562
+ // 续跑的是**已经数过的那一轮**:那条 user 消息还在会话里,本轮不该另占一个槽
563
+ //(另占的话,这条消息的用时会被写成两份、界面上看到两个数)。
564
+ // recordTurnTiming 按 turnIndex 覆盖,所以重试成功后的用时正好把失败那次盖掉。
565
+ const effectiveTurnIndex = resume ? Math.max(0, turnIndex - 1) : turnIndex;
545
566
 
546
567
  if (useExternal) {
547
568
  // 外部 CLI 引擎:CLI 自己跑循环,服务端只做 spawn + 事件翻译。
@@ -609,7 +630,9 @@ export function registerAgentRoutes({ app, getCurrentProjectPath, configManager,
609
630
  getContextBlock,
610
631
  // 每轮请求的上下文预算(解析结果)。agentChat 拿它调 prepareRequestMessages,
611
632
  // 与 CLI turn.js 同一条口径。
612
- requestBudget
633
+ requestBudget,
634
+ // 重试上一轮:不再压 user 消息,接着会话现有的尾巴跑
635
+ resume
613
636
  });
614
637
  }
615
638
 
@@ -617,10 +640,10 @@ export function registerAgentRoutes({ app, getCurrentProjectPath, configManager,
617
640
  // 让前端自己再算一遍(从它发请求到收到 done)必然与落盘值差出一点,刷新一下
618
641
  // (改读文件里的数)就会变,看着像同一轮有两个用时。
619
642
  const durationMs = Date.now() - turnStartedAt;
620
- recordTurnTiming(session, { turnIndex, durationMs });
643
+ recordTurnTiming(session, { turnIndex: effectiveTurnIndex, durationMs });
621
644
  // 收尾事件:前端拿它给这条回答定格「本轮用时」。放在 done / error 之后 ——
622
645
  // 中断(点停止、客户端断开)的轮次同样有值时,被停止的那一轮也能看到跑了多久。
623
- send({ type: 'turn_done', turnIndex, durationMs });
646
+ send({ type: 'turn_done', turnIndex: effectiveTurnIndex, durationMs });
624
647
 
625
648
  // 更新标题(新会话从第一条 user 消息自动生成)
626
649
  if (isNew) {
@@ -1,11 +0,0 @@
1
- import"./rolldown-runtime-C0FnF6B9.js";import{$n as e,At as t,Bn as n,Ct as r,Dn as i,En as a,Fr as o,Gt as s,Ht as ee,In as c,J as l,Jr as u,M as d,Nn as f,On as p,Pn as m,Tn as h,Tt as g,W as _,Wn as v,Xr as y,Yt as b,Zn as x,_n as S,a as C,an as w,br as T,cr as E,dn as D,ei as O,et as k,i as A,j,kn as M,kt as N,lr as P,lt as te,mt as ne,nr as F,or as I,pr as L,r as R,st as z,tt as re,vr as B,xn as V}from"./element-plus-Bh2bgoEC.js";import{i as ie,o as H}from"./vendor-DUF7rMpx.js";import{n as U,t as W}from"./_plugin-vue_export-helper-T5G-Ork1.js";import{t as ae}from"./configStore-BLuyXnKF.js";import{t as G}from"./CommonDialog-CyBkT3AE.js";import{a as oe,c as se,i as ce,l as le,n as ue,o as de,p as fe,r as pe,s as me,t as K,u as he}from"./agentConversations-S0q49dQw.js";import{c as ge,s as _e,t as ve}from"./CopySessionButton-CwWaki0s.js";n(),L();var q=c({__name:`MarketplacePanel`,props:{type:{type:String,required:!0}},setup(e,{expose:t}){t();let n=e,i=ae(),a=T([]),o=T([]),c=T(!1),l=T(``),u=T(``),d=T([]),f=T(`project`),p=T(new Set),m=T(new Set),g=T(new Set),_=h(()=>i.currentDirectory||``),v=null;async function y(){if(!c.value){c.value=!0;try{let e=new URLSearchParams({type:n.type,cwd:_.value});u.value&&e.set(`q`,u.value),d.value.length&&e.set(`sources`,d.value.join(`,`));let t=await(await fetch(`/api/agent/marketplace/catalog?${e}`)).json();t?.success?(a.value=t.groups||[],o.value=t.installed||[]):A.error(t?.error||U(`@MKT:加载失败`))}catch{A.error(U(`@MKT:加载失败`))}finally{c.value=!1}}}I(l,e=>{v&&clearTimeout(v),v=setTimeout(()=>{u.value=e.trim(),y()},400)}),I(d,()=>y()),I(()=>n.type,()=>{d.value=[],u.value=``,l.value=``,y()}),I(_,()=>y()),y();let x=h(()=>{let e=_.value?_.value.split(/[\\/]/).filter(Boolean).pop():``;return f.value===`project`?e||U(`@MKT:当前项目`):U(`@MKT:g ai 智能体`)});function S(e){return e.stars?`${C(e.stars)} ★`:e.downloads?`${C(e.downloads)} ${U(`@MKT:周下载`)}`:e.uses?`${C(e.uses)} ${U(`@MKT:次使用`)}`:``}function C(e){return e>=1e3?`${(e/1e3).toFixed(e>=1e4?0:1)}k`:String(e)}function E(e){return`${e.id}`}function D(e){return p.value.has(E(e))}let O=h(()=>{let e={project:[],global:[]};for(let t of o.value)(e[t.target]||=[]).push(t);return e}),k=B({visible:!1,item:null,env:{},args:``});function j(e){return n.type===`mcp`&&(!!e.envKeys?.length||!!e.args?.length)}function M(e){k.item=e,k.env=Object.fromEntries((e.envKeys||[]).map(e=>[e,``])),k.args=(e.args||[]).join(` `),k.visible=!0}function P(){let e=k.item;if(k.visible=!1,!e)return;let t=Object.fromEntries(Object.entries(k.env).filter(([,e])=>e.trim()!==``)),n=k.args.trim()?k.args.trim().match(/(?:[^\s"]+|"[^"]*")+/g)?.map(e=>e.replace(/^"|"$/g,``))??void 0:void 0;L(e,{env:t,...n?{args:n}:{}})}async function F(e){if(!e.installed||await z(e)){if(j(e)){M(e);return}await L(e,{})}}async function L(e,t){let r=E(e);p.value.add(r);try{let r=await fetch(`/api/agent/marketplace/install`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({type:n.type,target:f.value,cwd:_.value,item:{...e,...t}})}),i=await r.json().catch(()=>null);if(!r.ok||!i?.success){A.error(i?.error||U(`@MKT:安装失败`));return}i.item?.warning&&A.warning(i.item.warning),i.item?.envKeys?.length?A.warning(`${U(`@MKT:还缺环境变量`)}: ${i.item.envKeys.join(`, `)}`):A.success(U(`@MKT:安装成功`)),await y()}catch{A.error(U(`@MKT:安装失败`))}finally{p.value.delete(r)}}async function z(e){try{return await R.confirm(`${e.name} ${U(`@MKT:已安装,要重新安装吗?`)}`,U(`@MKT:重新安装`),{confirmButtonText:U(`@MKT:重装`),cancelButtonText:U(`@MKT:取消`),type:`warning`}),!0}catch{return!1}}async function V(e){let t=`${e.target}:${e.id}`;g.value.add(t);try{let t=await fetch(`/api/agent/marketplace/reveal`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({type:n.type,target:e.target,cwd:_.value,id:e.id})}),r=await t.json().catch(()=>null);(!t.ok||!r?.success)&&A.error(r?.error||U(`@MKT:打开失败`))}catch{A.error(U(`@MKT:打开失败`))}finally{g.value.delete(t)}}async function ie(e){try{await R.confirm(`${U(`@MKT:确定卸载`)} ${e.name}?`,U(`@MKT:卸载`),{confirmButtonText:U(`@MKT:卸载`),cancelButtonText:U(`@MKT:取消`),type:`warning`})}catch{return}let t=`${e.target}:${e.id}`;m.value.add(t);try{let t=new URLSearchParams({target:e.target,cwd:_.value}),r=await fetch(`/api/agent/marketplace/item/${n.type}/${encodeURIComponent(e.id)}?${t}`,{method:`DELETE`}),i=await r.json().catch(()=>null);if(!r.ok||!i?.success){A.error(i?.error||U(`@MKT:卸载失败`));return}A.success(U(`@MKT:已卸载`)),await y()}catch{A.error(U(`@MKT:卸载失败`))}finally{m.value.delete(t)}}let H={props:n,configStore:i,groups:a,installed:o,loading:c,searchInput:l,query:u,selectedSources:d,installTarget:f,installing:p,removing:m,revealing:g,cwd:_,get searchTimer(){return v},set searchTimer(e){v=e},loadCatalog:y,targetLabel:x,metricText:S,formatCount:C,cardKey:E,isBusy:D,installedByTarget:O,configDialog:k,needsConfig:j,openConfigDialog:M,confirmConfig:P,onInstall:F,doInstall:L,confirmReinstall:z,onReveal:V,onRemove:ie,get $t(){return U},get Search(){return s},get Refresh(){return ee},get Delete(){return te},get Link(){return N},get Check(){return re},get Star(){return b},get Download(){return ne},get Warning(){return w},get FolderOpened(){return r},CommonDialog:G};return Object.defineProperty(H,"__isScriptSetup",{enumerable:!1,value:!0}),H}});n(),D(),o();var ye={class:`marketplace-panel`},be={class:`mp-toolbar`},xe={class:`mp-search`},Se=[`placeholder`],Ce={class:`mp-toolbar-sub`},J={class:`mp-target-hint`},Y={class:`mp-source-filter`},we=[`onClick`],X={class:`mp-body`},Te={class:`mp-group-header`},Ee={class:`mp-group-title`},Z={key:0,class:`mp-group-count`},Q={key:1,class:`mp-group-error`},De=[`href`,`title`],$={key:0,class:`mp-grid`},Oe={class:`mp-card-head`},ke=[`title`],Ae={key:0,class:`mp-card-metric`},je=[`title`],Me={class:`mp-card-meta`},Ne=[`title`],Pe=[`title`],Fe={key:2,class:`mp-tag remote`},Ie={class:`mp-card-foot`},Le=[`href`],Re={key:1,class:`mp-installed-chip`},ze=[`disabled`,`onClick`],Be={key:3,class:`mp-not-installable`},Ve={key:1,class:`mp-empty`},He={key:0,class:`mp-group installed`},Ue={class:`mp-group-header`},We={class:`mp-group-title`},Ge={class:`mp-group-count`},Ke={key:0,class:`mp-installed-block`},qe={class:`mp-installed-target`},Je={class:`mp-installed-list`},Ye=[`title`],Xe=[`title`],Ze={key:1,class:`mp-meta-pkg`},Qe={key:2,class:`mp-tag warn`},$e=[`disabled`,`title`,`onClick`],et=[`disabled`,`onClick`],tt={key:1,class:`mp-empty all`},nt={key:0,class:`mkt-form`},rt={class:`mkt-form-name`},it=[`onUpdate:modelValue`,`placeholder`],at={key:0,class:`mkt-field`},ot={class:`mkt-hint`};function st(t,n,r,o,s,ee){let c=l,h=j,g=d,v=_,y=C;return x(),M(`div`,ye,[p(` ── 工具栏 ───────────────────────────────── `),a(`div`,be,[a(`div`,xe,[m(c,{class:`mp-search-icon`},{default:E(()=>[m(o.Search)]),_:1}),P(a(`input`,{"onUpdate:modelValue":n[0]||=e=>o.searchInput=e,class:`mp-search-input`,type:`text`,placeholder:r.type===`skill`?o.$t(`@MKT:搜索 Skill...`):o.$t(`@MKT:搜索 MCP 服务...`)},null,8,Se),[[S,o.searchInput]])]),m(g,{modelValue:o.installTarget,"onUpdate:modelValue":n[1]||=e=>o.installTarget=e,size:`small`,class:`mp-target`},{default:E(()=>[m(h,{value:`project`},{default:E(()=>[f(O(o.$t(`@MKT:当前项目`)),1)]),_:1}),m(h,{value:`global`},{default:E(()=>[f(O(o.$t(`@MKT:g ai 智能体`)),1)]),_:1})]),_:1},8,[`modelValue`]),m(v,{content:o.$t(`@MKT:刷新`),placement:`top`,"show-after":300},{default:E(()=>[a(`button`,{class:u([`mp-refresh`,{spinning:o.loading}]),onClick:o.loadCatalog},[m(c,null,{default:E(()=>[m(o.Refresh)]),_:1})],2)]),_:1},8,[`content`])]),a(`div`,Ce,[a(`span`,J,[f(O(o.$t(`@MKT:安装到`))+` `,1),a(`b`,null,O(o.targetLabel),1)]),a(`div`,Y,[(x(!0),M(V,null,e(o.groups,e=>(x(),M(`button`,{key:e.id,class:u([`mp-chip`,{active:o.selectedSources.includes(e.id)}]),onClick:()=>{let t=o.selectedSources.indexOf(e.id);t>=0?o.selectedSources.splice(t,1):o.selectedSources.push(e.id)}},O(e.label),11,we))),128)),o.selectedSources.length?(x(),M(`button`,{key:0,class:`mp-chip clear`,onClick:n[2]||=e=>o.selectedSources=[]},O(o.$t(`@MKT:全部来源`)),1)):p(`v-if`,!0)])]),p(` ── 内容 ─────────────────────────────────── `),P((x(),M(`div`,X,[(x(!0),M(V,null,e(o.groups,t=>(x(),M(`section`,{key:t.id,class:`mp-group`},[a(`header`,Te,[a(`h3`,Ee,O(t.label),1),t.status===`ok`?(x(),M(`span`,Z,O(t.items.length),1)):(x(),M(`span`,Q,[m(c,null,{default:E(()=>[m(o.Warning)]),_:1}),f(` `+O(t.error||o.$t(`@MKT:来源不可用`)),1)])),t.homepage?(x(),M(`a`,{key:2,class:`mp-group-link`,href:t.homepage,target:`_blank`,rel:`noopener noreferrer`,title:t.homepage},[m(c,null,{default:E(()=>[m(o.Link)]),_:1})],8,De)):p(`v-if`,!0)]),t.status===`ok`&&t.items.length?(x(),M(`div`,$,[(x(!0),M(V,null,e(t.items,t=>(x(),M(`article`,{key:o.cardKey(t),class:`mp-card`},[a(`div`,Oe,[a(`span`,{class:`mp-card-name`,title:t.name},O(t.name),9,ke),o.metricText(t)?(x(),M(`span`,Ae,[m(c,null,{default:E(()=>[t.stars?(x(),i(o.Star,{key:0})):(x(),i(o.Download,{key:1}))]),_:2},1024),f(` `+O(o.metricText(t)),1)])):p(`v-if`,!0)]),a(`p`,{class:`mp-card-desc`,title:t.description},O(t.description||o.$t(`@MKT:暂无描述`)),9,je),a(`div`,Me,[t.package?(x(),M(`code`,{key:0,class:`mp-meta-pkg`,title:t.package},O(t.package),9,Ne)):t.repository?(x(),M(`span`,{key:1,class:`mp-meta-repo`,title:t.repository+(t.subpath?` · `+t.subpath:``)},[f(O(t.repository),1),t.subpath?(x(),M(V,{key:0},[f(` · `+O(t.subpath),1)],64)):p(`v-if`,!0)],8,Pe)):p(`v-if`,!0),t.transport===`remote`?(x(),M(`span`,Fe,O(o.$t(`@MKT:远程服务`)),1)):p(`v-if`,!0),(x(!0),M(V,null,e((t.tags||[]).slice(0,2),e=>(x(),M(`span`,{key:e,class:`mp-tag`},O(e),1))),128))]),a(`div`,Ie,[t.upstream?(x(),M(`a`,{key:0,class:`mp-card-link`,href:t.upstream,target:`_blank`,rel:`noopener noreferrer`},O(o.$t(`@MKT:查看来源`)),9,Le)):p(`v-if`,!0),n[6]||=a(`span`,{class:`mp-foot-spacer`},null,-1),t.installed?(x(),M(`span`,Re,[m(c,null,{default:E(()=>[m(o.Check)]),_:1}),f(O(o.$t(`@MKT:已安装`)),1)])):t.installable===!1?(x(),M(`span`,Be,O(o.$t(`@MKT:暂不支持一键安装`)),1)):(x(),M(`button`,{key:2,class:`mp-install-btn`,disabled:o.isBusy(t),onClick:e=>o.onInstall(t)},O(o.isBusy(t)?o.$t(`@MKT:安装中...`):o.$t(`@MKT:安装`)),9,ze))])]))),128))])):t.status===`ok`?(x(),M(`div`,Ve,O(o.$t(`@MKT:该来源暂无结果`)),1)):p(`v-if`,!0)]))),128)),p(` ── 已安装 ─────────────────────────────── `),o.installed.length?(x(),M(`section`,He,[a(`header`,Ue,[a(`h3`,We,O(o.$t(`@MKT:已安装`)),1),a(`span`,Ge,O(o.installed.length),1)]),(x(),M(V,null,e([`project`,`global`],t=>a(`div`,{key:t},[o.installedByTarget[t]?.length?(x(),M(`div`,Ke,[a(`div`,qe,O(t===`project`?o.$t(`@MKT:当前项目`):o.$t(`@MKT:g ai 智能体`)),1),a(`div`,Je,[(x(!0),M(V,null,e(o.installedByTarget[t],e=>(x(),M(`div`,{key:t+e.id,class:`mp-installed-row`},[a(`span`,{class:`mp-installed-name`,title:e.name},O(e.name),9,Ye),p(` 实际落盘的标识:SKILL.md 里的 name 常与仓库名不同(如仓库 taste-skill
2
- 里的 skill 自称 brandkit),不显示这个用户就对不上广场卡片上的名字 `),e.id&&e.id!==e.name?(x(),M(`code`,{key:0,class:`mp-installed-id`,title:e.dir||e.file||e.id},O(e.id),9,Xe)):p(`v-if`,!0),e.package?(x(),M(`code`,Ze,O(e.package),1)):p(`v-if`,!0),e.requiredEnv?.length?(x(),M(`span`,Qe,O(o.$t(`@MKT:待配置`))+`: `+O(e.requiredEnv.join(`, `)),1)):p(`v-if`,!0),n[7]||=a(`span`,{class:`mp-foot-spacer`},null,-1),a(`button`,{class:`mp-reveal-btn`,disabled:o.revealing.has(t+e.id),title:e.dir||e.file||``,onClick:t=>o.onReveal(e)},[m(c,null,{default:E(()=>[m(o.FolderOpened)]),_:1}),f(` `+O(o.$t(`@MKT:打开文件夹`)),1)],8,$e),a(`button`,{class:`mp-remove-btn`,disabled:o.removing.has(t+e.id),onClick:t=>o.onRemove(e)},[m(c,null,{default:E(()=>[m(o.Delete)]),_:1}),f(` `+O(o.removing.has(t+e.id)?o.$t(`@MKT:卸载中...`):o.$t(`@MKT:卸载`)),1)],8,et)]))),128))])])):p(`v-if`,!0)])),64))])):p(`v-if`,!0),!o.loading&&!o.groups.length?(x(),M(`div`,tt,O(o.$t(`@MKT:加载失败`)),1)):p(`v-if`,!0)])),[[y,o.loading]]),p(` ── MCP 安装前配置 ───────────────────────── `),m(o.CommonDialog,{modelValue:o.configDialog.visible,"onUpdate:modelValue":n[5]||=e=>o.configDialog.visible=e,title:o.$t(`@MKT:配置 MCP`),width:`460px`,type:`flex`,"close-on-click-modal":!1,"append-to-body":``},{footer:E(()=>[a(`button`,{class:`mkt-cancel`,onClick:n[4]||=e=>o.configDialog.visible=!1},O(o.$t(`@MKT:取消`)),1),a(`button`,{class:`mkt-confirm`,onClick:o.confirmConfig},O(o.$t(`@MKT:安装`)),1)]),default:E(()=>[o.configDialog.item?(x(),M(`div`,nt,[a(`p`,rt,O(o.configDialog.item.name),1),(x(!0),M(V,null,e(Object.keys(o.configDialog.env),e=>(x(),M(`div`,{key:e,class:`mkt-field`},[a(`label`,null,O(e),1),P(a(`input`,{"onUpdate:modelValue":t=>o.configDialog.env[e]=t,type:`text`,autocomplete:`off`,placeholder:o.$t(`@MKT:环境变量值`),spellcheck:`false`},null,8,it),[[S,o.configDialog.env[e]]])]))),128)),o.configDialog.item.args?.length?(x(),M(`div`,at,[a(`label`,null,O(o.$t(`@MKT:启动参数`)),1),P(a(`input`,{"onUpdate:modelValue":n[3]||=e=>o.configDialog.args=e,type:`text`,autocomplete:`off`,spellcheck:`false`},null,512),[[S,o.configDialog.args]]),a(`p`,ot,O(o.$t(`@MKT:参数提示`)),1)])):p(`v-if`,!0)])):p(`v-if`,!0)]),_:1},8,[`modelValue`,`title`])])}var ct=W(q,[[`render`,st],[`__scopeId`,`data-v-e9f9e851`],[`__file`,`D:/workspace/github_workspace/zen-gitsync/src/ui/client/src/components/MarketplacePanel.vue`]]);n(),L();var lt=c({__name:`AgentView`,setup(e,{expose:n}){n();let r=ae(),i=T(null),{narrow:a}=ge(i),o=T(`chat`),s=T(`chat`),ee=h(()=>s.value===`mcp`?`mcp`:`skill`),c=h(()=>[{id:`chat`,label:U(`@AGENT:对话`),icon:k},{id:`skill`,label:U(`@AGENT:Skill 广场`),icon:g},{id:`mcp`,label:U(`@AGENT:MCP 广场`),icon:z}]),{sessions:u,sessionsLoading:d,currentSessionId:f,messages:p,isStreaming:m,sessionLoading:_,loadSessions:y,loadSession:b,deleteSession:x,renameSession:S,newSession:C,sendMessage:w,pendingQuestion:E,answeringQuestion:D,answerQuestion:O,isSessionGenerating:A,currentEngine:j,pendingEngine:M,isEngineLocked:N,pickEngine:P,stop:te,inputContextUsage:ne,queuedMessages:F,queuePaused:L,removeQueuedMessage:R,flushQueued:re}=he(),B=h(()=>N.value?j.value:M.value);function V(e){N.value||P(e)}let W=h(()=>{let e=r.theme;return e===`dark`||e===`auto`&&window.matchMedia(`(prefers-color-scheme: dark)`).matches?`dark`:`light`}),G=h(()=>se(u.value,A)),q=oe(),ye=h(()=>G.value.find(e=>e.id===f.value)?.title||U(`@AGENT:对话`)),be={"--welcome-avatar":`url("${K}")`},xe=h(()=>[{id:`p1`,label:U(`@AGENT:查看项目结构`),prompt:U(`@AGENT:prompt_p1`)},{id:`p2`,label:U(`@AGENT:分析代码质量`),prompt:U(`@AGENT:prompt_p2`)},{id:`p3`,label:U(`@AGENT:帮我提交代码`),prompt:U(`@AGENT:prompt_p3`)},{id:`p4`,label:U(`@AGENT:Git 状态检查`),prompt:U(`@AGENT:prompt_p4`)},{id:`p5`,label:U(`@AGENT:帮我启动项目`),prompt:U(`@AGENT:prompt_p5`)}]);async function Se(e){await w(e.text,e.files),await v(),X()}async function Ce(e){await w(e.prompt),await v(),X()}let J=de(),Y=T(null),we=me();function X(e=!0){Y.value?.scrollToBottom(e)}I(()=>p.value.length,()=>{v(()=>X(!1))});function Te(e){b(e),o.value=`chat`}function Ee(){C(),o.value=`chat`}let Z=T(280),Q=T(!1);function De(e){Q.value=!0;let t=e.clientX,n=Z.value,r=e=>{let r=e.clientX-t,i=Math.max(200,Math.min(500,n+r));Z.value=i},i=()=>{Q.value=!1,document.removeEventListener(`mousemove`,r),document.removeEventListener(`mouseup`,i)};document.addEventListener(`mousemove`,r),document.addEventListener(`mouseup`,i)}I(a,e=>{e||(o.value=`chat`)}),I(()=>[r.currentDirectory,m.value],async([e,t],n)=>{let r=!n||e!==n[0],i=!!n&&n[1]===!0&&!t;(r||i)&&await y(),f.value&&!m.value&&!u.value.some(e=>e.sessionId===f.value)&&C()},{immediate:!0});let $={configStore:r,rootRef:i,narrow:a,chatPage:o,activeTab:s,marketplaceType:ee,tabs:c,sessions:u,sessionsLoading:d,currentSessionId:f,messages:p,isStreaming:m,sessionLoading:_,loadSessions:y,loadSession:b,deleteSession:x,renameSession:S,newSession:C,sendMessage:w,pendingQuestion:E,answeringQuestion:D,answerQuestion:O,isSessionGenerating:A,currentEngine:j,pendingEngine:M,isEngineLocked:N,pickEngine:P,stop:te,inputContextUsage:ne,queuedMessages:F,queuePaused:L,removeQueuedMessage:R,flushQueued:re,displayEngine:B,handleEngineSelect:V,chatTheme:W,conversationItems:G,conversationLabels:q,currentSessionTitle:ye,welcomeAvatarStyle:be,presetQuestions:xe,onSend:Se,onSelectPreset:Ce,questionLabels:J,chatContainerRef:Y,queueLabels:we,scrollToBottom:X,selectSession:Te,handleNewSession:Ee,sidebarWidth:Z,isResizing:Q,startResize:De,get $t(){return U},get ElIcon(){return l},get Loading(){return t},get ChatContainer(){return ie},get ConversationList(){return H},get AGENT_UPLOAD_ACCEPT(){return le},get AGENT_ASSISTANT_NAME(){return ue},get AGENT_ASSISTANT_AVATAR(){return K},get MESSAGE_RAIL_CONFIG(){return ce},get MESSAGE_META_CONFIG(){return pe},get agentEngineName(){return fe},MarketplacePanel:ct,AgentEngineSelector:_e,CopySessionButton:ve};return Object.defineProperty($,"__isScriptSetup",{enumerable:!1,value:!0}),$}});n(),o();var ut={ref:`rootRef`,class:`agent-view`},dt={class:`agent-chat-area`},ft={class:`agent-tabs`,role:`tablist`},pt=[`aria-selected`,`onClick`],mt={class:`agent-tab-body`},ht={key:0,class:`agent-chat-pane`},gt={key:0,class:`agent-list-page`},_t={key:0,class:`agent-page-bar`},vt=[`title`,`aria-label`],yt={class:`agent-page-title`},bt={key:0,class:`chat-loading`};function xt(t,n,r,o,s,ee){return x(),M(`div`,ut,[p(` ═══ 宽屏:左侧会话列表(只在「对话」Tab 显示)═══\r
3
- 窄屏时这一整块不渲染,会话列表改成 body 里的一个整页(见 .agent-list-page) `),o.activeTab===`chat`&&!o.narrow?(x(),M(V,{key:0},[a(`aside`,{class:`agent-sidebar`,style:y({width:o.sidebarWidth+`px`})},[p(` 会话列表:UI 与交互都在组件库的 ConversationList 里 `),m(o.ConversationList,{class:`agent-conversations`,items:o.conversationItems,"active-id":o.currentSessionId,loading:o.sessionsLoading,labels:o.conversationLabels,onSelect:o.selectSession,onNew:o.handleNewSession,onRename:o.renameSession,onDelete:o.deleteSession},null,8,[`items`,`active-id`,`loading`,`labels`,`onRename`,`onDelete`])],4),p(` 拖拽分隔条 `),a(`div`,{class:u([`sidebar-resizer`,{active:o.isResizing}]),onMousedown:o.startResize},null,34)],64)):p(`v-if`,!0),p(` ═══ 右侧:Tab 栏 + 内容区 ═══ `),a(`main`,dt,[a(`nav`,ft,[(x(!0),M(V,null,e(o.tabs,e=>(x(),M(`button`,{key:e.id,type:`button`,class:u([`agent-tab`,{active:o.activeTab===e.id}]),role:`tab`,"aria-selected":o.activeTab===e.id,onClick:t=>o.activeTab=e.id},[m(o.ElIcon,null,{default:E(()=>[(x(),i(F(e.icon)))]),_:2},1024),a(`span`,null,O(e.label),1)],10,pt))),128)),p(` ═══ 引擎选择器 ═══\r
4
- 与工作台执行器下拉同一套交互(点击展开、未安装标出来、点了直接开安装弹窗)。\r
5
- 下拉本体收口在 AgentEngineSelector,文件空间 g ai 面板复用同一份实现。\r
6
- 会话一旦落盘就置灰:引擎在会话建立时锁定(三家续聊标识互不通用),要换请新建。 `),o.activeTab===`chat`?(x(),i(o.AgentEngineSelector,{key:0,class:`agent-engine`,engine:o.displayEngine,locked:o.isEngineLocked,onSelect:o.handleEngineSelect},null,8,[`engine`,`locked`])):p(`v-if`,!0),p(` 复制整条会话(默认精简范围,小箭头可选全量)——与工作台的「复制执行内容」同一套口径 `),o.activeTab===`chat`?(x(),i(o.CopySessionButton,{key:1,class:`agent-copy`,messages:o.messages,title:o.currentSessionTitle,engine:o.agentEngineName(o.displayEngine)},null,8,[`messages`,`title`,`engine`])):p(`v-if`,!0)]),a(`div`,mt,[p(` ── 对话 ── `),o.activeTab===`chat`?(x(),M(`div`,ht,[p(` 窄屏 · 会话列表页:列表独占整页 `),o.narrow&&o.chatPage===`list`?(x(),M(`div`,gt,[m(o.ConversationList,{class:`agent-conversations`,items:o.conversationItems,"active-id":o.currentSessionId,loading:o.sessionsLoading,labels:o.conversationLabels,onSelect:o.selectSession,onNew:o.handleNewSession,onRename:o.renameSession,onDelete:o.deleteSession},null,8,[`items`,`active-id`,`loading`,`labels`,`onRename`,`onDelete`])])):(x(),M(V,{key:1},[p(` 对话页(窄屏时顶上多一条返回条;宽屏左侧就摆着列表,不需要) `),o.narrow?(x(),M(`div`,_t,[a(`button`,{type:`button`,class:`agent-page-back`,title:o.$t(`@AGENT:返回会话列表`),"aria-label":o.$t(`@AGENT:返回会话列表`),onClick:n[0]||=e=>o.chatPage=`list`},[...n[1]||=[a(`svg`,{viewBox:`0 0 24 24`,width:`14`,height:`14`,fill:`none`,stroke:`currentColor`,"stroke-width":`2`,"stroke-linecap":`round`,"stroke-linejoin":`round`},[a(`line`,{x1:`19`,y1:`12`,x2:`5`,y2:`12`}),a(`polyline`,{points:`12 19 5 12 12 5`})],-1)]],8,vt),a(`span`,yt,O(o.currentSessionTitle),1)])):p(`v-if`,!0),p(`\r
7
- 对话宿主:单独开一层裁剪边界。\r
8
- 欢迎页在矮/窄窗口里会比可用高度还高,而它是垂直居中的 —— 溢出的部分\r
9
- 会**向上**跑出 .acu-chat,盖到刚加的返回条上(把返回条的点击也一起吃掉,\r
10
- e2e 实测就是这个症状)。宿主的 overflow:hidden 把它裁在自己这一格里。\r
11
- `),a(`div`,{class:`agent-chat-host`,style:o.welcomeAvatarStyle},[p(` 加载中 `),o.sessionLoading?(x(),M(`div`,bt,[m(o.ElIcon,{class:`is-loading`,size:32},{default:E(()=>[m(o.Loading)]),_:1}),a(`span`,null,O(o.$t(`@AGENT:加载会话中...`)),1)])):(x(),M(V,{key:1},[p(` ChatContainer `),m(o.ChatContainer,{ref:`chatContainerRef`,messages:o.messages,"preset-questions":o.presetQuestions,"welcome-title":o.$t(`@AGENT:智能体助手`),"welcome-description":o.$t(`@AGENT:我可以帮你阅读代码、执行命令、修改文件。选择下方话题或直接输入你的问题。`),"assistant-name":o.AGENT_ASSISTANT_NAME,"assistant-avatar":o.AGENT_ASSISTANT_AVATAR,theme:o.chatTheme,generating:o.isStreaming,"allow-queue":!0,queued:o.queuedMessages,"queue-paused":o.queuePaused,"queue-labels":o.queueLabels,"upload-config":{accept:o.AGENT_UPLOAD_ACCEPT},"context-usage":o.inputContextUsage,placeholder:o.isStreaming?o.$t(`@AGENT:生成中,发送后会排队,等本轮跑完自动接上…`):o.$t(`@AGENT:输入消息,Enter 发送`),question:o.pendingQuestion,"question-submitting":o.answeringQuestion,"question-labels":o.questionLabels,"plan-config":{labels:{title:o.$t(`@AGENT:计划`),raw:o.$t(`@AGENT:原始参数`)}},"message-rail-config":o.MESSAGE_RAIL_CONFIG,"message-meta-config":o.MESSAGE_META_CONFIG,onSend:o.onSend,onSelect:o.onSelectPreset,onStop:o.stop,onUnqueue:o.removeQueuedMessage,onFlushQueued:o.flushQueued,onAnswer:o.answerQuestion},null,8,[`messages`,`preset-questions`,`welcome-title`,`welcome-description`,`assistant-name`,`assistant-avatar`,`theme`,`generating`,`queued`,`queue-paused`,`queue-labels`,`upload-config`,`context-usage`,`placeholder`,`question`,`question-submitting`,`question-labels`,`plan-config`,`message-rail-config`,`message-meta-config`,`onStop`,`onUnqueue`,`onFlushQueued`,`onAnswer`])],2112))])],64))])):(x(),M(V,{key:1},[p(` ── Skill 广场 / MCP 广场 ── `),m(o.MarketplacePanel,{type:o.marketplaceType},null,8,[`type`])],2112))])])],512)}var St=W(lt,[[`render`,xt],[`__scopeId`,`data-v-bd681ddd`],[`__file`,`D:/workspace/github_workspace/zen-gitsync/src/ui/client/src/views/AgentView.vue`]]);export{St as default};
@@ -1 +0,0 @@
1
- .mindmap-preview[data-v-3969a8bf]{background:var(--bg-container,#fff);flex-direction:column;width:100%;height:100%;display:flex}.mindmap-preview-header[data-v-3969a8bf]{background:var(--bg-panel,var(--md-bg-subtle));font-size:var(--font-size-sm);align-items:center;gap:12px;padding:8px 14px;display:flex}.mindmap-preview-title[data-v-3969a8bf]{color:var(--text-primary,var(--md-text-strong));letter-spacing:.5px;font-weight:600}.mindmap-preview-hint[data-v-3969a8bf]{color:var(--text-secondary,var(--md-text-muted));font-size:var(--font-size-xs)}.mindmap-preview-canvas[data-v-3969a8bf]{flex:1;width:100%;min-height:0}.mindmap-preview-canvas[data-v-3969a8bf] .zm-mindmap,.mindmap-preview-canvas[data-v-3969a8bf]>*{width:100%;height:100%}.editor-agent-panel[data-v-7cc12c51]{background:var(--bg-panel);border-left:1px solid var(--border-color);flex-direction:column;flex-grow:1;flex-basis:0;min-width:200px;min-height:0;display:flex;overflow:hidden}.agent-panel-header[data-v-7cc12c51]{border-bottom:1px solid var(--border-color);flex-shrink:0;align-items:center;gap:8px;height:34px;padding:0 6px 0 12px;display:flex}.agent-panel-title[data-v-7cc12c51]{font-size:var(--font-size-xs);letter-spacing:.08em;text-transform:uppercase;color:var(--text-meta);white-space:nowrap;text-overflow:ellipsis;min-width:0;font-weight:700;overflow:hidden}.agent-panel-spacer[data-v-7cc12c51]{flex:1}.agent-panel-engine[data-v-7cc12c51]{flex:none}.agent-panel-icon-btn[data-v-7cc12c51]{border-radius:var(--radius-xs);width:24px;height:24px;color:var(--text-meta);cursor:pointer;transition:var(--transition-ui-fast);background:0 0;border:none;flex-shrink:0;justify-content:center;align-items:center;padding:0;display:flex}.agent-panel-icon-btn[data-v-7cc12c51]:hover{background:var(--bg-hover);color:var(--text-secondary)}.agent-panel-body[data-v-7cc12c51]{flex:1;grid-template:"stage"minmax(0,1fr)"composer"/minmax(0,1fr);min-height:0;display:grid}.agent-panel-body.is-wide[data-v-7cc12c51]{grid-template-columns:240px minmax(0,1fr);grid-template-areas:"list chat""list composer"}.agent-panel-convs[data-v-7cc12c51]{grid-area:stage;min-height:0;padding:8px 10px;overflow-y:auto}.agent-panel-chat[data-v-7cc12c51]{grid-area:stage;min-height:0;display:flex}.agent-panel-body.is-wide .agent-panel-convs[data-v-7cc12c51]{grid-area:list}.agent-panel-body.is-wide .agent-panel-chat[data-v-7cc12c51]{grid-area:chat}.agent-panel-composer[data-v-7cc12c51]{padding:var(--acu-space-3) var(--acu-space-4) var(--acu-space-4);background:linear-gradient(to top, var(--acu-bg) 70%, transparent);grid-area:composer}.agent-panel-composer[data-v-7cc12c51] .acu-input-queue{--acu-queue-edge:var(--role-pending-edge);--acu-queue-surface:var(--role-pending-surface);--acu-queue-ink:var(--role-pending-ink);--acu-queue-wash:var(--role-pending-wash)}.agent-panel-chat[data-v-7cc12c51]>.acu-chat{flex:1;min-width:0}.agent-panel-chat[data-v-7cc12c51] .acu-avatar--left{background:0 0}.agent-panel-chat[data-v-7cc12c51] .acu-bubble-main{position:relative}.agent-panel-chat[data-v-7cc12c51] .acu-message-meta{margin:0;position:absolute;top:0;right:0}.agent-panel-composer[data-v-7cc12c51] .acu-input-wrap>.agent-context-slot{gap:var(--acu-space-2,8px);padding:var(--acu-space-2,8px) var(--acu-space-2,8px) 0;flex-wrap:wrap;display:flex}.agent-panel-composer[data-v-7cc12c51] .acu-input-attachments>.agent-context-slot{display:inline-flex}.agent-context-att[data-v-7cc12c51]{align-items:center;gap:var(--acu-space-2,8px);max-width:180px;padding:var(--acu-space-2,8px) var(--acu-space-3,12px);border:1px solid var(--acu-border);border-radius:var(--acu-radius-sm,8px);background:var(--acu-bg,var(--bg-container));color:var(--acu-text-secondary,var(--text-secondary));font-size:var(--acu-font-size-xs,12px);display:inline-flex;position:relative}.agent-context-att-icon[data-v-7cc12c51]{opacity:.75;flex-shrink:0}.agent-context-att-name[data-v-7cc12c51]{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.agent-context-att-remove[data-v-7cc12c51]{border-radius:var(--acu-radius-full,999px);background:var(--acu-text,var(--text-primary));width:18px;height:18px;color:var(--acu-bg,var(--bg-container));cursor:pointer;box-shadow:var(--acu-shadow-sm,0 1px 2px #0003);transition:transform var(--acu-duration-fast,.15s) var(--acu-easing,ease);border:none;justify-content:center;align-items:center;padding:0;display:inline-flex;position:absolute;top:-6px;right:-6px}.agent-context-att-remove[data-v-7cc12c51]:hover{transform:scale(1.12)}.agent-context-att-remove[data-v-7cc12c51]:active{transform:scale(1.02)}.editor-view[data-v-fc2ebf13]{background:var(--bg-container);width:100%;height:100%;box-shadow:var(--shadow-sm);border-radius:0;display:flex;overflow:hidden}.editor-sidebar[data-v-fc2ebf13]{background:var(--bg-panel);flex-direction:column;flex-shrink:0;display:flex;overflow:hidden}.sidebar-header[data-v-fc2ebf13]{flex-shrink:0;justify-content:space-between;align-items:center;padding:8px 10px 6px;display:flex}.sidebar-title[data-v-fc2ebf13]{font-size:var(--font-size-xs);letter-spacing:.08em;text-transform:uppercase;color:var(--text-secondary);-webkit-user-select:none;user-select:none;font-weight:700}.sidebar-action-btn[data-v-fc2ebf13]{cursor:pointer;color:var(--text-meta);border-radius:var(--radius-base);background:0 0;border:none;align-items:center;padding:3px;display:flex}.sidebar-action-btn[data-v-fc2ebf13]:hover{color:var(--text-primary);background:var(--bg-hover)}.sidebar-search[data-v-fc2ebf13]{flex-shrink:0;align-items:center;padding:6px 10px;display:flex;position:relative}.sidebar-search-icon[data-v-fc2ebf13]{color:var(--text-meta);pointer-events:none;position:absolute;left:18px}.sidebar-search-input[data-v-fc2ebf13]{min-width:0;height:26px;font-size:var(--font-size-sm);color:var(--text-primary);background:var(--bg-input,#ffffff0a);border:1px solid var(--border-color);border-radius:var(--radius-base);transition:border-color var(--transition-fast), background var(--transition-fast);outline:none;flex:1;padding:0 24px 0 26px}.sidebar-search-input[data-v-fc2ebf13]::placeholder{color:var(--text-meta)}.sidebar-search-input[data-v-fc2ebf13]:hover{border-color:var(--text-tertiary)}.sidebar-search-input[data-v-fc2ebf13]:focus{border-color:var(--color-primary);background:var(--bg-panel)}.sidebar-search-clear[data-v-fc2ebf13]{cursor:pointer;width:18px;height:18px;color:var(--text-meta);transition:background var(--transition-fast), color var(--transition-fast);background:0 0;border:none;border-radius:50%;justify-content:center;align-items:center;padding:0;display:flex;position:absolute;right:14px}.sidebar-search-clear[data-v-fc2ebf13]:hover{background:var(--bg-hover);color:var(--text-primary)}.tree-name-hit[data-v-fc2ebf13]{color:var(--color-warning-light);border-radius:var(--radius-xs);background:#f59e0b26;padding:0 1px}.sidebar-tree[data-v-fc2ebf13]{flex:1;padding:4px 0;overflow:hidden auto}.tree-node[data-v-fc2ebf13]{cursor:pointer;border-radius:var(--radius-base);-webkit-user-select:none;user-select:none;height:24px;color:var(--text-primary);font-size:var(--font-size-mid);transition:background var(--transition-fast);white-space:nowrap;align-items:center;gap:4px;padding-right:8px;display:flex;overflow:hidden}.tree-node[data-v-fc2ebf13]:hover{background:var(--bg-hover)}.tree-node--active[data-v-fc2ebf13]{background:var(--tint-primary-12);color:var(--color-primary)}.tree-node--selected[data-v-fc2ebf13]{outline-offset:-1px;background:#63b3ed26;outline:1px solid #63b3ed59}.tree-arrow[data-v-fc2ebf13]{color:var(--text-meta);transition:transform var(--transition-fast);flex-shrink:0;align-items:center;width:12px;display:flex}.tree-arrow.expanded[data-v-fc2ebf13]{transform:rotate(90deg)}.tree-arrow-spacer[data-v-fc2ebf13]{flex-shrink:0;width:12px}.tree-icon[data-v-fc2ebf13]{font-size:var(--font-size-base);flex-shrink:0;align-items:center;line-height:1;display:flex}.tree-icon.mit-icon[data-v-fc2ebf13]{fill:currentColor;vertical-align:middle;width:14px;height:14px;display:inline-block}.tree-icon--dir[data-v-fc2ebf13]{color:#e8b84b}.tree-name[data-v-fc2ebf13]{text-overflow:ellipsis;min-width:0;font-size:var(--font-size-mid);flex:1;overflow:hidden}.tree-loading[data-v-fc2ebf13]{border:1.5px solid var(--color-primary);border-top-color:#0000;border-radius:50%;flex-shrink:0;width:10px;height:10px;animation:.8s linear infinite spin-fc2ebf13;display:inline-block}.tree-empty[data-v-fc2ebf13]{font-size:var(--font-size-sm);color:var(--text-meta);text-align:center;padding:24px 12px}.sidebar-loading[data-v-fc2ebf13]{flex:1;justify-content:center;align-items:center;display:flex}.spin-icon[data-v-fc2ebf13]{border:2px solid var(--color-primary);border-top-color:#0000;border-radius:50%;width:20px;height:20px;animation:.8s linear infinite spin-fc2ebf13;display:inline-block}@keyframes spin-fc2ebf13{to{transform:rotate(360deg)}}.editor-resizer[data-v-fc2ebf13]{cursor:col-resize;width:6px;transition:background var(--transition-fast);z-index:2;background:0 0;flex-shrink:0;position:relative}.editor-resizer[data-v-fc2ebf13]:after{content:"";background:var(--color-gray-300);border-radius:var(--radius-xs);width:2px;height:32px;transition:background var(--transition-fast), height var(--transition-fast);position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}.editor-resizer[data-v-fc2ebf13]:hover{background:var(--tint-primary-06)}.editor-resizer[data-v-fc2ebf13]:hover:after{background:var(--color-primary);height:48px;box-shadow:0 0 8px #3b82f666}.editor-main[data-v-fc2ebf13]{flex-direction:column;flex:1;display:flex;overflow:hidden}.editor-tabs[data-v-fc2ebf13]{background:var(--bg-panel);scrollbar-width:none;flex-shrink:0;display:flex;overflow:auto hidden}.editor-tabs[data-v-fc2ebf13]::-webkit-scrollbar{display:none}.editor-tab[data-v-fc2ebf13]{cursor:pointer;height:34px;font-size:var(--font-size-mid);color:var(--text-secondary);transition:background var(--transition-fast), color var(--transition-fast);-webkit-user-select:none;user-select:none;flex-shrink:0;align-items:center;gap:6px;min-width:80px;max-width:200px;padding:0 12px;display:flex}.editor-tab[data-v-fc2ebf13]:hover{background:var(--bg-hover);color:var(--text-primary)}.editor-tab.active[data-v-fc2ebf13]{background:var(--bg-container);color:var(--text-primary);border-bottom:2px solid var(--color-primary)}.tab-name[data-v-fc2ebf13]{text-overflow:ellipsis;white-space:nowrap;flex:1;overflow:hidden}.tab-dirty-dot[data-v-fc2ebf13]{background:var(--color-warning);border-radius:50%;flex-shrink:0;width:6px;height:6px}.tab-close[data-v-fc2ebf13]{cursor:pointer;color:var(--text-meta);border-radius:var(--radius-base);opacity:0;transition:opacity var(--transition-fast), background var(--transition-fast);background:0 0;border:none;flex-shrink:0;align-items:center;padding:2px;display:flex}.editor-tab:hover .tab-close[data-v-fc2ebf13],.editor-tab.active .tab-close[data-v-fc2ebf13]{opacity:1}.tab-close[data-v-fc2ebf13]:hover{background:var(--bg-hover);color:var(--color-danger-dark)}.editor-empty[data-v-fc2ebf13]{flex:1}.monaco-container[data-v-fc2ebf13]{flex:1;min-width:0;overflow:hidden}.monaco-container.hidden[data-v-fc2ebf13]{display:none}.image-tab-placeholder[data-v-fc2ebf13]{justify-content:center;align-items:center;gap:var(--spacing-sm);color:var(--text-secondary);background:var(--bg-container);text-align:center;padding:var(--spacing-lg);flex-direction:column;flex:1;display:flex}.image-tab-placeholder-title[data-v-fc2ebf13]{font-size:var(--font-size-md);font-weight:var(--font-weight-medium);color:var(--text-primary);word-break:break-all;margin:0}.image-tab-placeholder-hint[data-v-fc2ebf13]{font-size:var(--font-size-sm);color:var(--text-meta);margin:0}.editor-body[data-v-fc2ebf13]{flex:1;min-height:0;display:flex;overflow:hidden}.preview-resizer[data-v-fc2ebf13]{cursor:col-resize;width:6px;transition:background var(--transition-fast);z-index:2;background:0 0;flex-shrink:0;order:99;margin-left:auto;position:relative}.preview-resizer[data-v-fc2ebf13]:after{content:"";background:var(--color-gray-300);border-radius:var(--radius-xs);width:2px;height:32px;transition:background var(--transition-fast), height var(--transition-fast);position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}.preview-resizer[data-v-fc2ebf13]:hover{background:var(--tint-primary-06)}.preview-resizer[data-v-fc2ebf13]:hover:after{background:var(--color-primary);height:48px;box-shadow:0 0 8px #3b82f666}.preview-panel[data-v-fc2ebf13]{background:var(--bg-panel);flex-direction:column;flex:1 1 0;min-width:200px;display:flex;overflow:hidden}.preview-header[data-v-fc2ebf13]{background:var(--bg-panel);flex-shrink:0;align-items:center;gap:6px;height:34px;padding:0 10px;display:flex}.preview-title[data-v-fc2ebf13]{font-size:var(--font-size-xs);letter-spacing:.08em;text-transform:uppercase;color:var(--text-secondary);-webkit-user-select:none;user-select:none;font-weight:700}.preview-ext-badge[data-v-fc2ebf13]{font-size:var(--font-size-xs);border-radius:var(--radius-base);background:var(--tint-primary-16);color:var(--color-primary);letter-spacing:.04em;padding:1px 5px;font-weight:600}.preview-header-spacer[data-v-fc2ebf13]{flex:1}.preview-close-btn[data-v-fc2ebf13]{cursor:pointer;color:var(--text-meta);border-radius:var(--radius-base);transition:color var(--transition-fast), background var(--transition-fast);background:0 0;border:none;align-items:center;padding:3px;display:flex}.preview-close-btn[data-v-fc2ebf13]:hover{color:var(--color-danger-dark);background:var(--bg-hover)}.preview-body[data-v-fc2ebf13]{flex-direction:column;flex:1;display:flex;overflow:hidden}.preview-iframe[data-v-fc2ebf13]{background:0 0;border:none;flex:1;width:100%;height:100%}.preview-markdown[data-v-fc2ebf13]{flex:1;width:100%;height:100%;overflow:auto}.preview-mindmap[data-v-fc2ebf13]{background:var(--bg-container,#fff);flex-direction:column;flex:1;width:100%;height:100%;display:flex;overflow:hidden}.mindmap-toggle-btn.disabled[data-v-fc2ebf13]{opacity:.45;cursor:not-allowed}.mindmap-toggle-btn.active[data-v-fc2ebf13]{color:#fff;background:linear-gradient(#6366f1 0%,#4f46e5 100%);border-color:#4f46e5}.preview-image-wrap[data-v-fc2ebf13]{background:var(--bg-container);flex:1;justify-content:center;align-items:center;padding:16px;display:flex;overflow:auto}.preview-image-wrap img[data-v-fc2ebf13]{object-fit:contain;border-radius:var(--radius-base);max-width:100%;max-height:100%;box-shadow:0 2px 12px #0003}.editor-tabs-spacer[data-v-fc2ebf13]{flex:1}.preview-toggle-btn[data-v-fc2ebf13]{cursor:pointer;height:34px;color:var(--text-secondary);font-size:var(--font-size-sm);white-space:nowrap;transition:background var(--transition-fast), color var(--transition-fast);background:0 0;border:none;flex-shrink:0;align-items:center;gap:5px;padding:0 10px;display:flex}.preview-toggle-btn[data-v-fc2ebf13]:hover{background:var(--bg-hover);color:var(--text-primary)}.preview-toggle-btn.active[data-v-fc2ebf13]{color:var(--color-primary);background:var(--tint-primary-08)}.sidebar-actions[data-v-fc2ebf13]{align-items:center;gap:2px;display:flex}.tree-inline-input-row[data-v-fc2ebf13]{cursor:default;background:var(--bg-hover)}.tree-inline-input[data-v-fc2ebf13]{min-width:0;height:20px;font-size:var(--font-size-mid);background:var(--bg-container);border:1px solid var(--color-primary);border-radius:var(--radius-base);color:var(--text-primary);outline:none;flex:1;padding:0 5px;font-family:inherit}.ctx-menu[data-v-fc2ebf13]{z-index:var(--z-menu-float);background:var(--bg-container);border:1px solid var(--border-color);border-radius:var(--radius-md);-webkit-user-select:none;user-select:none;min-width:160px;padding:4px;position:fixed;box-shadow:0 8px 24px #00000059}.ctx-menu-icon[data-v-fc2ebf13]{flex-shrink:0;width:13px;height:13px}.ctx-menu-item[data-v-fc2ebf13]{border-radius:var(--radius-base);cursor:pointer;width:100%;font-size:var(--font-size-mid);color:var(--text-primary);text-align:left;transition:background var(--transition-fast);background:0 0;border:none;align-items:center;gap:8px;padding:6px 10px;display:flex}.ctx-menu-item[data-v-fc2ebf13]:hover{background:var(--bg-hover)}.ctx-menu-item--danger[data-v-fc2ebf13]{color:var(--color-danger-dark)}.ctx-menu-item--danger[data-v-fc2ebf13]:hover{background:#ef44441a}.ctx-menu-sep[data-v-fc2ebf13]{background:var(--border-color);height:1px;margin:4px 2px}