duaer-spec 0.27.2 → 0.27.3

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 (55) hide show
  1. package/.duaer/active-job.json +3 -3
  2. package/.duaer/specs/281-release-0272/delivery.json +8 -0
  3. package/.duaer/specs/281-release-0272/tasks.md +1 -1
  4. package/.duaer/specs/282-dispatch-confirm-lock/delivery.json +12 -0
  5. package/.duaer/specs/282-dispatch-confirm-lock/spec.md +26 -0
  6. package/.duaer/specs/282-dispatch-confirm-lock/tasks.md +5 -0
  7. package/.duaer/specs/283-desk-theme-compact/delivery.json +12 -0
  8. package/.duaer/specs/283-desk-theme-compact/spec.md +26 -0
  9. package/.duaer/specs/283-desk-theme-compact/tasks.md +5 -0
  10. package/.duaer/specs/284-dispatch-graph-regen/delivery.json +12 -0
  11. package/.duaer/specs/284-dispatch-graph-regen/spec.md +33 -0
  12. package/.duaer/specs/284-dispatch-graph-regen/tasks.md +5 -0
  13. package/.duaer/specs/285-desk-light-compact/delivery.json +11 -0
  14. package/.duaer/specs/285-desk-light-compact/spec.md +13 -0
  15. package/.duaer/specs/285-desk-light-compact/tasks.md +5 -0
  16. package/.duaer/specs/286-modules-bug-theme/delivery.json +10 -0
  17. package/.duaer/specs/286-modules-bug-theme/spec.md +26 -0
  18. package/.duaer/specs/286-modules-bug-theme/tasks.md +5 -0
  19. package/.duaer/specs/287-progress-modules-ux/delivery.json +10 -0
  20. package/.duaer/specs/287-progress-modules-ux/spec.md +31 -0
  21. package/.duaer/specs/287-progress-modules-ux/tasks.md +5 -0
  22. package/.duaer/specs/288-settings-close-compact/delivery.json +10 -0
  23. package/.duaer/specs/288-settings-close-compact/spec.md +17 -0
  24. package/.duaer/specs/288-settings-close-compact/tasks.md +5 -0
  25. package/.duaer/specs/289-settings-close-click/delivery.json +10 -0
  26. package/.duaer/specs/289-settings-close-click/spec.md +16 -0
  27. package/.duaer/specs/289-settings-close-click/tasks.md +4 -0
  28. package/.duaer/specs/290-remove-quicknav/delivery.json +10 -0
  29. package/.duaer/specs/290-remove-quicknav/spec.md +11 -0
  30. package/.duaer/specs/290-remove-quicknav/tasks.md +4 -0
  31. package/.duaer/specs/291-release-0273/spec.md +12 -0
  32. package/.duaer/specs/291-release-0273/tasks.md +4 -0
  33. package/CHANGELOG.md +56 -0
  34. package/bin/duaer-live.mjs +11 -20
  35. package/bin/live-modules.mjs +23 -3
  36. package/docs/agent/e2e-test-plan.md +16 -5
  37. package/docs/agent/live-desk.md +24 -11
  38. package/package.json +1 -1
  39. package/web/live-dev/app.js +329 -167
  40. package/web/live-dev/dispatch-center.html +29 -1
  41. package/web/live-dev/dispatch-center.js +25 -1
  42. package/web/live-dev/i18n-de.js +5 -0
  43. package/web/live-dev/i18n-es.js +5 -0
  44. package/web/live-dev/i18n-fr.js +5 -0
  45. package/web/live-dev/i18n-ja.js +5 -0
  46. package/web/live-dev/i18n-ko.js +5 -0
  47. package/web/live-dev/i18n-pt-BR.js +5 -0
  48. package/web/live-dev/i18n-ru.js +5 -0
  49. package/web/live-dev/i18n-vi.js +5 -0
  50. package/web/live-dev/i18n-zh-TW.js +5 -0
  51. package/web/live-dev/i18n.js +10 -0
  52. package/web/live-dev/index.html +31 -32
  53. package/web/live-dev/styles.css +296 -315
  54. package/web/live-dev/task-graph.mjs +4 -1
  55. package/web/live-dev/theme.mjs +63 -0
@@ -10,6 +10,7 @@ import {
10
10
  setLocale,
11
11
  applyDomI18n,
12
12
  } from "./i18n.js";
13
+ import { getTheme, initTheme, toggleTheme } from "./theme.mjs";
13
14
  import { structuredHtml, escapeHtml, reqEditModel, serializeReqEdit } from "./structured-html.mjs";
