zen-gitsync 2.13.21 → 2.13.23

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 (37) hide show
  1. package/package.json +1 -1
  2. package/scripts/README_COLOR_CONVERTER.md +196 -196
  3. package/scripts/README_FONTSIZE_CONVERTER.md +278 -278
  4. package/scripts/README_SPACING_CONVERTER.md +126 -126
  5. package/scripts/README_STYLE_VARS.md +180 -180
  6. package/src/ui/public/assets/{AppVersionBadge-D9w7Usgz.js → AppVersionBadge-BUS8w2f8.js} +2 -2
  7. package/src/ui/public/assets/{BranchSelector-BJpA20eU.js → BranchSelector-Dm7nENer.js} +1 -1
  8. package/src/ui/public/assets/{CommandConsole-DUyDJSaq.js → CommandConsole-C1W9ZQkE.js} +2 -2
  9. package/src/ui/public/assets/{CommitForm-BCWCchcJ.js → CommitForm-C76W1ZZH.js} +1 -1
  10. package/src/ui/public/assets/{CustomCommandManager-DRzAgnp3.js → CustomCommandManager-JKx8JYqn.js} +1 -1
  11. package/src/ui/public/assets/{EditorView-DuB3Btcp.js → EditorView-CriUYDvi.js} +0 -0
  12. package/src/ui/public/assets/{FileDiffViewer-DldATw5k.js → FileDiffViewer-DNWsiIGt.js} +1 -1
  13. package/src/ui/public/assets/{FlowExecutionViewer-BNuIB5Bi.js → FlowExecutionViewer-CTDh2bCD.js} +1 -1
  14. package/src/ui/public/assets/{FlowOrchestrationWorkspace-BcA7LT02.js → FlowOrchestrationWorkspace-Chah2aGN.js} +1 -1
  15. package/src/ui/public/assets/{GitStatus-BFpSu8gV.js → GitStatus-DP-DQsiS.js} +1 -1
  16. package/src/ui/public/assets/{ImagePreview-C8eIs5yL.js → ImagePreview-D8klNa6F.js} +1 -1
  17. package/src/ui/public/assets/{LogList-C4mS0mZ4.js → LogList-Z76_5USQ.js} +1 -1
  18. package/src/ui/public/assets/{ProjectStartupButton-BBRjQvH3.js → ProjectStartupButton-C5oF4unq.js} +1 -1
  19. package/src/ui/public/assets/{RemoteRepoCard-OUVJTKSC.js → RemoteRepoCard-B-fGjb8Y.js} +1 -1
  20. package/src/ui/public/assets/ResetToRemoteButton-BTIEz5PB.css +1 -0
  21. package/src/ui/public/assets/{ResetToRemoteButton-Bpqx8Rhc.js → ResetToRemoteButton-BixSoEVs.js} +1 -1
  22. package/src/ui/public/assets/{SourceMapView-CDETiU3C.js → SourceMapView-C9HZiJ9N.js} +1 -1
  23. package/src/ui/public/assets/{TemplateManager-idRwh__b.js → TemplateManager-C7SuKq0f.js} +1 -1
  24. package/src/ui/public/assets/{UserInputNode-pnY8iicS.js → UserInputNode-CYy0UpzR.js} +1 -1
  25. package/src/ui/public/assets/WorkbenchView-A9Oik707.js +12 -0
  26. package/src/ui/public/assets/WorkbenchView-DvML7yIv.css +1 -0
  27. package/src/ui/public/assets/{_plugin-vue_export-helper-vLPduyLG.js → _plugin-vue_export-helper-BfbhkIv4.js} +2 -2
  28. package/src/ui/public/assets/index-CDa9xf0V.css +1 -0
  29. package/src/ui/public/assets/{index-Bi1D0fFa.js → index-DPSGvy34.js} +3 -3
  30. package/src/ui/public/favicon.svg +75 -75
  31. package/src/ui/public/index.html +20 -20
  32. package/src/ui/public/logo.svg +74 -74
  33. package/src/ui/server/routes/workbench.js +171 -31
  34. package/src/ui/public/assets/ResetToRemoteButton-DQuq2KfL.css +0 -1
  35. package/src/ui/public/assets/WorkbenchView-B83HdnYr.js +0 -12
  36. package/src/ui/public/assets/WorkbenchView-DslIMLgz.css +0 -1
  37. package/src/ui/public/assets/index-DYjISu_4.css +0 -1
@@ -66,11 +66,11 @@ const DEFAULT_INSTRUCTION = `你是一名资深软件架构师。
66
66
  - 2 层目录树(最多 200 行)
67
67
 
68
68
  【输出要求】
69
- 1. 给出一段 400-800 字的中文「项目架构说明」,覆盖:项目整体定位、技术栈、模块划分、核心流程、关键设计决策。
69
+ 1. 给出一段中文「项目架构说明」,长度不限,模型自行决定篇幅与详尽程度,能写多详细就多详细。覆盖:项目整体定位、技术栈、模块划分、核心流程、关键设计决策。
70
70
  2. 必须引用子项目里实际存在的文件路径、目录名、依赖名,不要编造。
71
71
  3. 多个子项目时:先逐个说明,最后输出一段「整体架构」总结它们之间的关系。
72
72
  4. 语气专业、具体、面向接手这个项目的开发者。
73
- 5. 只返回 JSON:{ "name": "项目名(10-20字)", "summary": "架构说明正文" }。`;
73
+ 5. 只返回 JSON:{ "name": "项目名(建议:项目名+架构说明)", "summary": "架构说明正文" }。`;
74
74
 
75
75
  // 单个附件最大 5MB;与 Anthropic Messages API 文档约束一致
76
76
  const MAX_IMAGE_BYTES = 5 * 1024 * 1024;
@@ -388,8 +388,70 @@ async function listDirTree(projectPath, maxDepth = 2, maxEntries = 400) {
388
388
  return lines.join('\n');
389
389
  }
390
390
 
391
+ // 剥离 LLM 输出的 思考块、```json``` 代码块围栏,以及首尾说明文字。
392
+ // 原贪婪正则 /({[\s\S]*})/ 会把 思考块(内含未配对花括号或字符串)一起吞进 JSON.parse,
393
+ // 在 DeepSeek 等会输出 推理链的模型上偶发 "Unterminated string in JSON"。
394
+ function stripThinkingBlocks(content) {
395
+ let s = String(content || '');
396
+ // 1) 显式 思考块(成对标签)
397
+ s = s.replace(/<think(?:ing)?>[\s\S]*?<\/think(?:ing)?>/gi, '');
398
+ // 2) 某些 provider 流式结尾会留下裸 <think>...</think> 之外的残段(无结束标签时被截断)
399
+ s = s.replace(/<think(?:ing)?>[\s\S]*$/gi, '');
400
+ // 3) ```json ... ``` 代码块围栏(保留内部 JSON)
401
+ s = s.replace(/```json\s*/gi, '').replace(/```/g, '');
402
+ return s;
403
+ }
404
+
405
+ // 在剥离 思考块 的文本里定位第一个「平衡花括号对象」的起点/终点。
406
+ // 跳过字符串内的 {/}(含转义)以及 Jinja {{ }} 模板占位符;
407
+ // 同时只在「JSON 特征显著」的 { 处起算 —— 即该 { 之后不远处出现 " 字符串。
408
+ // 这能避免把 LLM 思考文本里的裸 { ... }(如 "see { views, stores }")误当 JSON 起点。
409
+ function extractFirstJsonObject(content) {
410
+ const s = String(content || '');
411
+ let depth = 0;
412
+ let inStr = false;
413
+ let escape = false;
414
+ let start = -1;
415
+ for (let i = 0; i < s.length; i++) {
416
+ const ch = s[i];
417
+ if (inStr) {
418
+ if (escape) { escape = false; continue; }
419
+ if (ch === '\\') { escape = true; continue; }
420
+ if (ch === '"') { inStr = false; }
421
+ continue;
422
+ }
423
+ if (ch === '"') { inStr = true; continue; }
424
+ // 跳过 Jinja {{ }} 占位符(提示词模板常用,勿当 JSON)
425
+ if (ch === '{' && s[i + 1] === '{') { i++; continue; }
426
+ if (ch === '}' && s[i - 1] === '}') { continue; }
427
+ if (ch === '{') {
428
+ if (depth === 0) {
429
+ // 启发式判断「这看起来像 JSON 起点」:
430
+ // 1. { 之前紧邻「真正的语义字符」(字母/数字/中文/下划线) → 不是 JSON 起点(散文里的花括号)
431
+ // 但接受 . , ; : 等句末标点(LLM 经常以「总结:{...}」直接开头 JSON)
432
+ // 2. { 之后跳过空白第一个字符必须是 " 或 }(空对象)
433
+ const before = i > 0 ? s[i - 1] : '';
434
+ if (/[A-Za-z0-9_一-龥]/.test(before)) continue;
435
+ let j = i + 1;
436
+ while (j < s.length && /\s/.test(s[j])) j++;
437
+ const firstNonWs = s[j];
438
+ if (firstNonWs !== '"' && firstNonWs !== '}') continue;
439
+ start = i;
440
+ }
441
+ depth++;
442
+ } else if (ch === '}') {
443
+ if (depth > 0) depth--;
444
+ if (depth === 0 && start !== -1) {
445
+ return s.slice(start, i + 1);
446
+ }
447
+ }
448
+ }
449
+ // 兜底:找不到平衡对象时返回原文(让上层 JSON.parse 报清晰错误)
450
+ return s;
451
+ }
452
+
391
453
  async function callLlmJson(model, prompt, opts = {}) {
392
- const { maxTokens = 1500, timeoutMs = 60000, images = [] } = opts;
454
+ const { timeoutMs = 60000, images = [] } = opts;
393
455
  const { default: fetch } = await import('node-fetch').catch(() => ({ default: globalThis.fetch }));
394
456
  const url = `${String(model.baseURL || '').replace(/\/$/, '')}/chat/completions`;
395
457
  const headers = { 'Content-Type': 'application/json' };
@@ -410,7 +472,6 @@ async function callLlmJson(model, prompt, opts = {}) {
410
472
  const body = JSON.stringify({
411
473
  model: model.model,
412
474
  messages: [{ role: 'user', content: userContent }],
413
- max_tokens: maxTokens,
414
475
  temperature: 0.4,
415
476
  response_format: { type: 'json_object' },
416
477
  stream: false,
@@ -422,18 +483,37 @@ async function callLlmJson(model, prompt, opts = {}) {
422
483
  const resp = await fetch(url, { method: 'POST', headers, body, signal: controller.signal });
423
484
  const data = await resp.json().catch(() => ({}));
424
485
  if (!resp.ok) throw new Error(data?.error?.message || `HTTP ${resp.status}`);
425
- const content = data?.choices?.[0]?.message?.content || '{}';
486
+ const content = data?.choices?.[0]?.message?.content || '';
487
+ if (!content.trim()) {
488
+ throw new Error('LLM 返回内容为空');
489
+ }
426
490
  try {
427
- const m = content.match(/```json\s*([\s\S]*?)```/) || content.match(/({[\s\S]*})/);
428
- return JSON.parse(m ? m[1] : content);
429
- } catch {
430
- return {};
491
+ const cleaned = stripThinkingBlocks(content);
492
+ return JSON.parse(extractFirstJsonObject(cleaned));
493
+ } catch (err) {
494
+ const snippet = content.length > 500 ? content.slice(0, 500) + '…' : content;
495
+ throw new Error(`LLM 返回非 JSON 或被截断:${err.message};原始内容片段:${snippet}`);
431
496
  }
432
497
  } finally {
433
498
  clearTimeout(timer);
434
499
  }
435
500
  }
436
501
 
502
+ async function callLlmJsonWithRetry(model, prompt, opts = {}, retries = 1) {
503
+ let lastErr;
504
+ for (let i = 0; i <= retries; i++) {
505
+ try {
506
+ return await callLlmJson(model, prompt, opts);
507
+ } catch (err) {
508
+ lastErr = err;
509
+ if (i < retries) {
510
+ await new Promise(r => setTimeout(r, 800 * (i + 1)));
511
+ }
512
+ }
513
+ }
514
+ throw lastErr;
515
+ }
516
+
437
517
  /**
438
518
  * 流式调用 OpenAI 兼容 LLM。每收到一个 chunk 调 onDelta 回调。
439
519
  * onDelta 接收 { thinking?: string, content?: string },二选一。
@@ -1118,13 +1198,21 @@ async function runTaskQueue(task, repoPath, branch, opts) {
1118
1198
  const priorOutputs = fromIndex > 0
1119
1199
  ? await collectPriorOutputsUpTo(task, fromIndex)
1120
1200
  : []
1201
+ // 连续模式(默认):任意 sub 终态非 done(cancelled / error)→ 整批停,后续 sub 保持 todo。
1202
+ // AI 拆出来的 sub 一般前后强依赖(前一步产出是后一步输入),出错就停下来让用户决策。
1203
+ // 关闭后回退旧行为:单个 sub 失败不影响后续 sub 继续跑。
1204
+ const sequential = task.sequential !== false
1121
1205
  for (let i = fromIndex; i < task.subtasks.length; i++) {
1122
1206
  const sub = task.subtasks[i]
1123
1207
  if (sub.status === 'done') continue;
1124
- await runSingleSubtask(task, sub, repoPath, branch, priorOutputs)
1208
+ const outcome = await runSingleSubtask(task, sub, repoPath, branch, priorOutputs)
1125
1209
  // 逐 sub 落盘:之前只在队列跑完才 persistTaskAfterRun,中途崩溃会丢已完成
1126
1210
  // sub 的状态。runSingleSubtask 已经把 sub.status 改完,这里补一次落盘。
1127
1211
  await persistTaskAfterRun(task)
1212
+ if (sequential && outcome !== 'done') {
1213
+ // cancelled / error → 后续 sub 全部保持 todo,不再继续
1214
+ break
1215
+ }
1128
1216
  }
1129
1217
  }
1130
1218
 
@@ -1325,7 +1413,12 @@ ${prompt}`
1325
1413
  job.exitCode = 130; // 128 + SIGINT(2),约定俗成的"用户取消"退出码
1326
1414
  job.status = 'cancelled';
1327
1415
  job.error = '用户已停止执行';
1328
- // sub 不改状态——cancelled job 维度,同 task 后续 sub 仍可继续执行
1416
+ // 同步把闭包里的 sub 也置 cancelled(cancel 接口的 syncSubToCancelled 改了磁盘 task
1417
+ // 引用,但 runSingleSubtask 形参里的 sub 是另一份内存引用)。否则 finally publish sub:update
1418
+ // 会用 status='running' 覆盖掉前端已渲染的 cancelled 状态,导致 UI 反复跳回 running。
1419
+ // 同时 sequential=true 时 runTaskQueue 会看到 outcome='cancelled',break 不再跑后续 sub。
1420
+ sub.status = 'cancelled';
1421
+ if (!sub.error) sub.error = '用户已停止执行';
1329
1422
  } else {
1330
1423
  job.exitCode = 0;
1331
1424
  job.status = 'done';
@@ -1359,6 +1452,36 @@ ${prompt}`
1359
1452
  console.warn('[workbench] flushJobsSaveNow failed (job id=' + job.id + ', status=' + job.status + '):', err && err.message || err)
1360
1453
  }
1361
1454
  }
1455
+ // 把 sub 的终态返回给 runTaskQueue,用于「连续模式」判断要不要 break 整批队列
1456
+ return job.status // 'done' | 'cancelled' | 'error'
1457
+ }
1458
+
1459
+ /**
1460
+ * 把被取消的 sub 同步置 'cancelled' 并落盘。
1461
+ * cancelJob 路径专用:前端 taskIsRunning/sub.is-running 都看 sub.status,不改就会出现
1462
+ * "主任务黄点 + sub running 动效 + 右侧执行完成"三处不一致。
1463
+ *
1464
+ * 简单任务的虚拟 subId 不在 tasks.json 里(task.subtasks 是 complex 才有),所以这里
1465
+ * 找不到 sub 时静默返回;简单任务的 running 状态由 job 数组单独维护(见 taskIsRunning)。
1466
+ *
1467
+ * @returns {{ taskId: string, sub: object } | null} 找到并更新时返回新 sub,否则 null
1468
+ */
1469
+ async function syncSubToCancelled(job) {
1470
+ if (!job || !job.taskId || !job.subId) return null
1471
+ const data = await readJson(TASKS_FILE, { tasks: [] })
1472
+ const task = (data.tasks || []).find(x => x.id === job.taskId)
1473
+ if (!task || !Array.isArray(task.subtasks)) return null
1474
+ const sub = task.subtasks.find(s => s && s.id === job.subId)
1475
+ if (!sub) return null
1476
+ if (sub.status === 'cancelled') return { taskId: task.id, sub } // 已置过,幂等返回
1477
+ sub.status = 'cancelled'
1478
+ sub.error = '用户已停止执行'
1479
+ sub.errorAt = nowIso()
1480
+ task.updatedAt = nowIso()
1481
+ await writeJson(TASKS_FILE, data)
1482
+ publish('sub:update', { taskId: task.id, sub })
1483
+ publish('task:update', task)
1484
+ return { taskId: task.id, sub }
1362
1485
  }
1363
1486
 
1364
1487
  /** 把 task.subtasks 写回 tasks.json,并广播 task:update。runTaskQueue 和"单 sub 执行"共用。 */
@@ -1488,7 +1611,7 @@ export function registerWorkbenchRoutes({ app, getCurrentProjectPath, getProject
1488
1611
  }
1489
1612
 
1490
1613
  const projectName = path.basename(projectPath);
1491
- const LLM_OPTS = { maxTokens: 4000, timeoutMs: 1200000 };
1614
+ const LLM_OPTS = { timeoutMs: 1200000 };
1492
1615
 
1493
1616
  // ── 第一阶段:基于可编辑指令 + 根目录概览,生成「可复用的提示词模板」 ──
1494
1617
  const overviewBlock = subProjects.map(sp =>
@@ -1518,12 +1641,12 @@ ${subProjects.map(sp => {
1518
1641
 
1519
1642
  只返回 JSON:
1520
1643
  {
1521
- "name": "项目名(10-20字)",
1522
- "template": "可复用的提示词模板(300-600字),应明确使用 {{task.title}} / {{task.desc}} / {{sub.title}} / {{sub.desc}} / {{repo.path}} / {{branch}} 这 6 个变量"
1644
+ "name": "项目名(建议:项目名+架构说明,可根据实际项目特征调整)",
1645
+ "template": "可复用的提示词模板,长度不限,请充分覆盖 {{task.title}} / {{task.desc}} / {{sub.title}} / {{sub.desc}} / {{repo.path}} / {{branch}} 这 6 个变量的用法与上下文"
1523
1646
  }`;
1524
1647
 
1525
- const first = await callLlmJson(model, firstPrompt, LLM_OPTS);
1526
- const templateName = String(first.name || '').trim() || projectName || '项目架构说明';
1648
+ const first = await callLlmJsonWithRetry(model, firstPrompt, LLM_OPTS);
1649
+ const templateName = String(first.name || '').trim() || `${projectName}架构说明`;
1527
1650
  const template = String(first.template || '').trim();
1528
1651
 
1529
1652
  // ── 第二阶段:为每个子项目分别生成总结(单子项目 = 现在的行为) ──
@@ -1551,13 +1674,17 @@ ${sp.readme || '(无)'}
1551
1674
 
1552
1675
  只返回 JSON:
1553
1676
  {
1554
- "summary": "该子项目的架构说明(300-600字)"
1677
+ "summary": "该子项目的架构说明,长度不限,模型自行决定篇幅与详尽程度,能写多详细就多详细"
1555
1678
  }`;
1556
- const r = await callLlmJson(model, subPrompt, LLM_OPTS);
1679
+ const r = await callLlmJsonWithRetry(model, subPrompt, LLM_OPTS);
1557
1680
  return { name: sp.name, root: sp.root, summary: String(r.summary || '').trim() };
1558
1681
  }
1559
1682
 
1560
- const subSummaries = await Promise.all(subProjects.map(summarizeOneSub));
1683
+ // 串行执行,避免并发触发 provider 限流(429)导致整批失败
1684
+ const subSummaries = [];
1685
+ for (const sp of subProjects) {
1686
+ subSummaries.push(await summarizeOneSub(sp));
1687
+ }
1561
1688
 
1562
1689
  // ── 第三阶段:仅多子项目时合并(单子项目直接拿它的 summary) ──
1563
1690
  let finalSummary = '';
@@ -1566,32 +1693,31 @@ ${sp.readme || '(无)'}
1566
1693
  if (subSummaries.length === 1) {
1567
1694
  finalSummary = subSummaries[0].summary;
1568
1695
  } else {
1569
- const mergePrompt = `你是项目架构师。下列是同一仓库下 N 个子项目的架构说明,请合并输出**单一**的「项目架构说明」(800-1500字),覆盖:项目整体定位、技术栈、模块划分、子项目间关系、核心流程、关键设计决策。
1696
+ const mergePrompt = `你是项目架构师。下列是同一仓库下 N 个子项目的架构说明,请合并输出**单一**的「项目架构说明」,长度不限,模型自行决定篇幅与详尽程度。覆盖:项目整体定位、技术栈、模块划分、子项目间关系、核心流程、关键设计决策。
1570
1697
  子项目之间用清晰的小标题或编号分隔。最后输出一段「整体架构」总结它们如何协同。
1571
1698
  只引用实际出现的子项目名 / 文件路径 / 依赖名,不要编造。只返回 JSON:
1572
1699
 
1573
1700
  {
1574
- "name": "项目名(10-20字)",
1701
+ "name": "项目名(建议:项目名+架构说明)",
1575
1702
  "summary": "合并后的架构说明"
1576
1703
  }
1577
1704
 
1578
1705
  ## 子项目说明
1579
1706
  ${subSummaries.map((s, i) => `\n### [${i + 1}] ${s.name} (${s.root})\n${s.summary || '(空)'}`).join('\n')}`;
1580
1707
 
1581
- const merged = await callLlmJson(model, mergePrompt, LLM_OPTS);
1708
+ const merged = await callLlmJsonWithRetry(model, mergePrompt, LLM_OPTS);
1582
1709
  finalSummary = String(merged.summary || '').trim()
1583
1710
  || subSummaries.map(s => `### ${s.name}\n${s.summary}`).join('\n\n');
1584
1711
  finalName = String(merged.name || '').trim() || templateName;
1585
1712
  }