14
15
  import {
15
16
  deriveProjectDeliveryStatus,
@@ -110,6 +111,8 @@ const state = {
110
111
  dispatchGraphReady: false,
111
112
  dispatchGraphBuilding: false,
112
113
  dispatchGraphUrl: null,
114
+ /** True after at least one successful graph build this desk session (regen copy). */
115
+ dispatchGraphBuiltOnce: false,
113
116
  /** Latest /api/status progress.tasks for dispatch-graph run colors. */
114
117
  lastJobProgress: null,
115
118
  recommendedWorkerCount: 1,
@@ -231,9 +234,6 @@ const el = {
231
234
  settingsClose: document.getElementById("settingsClose"),
232
235
  desk: document.getElementById("desk"),
233
236
  log: document.getElementById("log"),
234
- chatQuickNav: document.getElementById("chatQuickNav"),
235
- chatQuickNavMenu: document.getElementById("chatQuickNavMenu"),
236
- chatQuickNavWrap: document.getElementById("chatQuickNavWrap"),
237
237
  chatEmpty: document.getElementById("chatEmpty"),
238
238
  form: document.getElementById("composer"),
239
239
  input: document.getElementById("input"),
@@ -448,6 +448,8 @@ const el = {
448
448
  cardPanel: document.querySelector(".card-panel"),
449
449
  updateNotice: document.getElementById("updateNotice"),
450
450
  langSelect: document.getElementById("langSelect"),
451
+ themeToggle: document.getElementById("themeToggle"),
452
+ themeToggleLabel: document.getElementById("themeToggleLabel"),
451
453
  historyToggle: document.getElementById("historyToggle"),
452
454
  employeeToggle: document.getElementById("employeeToggle"),
453
455
  employeePanel: document.getElementById("employeePanel"),
@@ -592,69 +594,6 @@ function scrollChatToLatest() {
592
594
  el.log.scrollTop = el.log.scrollHeight;
593
595
  }
594
596
 
595
- function setChatQuickNavOpen(open) {
596
- if (!el.chatQuickNav || !el.chatQuickNavMenu) return;
597
- el.chatQuickNav.setAttribute("aria-expanded", open ? "true" : "false");
598
- el.chatQuickNavMenu.hidden = !open;
599
- }
600
-
601
- function quickNavTarget(kind) {
602
- if (kind === "chat") return el.log;
603
- if (kind === "card") {
604
- if (el.revisePanel && !el.revisePanel.hidden) return el.revisePanel;
605
- return el.confirm || el.cardPanel;
606
- }
607
- if (kind === "result") {
608
- if (el.previewPanel && !el.previewPanel.hidden) return el.previewPanel;
609
- if (el.revisePanel && !el.revisePanel.hidden) return el.revisePanel;
610
- return el.confirm || el.cardPanel;
611
- }
612
- if (kind === "progress") {
613
- return activeProgressFocusEl() || el.progressCol;
614
- }
615
- return null;
616
- }
617
-
618
- function jumpQuickNav(kind) {
619
- setChatQuickNavOpen(false);
620
- if (kind === "chat") {
621
- clearPanelUserScroll(el.log);
622
- scrollChatToLatest();
623
- return;
624
- }
625
- const target = quickNavTarget(kind);
626
- if (!target) return;
627
- if (kind === "progress") {
628
- clearPanelUserScroll(el.progressCol);
629
- scrollPanelToTarget(el.progressCol, target, { smooth: true, force: true });
630
- return;
631
- }
632
- clearPanelUserScroll(el.cardPanel);
633
- scrollPanelToTarget(el.cardPanel, target, { smooth: true, force: true });
634
- }
635
-
636
- function wireChatQuickNav() {
637
- if (!el.chatQuickNav || el.chatQuickNav.dataset.bound === "1") return;
638
- el.chatQuickNav.dataset.bound = "1";
639
- el.chatQuickNav.addEventListener("click", (ev) => {
640
- ev.stopPropagation();
641
- const open = el.chatQuickNav.getAttribute("aria-expanded") !== "true";
642
- setChatQuickNavOpen(open);
643
- });
644
- el.chatQuickNavMenu?.addEventListener("click", (ev) => {
645
- const btn = ev.target?.closest?.("[data-nav]");
646
- if (!btn) return;
647
- ev.preventDefault();
648
- jumpQuickNav(btn.getAttribute("data-nav"));
649
- });
650
- document.addEventListener("click", (ev) => {
651
- if (!el.chatQuickNavWrap?.contains(ev.target)) setChatQuickNavOpen(false);
652
- });
653
- document.addEventListener("keydown", (ev) => {
654
- if (ev.key === "Escape") setChatQuickNavOpen(false);
655
- });
656
- }
657
-
658
597
  function syncChatPlaceholder() {
659
598
  if (!el.input) return;
660
599
  el.input.placeholder =
@@ -888,7 +827,21 @@ function explainChatBlocked() {
888
827
  addBubble("bot", msg);
889
828
  }
890
829
 
830
+ function syncThemeToggleUi() {
831
+ const theme = getTheme();
832
+ if (el.themeToggle) {
833
+ el.themeToggle.dataset.theme = theme;
834
+ el.themeToggle.setAttribute("aria-pressed", theme === "dark" ? "true" : "false");
835
+ el.themeToggle.setAttribute("aria-label", t("theme.aria"));
836
+ }
837
+ if (el.themeToggleLabel) {
838
+ el.themeToggleLabel.textContent =
839
+ theme === "dark" ? t("theme.toLight") : t("theme.toDark");
840
+ }
841
+ }
842
+
891
843
  function syncDynamicI18n() {
844
+ syncThemeToggleUi();
892
845
  for (const p of state.providers) {
893
846
  if (p.id === "custom") p.label = t("provider.custom");
894
847
  }
@@ -1019,7 +972,8 @@ function renderModuleTabs() {
1019
972
  if (!el.moduleTabs) return;
1020
973
  ensureModulesSeed();
1021
974
  const list = state.modules;
1022
- el.moduleTabs.hidden = state.deskKind === "bug" || list.length < 1;
975
+ // Keep tabs visible in bug mode so prior requirement modules stay reachable.
976
+ el.moduleTabs.hidden = list.length < 1;
1023
977
  el.moduleTabs.replaceChildren();
1024
978
  for (const m of list) {
1025
979
  const btn = document.createElement("button");
@@ -1031,20 +985,24 @@ function renderModuleTabs() {
1031
985
  btn.setAttribute("aria-selected", active ? "true" : "false");
1032
986
  if (active) btn.classList.add("is-active");
1033
987
  if (m.status === "confirmed") btn.classList.add("is-confirmed");
988
+ if (m.id === "bug") btn.classList.add("is-bug");
1034
989
  btn.textContent = `${m.title} · ${statusLabel(m.status)}`;
1035
990
  btn.addEventListener("click", () => {
1036
991
  if (m.id === state.activeModuleId) return;
1037
992
  syncActiveModuleCardFromFields();
1038
993
  state.activeModuleId = m.id;
1039
994
  applyActiveModuleToFields();
995
+ applyConfirmCardChrome();
1040
996
  resetValidateGate();
1041
997
  setConfirmFieldsReadonly(
1042
- m.status === "confirmed" || modulesAllConfirmedLocal(),
998
+ m.status === "confirmed" ||
999
+ (modulesAllConfirmedLocal() && m.id !== "bug"),
1043
1000
  );
1044
1001
  renderModuleTabs();
1045
1002
  syncConfirmEnabled();
1046
1003
  if (
1047
1004
  m.status !== "confirmed" &&
1005
+ !(state.deskKind === "bug" && m.id !== "bug") &&
1048
1006
  !modulesAllConfirmedLocal() &&
1049
1007
  state.mode !== "revise"
1050
1008
  ) {
@@ -1082,18 +1040,21 @@ function mergeModulesFromChatPayload(data) {
1082
1040
  externalDeps: String(data.externalDeps || data.modules?.[0]?.card?.externalDeps || activeModule()?.card?.externalDeps || ""),
1083
1041
  perfBudget: String(data.perfBudget || data.modules?.[0]?.card?.perfBudget || activeModule()?.card?.perfBudget || ""),
1084
1042
  };
1085
- const prev = activeModule();
1043
+ const kept = (state.modules || []).filter((m) => m.id !== "bug");
1044
+ const prevBug = (state.modules || []).find((m) => m.id === "bug");
1045
+ const bugConfirmed = prevBug?.status === "confirmed";
1086
1046
  state.modules = [
1047
+ ...kept,
1087
1048
  {
1088
1049
  id: "bug",
1089
1050
  title: t("card.bugModuleTitle"),
1090
- status: prev?.status === "confirmed" ? "confirmed" : "draft",
1091
- card: prev?.status === "confirmed" ? prev.card : card,
1051
+ status: bugConfirmed ? "confirmed" : "draft",
1052
+ card: bugConfirmed ? prevBug.card : card,
1092
1053
  dependsOn: [],
1093
1054
  },
1094
1055
  ];
1095
1056
  state.activeModuleId = "bug";
1096
- if (prev?.status !== "confirmed") applyActiveModuleToFields();
1057
+ if (!bugConfirmed) applyActiveModuleToFields();
1097
1058
  renderModuleTabs();
1098
1059
  return;
1099
1060
  }
@@ -1268,25 +1229,81 @@ function renderTaskPoolList(tasks) {
1268
1229
  }
1269
1230
  }
1270
1231
 
1271
- function syncOpenTaskGraphButton() {
1272
- if (!el.openTaskGraph) return;
1273
- el.openTaskGraph.disabled = !state.dispatchGraphReady;
1232
+ function syncConfirmWorkersGraphButton() {
1233
+ const btn = el.confirmWorkersGraph;
1234
+ if (!btn) return;
1235
+ const busy = Boolean(state.dispatchGraphBuilding);
1236
+ const locked = Boolean(state.dispatchGraphReady);
1237
+ const regen =
1238
+ !locked &&
1239
+ !busy &&
1240
+ Boolean(state.dispatchGraphBuiltOnce);
1241
+ btn.disabled = busy || locked;
1242
+ btn.setAttribute("aria-busy", busy ? "true" : "false");
1243
+ btn.classList.toggle("is-busy", busy);
1244
+ btn.classList.toggle("is-locked", locked && !busy);
1245
+ btn.classList.toggle("is-regen", regen);
1246
+ if (busy) {
1247
+ btn.textContent = t("dispatch.graphBuilding");
1248
+ } else if (locked) {
1249
+ btn.textContent = t("dispatch.confirmWorkersLocked");
1250
+ } else if (regen) {
1251
+ btn.textContent = t("dispatch.regenWorkersGraph");
1252
+ } else {
1253
+ btn.textContent = t("dispatch.confirmWorkersGraph");
1254
+ }
1274
1255
  }
1275
1256
 
1276
- function markDispatchGraphStale() {
1277
- if (!state.dispatchGraphReady) {
1278
- syncOpenTaskGraphButton();
1279
- return;
1257
+ function syncOpenTaskGraphButton() {
1258
+ if (el.openTaskGraph) {
1259
+ el.openTaskGraph.disabled = !state.dispatchGraphReady;
1280
1260
  }
1261
+ syncConfirmWorkersGraphButton();
1262
+ }
1263
+
1264
+ /**
1265
+ * Drop the current 派工图 so the operator must confirm+build again.
1266
+ * @param {{ silent?: boolean, hidePanel?: boolean }} [opts]
1267
+ */
1268
+ function clearDispatchGraphState(opts = {}) {
1269
+ const silent = Boolean(opts.silent);
1270
+ const hidePanel = Boolean(opts.hidePanel);
1281
1271
  state.dispatchGraphReady = false;
1272
+ state.dispatchGraphBuilding = false;
1273
+ state.dispatchGraphUrl = null;
1274
+ if (hidePanel && el.dispatch) el.dispatch.hidden = true;
1282
1275
  syncOpenTaskGraphButton();
1283
- if (el.dispatchErr) {
1276
+ renderWorkerCountList();
1277
+ if (!silent && el.dispatchErr && state.dispatchGraphBuiltOnce) {
1284
1278
  el.dispatchErr.hidden = false;
1285
1279
  el.dispatchErr.textContent = t("dispatch.graphStale");
1286
1280
  }
1287
1281
  schedulePersistProjectDesk();
1288
1282
  }
1289
1283
 
1284
+ /**
1285
+ * Start a new kickoff wave: fresh task pool + new 派工图 required.
1286
+ * @param {{ hidePanel?: boolean, silent?: boolean }} [opts]
1287
+ */
1288
+ function resetDispatchGraphForNewWave(opts = {}) {
1289
+ state.taskPool = null;
1290
+ if (el.taskPoolList) el.taskPoolList.replaceChildren();
1291
+ if (el.taskPoolEmpty) el.taskPoolEmpty.hidden = false;
1292
+ clearDispatchGraphState({
1293
+ silent: opts.silent !== false,
1294
+ hidePanel: Boolean(opts.hidePanel),
1295
+ });
1296
+ }
1297
+
1298
+ function markDispatchGraphStale() {
1299
+ if (!state.dispatchGraphReady && !state.dispatchGraphUrl) {
1300
+ syncOpenTaskGraphButton();
1301
+ renderWorkerCountList();
1302
+ return;
1303
+ }
1304
+ clearDispatchGraphState({ silent: false, hidePanel: false });
1305
+ }
1306
+
1290
1307
  function applyRecommendedWorkerCount(tasks) {
1291
1308
  const rec = recommendWorkerCount(tasks, { max: 4 });
1292
1309
  state.recommendedWorkerCount = rec;
@@ -1304,12 +1321,15 @@ function renderWorkerCountList() {
1304
1321
  el.workerCountList.replaceChildren();
1305
1322
  const cur = Math.max(1, Math.min(4, Number(state.workerCount) || 1));
1306
1323
  state.workerCount = cur;
1324
+ const chipsLocked =
1325
+ Boolean(state.dispatchGraphReady) || Boolean(state.dispatchGraphBuilding);
1307
1326
  for (let n = 1; n <= 4; n += 1) {
1308
1327
  const b = document.createElement("button");
1309
1328
  b.type = "button";
1310
1329
  b.className = "worker-count-chip";
1311
1330
  b.setAttribute("aria-pressed", n === cur ? "true" : "false");
1312
1331
  b.setAttribute("aria-label", String(n));
1332
+ b.disabled = chipsLocked;
1313
1333
  const num = document.createElement("span");
1314
1334
  num.textContent = String(n);
1315
1335
  b.appendChild(num);
@@ -1323,6 +1343,7 @@ function renderWorkerCountList() {
1323
1343
  sub.textContent = label;
1324
1344
  b.appendChild(sub);
1325
1345
  b.addEventListener("click", () => {
1346
+ if (chipsLocked) return;
1326
1347
  if (state.workerCount !== n) {
1327
1348
  state.workerCount = n;
1328
1349
  markDispatchGraphStale();
@@ -1333,9 +1354,19 @@ function renderWorkerCountList() {
1333
1354
  }
1334
1355
  }
1335
1356
 
1336
- function decomposeTasksFromModules() {
1357
+ function confirmedModulesForPool() {
1337
1358
  ensureModulesSeed();
1338
1359
  const confirmed = state.modules.filter((m) => m.status === "confirmed");
1360
+ if (state.deskKind === "bug") {
1361
+ const bug = confirmed.filter((m) => m.id === "bug");
1362
+ return bug.length ? bug : confirmed.slice(0, 1);
1363
+ }
1364
+ return confirmed.filter((m) => m.id !== "bug");
1365
+ }
1366
+
1367
+ function decomposeTasksFromModules() {
1368
+ ensureModulesSeed();
1369
+ const confirmed = confirmedModulesForPool();
1339
1370
  if (!confirmed.length) {
1340
1371
  state.taskPool = null;
1341
1372
  renderTaskPoolList([]);
@@ -1350,9 +1381,7 @@ function decomposeTasksFromModules() {
1350
1381
  applyRecommendedWorkerCount(annotated);
1351
1382
  renderWorkerCountList();
1352
1383
  renderTaskPoolList(annotated);
1353
- state.dispatchGraphReady = false;
1354
- syncOpenTaskGraphButton();
1355
- schedulePersistProjectDesk();
1384
+ clearDispatchGraphState({ silent: true, hidePanel: false });
1356
1385
  return state.taskPool;
1357
1386
  }
1358
1387
 
@@ -1363,7 +1392,7 @@ function syncTaskPoolPreview() {
1363
1392
  async function syncTaskPoolPreviewAsync() {
1364
1393
  if (!el.taskPoolPreview) return;
1365
1394
  ensureModulesSeed();
1366
- const confirmed = state.modules.filter((m) => m.status === "confirmed");
1395
+ const confirmed = confirmedModulesForPool();
1367
1396
  if (!confirmed.length || !state.locked || !state.architecture?.confirmed) {
1368
1397
  if (el.taskPoolEmpty) el.taskPoolEmpty.hidden = false;
1369
1398
  renderTaskPoolList([]);
@@ -1378,20 +1407,14 @@ async function syncTaskPoolPreviewAsync() {
1378
1407
  }
1379
1408
 
1380
1409
  function setConfirmWorkersGraphBusy(busy) {
1381
- const btn = el.confirmWorkersGraph;
1382
- if (!btn) return;
1383
- btn.disabled = Boolean(busy);
1384
- btn.setAttribute("aria-busy", busy ? "true" : "false");
1385
- btn.classList.toggle("is-busy", Boolean(busy));
1386
- if (busy) {
1387
- btn.textContent = t("dispatch.graphBuilding");
1388
- } else {
1389
- btn.textContent = t("dispatch.confirmWorkersGraph");
1390
- }
1410
+ state.dispatchGraphBuilding = Boolean(busy);
1411
+ syncConfirmWorkersGraphButton();
1412
+ renderWorkerCountList();
1391
1413
  }
1392
1414
 
1393
1415
  async function confirmWorkersAndBuildGraph() {
1394
1416
  if (state.dispatchGraphBuilding) return;
1417
+ if (state.dispatchGraphReady) return;
1395
1418
  ensureModulesSeed();
1396
1419
  if (!state.architecture?.confirmed) {
1397
1420
  if (el.dispatchErr) {
@@ -1439,13 +1462,16 @@ async function confirmWorkersAndBuildGraph() {
1439
1462
  }
1440
1463
  state.dispatchGraphUrl = String(data.url || "").trim() || null;
1441
1464
  state.dispatchGraphReady = true;
1465
+ state.dispatchGraphBuiltOnce = true;
1442
1466
  syncOpenTaskGraphButton();
1467
+ renderWorkerCountList();
1443
1468
  schedulePersistProjectDesk();
1444
1469
  addBubble("bot", t("dispatch.graphBuilt", { n: String(workerCount) }));
1445
1470
  } catch (err) {
1446
1471
  state.dispatchGraphReady = false;
1447
1472
  state.dispatchGraphUrl = null;
1448
1473
  syncOpenTaskGraphButton();
1474
+ renderWorkerCountList();
1449
1475
  const msg =
1450
1476
  err instanceof Error ? err.message : t("dispatch.graphBuildFail");
1451
1477
  if (el.dispatchErr) {
@@ -1508,12 +1534,7 @@ async function openDispatchGraphPresent() {
1508
1534
  }
1509
1535
 
1510
1536
  function invalidateDispatchAfterArchChange() {
1511
- state.dispatchGraphReady = false;
1512
- state.dispatchGraphBuilding = false;
1513
- state.dispatchGraphUrl = null;
1514
- setConfirmWorkersGraphBusy(false);
1515
- syncOpenTaskGraphButton();
1516
- if (el.dispatch) el.dispatch.hidden = true;
1537
+ clearDispatchGraphState({ silent: true, hidePanel: true });
1517
1538
  }
1518
1539
 
1519
1540
  function openDispatchCenterPage(projectPath) {
@@ -2048,9 +2069,9 @@ function restoreConfirmCardFromOriginal() {
2048
2069
  syncReqSections();
2049
2070
  }
2050
2071
 
2051
- /** Top confirm card chrome never becomes 改进卡. */
2072
+ /** Top confirm card chrome never becomes 改进卡. Labels follow the active tab. */
2052
2073
  function applyConfirmCardChrome() {
2053
- const bug = state.deskKind === "bug";
2074
+ const bug = activeModule()?.id === "bug";
2054
2075
  if (el.cardMark) el.cardMark.textContent = t(bug ? "card.bugMark" : "card.mark");
2055
2076
  if (el.cardTitle) el.cardTitle.textContent = t(bug ? "card.bugTitle" : "card.title");
2056
2077
  if (el.lblGoal) el.lblGoal.textContent = t(bug ? "card.bugGoal" : "card.goal");
@@ -2122,6 +2143,52 @@ function syncStartBugFixButtons() {
2122
2143
  }
2123
2144
  }
2124
2145
 
2146
+ function ensureBugModuleOnDesk() {
2147
+ let kept = (state.modules || []).filter((m) => m && m.id !== "bug");
2148
+ // Recover a readonly requirement tab when an older wipe left only `bug`.
2149
+ if (
2150
+ !kept.length &&
2151
+ state.originalCard &&
2152
+ (state.originalCard.goal || state.originalCard.acceptance)
2153
+ ) {
2154
+ kept = [
2155
+ {
2156
+ id: "main",
2157
+ title: "Main",
2158
+ status: "confirmed",
2159
+ card: {
2160
+ goal: String(state.originalCard.goal || ""),
2161
+ outOfScope: String(state.originalCard.outOfScope || ""),
2162
+ acceptance: String(state.originalCard.acceptance || ""),
2163
+ assumptions: String(state.originalCard.assumptions || ""),
2164
+ deviceMatrix: String(state.originalCard.deviceMatrix || ""),
2165
+ criticalPaths: String(state.originalCard.criticalPaths || ""),
2166
+ exceptionCases: String(state.originalCard.exceptionCases || ""),
2167
+ apiContract: String(state.originalCard.apiContract || ""),
2168
+ envChecklist: String(state.originalCard.envChecklist || ""),
2169
+ dataPrecheck: String(state.originalCard.dataPrecheck || ""),
2170
+ externalDeps: String(state.originalCard.externalDeps || ""),
2171
+ perfBudget: String(state.originalCard.perfBudget || ""),
2172
+ },
2173
+ dependsOn: [],
2174
+ },
2175
+ ];
2176
+ }
2177
+ let bug = (state.modules || []).find((m) => m?.id === "bug");
2178
+ if (!bug) {
2179
+ bug = {
2180
+ id: "bug",
2181
+ title: t("card.bugModuleTitle"),
2182
+ status: "draft",
2183
+ card: { goal: "", outOfScope: "", acceptance: "", assumptions: "" },
2184
+ dependsOn: [],
2185
+ };
2186
+ }
2187
+ state.modules = [...kept, bug];
2188
+ state.activeModuleId = "bug";
2189
+ return bug;
2190
+ }
2191
+
2125
2192
  function beginBugFixFromCta() {
2126
2193
  if (state.busy || state.reviseDispatching) return;
2127
2194
  if (!state.projectPath) {
@@ -2131,13 +2198,16 @@ function beginBugFixFromCta() {
2131
2198
  const fromDelivery = Boolean(state.locked || state.lastDeliveryAccepted);
2132
2199
  if (fromDelivery) {
2133
2200
  // Fresh defect card on this project (same spirit as「再改一版」).
2201
+ // Keep prior requirement modules — append a bug tab instead of wiping them.
2134
2202
  state.locked = false;
2135
2203
  state.lastDeliveryAccepted = false;
2136
2204
  state.jobId = null;
2137
2205
  state.dispatchPhase = null;
2138
2206
  state.taskPool = null;
2139
2207
  state.dispatchGraphReady = false;
2208
+ state.dispatchGraphBuilding = false;
2140
2209
  state.dispatchGraphUrl = null;
2210
+ state.dispatchGraphBuiltOnce = false;
2141
2211
  state.reviseLocked = false;
2142
2212
  state.revisePlanConfirmed = false;
2143
2213
  state.architecture = {
@@ -2154,16 +2224,14 @@ function beginBugFixFromCta() {
2154
2224
  if (el.dispatch) el.dispatch.hidden = true;
2155
2225
  setConfirmFieldsReadonly(false);
2156
2226
  state.deskKind = "bug";
2157
- state.modules = [
2158
- {
2159
- id: "bug",
2160
- title: t("card.bugModuleTitle"),
2161
- status: "draft",
2162
- card: { goal: "", outOfScope: "", acceptance: "", assumptions: "" },
2163
- dependsOn: [],
2164
- },
2165
- ];
2166
- state.activeModuleId = "bug";
2227
+ ensureModulesSeed();
2228
+ ensureBugModuleOnDesk();
2229
+ const bug = state.modules.find((m) => m.id === "bug");
2230
+ if (bug) {
2231
+ bug.status = "draft";
2232
+ bug.card = { goal: "", outOfScope: "", acceptance: "", assumptions: "" };
2233
+ bug.title = t("card.bugModuleTitle");
2234
+ }
2167
2235
  applyActiveModuleToFields();
2168
2236
  applyConfirmCardChrome();
2169
2237
  renderModuleTabs();
@@ -2199,45 +2267,47 @@ function isFeatureIntentText(text) {
2199
2267
  function enterDeskKind(kind) {
2200
2268
  const next = kind === "bug" ? "bug" : "feature";
2201
2269
  if (state.deskKind === next && next === "bug" && !state.locked) {
2270
+ ensureModulesSeed();
2271
+ ensureBugModuleOnDesk();
2272
+ applyActiveModuleToFields();
2273
+ applyConfirmCardChrome();
2274
+ renderModuleTabs();
2202
2275
  syncStartBugFixButtons();
2203
2276
  return;
2204
2277
  }
2205
2278
  if (state.locked && next !== "bug") return;
2206
2279
  state.deskKind = next;
2207
2280
  if (next === "bug") {
2208
- const card = activeModule()?.card || {
2209
- goal: "",
2210
- outOfScope: "",
2211
- acceptance: "",
2212
- assumptions: "",
2213
- };
2214
- state.modules = [
2215
- {
2216
- id: "bug",
2217
- title: t("card.bugModuleTitle"),
2218
- status: "draft",
2219
- card: state.locked
2220
- ? { goal: "", outOfScope: "", acceptance: "", assumptions: "" }
2221
- : card,
2222
- dependsOn: [],
2223
- },
2224
- ];
2225
- state.activeModuleId = "bug";
2281
+ ensureModulesSeed();
2282
+ ensureBugModuleOnDesk();
2283
+ const bug = state.modules.find((m) => m.id === "bug");
2284
+ if (bug && bug.status !== "confirmed") {
2285
+ if (state.locked) {
2286
+ bug.card = { goal: "", outOfScope: "", acceptance: "", assumptions: "" };
2287
+ }
2288
+ bug.title = t("card.bugModuleTitle");
2289
+ }
2226
2290
  applyActiveModuleToFields();
2227
- } else if (
2228
- state.modules.length === 1 &&
2229
- state.modules[0]?.id === "bug"
2230
- ) {
2231
- state.modules = [
2232
- {
2233
- id: "main",
2234
- title: "Main",
2235
- status: "draft",
2236
- card: { ...state.modules[0].card },
2237
- dependsOn: [],
2238
- },
2239
- ];
2240
- state.activeModuleId = "main";
2291
+ } else {
2292
+ // Leave bug mode: keep requirement modules; drop the defect tab.
2293
+ const kept = (state.modules || []).filter((m) => m && m.id !== "bug");
2294
+ if (kept.length) {
2295
+ state.modules = kept;
2296
+ if (!kept.some((m) => m.id === state.activeModuleId)) {
2297
+ state.activeModuleId = kept[0].id;
2298
+ }
2299
+ } else {
2300
+ state.modules = [
2301
+ {
2302
+ id: "main",
2303
+ title: "Main",
2304
+ status: "draft",
2305
+ card: { goal: "", outOfScope: "", acceptance: "", assumptions: "" },
2306
+ dependsOn: [],
2307
+ },
2308
+ ];
2309
+ state.activeModuleId = "main";
2310
+ }
2241
2311
  applyActiveModuleToFields();
2242
2312
  }
2243
2313
  applyConfirmCardChrome();
@@ -2986,6 +3056,7 @@ async function persistProjectChat() {
2986
3056
  bugHotfix: Boolean(state.bugHotfix),
2987
3057
  dispatchGraphReady: Boolean(state.dispatchGraphReady),
2988
3058
  dispatchGraphUrl: state.dispatchGraphUrl || null,
3059
+ dispatchGraphBuiltOnce: Boolean(state.dispatchGraphBuiltOnce),
2989
3060
  reviseCard: reviseCardValues(),
2990
3061
  reviseCards: cardsLite,
2991
3062
  bugCards: bugCardsLite,
@@ -3127,6 +3198,7 @@ function clearDeskWorkspace() {
3127
3198
  state.dispatchGraphReady = false;
3128
3199
  state.dispatchGraphBuilding = false;
3129
3200
  state.dispatchGraphUrl = null;
3201
+ state.dispatchGraphBuiltOnce = false;
3130
3202
  state.recommendedWorkerCount = 1;
3131
3203
  state.mode = "specify";
3132
3204
  if (el.moduleTabs) {
@@ -3377,8 +3449,20 @@ async function loadProjectChatIntoUi(projectPath) {
3377
3449
  state.workerCount = Number(data.workerCount) > 0 ? Number(data.workerCount) : 1;
3378
3450
  state.deskKind = data.deskKind === "bug" ? "bug" : "feature";
3379
3451
  state.bugHotfix = Boolean(data.bugHotfix);
3452
+ if (state.deskKind === "bug") {
3453
+ ensureBugModuleOnDesk();
3454
+ if (data.activeModuleId) {
3455
+ const want = String(data.activeModuleId);
3456
+ if (state.modules.some((m) => m.id === want)) {
3457
+ state.activeModuleId = want;
3458
+ }
3459
+ }
3460
+ }
3380
3461
  state.dispatchGraphReady = Boolean(data.dispatchGraphReady);
3381
3462
  state.dispatchGraphUrl = String(data.dispatchGraphUrl || "").trim() || null;
3463
+ state.dispatchGraphBuiltOnce = Boolean(
3464
+ data.dispatchGraphBuiltOnce || data.dispatchGraphReady || data.dispatchGraphUrl,
3465
+ );
3382
3466
  if (state.taskPool?.tasks?.length) {
3383
3467
  state.recommendedWorkerCount = recommendWorkerCount(state.taskPool.tasks, {
3384
3468
  max: 4,
@@ -3782,15 +3866,25 @@ function setSettingsOpen(open) {
3782
3866
  }
3783
3867
  }
3784
3868
  el.settingsPanel.hidden = !want;
3869
+ el.settingsPanel.setAttribute("aria-hidden", want ? "false" : "true");
3785
3870
  if (el.cfgOpen) {
3786
3871
  el.cfgOpen.setAttribute("aria-expanded", want ? "true" : "false");
3787
3872
  }
3788
3873
  if (el.settingsClose) {
3789
3874
  el.settingsClose.hidden = false;
3875
+ el.settingsClose.disabled = false;
3790
3876
  }
3791
3877
  syncDrawerBackdrop();
3792
3878
  }
3793
3879
 
3880
+ function closeSettingsDrawer(ev) {
3881
+ if (ev) {
3882
+ ev.preventDefault();
3883
+ ev.stopPropagation();
3884
+ }
3885
+ setSettingsOpen(false);
3886
+ }
3887
+
3794
3888
  function fillAliyunFields(cfg) {
3795
3889
  if (el.cfgAliyunId) {
3796
3890
  el.cfgAliyunId.value = "";
@@ -3882,7 +3976,10 @@ function showSetup(cfg) {
3882
3976
  applyProvider(id, { fillEmptyOnly: Boolean(cfg?.baseUrl || cfg?.model) });
3883
3977
  // Always show the desk; open Settings so the operator can fill the model.
3884
3978
  if (el.desk) el.desk.hidden = false;
3885
- state.ready = false;
3979
+ // Re-open from the gear must not clear an already-ready session.
3980
+ if (!cfg?.ready) {
3981
+ state.ready = false;
3982
+ }
3886
3983
  syncComposerEnabled();
3887
3984
  setSettingsOpen(true);
3888
3985
  if (el.meta) {
@@ -4376,21 +4473,30 @@ el.clearAwsCfg?.addEventListener("click", () => {
4376
4473
  });
4377
4474
 
4378
4475
  el.cfgOpen?.addEventListener("click", () => {
4379
- const open = el.settingsPanel?.hidden !== false;
4380
- if (open) {
4476
+ if (el.settingsPanel?.hidden) {
4381
4477
  showSetup({
4382
4478
  ...(state.lastCfg || {}),
4383
4479
  providers: state.providers,
4384
4480
  provider: state.providerId || state.lastCfg?.provider,
4385
4481
  });
4386
- } else if (state.ready) {
4387
- setSettingsOpen(false);
4482
+ } else {
4483
+ closeSettingsDrawer();
4388
4484
  }
4389
4485
  });
4390
4486
 
4391
- el.settingsClose?.addEventListener("click", () => {
4392
- if (state.ready) setSettingsOpen(false);
4393
- });
4487
+ el.settingsClose?.addEventListener("click", closeSettingsDrawer);
4488
+ // Capture-phase backup: sticky head / overlays must not swallow 关闭.
4489
+ el.settingsPanel?.addEventListener(
4490
+ "click",
4491
+ (ev) => {
4492
+ const t = ev.target;
4493
+ if (!(t instanceof Element)) return;
4494
+ if (t.id === "settingsClose" || t.closest?.("#settingsClose")) {
4495
+ closeSettingsDrawer(ev);
4496
+ }
4497
+ },
4498
+ true,
4499
+ );
4394
4500
 
4395
4501
  el.form.addEventListener("submit", (e) => {
4396
4502
  e.preventDefault();
@@ -4875,7 +4981,7 @@ async function autoHandleFromGate(kind, btn) {
4875
4981
 
4876
4982
  async function applyConfirmSuccess(data) {
4877
4983
  if (Array.isArray(data.modules) && data.modules.length) {
4878
- state.modules = data.modules.map((m) => ({
4984
+ const mapped = data.modules.map((m) => ({
4879
4985
  id: String(m.id),
4880
4986
  title: String(m.title || m.id),
4881
4987
  status: m.status === "confirmed" ? "confirmed" : String(m.status || "draft"),
@@ -4895,6 +5001,29 @@ async function applyConfirmSuccess(data) {
4895
5001
  },
4896
5002
  dependsOn: Array.isArray(m.dependsOn) ? m.dependsOn.map(String) : [],
4897
5003
  }));
5004
+ const bugConfirm =
5005
+ state.deskKind === "bug" ||
5006
+ data.deskKind === "bug" ||
5007
+ mapped.some((m) => m.id === "bug");
5008
+ if (bugConfirm) {
5009
+ // Never let a bug-only server payload wipe prior requirement modules.
5010
+ const kept = (state.modules || []).filter((m) => m && m.id !== "bug");
5011
+ const byId = new Map(kept.map((m) => [m.id, m]));
5012
+ for (const m of mapped) {
5013
+ if (m.id === "bug") continue;
5014
+ byId.set(m.id, m);
5015
+ }
5016
+ const bug = mapped.find((m) => m.id === "bug") || mapped[0];
5017
+ state.modules = bug
5018
+ ? [...byId.values()].filter((m) => m.id !== "bug").concat([
5019
+ bug.id === "bug"
5020
+ ? bug
5021
+ : { ...bug, id: "bug", title: bug.title || t("card.bugModuleTitle") },
5022
+ ])
5023
+ : [...byId.values()];
5024
+ } else {
5025
+ state.modules = mapped;
5026
+ }
4898
5027
  }
4899
5028
  if (data.activeModuleId || data.moduleId) {
4900
5029
  state.activeModuleId = String(data.activeModuleId || data.moduleId);
@@ -4920,7 +5049,11 @@ async function applyConfirmSuccess(data) {
4920
5049
  }
4921
5050
  applyActiveModuleToFields();
4922
5051
  }
4923
- const allDone = Boolean(data.modulesAllConfirmed) || modulesAllConfirmedLocal();
5052
+ const allDone =
5053
+ Boolean(data.modulesAllConfirmed) ||
5054
+ (state.deskKind === "bug"
5055
+ ? Boolean(state.modules?.some((m) => m.id === "bug" && m.status === "confirmed"))
5056
+ : modulesAllConfirmedLocal());
4924
5057
  state.locked = allDone;
4925
5058
  // Brief is written at kickoff — do not set jobId from module confirm.
4926
5059
  if (data.jobId) state.jobId = data.jobId;
@@ -5709,7 +5842,8 @@ function confirmArchitecture() {
5709
5842
  schedulePersistProjectDesk();
5710
5843
  addBubble("bot", t("arch.hintConfirmed"));
5711
5844
  // Decompose → recommend workers → confirm graph, then launch (incl. revise).
5712
- void showDispatchPanel();
5845
+ // Always start a new 派工图 wave after architecture confirm.
5846
+ void showDispatchPanel({ forceNewWave: true });
5713
5847
  }
5714
5848
 
5715
5849
  async function autoFixAccept(btn, issues, kind = "confirm") {
@@ -5863,7 +5997,8 @@ async function tryRecoverDispatchDone() {
5863
5997
  }
5864
5998
  }
5865
5999
 
5866
- async function showDispatchPanel() {
6000
+ async function showDispatchPanel(opts = {}) {
6001
+ const forceNewWave = Boolean(opts.forceNewWave);
5867
6002
  el.dispatch.hidden = false;
5868
6003
  el.dispatchErr.hidden = true;
5869
6004
  el.dispatchStatus.hidden = true;
@@ -5872,7 +6007,9 @@ async function showDispatchPanel() {
5872
6007
  if (state.projectPath && el.repoPath) {
5873
6008
  el.repoPath.value = state.projectPath;
5874
6009
  }
5875
- if (!state.taskPool?.tasks?.length) {
6010
+ // Each architecture-confirm wave needs a fresh pool + new 派工图.
6011
+ if (forceNewWave || !state.taskPool?.tasks?.length) {
6012
+ resetDispatchGraphForNewWave({ silent: true, hidePanel: false });
5876
6013
  decomposeTasksFromModules();
5877
6014
  } else {
5878
6015
  applyRecommendedWorkerCount(state.taskPool.tasks);
@@ -6190,7 +6327,7 @@ function openBaselineChange() {
6190
6327
  confirmed: false,
6191
6328
  status: state.architecture.url ? "preview" : "idle",
6192
6329
  };
6193
- state.dispatchGraphReady = false;
6330
+ clearDispatchGraphState({ silent: true, hidePanel: false });
6194
6331
  if (el.baselineChangeReason) el.baselineChangeReason.value = "";
6195
6332
  if (el.baselineSigner) el.baselineSigner.value = "";
6196
6333
  applyActiveModuleToFields();
@@ -7878,6 +8015,8 @@ function enterReviseMode() {
7878
8015
  state.reviseLocked = false;
7879
8016
  state.reviseDispatching = false;
7880
8017
  state.revisePlanConfirmed = false;
8018
+ // New improve wave — prior 派工图 must not stay locked/ready.
8019
+ resetDispatchGraphForNewWave({ silent: true, hidePanel: true });
7881
8020
  // Keep prior revise dialogue history (do not wipe reviseMessages).
7882
8021
  // Advance to the next iteration card — never overwrite prior reviseCards.
7883
8022
  const nextRev = nextReviseRevisionNumber();
@@ -8271,7 +8410,7 @@ async function confirmReviseAndDispatch() {
8271
8410
  el.reviseErr.hidden = false;
8272
8411
  el.reviseErr.textContent = t("dispatch.needGraphConfirm");
8273
8412
  }
8274
- void showDispatchPanel();
8413
+ void showDispatchPanel({ forceNewWave: !state.taskPool?.tasks?.length });
8275
8414
  return;
8276
8415
  }
8277
8416
  await dispatchReviseAgent();
@@ -8603,6 +8742,18 @@ document.addEventListener("keydown", (ev) => {
8603
8742
  if (ev.key !== "Escape") return;
8604
8743
  if (el.deployPicker && !el.deployPicker.hidden) {
8605
8744
  closeDeployPicker();
8745
+ return;
8746
+ }
8747
+ if (el.settingsPanel && !el.settingsPanel.hidden) {
8748
+ closeSettingsDrawer();
8749
+ return;
8750
+ }
8751
+ if (el.employeePanel && !el.employeePanel.hidden) {
8752
+ setEmployeeOpen(false);
8753
+ return;
8754
+ }
8755
+ if (el.historyPanel && !el.historyPanel.hidden) {
8756
+ setHistoryOpen(false);
8606
8757
  }
8607
8758
  });
8608
8759
 
@@ -8717,10 +8868,6 @@ function setHistoryOpen(open) {
8717
8868
  if (!el.historyPanel) return;
8718
8869
  const want = Boolean(open);
8719
8870
  if (want && el.settingsPanel && !el.settingsPanel.hidden) {
8720
- if (!state.ready) {
8721
- // Cannot open projects over mandatory first-time settings.
8722
- return;
8723
- }
8724
8871
  el.settingsPanel.hidden = true;
8725
8872
  if (el.cfgOpen) el.cfgOpen.setAttribute("aria-expanded", "false");
8726
8873
  }
@@ -9121,8 +9268,15 @@ if (el.openTaskGraph) {
9121
9268
  }
9122
9269
  if (el.redecomposeTasks) {
9123
9270
  el.redecomposeTasks.addEventListener("click", () => {
9271
+ const hadGraph =
9272
+ state.dispatchGraphReady ||
9273
+ Boolean(state.dispatchGraphUrl) ||
9274
+ state.dispatchGraphBuiltOnce;
9124
9275
  decomposeTasksFromModules();
9125
- markDispatchGraphStale();
9276
+ if (hadGraph && el.dispatchErr) {
9277
+ el.dispatchErr.hidden = false;
9278
+ el.dispatchErr.textContent = t("dispatch.graphStale");
9279
+ }
9126
9280
  });
9127
9281
  }
9128
9282
  if (el.confirmWorkersGraph) {
@@ -9154,7 +9308,7 @@ if (el.historyClose) {
9154
9308
  if (el.historyBackdrop) {
9155
9309
  el.historyBackdrop.addEventListener("click", () => {
9156
9310
  if (el.settingsPanel && !el.settingsPanel.hidden) {
9157
- if (state.ready) setSettingsOpen(false);
9311
+ closeSettingsDrawer();
9158
9312
  return;
9159
9313
  }
9160
9314
  if (el.employeePanel && !el.employeePanel.hidden) {
@@ -9239,6 +9393,14 @@ onLocaleChange(() => {
9239
9393
  });
9240
9394
 
9241
9395
  const initialLocale = initI18n();
9396
+ initTheme();
9397
+ syncThemeToggleUi();
9398
+ if (el.themeToggle) {
9399
+ el.themeToggle.addEventListener("click", () => {
9400
+ toggleTheme();
9401
+ syncThemeToggleUi();
9402
+ });
9403
+ }
9242
9404
  if (el.langSelect) {
9243
9405
  el.langSelect.value = initialLocale;
9244
9406
  el.langSelect.addEventListener("change", () => {
@@ -9246,13 +9408,13 @@ if (el.langSelect) {
9246
9408
  });
9247
9409
  }
9248
9410
  applyDomI18n();
9411
+ syncThemeToggleUi();
9249
9412
  syncChatPlaceholder();
9250
9413
  wireReqSections();
9251
9414
  syncComposerEnabled();
9252
9415
  renderWorkerCountList();
9253
9416
  wirePanelScrollHold(el.cardPanel);
9254
9417
  wirePanelScrollHold(el.progressCol);
9255
- wireChatQuickNav();
9256
9418
 
9257
9419
  if (el.autoFixCard) {
9258
9420
  el.autoFixCard.addEventListener("click", () => {