1586
1713
 
1714
+ // 名称始终固定为「<项目名>架构说明」,不信任模型返回的 name 字段
1715
+ finalName = `${projectName}架构说明`;
1716
+
1587
1717
  if (!finalSummary) {
1588
- // 兜底:仅返回模板
1589
- return res.json({
1590
- success: true,
1591
- name: finalName,
1592
- template,
1593
- result: '',
1594
- content: template
1718
+ return res.status(502).json({
1719
+ success: false,
1720
+ error: '模型返回为空,请稍后重试'
1595
1721
  });
1596
1722
  }
1597
1723
 
@@ -2273,11 +2399,14 @@ ${desc ? `描述:${desc}` : '描述:(无)'}${attachmentBlock}${templateB
2273
2399
 
2274
2400
  app.post('/api/workbench/tasks', async (req, res) => {
2275
2401
  try {
2276
- const { id, title, desc, promptId, subtasks, type: rawType, simpleOverride } = req.body || {};
2402
+ const { id, title, desc, promptId, subtasks, type: rawType, simpleOverride, sequential: rawSequential } = req.body || {};
2277
2403
  // title 非必填:允许空字符串,UI 层会用"未命名任务"占位
2278
2404
  const safeTitle = typeof title === 'string' ? title.trim() : '';
2279
2405
  // type 归一化:仅接受 'simple' | 'complex',缺省/未知一律按 complex
2280
2406
  const taskType = rawType === 'simple' ? 'simple' : 'complex';
2407
+ // sequential 归一化:仅接受 boolean,缺省/非布尔一律 true(连续模式),
2408
+ // 旧任务没字段时也是 true,保留连续默认行为
2409
+ const sequential = rawSequential === false ? false : true;
2281
2410
  const safeOverride = typeof simpleOverride === 'string' ? simpleOverride.slice(0, 8000) : '';
2282
2411
  const data = await readJson(TASKS_FILE, { tasks: [] });
2283
2412
  const tasks = data.tasks || [];
@@ -2293,6 +2422,8 @@ ${desc ? `描述:${desc}` : '描述:(无)'}${attachmentBlock}${templateB
2293
2422
  desc: desc || '',
2294
2423
  promptId: promptId || null,
2295
2424
  type: taskType,
2425
+ // 复杂任务可显式关闭连续模式;简单任务无意义,固定 true
2426
+ sequential: taskType === 'complex' ? sequential : true,
2296
2427
  simpleOverride: taskType === 'simple' ? safeOverride : '',
2297
2428
  subtasks: Array.isArray(subtasks) ? subtasks.map(s => ({
2298
2429
  id: s.id || genId(),
@@ -2326,6 +2457,8 @@ ${desc ? `描述:${desc}` : '描述:(无)'}${attachmentBlock}${templateB
2326
2457
  desc: desc || '',
2327
2458
  promptId: promptId || null,
2328
2459
  type: taskType,
2460
+ // 新建任务:简单任务固定 true,复杂任务用传入值(默认 true)
2461
+ sequential: taskType === 'complex' ? sequential : true,
2329
2462
  simpleOverride: taskType === 'simple' ? safeOverride : '',
2330
2463
  projectPath: currentProjectPath || '',
2331
2464
  subtasks: Array.isArray(subtasks) ? subtasks.map(s => ({
@@ -2527,7 +2660,11 @@ ${desc ? `描述:${desc}` : '描述:(无)'}${attachmentBlock}${templateB
2527
2660
  // - 找到正在运行的 job,调 child.kill() 终止 claude 进程
2528
2661
  // - Windows 下用 taskkill /T /F 杀进程树(claude 进程可能 fork 出子进程)
2529
2662
  // - 加入 cancelledJobs 集合,runTaskQueue 退出循环后会把 job 标为 'cancelled'
2530
- // - 只影响这一个 sub;同 task 后续 sub 仍按队列顺序继续执行
2663
+ // - 同步把 sub.status 改为 'cancelled' 并 publish sub:update,
2664
+ // 否则前端 sub 列表/任务头黄点会一直显示 running(sub.status 没动)。
2665
+ // 历史这里只改 job.status 导致 UI 误显示「执行中」。
2666
+ // - 只影响这一个 sub;同 task 后续 sub 在 sequential=false 时继续执行,
2667
+ // sequential=true 时由 runTaskQueue 自然 break(见 runSingleSubtask 的 cancelled 分支)
2531
2668
  app.post('/api/workbench/jobs/:id/cancel', (req, res) => {
2532
2669
  const job = jobs.get(req.params.id)
2533
2670
  if (!job) {
@@ -2542,6 +2679,9 @@ ${desc ? `描述:${desc}` : '描述:(无)'}${attachmentBlock}${templateB
2542
2679
  job.error = '用户已停止执行'
2543
2680
  job.endedAt = nowIso()
2544
2681
  publish('job:update', { ...job }) // 用浅拷贝避免序列化 child 引用
2682
+ // 同步把 sub 也置 cancelled,否则 taskIsRunning(sub.status==='running') 一直 true
2683
+ // → 左侧任务头黄点 + 右侧 sub 列表 running 动效都不消失
2684
+ const subInfo = syncSubToCancelled(job)
2545
2685
  // 终态:fire-and-forget 同步落盘,cancel 是显式操作,要保证不丢
2546
2686
  flushJobsSaveNow().catch(err => console.warn('[workbench] jobs save failed:', err.message))
2547
2687
  const child = job.child
@@ -1 +0,0 @@
1
- .unstage-all-button[data-v-6fd84bc7] .el-button.from-drawer{padding:6px var(--spacing-md);font-size:var(--font-size-sm);height:32px}.unstage-all-button[data-v-6fd84bc7] .el-button.from-form{padding:10px var(--spacing-lg);height:40px}.git-command-preview[data-v-8150488e]{margin-top:var(--spacing-base)}.preview-container[data-v-8150488e]{background:var(--bg-console);border:1px solid var(--border-console);border-radius:var(--radius-lg);align-items:center;gap:0;transition:border-color .2s;display:flex;overflow:hidden}.preview-container[data-v-8150488e]:hover{border-color:var(--border-console-hover)}.preview-title[data-v-8150488e]{font-size:var(--font-size-xs);font-family:var(--font-mono);color:var(--text-console-muted);white-space:nowrap;background:var(--bg-console-cmd);flex-shrink:0;align-items:center;gap:6px;padding:7px 10px 7px 14px;display:flex}.preview-title[data-v-8150488e]:before{content:"$";color:var(--text-console-prompt);font-size:13px;font-weight:700}.preview-content[data-v-8150488e]{padding:7px var(--spacing-base);font-family:var(--font-mono);font-size:var(--font-size-sm);color:var(--text-console);white-space:nowrap;background:0 0;flex:1;font-weight:500;line-height:1.4;overflow-x:auto}.preview-content[data-v-8150488e]::-webkit-scrollbar{height:3px}.preview-content[data-v-8150488e]::-webkit-scrollbar-track{background:0 0}.preview-content[data-v-8150488e]::-webkit-scrollbar-thumb{background:var(--console-scrollbar);border-radius:2px}[data-v-8150488e]::deep(.copy-command-btn){flex-shrink:0;margin-right:6px;color:var(--text-console-muted)!important}[data-v-8150488e]::deep(.copy-command-btn):hover{color:var(--text-console)!important;background:var(--bg-console-soft)!important}.merge-branch-button[data-v-9dd82c62]{display:inline-block}.merge-dialog-content[data-v-9dd82c62]{gap:var(--spacing-base);flex-direction:column;display:flex}.merge-info-card[data-v-9dd82c62]{align-items:flex-start;gap:var(--spacing-base);padding:var(--spacing-base);border-radius:var(--radius-xl);background:linear-gradient(135deg,#eff6ff 0%,#dbeafe 100%);border:1px solid #93c5fd;display:flex;position:relative;overflow:hidden}.merge-info-card[data-v-9dd82c62]:before{content:"";background:linear-gradient(90deg,#3b82f6 0%,#2563eb 100%);height:3px;position:absolute;top:0;left:0;right:0}.merge-info-card .info-icon[data-v-9dd82c62]{color:#fff;width:40px;height:40px;font-size:var(--font-size-lg);box-shadow:var(--shadow-md);background:linear-gradient(135deg,#3b82f6 0%,#2563eb 100%);border-radius:10px;flex-shrink:0;justify-content:center;align-items:center;display:flex}.merge-info-card .info-content[data-v-9dd82c62]{flex:1}.merge-info-card .info-content h4[data-v-9dd82c62]{font-size:var(--font-size-md);color:#1e3a8a;margin:0 0 6px;font-weight:600;line-height:1.2}.merge-info-card .info-content p[data-v-9dd82c62]{color:#1e40af;margin:0;line-height:1.4}.merge-info-card .info-content code[data-v-9dd82c62]{padding:var(--spacing-xs) 6px;border-radius:var(--radius-base);color:#1e40af;background:#3b82f61a;font-family:Consolas,Monaco,Courier New,monospace;font-weight:600}.merge-form[data-v-9dd82c62]{gap:var(--spacing-base);flex-direction:column;display:flex}.merge-form[data-v-9dd82c62] .el-form-item{margin-bottom:0}.merge-form[data-v-9dd82c62] .el-form-item__label{color:var(--color-text-title);margin-bottom:var(--spacing-base);font-weight:600}.merge-form[data-v-9dd82c62] .el-select .el-input__wrapper,.merge-form[data-v-9dd82c62] .el-select .el-textarea__inner,.merge-form[data-v-9dd82c62] .el-input .el-input__wrapper,.merge-form[data-v-9dd82c62] .el-input .el-textarea__inner,.merge-form[data-v-9dd82c62] .el-textarea .el-input__wrapper,.merge-form[data-v-9dd82c62] .el-textarea .el-textarea__inner{border-radius:var(--radius-lg);border:2px solid var(--color-gray-200);transition:all .3s}.merge-form[data-v-9dd82c62] .el-select .el-input__wrapper:hover,.merge-form[data-v-9dd82c62] .el-select .el-textarea__inner:hover,.merge-form[data-v-9dd82c62] .el-input .el-input__wrapper:hover,.merge-form[data-v-9dd82c62] .el-input .el-textarea__inner:hover,.merge-form[data-v-9dd82c62] .el-textarea .el-input__wrapper:hover,.merge-form[data-v-9dd82c62] .el-textarea .el-textarea__inner:hover{border-color:var(--color-gray-300)}.merge-form[data-v-9dd82c62] .el-select .el-input__wrapper.is-focus,.merge-form[data-v-9dd82c62] .el-select .el-textarea__inner:focus-visible,.merge-form[data-v-9dd82c62] .el-input .el-input__wrapper.is-focus,.merge-form[data-v-9dd82c62] .el-input .el-textarea__inner:focus-visible,.merge-form[data-v-9dd82c62] .el-textarea .el-input__wrapper.is-focus,.merge-form[data-v-9dd82c62] .el-textarea .el-textarea__inner:focus-visible{border-color:#3b82f6;box-shadow:0 0 0 3px #3b82f61a}.merge-form[data-v-9dd82c62] .el-radio-group{gap:var(--spacing-base);display:flex}.merge-form[data-v-9dd82c62] .el-radio-group .el-radio-button{flex:1}.merge-form[data-v-9dd82c62] .el-radio-group .el-radio-button__inner{border-radius:var(--radius-lg);width:100%;transition:all .3s}.merge-options[data-v-9dd82c62]{padding:var(--spacing-base);border-radius:var(--radius-xl)}.options-title[data-v-9dd82c62]{align-items:center;gap:var(--spacing-base);margin:0 0 var(--spacing-md) 0;color:var(--color-text-title);font-weight:600;display:flex}.options-title[data-v-9dd82c62] .el-icon{color:var(--color-gray-500);font-size:var(--font-size-md)}.options-horizontal[data-v-9dd82c62]{align-items:center;gap:var(--spacing-2xl);flex-wrap:wrap;display:flex}.options-horizontal[data-v-9dd82c62] .el-checkbox{white-space:nowrap;margin:0}.options-horizontal[data-v-9dd82c62] .el-checkbox .el-checkbox__label{color:var(--color-text-title);font-weight:500}.default-message-hint[data-v-9dd82c62]{justify-content:space-between;align-items:center;gap:var(--spacing-sm);margin-top:var(--spacing-xs);font-size:var(--font-size-sm);color:var(--text-secondary);display:flex}.default-message-hint .hint-content[data-v-9dd82c62]{align-items:center;gap:var(--spacing-xs);flex:1;min-width:0;display:flex}.default-message-hint .hint-content span[data-v-9dd82c62]{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.default-message-hint .el-icon[data-v-9dd82c62]{font-size:var(--font-size-md);color:var(--color-info);flex-shrink:0}.default-message-hint .copy-btn[data-v-9dd82c62]{flex-shrink:0;height:auto;padding:0}[data-v-9dd82c62] .merge-dialog .common-dialog__footer .el-button{border-radius:var(--radius-lg);font-weight:500;transition:all .3s}[data-v-9dd82c62] .merge-dialog .common-dialog__footer .el-button:hover{transform:scale(1.02)}[data-v-9dd82c62] .merge-dialog .common-dialog__footer .el-button:active{transform:scale(.98)}[data-v-9dd82c62] .merge-dialog .common-dialog__footer .el-button:focus-visible{outline:2px solid var(--color-primary);outline-offset:2px}.stash-changes-button[data-v-bcdf10c3]{display:inline-block}.stash-dialog-content[data-v-bcdf10c3]{gap:var(--spacing-base);flex-direction:column;display:flex}.stash-info-card[data-v-bcdf10c3]{align-items:flex-start;gap:var(--spacing-base);padding:var(--spacing-base);border-radius:var(--radius-xl);background:linear-gradient(135deg,#f0f9ff 0%,#e0f2fe 100%);border:1px solid #bae6fd;display:flex;position:relative;overflow:hidden}.stash-info-card[data-v-bcdf10c3]:before{content:"";background:linear-gradient(90deg,#0ea5e9 0%,#0284c7 100%);height:3px;position:absolute;top:0;left:0;right:0}.stash-info-card .info-icon[data-v-bcdf10c3]{color:#fff;width:40px;height:40px;font-size:var(--font-size-lg);box-shadow:var(--shadow-md);background:linear-gradient(135deg,#0ea5e9 0%,#0284c7 100%);border-radius:10px;flex-shrink:0;justify-content:center;align-items:center;display:flex}.stash-info-card .info-content[data-v-bcdf10c3]{flex:1}.stash-info-card .info-content h4[data-v-bcdf10c3]{font-size:var(--font-size-md);color:#0c4a6e;margin:0 0 6px;font-weight:600;line-height:1.2}.stash-info-card .info-content p[data-v-bcdf10c3]{color:#075985;margin:0;line-height:1.4}.stash-form[data-v-bcdf10c3]{gap:var(--spacing-base);flex-direction:column;display:flex}.stash-form[data-v-bcdf10c3] .el-form-item{margin-bottom:0}.stash-form[data-v-bcdf10c3] .el-form-item__label{color:var(--color-text-title);margin-bottom:var(--spacing-base);font-weight:600}.stash-form[data-v-bcdf10c3] .el-textarea .el-textarea__inner{border-radius:var(--radius-lg);border:2px solid var(--color-gray-200);line-height:1.5;transition:all .3s}.stash-form[data-v-bcdf10c3] .el-textarea .el-textarea__inner:hover{border-color:var(--color-gray-300)}.stash-form[data-v-bcdf10c3] .el-textarea .el-textarea__inner:focus-visible{border-color:#3b82f6;box-shadow:0 0 0 3px #3b82f61a}.stash-options[data-v-bcdf10c3]{padding:var(--spacing-base);border-radius:var(--radius-xl)}.options-title[data-v-bcdf10c3]{align-items:center;gap:var(--spacing-base);margin:0 0 var(--spacing-base) 0;color:var(--color-text-title);font-weight:600;display:flex}.options-title[data-v-bcdf10c3] .el-icon{color:var(--color-gray-500);font-size:var(--font-size-md)}.option-item[data-v-bcdf10c3]{margin-bottom:var(--spacing-base)}.option-item[data-v-bcdf10c3]:last-child{margin-bottom:0}.option-item[data-v-bcdf10c3] .el-checkbox{margin-bottom:var(--spacing-base)}.option-label[data-v-bcdf10c3]{color:var(--color-text-title);font-weight:500}.option-desc[data-v-bcdf10c3]{font-size:var(--font-size-sm);color:var(--color-gray-500);padding-left:var(--spacing-2xl);margin:0;line-height:1.4}.option-desc.disabled[data-v-bcdf10c3]{color:var(--color-gray-400)}.stash-preview[data-v-bcdf10c3]{padding:var(--spacing-base);border-radius:var(--radius-xl);background:linear-gradient(135deg,#fefce8 0%,#fef3c7 100%);border:1px solid #fbbf24;position:relative}.stash-preview[data-v-bcdf10c3]:before{content:"";background:linear-gradient(90deg, var(--git-status-modified) 0%, #d97706 100%);border-radius:12px 12px 0 0;height:3px;position:absolute;top:0;left:0;right:0}.preview-title[data-v-bcdf10c3]{align-items:center;gap:var(--spacing-base);color:#92400e;font-weight:600;display:flex}.preview-title[data-v-bcdf10c3] .el-icon{color:#d97706;font-size:var(--font-size-md)}.file-count-info[data-v-bcdf10c3]{gap:var(--spacing-base);flex-wrap:wrap;display:flex}.file-count-info[data-v-bcdf10c3] .el-tag{border-radius:var(--radius-md);padding:var(--spacing-sm) var(--spacing-base)}[data-v-bcdf10c3] .stash-dialog .common-dialog__footer .el-button{border-radius:var(--radius-lg);font-weight:500;transition:all .3s}[data-v-bcdf10c3] .stash-dialog .common-dialog__footer .el-button:hover{transform:scale(1.02)}[data-v-bcdf10c3] .stash-dialog .common-dialog__footer .el-button:active{transform:scale(.98)}[data-v-bcdf10c3] .stash-dialog .common-dialog__footer .el-button:focus-visible{outline:2px solid var(--color-primary);outline-offset:2px}.stash-list-button[data-v-392ff82b]{display:inline-block}.stash-list-content[data-v-392ff82b]{min-height:400px;padding:0}[data-v-392ff82b] .stash-list-dialog .common-dialog__header{background:var(--color-primary);color:#fff;padding:var(--spacing-xl) var(--spacing-2xl);border-radius:8px 8px 0 0;margin:0}[data-v-392ff82b] .stash-list-dialog .common-dialog__title{font-size:var(--font-size-lg);color:#fff;font-weight:600}[data-v-392ff82b] .stash-list-dialog .common-dialog__close{color:#fff;font-size:var(--font-size-lg)}[data-v-392ff82b] .stash-list-dialog .common-dialog__close:hover{color:var(--border-card)}.stash-header[data-v-392ff82b]{margin-bottom:var(--spacing-base);padding:var(--spacing-base);background:linear-gradient(135deg, var(--color-white) 0%, #f8f9fa 100%);border-radius:var(--radius-lg);border:1px solid var(--border-component);box-shadow:var(--shadow-md);justify-content:space-between;align-items:center;display:flex}.stash-stats[data-v-392ff82b]{align-items:center;gap:var(--spacing-base);display:flex}.stat-item[data-v-392ff82b]{align-items:center;gap:var(--spacing-base);padding:var(--spacing-xs) var(--spacing-base);background:var(--color-primary);border-radius:var(--radius-lg);color:#fff;display:flex}.stat-icon[data-v-392ff82b]{font-size:var(--font-size-md)}.stat-number[data-v-392ff82b]{font-size:var(--font-size-lg);font-weight:600}.stat-label[data-v-392ff82b]{opacity:.9}.stash-actions-header[data-v-392ff82b]{gap:var(--spacing-base);display:flex}.clear-all-btn[data-v-392ff82b]{border-radius:var(--radius-lg);padding:var(--spacing-xs) var(--spacing-base);font-weight:500;transition:all .3s}.clear-all-btn[data-v-392ff82b]:hover{transform:scale(1.02)}.clear-all-btn[data-v-392ff82b]:active{transform:scale(.98)}.clear-all-btn[data-v-392ff82b]:focus-visible{outline:2px solid var(--color-primary);outline-offset:2px}.stash-list-container[data-v-392ff82b]{min-height:300px}.stash-cards[data-v-392ff82b]{gap:var(--spacing-base);flex-direction:column;display:flex}.stash-card[data-v-392ff82b]{background:var(--bg-container);border-radius:var(--radius-lg);border:1px solid var(--border-component);box-shadow:var(--shadow-md);transition:all .3s;overflow:hidden}.stash-card[data-v-392ff82b]:hover{border-color:var(--color-primary);transform:scale(1.02)}.stash-card[data-v-392ff82b]:active{transform:scale(.98)}.stash-card[data-v-392ff82b]:focus-visible{outline:2px solid var(--color-primary);outline-offset:2px}.stash-card-latest[data-v-392ff82b]{border:2px solid var(--color-success);box-shadow:var(--shadow-md)}.stash-card-latest[data-v-392ff82b]:hover{border-color:var(--color-success);box-shadow:var(--shadow-lg)}.stash-card-content[data-v-392ff82b]{padding:var(--spacing-base);align-items:center;gap:var(--spacing-base);display:flex}.stash-info[data-v-392ff82b]{gap:var(--spacing-sm);flex-direction:column;flex:1;min-width:0;display:flex}.stash-main-info[data-v-392ff82b]{align-items:center;gap:var(--spacing-base);flex-wrap:wrap;display:flex}.stash-id-badge[data-v-392ff82b]{align-items:center;gap:var(--spacing-base);flex-shrink:0;display:flex}.badge-icon[data-v-392ff82b]{color:var(--text-tertiary)}.stash-id-text[data-v-392ff82b]{font-family:Consolas,Monaco,Courier New,monospace;font-size:var(--font-size-sm);background:var(--bg-panel);color:var(--text-secondary);padding:3px var(--spacing-base);border-radius:var(--radius-base);letter-spacing:.5px;border:1px solid var(--border-card);font-weight:500}.latest-tag[data-v-392ff82b]{margin-left:var(--spacing-sm)}.stash-description[data-v-392ff82b]{flex:1;min-width:0}.description-text[data-v-392ff82b]{word-break:break-word;text-overflow:ellipsis;white-space:nowrap;margin:0;font-weight:500;line-height:1.4;overflow:hidden}.stash-card-actions[data-v-392ff82b]{flex-shrink:0;align-items:center;gap:6px;display:none}.stash-card:hover .stash-card-actions[data-v-392ff82b]{display:flex}.action-btn[data-v-392ff82b]{border-radius:var(--radius-md);min-width:60px;padding:6px var(--spacing-md);font-weight:500;font-size:var(--font-size-sm);transition:all .3s}.action-btn[data-v-392ff82b]:hover{transform:scale(1.02)}.action-btn[data-v-392ff82b]:active{transform:scale(.98)}.action-btn[data-v-392ff82b]:focus-visible{outline:2px solid var(--color-primary);outline-offset:2px}.view-btn[data-v-392ff82b]:hover,.apply-btn[data-v-392ff82b]:hover,.apply-pop-btn[data-v-392ff82b]:hover,.delete-btn[data-v-392ff82b]:hover{box-shadow:var(--shadow-md)}.stash-content[data-v-392ff82b]{gap:var(--spacing-base);flex-direction:column;height:100%;display:flex}.stash-info-row[data-v-392ff82b]{align-items:center;gap:var(--spacing-base);padding:var(--spacing-base);border-radius:var(--radius-md);border:1px solid var(--border-component);flex-shrink:0;display:flex}.stash-id[data-v-392ff82b],.stash-description[data-v-392ff82b]{align-items:center;gap:var(--spacing-base);display:flex}.info-label[data-v-392ff82b]{font-size:var(--font-size-sm);color:var(--text-secondary);white-space:nowrap;font-weight:500}.stash-id-value[data-v-392ff82b]{font-family:ui-monospace,SFMono-Regular,SF Mono,Monaco,Inconsolata,Roboto Mono,Droid Sans Mono,Consolas,Courier New,monospace;font-size:var(--font-size-sm);background-color:var(--border-component);color:var(--color-text);padding:var(--spacing-xs) 6px;border-radius:var(--radius-sm);border:1px solid var(--border-component)}.stash-description[data-v-392ff82b]{flex:1}.stash-description-value[data-v-392ff82b]{word-break:break-all;font-weight:400}.stash-main-content[data-v-392ff82b]{flex-direction:column;flex:1;min-height:0;display:flex}[data-v-9a8027f9] .git-reset-icon{vertical-align:middle;width:1em;height:1em;margin-right:4px}