gsd-pi 2.66.1-dev.0df32ec → 2.66.1-dev.3cea7ac

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 (230) hide show
  1. package/dist/resources/extensions/ask-user-questions.js +79 -11
  2. package/dist/resources/extensions/claude-code-cli/partial-builder.js +4 -3
  3. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +10 -3
  4. package/dist/resources/extensions/gsd/auto/loop.js +13 -1
  5. package/dist/resources/extensions/gsd/auto/phases.js +10 -4
  6. package/dist/resources/extensions/gsd/auto/run-unit.js +10 -2
  7. package/dist/resources/extensions/gsd/auto/session.js +1 -1
  8. package/dist/resources/extensions/gsd/auto-dashboard.js +65 -15
  9. package/dist/resources/extensions/gsd/auto-dispatch.js +30 -28
  10. package/dist/resources/extensions/gsd/auto-prompts.js +6 -6
  11. package/dist/resources/extensions/gsd/auto-recovery.js +11 -12
  12. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +18 -6
  13. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +59 -5
  14. package/dist/resources/extensions/gsd/bootstrap/register-shortcuts.js +8 -5
  15. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +186 -14
  16. package/dist/resources/extensions/gsd/codebase-generator.js +4 -0
  17. package/dist/resources/extensions/gsd/commands/handlers/core.js +3 -3
  18. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +10 -4
  19. package/dist/resources/extensions/gsd/custom-workflow-engine.js +3 -1
  20. package/dist/resources/extensions/gsd/detection.js +6 -0
  21. package/dist/resources/extensions/gsd/files.js +19 -2
  22. package/dist/resources/extensions/gsd/guided-flow.js +12 -8
  23. package/dist/resources/extensions/gsd/index.js +1 -1
  24. package/dist/resources/extensions/gsd/parallel-monitor-overlay.js +2 -0
  25. package/dist/resources/extensions/gsd/parsers-legacy.js +3 -1
  26. package/dist/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
  27. package/dist/resources/extensions/gsd/prompts/discuss-prepared.md +7 -7
  28. package/dist/resources/extensions/gsd/prompts/discuss.md +3 -3
  29. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +3 -3
  30. package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +3 -1
  31. package/dist/resources/extensions/gsd/prompts/rethink.md +6 -2
  32. package/dist/resources/extensions/gsd/prompts/system.md +1 -1
  33. package/dist/resources/extensions/gsd/prompts/triage-captures.md +1 -1
  34. package/dist/resources/extensions/gsd/prompts/validate-milestone.md +4 -4
  35. package/dist/resources/extensions/gsd/prompts/worktree-merge.md +3 -1
  36. package/dist/resources/extensions/gsd/safety/file-change-validator.js +2 -1
  37. package/dist/resources/extensions/gsd/state.js +2 -1
  38. package/dist/resources/extensions/gsd/visualizer-overlay.js +27 -26
  39. package/dist/resources/extensions/gsd/workflow-reconcile.js +46 -7
  40. package/dist/resources/extensions/remote-questions/manager.js +8 -0
  41. package/dist/resources/extensions/shared/interview-ui.js +10 -0
  42. package/dist/web/standalone/.next/BUILD_ID +1 -1
  43. package/dist/web/standalone/.next/app-path-routes-manifest.json +17 -17
  44. package/dist/web/standalone/.next/build-manifest.json +2 -2
  45. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  46. package/dist/web/standalone/.next/required-server-files.json +1 -1
  47. package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
  48. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  56. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/index.html +1 -1
  64. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  66. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  69. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app-paths-manifest.json +17 -17
  71. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  72. package/dist/web/standalone/.next/server/pages/500.html +2 -2
  73. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  74. package/dist/web/standalone/server.js +1 -1
  75. package/package.json +1 -1
  76. package/packages/pi-ai/dist/providers/anthropic-shared.d.ts.map +1 -1
  77. package/packages/pi-ai/dist/providers/anthropic-shared.js +4 -3
  78. package/packages/pi-ai/dist/providers/anthropic-shared.js.map +1 -1
  79. package/packages/pi-ai/dist/utils/json-parse.d.ts.map +1 -1
  80. package/packages/pi-ai/dist/utils/json-parse.js +11 -1
  81. package/packages/pi-ai/dist/utils/json-parse.js.map +1 -1
  82. package/packages/pi-ai/dist/utils/repair-tool-json.d.ts.map +1 -1
  83. package/packages/pi-ai/dist/utils/repair-tool-json.js +60 -1
  84. package/packages/pi-ai/dist/utils/repair-tool-json.js.map +1 -1
  85. package/packages/pi-ai/dist/utils/tests/json-parse.test.d.ts +2 -0
  86. package/packages/pi-ai/dist/utils/tests/json-parse.test.d.ts.map +1 -0
  87. package/packages/pi-ai/dist/utils/tests/json-parse.test.js +14 -0
  88. package/packages/pi-ai/dist/utils/tests/json-parse.test.js.map +1 -0
  89. package/packages/pi-ai/dist/utils/tests/repair-tool-json.test.js +10 -0
  90. package/packages/pi-ai/dist/utils/tests/repair-tool-json.test.js.map +1 -1
  91. package/packages/pi-ai/src/providers/anthropic-shared.ts +4 -3
  92. package/packages/pi-ai/src/utils/json-parse.ts +11 -1
  93. package/packages/pi-ai/src/utils/repair-tool-json.ts +69 -1
  94. package/packages/pi-ai/src/utils/tests/json-parse.test.ts +17 -0
  95. package/packages/pi-ai/src/utils/tests/repair-tool-json.test.ts +13 -0
  96. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/provider-display-name.test.d.ts +2 -0
  97. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/provider-display-name.test.d.ts.map +1 -0
  98. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/provider-display-name.test.js +17 -0
  99. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/provider-display-name.test.js.map +1 -0
  100. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.d.ts.map +1 -1
  101. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js +2 -1
  102. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js.map +1 -1
  103. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.d.ts +1 -0
  104. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  105. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js +9 -2
  106. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js.map +1 -1
  107. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts.map +1 -1
  108. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js +2 -1
  109. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js.map +1 -1
  110. package/packages/pi-coding-agent/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
  111. package/packages/pi-coding-agent/dist/modes/interactive/components/scoped-models-selector.js +2 -1
  112. package/packages/pi-coding-agent/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
  113. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +2 -2
  114. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  115. package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/provider-display-name.test.ts +18 -0
  116. package/packages/pi-coding-agent/src/modes/interactive/components/footer.ts +2 -1
  117. package/packages/pi-coding-agent/src/modes/interactive/components/model-selector.ts +11 -2
  118. package/packages/pi-coding-agent/src/modes/interactive/components/provider-manager.ts +2 -1
  119. package/packages/pi-coding-agent/src/modes/interactive/components/scoped-models-selector.ts +2 -1
  120. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +2 -2
  121. package/packages/pi-tui/dist/__tests__/autocomplete.test.js +13 -0
  122. package/packages/pi-tui/dist/__tests__/autocomplete.test.js.map +1 -1
  123. package/packages/pi-tui/dist/__tests__/stdin-buffer.test.d.ts +2 -0
  124. package/packages/pi-tui/dist/__tests__/stdin-buffer.test.d.ts.map +1 -0
  125. package/packages/pi-tui/dist/__tests__/stdin-buffer.test.js +35 -0
  126. package/packages/pi-tui/dist/__tests__/stdin-buffer.test.js.map +1 -0
  127. package/packages/pi-tui/dist/__tests__/tui.test.d.ts +2 -0
  128. package/packages/pi-tui/dist/__tests__/tui.test.d.ts.map +1 -0
  129. package/packages/pi-tui/dist/__tests__/tui.test.js +43 -0
  130. package/packages/pi-tui/dist/__tests__/tui.test.js.map +1 -0
  131. package/packages/pi-tui/dist/autocomplete.d.ts.map +1 -1
  132. package/packages/pi-tui/dist/autocomplete.js +9 -7
  133. package/packages/pi-tui/dist/autocomplete.js.map +1 -1
  134. package/packages/pi-tui/dist/components/__tests__/editor.test.d.ts +2 -0
  135. package/packages/pi-tui/dist/components/__tests__/editor.test.d.ts.map +1 -0
  136. package/packages/pi-tui/dist/components/__tests__/editor.test.js +54 -0
  137. package/packages/pi-tui/dist/components/__tests__/editor.test.js.map +1 -0
  138. package/packages/pi-tui/dist/components/editor.d.ts +3 -1
  139. package/packages/pi-tui/dist/components/editor.d.ts.map +1 -1
  140. package/packages/pi-tui/dist/components/editor.js +14 -3
  141. package/packages/pi-tui/dist/components/editor.js.map +1 -1
  142. package/packages/pi-tui/dist/stdin-buffer.d.ts.map +1 -1
  143. package/packages/pi-tui/dist/stdin-buffer.js +6 -0
  144. package/packages/pi-tui/dist/stdin-buffer.js.map +1 -1
  145. package/packages/pi-tui/dist/tui.d.ts.map +1 -1
  146. package/packages/pi-tui/dist/tui.js +8 -0
  147. package/packages/pi-tui/dist/tui.js.map +1 -1
  148. package/packages/pi-tui/src/__tests__/autocomplete.test.ts +15 -0
  149. package/packages/pi-tui/src/__tests__/stdin-buffer.test.ts +43 -0
  150. package/packages/pi-tui/src/__tests__/tui.test.ts +50 -0
  151. package/packages/pi-tui/src/autocomplete.ts +9 -7
  152. package/packages/pi-tui/src/components/__tests__/editor.test.ts +64 -0
  153. package/packages/pi-tui/src/components/editor.ts +14 -3
  154. package/packages/pi-tui/src/stdin-buffer.ts +7 -0
  155. package/packages/pi-tui/src/tui.ts +9 -0
  156. package/src/resources/extensions/ask-user-questions.ts +103 -11
  157. package/src/resources/extensions/claude-code-cli/partial-builder.ts +4 -3
  158. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +12 -3
  159. package/src/resources/extensions/claude-code-cli/tests/partial-builder.test.ts +17 -0
  160. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +18 -0
  161. package/src/resources/extensions/gsd/auto/loop-deps.ts +2 -1
  162. package/src/resources/extensions/gsd/auto/loop.ts +14 -1
  163. package/src/resources/extensions/gsd/auto/phases.ts +10 -5
  164. package/src/resources/extensions/gsd/auto/run-unit.ts +14 -2
  165. package/src/resources/extensions/gsd/auto/session.ts +1 -1
  166. package/src/resources/extensions/gsd/auto-dashboard.ts +76 -16
  167. package/src/resources/extensions/gsd/auto-dispatch.ts +36 -35
  168. package/src/resources/extensions/gsd/auto-prompts.ts +5 -6
  169. package/src/resources/extensions/gsd/auto-recovery.ts +15 -15
  170. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +27 -6
  171. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +67 -6
  172. package/src/resources/extensions/gsd/bootstrap/register-shortcuts.ts +11 -8
  173. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +209 -16
  174. package/src/resources/extensions/gsd/codebase-generator.ts +4 -0
  175. package/src/resources/extensions/gsd/commands/handlers/core.ts +6 -6
  176. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +11 -4
  177. package/src/resources/extensions/gsd/custom-workflow-engine.ts +3 -1
  178. package/src/resources/extensions/gsd/detection.ts +6 -0
  179. package/src/resources/extensions/gsd/files.ts +21 -2
  180. package/src/resources/extensions/gsd/guided-flow.ts +15 -8
  181. package/src/resources/extensions/gsd/index.ts +6 -0
  182. package/src/resources/extensions/gsd/parallel-monitor-overlay.ts +2 -0
  183. package/src/resources/extensions/gsd/parsers-legacy.ts +3 -1
  184. package/src/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
  185. package/src/resources/extensions/gsd/prompts/discuss-prepared.md +7 -7
  186. package/src/resources/extensions/gsd/prompts/discuss.md +3 -3
  187. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +3 -3
  188. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +3 -1
  189. package/src/resources/extensions/gsd/prompts/rethink.md +6 -2
  190. package/src/resources/extensions/gsd/prompts/system.md +1 -1
  191. package/src/resources/extensions/gsd/prompts/triage-captures.md +1 -1
  192. package/src/resources/extensions/gsd/prompts/validate-milestone.md +4 -4
  193. package/src/resources/extensions/gsd/prompts/worktree-merge.md +3 -1
  194. package/src/resources/extensions/gsd/safety/file-change-validator.ts +4 -1
  195. package/src/resources/extensions/gsd/state.ts +2 -1
  196. package/src/resources/extensions/gsd/tests/auto-dashboard.test.ts +52 -1
  197. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +50 -2
  198. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +48 -0
  199. package/src/resources/extensions/gsd/tests/codebase-generator.test.ts +22 -0
  200. package/src/resources/extensions/gsd/tests/core-overlay-fallback.test.ts +44 -0
  201. package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +7 -1
  202. package/src/resources/extensions/gsd/tests/custom-workflow-engine.test.ts +31 -0
  203. package/src/resources/extensions/gsd/tests/detection.test.ts +37 -0
  204. package/src/resources/extensions/gsd/tests/file-change-validator.test.ts +50 -0
  205. package/src/resources/extensions/gsd/tests/gsd-tools.test.ts +35 -0
  206. package/src/resources/extensions/gsd/tests/guided-flow-session-isolation.test.ts +34 -0
  207. package/src/resources/extensions/gsd/tests/health-widget.test.ts +45 -0
  208. package/src/resources/extensions/gsd/tests/integration/auto-recovery.test.ts +53 -13
  209. package/src/resources/extensions/gsd/tests/integration/state-machine-runtime-failures.test.ts +2 -2
  210. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +1 -1
  211. package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +3 -4
  212. package/src/resources/extensions/gsd/tests/parallel-monitor-overlay.test.ts +21 -0
  213. package/src/resources/extensions/gsd/tests/parallel-research-dispatch.test.ts +71 -2
  214. package/src/resources/extensions/gsd/tests/parsers.test.ts +25 -0
  215. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +8 -1
  216. package/src/resources/extensions/gsd/tests/queue-execution-guard.test.ts +9 -0
  217. package/src/resources/extensions/gsd/tests/reactive-graph.test.ts +19 -0
  218. package/src/resources/extensions/gsd/tests/register-shortcuts.test.ts +73 -0
  219. package/src/resources/extensions/gsd/tests/remote-questions.test.ts +98 -0
  220. package/src/resources/extensions/gsd/tests/smart-entry-complete.test.ts +2 -2
  221. package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +26 -0
  222. package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +59 -0
  223. package/src/resources/extensions/gsd/tests/workflow-reconcile.test.ts +91 -0
  224. package/src/resources/extensions/gsd/tests/write-gate.test.ts +210 -35
  225. package/src/resources/extensions/gsd/visualizer-overlay.ts +31 -27
  226. package/src/resources/extensions/gsd/workflow-reconcile.ts +59 -8
  227. package/src/resources/extensions/remote-questions/manager.ts +9 -0
  228. package/src/resources/extensions/shared/interview-ui.ts +13 -0
  229. /package/dist/web/standalone/.next/static/{Zw5aZFHFtOwjJSOsINh1m → HxFcJ8GrYNPsg9ARz7GPz}/_buildManifest.js +0 -0
  230. /package/dist/web/standalone/.next/static/{Zw5aZFHFtOwjJSOsINh1m → HxFcJ8GrYNPsg9ARz7GPz}/_ssgManifest.js +0 -0
@@ -9,6 +9,7 @@ import { deriveState, isValidationTerminal } from "../state.ts";
9
9
  import { resolveExpectedArtifactPath, diagnoseExpectedArtifact } from "../auto-artifact-paths.ts";
10
10
  import { verifyExpectedArtifact, buildLoopRemediationSteps } from "../auto-recovery.ts";
11
11
  import { resolveDispatch, type DispatchContext } from "../auto-dispatch.ts";
12
+ import { buildValidateMilestonePrompt } from "../auto-prompts.ts";
12
13
  import type { GSDState } from "../types.ts";
13
14
  import { clearPathCache } from "../paths.ts";
14
15
  import { clearParseCache } from "../files.ts";
@@ -57,6 +58,12 @@ function writeSliceSummary(base: string, mid: string, sid: string, content: stri
57
58
  writeFileSync(join(dir, `${sid}-SUMMARY.md`), content);
58
59
  }
59
60
 
61
+ function writeSliceAssessment(base: string, mid: string, sid: string, content: string): void {
62
+ const dir = join(base, ".gsd", "milestones", mid, "slices", sid);
63
+ mkdirSync(dir, { recursive: true });
64
+ writeFileSync(join(dir, `${sid}-ASSESSMENT.md`), content);
65
+ }
66
+
60
67
  const ALL_DONE_ROADMAP = `# M001: Test Milestone
61
68
 
62
69
  ## Vision
@@ -192,6 +199,25 @@ test("deriveState returns complete when both VALIDATION and SUMMARY exist", asyn
192
199
  }
193
200
  });
194
201
 
202
+ test("buildValidateMilestonePrompt inlines ASSESSMENT evidence instead of UAT spec", async () => {
203
+ const base = makeTmpBase();
204
+ try {
205
+ writeRoadmap(base, "M001", ALL_DONE_ROADMAP);
206
+ const dir = join(base, ".gsd", "milestones", "M001");
207
+ writeFileSync(join(dir, "M001-CONTEXT.md"), CONTEXT_FILE);
208
+ writeSliceSummary(base, "M001", "S01", "# S01 Summary\nDelivered.");
209
+ writeFileSync(join(dir, "slices", "S01", "S01-UAT.md"), "# UAT Spec\nDo the thing.\n");
210
+ writeSliceAssessment(base, "M001", "S01", "---\nverdict: PASS\n---\n# Assessment\nEvidence captured.");
211
+
212
+ const prompt = await buildValidateMilestonePrompt("M001", "Test Milestone", base);
213
+ assert.match(prompt, /S01 Assessment/i, "prompt should inline assessment evidence");
214
+ assert.match(prompt, /verdict: PASS/i, "prompt should include the assessment verdict");
215
+ assert.doesNotMatch(prompt, /UAT Spec/i, "prompt should not inline the raw UAT spec as evidence");
216
+ } finally {
217
+ cleanup(base);
218
+ }
219
+ });
220
+
195
221
  // ─── Dispatch rule ────────────────────────────────────────────────────────
196
222
 
197
223
  test("dispatch rule matches validating-milestone phase", async () => {
@@ -233,3 +233,62 @@ assert.ok(
233
233
  overlaySrc.includes('from "../shared/mod.js"'),
234
234
  "imports from shared barrel",
235
235
  );
236
+
237
+ test("visualizer overlay closes on escape in filter and help submodes", async () => {
238
+ const mod = await import("../visualizer-overlay.js");
239
+
240
+ const mockTui = { requestRender: () => {} };
241
+ const mockTheme = {
242
+ fg: (_color: string, text: string) => text,
243
+ bold: (text: string) => text,
244
+ };
245
+
246
+ let closedFilter = false;
247
+ const filterOverlay = new mod.GSDVisualizerOverlay(
248
+ mockTui,
249
+ mockTheme as any,
250
+ () => { closedFilter = true; },
251
+ );
252
+ filterOverlay.filterMode = true;
253
+ filterOverlay.handleInput("\u0003");
254
+ assert.equal(closedFilter, true, "Ctrl+C closes while filter mode is active");
255
+ filterOverlay.dispose();
256
+
257
+ let closedHelp = false;
258
+ const helpOverlay = new mod.GSDVisualizerOverlay(
259
+ mockTui,
260
+ mockTheme as any,
261
+ () => { closedHelp = true; },
262
+ );
263
+ helpOverlay.showHelp = true;
264
+ helpOverlay.handleInput("\u001b");
265
+ assert.equal(closedHelp, true, "Escape closes while help overlay is visible");
266
+ helpOverlay.dispose();
267
+ });
268
+
269
+ test("visualizer overlay tab hitboxes include rendered badges", async () => {
270
+ const mod = await import("../visualizer-overlay.js");
271
+
272
+ const mockTui = { requestRender: () => {} };
273
+ const mockTheme = {
274
+ fg: (_color: string, text: string) => text,
275
+ bold: (text: string) => text,
276
+ };
277
+
278
+ const overlay = new mod.GSDVisualizerOverlay(
279
+ mockTui,
280
+ mockTheme as any,
281
+ () => {},
282
+ );
283
+ overlay.loading = true;
284
+ overlay.data = { captures: { pendingCount: 3 } } as any;
285
+
286
+ const lines = overlay.render(120);
287
+ const tabLine = lines.find((line: string) => line.includes("Captures") && line.includes("(3)"));
288
+ assert.ok(tabLine, "rendered tab bar includes captures badge");
289
+ const plain = tabLine!.replace(/\x1b\[[0-9;]*m/g, "");
290
+ const badgeColumn = plain.indexOf("(3)") + 2;
291
+ overlay.handleInput(`\x1b[<0;${badgeColumn};2M`);
292
+ assert.equal(overlay.activeTab, 8, "clicking the badge area selects the captures tab");
293
+ overlay.dispose();
294
+ });
@@ -0,0 +1,91 @@
1
+ import test, { afterEach } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdtempSync, mkdirSync, rmSync, existsSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+
7
+ import { appendEvent, readEvents } from "../workflow-events.ts";
8
+ import { listConflicts, reconcileWorktreeLogs, resolveConflict } from "../workflow-reconcile.ts";
9
+ import { closeDatabase } from "../gsd-db.ts";
10
+
11
+ const tmpDirs: string[] = [];
12
+
13
+ function makeTmpRepo(): { main: string; worktree: string } {
14
+ const root = mkdtempSync(join(tmpdir(), "workflow-reconcile-"));
15
+ const main = join(root, "main");
16
+ const worktree = join(root, "worktree");
17
+ mkdirSync(main, { recursive: true });
18
+ mkdirSync(worktree, { recursive: true });
19
+ tmpDirs.push(root);
20
+ return { main, worktree };
21
+ }
22
+
23
+ afterEach(() => {
24
+ closeDatabase();
25
+ for (const dir of tmpDirs) {
26
+ try {
27
+ rmSync(dir, { recursive: true, force: true });
28
+ } catch {
29
+ // Best-effort cleanup on platforms that keep files open briefly.
30
+ }
31
+ }
32
+ tmpDirs.length = 0;
33
+ });
34
+
35
+ test("resolveConflict(pick=main) rewrites the worktree log durably", () => {
36
+ const { main, worktree } = makeTmpRepo();
37
+
38
+ appendEvent(main, {
39
+ cmd: "plan_milestone",
40
+ params: { milestoneId: "M001", title: "Base Milestone" },
41
+ ts: "2026-01-01T00:00:00.000Z",
42
+ actor: "agent",
43
+ });
44
+ appendEvent(worktree, {
45
+ cmd: "plan_milestone",
46
+ params: { milestoneId: "M001", title: "Base Milestone" },
47
+ ts: "2026-01-01T00:00:00.000Z",
48
+ actor: "agent",
49
+ });
50
+
51
+ appendEvent(main, {
52
+ cmd: "plan_milestone",
53
+ params: { milestoneId: "M001", title: "Main Choice" },
54
+ ts: "2026-01-01T00:01:00.000Z",
55
+ actor: "agent",
56
+ });
57
+
58
+ appendEvent(worktree, {
59
+ cmd: "plan_milestone",
60
+ params: { milestoneId: "M001", title: "Worktree Choice" },
61
+ ts: "2026-01-01T00:01:00.000Z",
62
+ actor: "agent",
63
+ });
64
+
65
+ const initial = reconcileWorktreeLogs(main, worktree);
66
+ assert.equal(initial.conflicts.length, 1, "expected one conflict before resolution");
67
+ assert.ok(listConflicts(main).length === 1, "CONFLICTS.md should exist after detection");
68
+
69
+ resolveConflict(main, worktree, "milestone:M001", "main");
70
+
71
+ assert.equal(listConflicts(main).length, 0, "conflict file should be cleared after resolving main");
72
+ const conflictsPath = join(main, ".gsd", "CONFLICTS.md");
73
+ assert.equal(
74
+ existsSync(conflictsPath),
75
+ false,
76
+ "CONFLICTS.md should be removed after the last conflict is resolved",
77
+ );
78
+
79
+ const wtEvents = readEvents(join(worktree, ".gsd", "event-log.jsonl"));
80
+ assert.ok(
81
+ wtEvents.some((e) => e.cmd === "plan_milestone" && e.params.title === "Main Choice"),
82
+ "worktree log should be rewritten to the main-side resolution",
83
+ );
84
+ assert.ok(
85
+ !wtEvents.some((e) => e.cmd === "plan_milestone" && e.params.title === "Worktree Choice"),
86
+ "worktree log should no longer contain the discarded conflict event",
87
+ );
88
+
89
+ const second = reconcileWorktreeLogs(main, worktree);
90
+ assert.equal(second.conflicts.length, 0, "reconcile should stay clean after choosing main");
91
+ });
@@ -3,9 +3,9 @@
3
3
  *
4
4
  * Exercises shouldBlockContextWrite() — a pure function that implements:
5
5
  * (a) toolName !== "write" → pass
6
- * (b) milestoneId null pass (not in discussion)
6
+ * (b) milestone context must resolve to a verified milestone
7
7
  * (c) path doesn't match /M\d+-CONTEXT\.md$/ → pass
8
- * (d) depthVerified → pass
8
+ * (d) non-context files → pass
9
9
  * (e) else → block with actionable reason
10
10
  */
11
11
 
@@ -14,12 +14,12 @@ import assert from 'node:assert/strict';
14
14
  import {
15
15
  isDepthConfirmationAnswer,
16
16
  shouldBlockContextWrite,
17
- isDepthVerified,
18
- isQueuePhaseActive,
19
17
  setQueuePhaseActive,
20
18
  } from '../index.ts';
21
19
  import {
22
20
  markDepthVerified,
21
+ isMilestoneDepthVerified,
22
+ shouldBlockContextArtifactSave,
23
23
  clearDiscussionFlowState,
24
24
  resetWriteGateState,
25
25
  } from '../bootstrap/write-gate.ts';
@@ -53,26 +53,27 @@ test('write-gate: blocks CONTEXT.md write during discussion without depth verifi
53
53
  // ─── Scenario 3: Allows CONTEXT.md write after depth verification ──
54
54
 
55
55
  test('write-gate: allows CONTEXT.md write after depth verification', () => {
56
+ clearDiscussionFlowState();
57
+ markDepthVerified('M001');
56
58
  const result = shouldBlockContextWrite(
57
59
  'write',
58
60
  '/Users/dev/project/.gsd/milestones/M001/M001-CONTEXT.md',
59
61
  'M001',
60
- true,
61
62
  );
62
63
  assert.strictEqual(result.block, false, 'should not block after depth verification');
63
64
  assert.strictEqual(result.reason, undefined, 'should have no reason');
65
+ clearDiscussionFlowState();
64
66
  });
65
67
 
66
- // ─── Scenario 4: Allows CONTEXT.md write outside discussion phase (milestoneId null) ──
68
+ // ─── Scenario 4: Ambiguous session context no longer bypasses the gate ──
67
69
 
68
- test('write-gate: allows CONTEXT.md write outside discussion phase', () => {
70
+ test('write-gate: blocks CONTEXT.md write when milestoneId is ambiguous', () => {
69
71
  const result = shouldBlockContextWrite(
70
72
  'write',
71
73
  '.gsd/milestones/M001/M001-CONTEXT.md',
72
74
  null,
73
- false,
74
75
  );
75
- assert.strictEqual(result.block, false, 'should not block outside discussion phase');
76
+ assert.strictEqual(result.block, true, 'should block when milestone context is ambiguous');
76
77
  });
77
78
 
78
79
  // ─── Scenario 5: Allows non-CONTEXT.md writes during discussion ──
@@ -83,7 +84,6 @@ test('write-gate: allows non-CONTEXT.md writes during discussion', () => {
83
84
  'write',
84
85
  '.gsd/milestones/M001/M001-DISCUSSION.md',
85
86
  'M001',
86
- false,
87
87
  );
88
88
  assert.strictEqual(r1.block, false, 'DISCUSSION.md should pass');
89
89
 
@@ -92,7 +92,6 @@ test('write-gate: allows non-CONTEXT.md writes during discussion', () => {
92
92
  'write',
93
93
  '.gsd/milestones/M001/slices/S01/S01-PLAN.md',
94
94
  'M001',
95
- false,
96
95
  );
97
96
  assert.strictEqual(r2.block, false, 'slice plan should pass');
98
97
 
@@ -101,7 +100,6 @@ test('write-gate: allows non-CONTEXT.md writes during discussion', () => {
101
100
  'write',
102
101
  'src/index.ts',
103
102
  'M001',
104
- false,
105
103
  );
106
104
  assert.strictEqual(r3.block, false, 'regular code file should pass');
107
105
  });
@@ -113,7 +111,6 @@ test('write-gate: regex does not match slice context files (S01-CONTEXT.md)', ()
113
111
  'write',
114
112
  '.gsd/milestones/M001/slices/S01/S01-CONTEXT.md',
115
113
  'M001',
116
- false,
117
114
  );
118
115
  assert.strictEqual(result.block, false, 'S01-CONTEXT.md should not be blocked');
119
116
  });
@@ -125,7 +122,6 @@ test('write-gate: blocked reason contains depth_verification keyword and anti-by
125
122
  'write',
126
123
  '.gsd/milestones/M999/M999-CONTEXT.md',
127
124
  'M999',
128
- false,
129
125
  );
130
126
  assert.strictEqual(result.block, true);
131
127
  assert.ok(result.reason!.includes('depth_verification'), 'reason should mention depth_verification question id');
@@ -141,7 +137,6 @@ test('write-gate: blocks CONTEXT.md write in queue mode without depth verificati
141
137
  'write',
142
138
  '.gsd/milestones/M001/M001-CONTEXT.md',
143
139
  null, // no milestoneId in queue mode
144
- false, // not depth-verified
145
140
  true, // queue phase active
146
141
  );
147
142
  assert.strictEqual(result.block, true, 'should block in queue mode without depth verification');
@@ -151,48 +146,228 @@ test('write-gate: blocks CONTEXT.md write in queue mode without depth verificati
151
146
  // ─── Scenario 9: Queue mode allows CONTEXT.md write after depth verification ──
152
147
 
153
148
  test('write-gate: allows CONTEXT.md write in queue mode after depth verification', () => {
149
+ clearDiscussionFlowState();
150
+ markDepthVerified('M001');
154
151
  const result = shouldBlockContextWrite(
155
152
  'write',
156
153
  '.gsd/milestones/M001/M001-CONTEXT.md',
157
154
  null, // no milestoneId in queue mode
158
- true, // depth-verified
159
155
  true, // queue phase active
160
156
  );
161
157
  assert.strictEqual(result.block, false, 'should not block in queue mode after depth verification');
158
+ clearDiscussionFlowState();
162
159
  });
163
160
 
164
- // ─── Scenario 10: markDepthVerified works in queue-only mode (no milestoneId) ──
165
- // This is the core regression for #1812: in queue mode, the tool_result handler
166
- // must call markDepthVerified() even when getDiscussionMilestoneId() is null.
161
+ // ─── Scenario 10: depth verification is scoped per milestone, not global ──
167
162
 
168
- test('write-gate: markDepthVerified unblocks queue-mode writes when milestoneId is null', () => {
163
+ test('write-gate: markDepthVerified unlocks only the matching milestone', () => {
169
164
  clearDiscussionFlowState();
170
- setQueuePhaseActive(true);
165
+ markDepthVerified('M001');
171
166
 
172
- // Before marking: should block
173
- const blocked = shouldBlockContextWrite(
167
+ const allowed = shouldBlockContextWrite(
174
168
  'write',
175
169
  '.gsd/milestones/M001/M001-CONTEXT.md',
176
170
  null,
177
- isDepthVerified(),
178
- isQueuePhaseActive(),
179
171
  );
180
- assert.strictEqual(blocked.block, true, 'should block before markDepthVerified');
172
+ assert.strictEqual(allowed.block, false, 'should allow the verified milestone');
181
173
 
182
- // Simulate what the fixed tool_result handler does
183
- markDepthVerified();
184
-
185
- // After marking: should pass
186
- const allowed = shouldBlockContextWrite(
174
+ const blockedOther = shouldBlockContextWrite(
187
175
  'write',
188
- '.gsd/milestones/M001/M001-CONTEXT.md',
176
+ '.gsd/milestones/M002/M002-CONTEXT.md',
189
177
  null,
190
- isDepthVerified(),
191
- isQueuePhaseActive(),
192
178
  );
193
- assert.strictEqual(allowed.block, false, 'should allow after markDepthVerified in queue mode');
179
+ assert.strictEqual(blockedOther.block, true, 'other milestones should remain blocked');
180
+ assert.strictEqual(isMilestoneDepthVerified('M001'), true);
181
+ assert.strictEqual(isMilestoneDepthVerified('M002'), false);
182
+
183
+ clearDiscussionFlowState();
184
+ });
185
+
186
+ // ─── Scenario 11: gsd_summary_save CONTEXT contract is milestone-scoped ──
187
+
188
+ test('write-gate: gsd_summary_save only blocks final milestone CONTEXT writes', () => {
189
+ clearDiscussionFlowState();
190
+
191
+ assert.strictEqual(
192
+ shouldBlockContextArtifactSave('CONTEXT-DRAFT', 'M001').block,
193
+ false,
194
+ 'draft CONTEXT should be allowed',
195
+ );
196
+ assert.strictEqual(
197
+ shouldBlockContextArtifactSave('CONTEXT', 'M001', 'S01').block,
198
+ false,
199
+ 'slice CONTEXT should be allowed',
200
+ );
201
+ assert.strictEqual(
202
+ shouldBlockContextArtifactSave('CONTEXT', 'M001').block,
203
+ true,
204
+ 'final milestone CONTEXT should block before verification',
205
+ );
206
+
207
+ markDepthVerified('M001');
208
+ assert.strictEqual(
209
+ shouldBlockContextArtifactSave('CONTEXT', 'M001').block,
210
+ false,
211
+ 'final milestone CONTEXT should pass after verification',
212
+ );
213
+
214
+ clearDiscussionFlowState();
215
+ });
194
216
 
217
+ // ═══════════════════════════════════════════════════════════════════════
218
+ // Discussion gate enforcement tests (pending gate mechanism)
219
+ // ═══════════════════════════════════════════════════════════════════════
220
+
221
+ import {
222
+ isGateQuestionId,
223
+ shouldBlockPendingGate,
224
+ shouldBlockPendingGateBash,
225
+ setPendingGate,
226
+ clearPendingGate,
227
+ getPendingGate,
228
+ } from '../bootstrap/write-gate.ts';
229
+
230
+ // ─── Scenario 19: isGateQuestionId recognizes all gate patterns ──
231
+
232
+ test('write-gate: isGateQuestionId recognizes all gate patterns', () => {
233
+ assert.strictEqual(isGateQuestionId('layer1_scope_gate'), true);
234
+ assert.strictEqual(isGateQuestionId('layer2_architecture_gate'), true);
235
+ assert.strictEqual(isGateQuestionId('layer3_error_gate'), true);
236
+ assert.strictEqual(isGateQuestionId('layer4_quality_gate'), true);
237
+ assert.strictEqual(isGateQuestionId('depth_verification'), true);
238
+ assert.strictEqual(isGateQuestionId('depth_verification_M002'), true);
239
+ assert.strictEqual(isGateQuestionId('my_layer1_scope_gate_question'), true);
240
+ // Non-gate question IDs
241
+ assert.strictEqual(isGateQuestionId('project_intent'), false);
242
+ assert.strictEqual(isGateQuestionId('feature_priority'), false);
243
+ assert.strictEqual(isGateQuestionId(''), false);
244
+ });
245
+
246
+ // ─── Scenario 20: setPendingGate / getPendingGate / clearPendingGate lifecycle ──
247
+
248
+ test('write-gate: pending gate lifecycle (set, get, clear)', () => {
195
249
  clearDiscussionFlowState();
250
+ assert.strictEqual(getPendingGate(), null, 'starts null');
251
+
252
+ setPendingGate('layer1_scope_gate');
253
+ assert.strictEqual(getPendingGate(), 'layer1_scope_gate', 'set correctly');
254
+
255
+ clearPendingGate();
256
+ assert.strictEqual(getPendingGate(), null, 'cleared correctly');
257
+
258
+ // clearDiscussionFlowState also clears pending gate
259
+ setPendingGate('layer2_architecture_gate');
260
+ clearDiscussionFlowState();
261
+ assert.strictEqual(getPendingGate(), null, 'clearDiscussionFlowState clears pending gate');
262
+ });
263
+
264
+ // ─── Scenario 21: shouldBlockPendingGate blocks non-safe tools when gate is pending ──
265
+
266
+ test('write-gate: shouldBlockPendingGate blocks write/edit during pending gate', () => {
267
+ clearDiscussionFlowState();
268
+ setPendingGate('layer1_scope_gate');
269
+
270
+ // write should be blocked during discussion
271
+ const writeResult = shouldBlockPendingGate('write', 'M001', false);
272
+ assert.strictEqual(writeResult.block, true, 'write should be blocked');
273
+ assert.ok(writeResult.reason!.includes('layer1_scope_gate'), 'reason mentions the gate');
274
+
275
+ // edit should be blocked
276
+ const editResult = shouldBlockPendingGate('edit', 'M001', false);
277
+ assert.strictEqual(editResult.block, true, 'edit should be blocked');
278
+
279
+ // gsd tools should be blocked
280
+ const gsdResult = shouldBlockPendingGate('gsd_plan_milestone', 'M001', false);
281
+ assert.strictEqual(gsdResult.block, true, 'gsd tools should be blocked');
282
+
283
+ clearDiscussionFlowState();
284
+ });
285
+
286
+ // ─── Scenario 22: shouldBlockPendingGate allows safe tools when gate is pending ──
287
+
288
+ test('write-gate: shouldBlockPendingGate allows read-only and ask_user_questions during pending gate', () => {
289
+ clearDiscussionFlowState();
290
+ setPendingGate('layer1_scope_gate');
291
+
292
+ // ask_user_questions is always safe (model needs to re-ask)
293
+ assert.strictEqual(shouldBlockPendingGate('ask_user_questions', 'M001').block, false);
294
+ // read-only tools are safe
295
+ assert.strictEqual(shouldBlockPendingGate('read', 'M001').block, false);
296
+ assert.strictEqual(shouldBlockPendingGate('grep', 'M001').block, false);
297
+ assert.strictEqual(shouldBlockPendingGate('glob', 'M001').block, false);
298
+ assert.strictEqual(shouldBlockPendingGate('ls', 'M001').block, false);
299
+
300
+ clearDiscussionFlowState();
301
+ });
302
+
303
+ // ─── Scenario 23: shouldBlockPendingGate still blocks when the session is ambiguous ──
304
+
305
+ test('write-gate: shouldBlockPendingGate blocks outside discussion when a gate is pending', () => {
306
+ clearDiscussionFlowState();
307
+ setPendingGate('layer1_scope_gate');
308
+
309
+ // No milestoneId and no queue phase — still block because the gate is pending
310
+ const result = shouldBlockPendingGate('write', null, false);
311
+ assert.strictEqual(result.block, true, 'should block even when milestoneId is null');
312
+
313
+ clearDiscussionFlowState();
314
+ });
315
+
316
+ // ─── Scenario 24: shouldBlockPendingGate blocks in queue mode ──
317
+
318
+ test('write-gate: shouldBlockPendingGate blocks in queue mode when gate is pending', () => {
319
+ clearDiscussionFlowState();
320
+ setQueuePhaseActive(true);
321
+ setPendingGate('depth_verification');
322
+
323
+ const result = shouldBlockPendingGate('write', null, true);
324
+ assert.strictEqual(result.block, true, 'should block in queue mode');
325
+
326
+ clearDiscussionFlowState();
327
+ });
328
+
329
+ // ─── Scenario 25: shouldBlockPendingGateBash allows read-only commands ──
330
+
331
+ test('write-gate: shouldBlockPendingGateBash allows read-only commands during pending gate', () => {
332
+ clearDiscussionFlowState();
333
+ setPendingGate('layer2_architecture_gate');
334
+
335
+ assert.strictEqual(shouldBlockPendingGateBash('cat file.txt', 'M001').block, false);
336
+ assert.strictEqual(shouldBlockPendingGateBash('git log --oneline', 'M001').block, false);
337
+ assert.strictEqual(shouldBlockPendingGateBash('grep -r pattern .', 'M001').block, false);
338
+ assert.strictEqual(shouldBlockPendingGateBash('ls -la', 'M001').block, false);
339
+
340
+ clearDiscussionFlowState();
341
+ });
342
+
343
+ // ─── Scenario 26: shouldBlockPendingGateBash blocks mutating commands ──
344
+
345
+ test('write-gate: shouldBlockPendingGateBash blocks mutating commands during pending gate', () => {
346
+ clearDiscussionFlowState();
347
+ setPendingGate('layer2_architecture_gate');
348
+
349
+ const result = shouldBlockPendingGateBash('npm run build', 'M001');
350
+ assert.strictEqual(result.block, true, 'mutating bash should be blocked');
351
+ assert.ok(result.reason!.includes('layer2_architecture_gate'));
352
+
353
+ clearDiscussionFlowState();
354
+ });
355
+
356
+ // ─── Scenario 27: no pending gate means no blocking ──
357
+
358
+ test('write-gate: no pending gate means no blocking', () => {
359
+ clearDiscussionFlowState();
360
+
361
+ assert.strictEqual(shouldBlockPendingGate('write', 'M001').block, false);
362
+ assert.strictEqual(shouldBlockPendingGateBash('npm run build', 'M001').block, false);
363
+ });
364
+
365
+ // ─── Scenario 28: resetWriteGateState clears pending gate ──
366
+
367
+ test('write-gate: resetWriteGateState clears pending gate', () => {
368
+ setPendingGate('layer3_error_gate');
369
+ resetWriteGateState();
370
+ assert.strictEqual(getPendingGate(), null);
196
371
  });
197
372
 
198
373
  // ─── Standard options fixture used across depth confirmation tests ──
@@ -34,6 +34,24 @@ const TAB_LABELS = [
34
34
  "0 Export",
35
35
  ];
36
36
 
37
+ type TabBarEntry = { label: string; width: number };
38
+
39
+ function buildTabBarEntries(activeTab: number, filterText: string, capturesPendingCount?: number): TabBarEntry[] {
40
+ return TAB_LABELS.map((label, i) => {
41
+ let displayLabel = label;
42
+ if (i === activeTab && filterText) {
43
+ displayLabel += " \u2731";
44
+ }
45
+ if (i === 8 && capturesPendingCount) {
46
+ displayLabel += ` (${capturesPendingCount})`;
47
+ }
48
+ return {
49
+ label: displayLabel,
50
+ width: visibleWidth(displayLabel) + 2,
51
+ };
52
+ });
53
+ }
54
+
37
55
  export class GSDVisualizerOverlay {
38
56
  private tui: { requestRender: () => void };
39
57
  private theme: Theme;
@@ -116,15 +134,14 @@ export class GSDVisualizerOverlay {
116
134
  }
117
135
 
118
136
  handleInput(data: string): void {
137
+ if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c"))) {
138
+ this.dispose();
139
+ this.onClose();
140
+ return;
141
+ }
142
+
119
143
  // Filter mode input routing
120
144
  if (this.filterMode) {
121
- if (matchesKey(data, Key.escape)) {
122
- this.filterMode = false;
123
- this.filterText = "";
124
- this.invalidate();
125
- this.tui.requestRender();
126
- return;
127
- }
128
145
  if (matchesKey(data, Key.enter)) {
129
146
  this.filterMode = false;
130
147
  this.invalidate();
@@ -179,8 +196,9 @@ export class GSDVisualizerOverlay {
179
196
  // Left click — check if on tab bar row
180
197
  if (mouse.y === 2) {
181
198
  let xPos = 3;
182
- for (let i = 0; i < TAB_LABELS.length; i++) {
183
- const tabWidth = TAB_LABELS[i].length + 2;
199
+ const tabs = buildTabBarEntries(this.activeTab, this.filterText, this.data?.captures?.pendingCount);
200
+ for (let i = 0; i < tabs.length; i++) {
201
+ const tabWidth = tabs[i]!.width;
184
202
  if (mouse.x >= xPos && mouse.x < xPos + tabWidth) {
185
203
  this.activeTab = i;
186
204
  this.invalidate();
@@ -194,12 +212,6 @@ export class GSDVisualizerOverlay {
194
212
  return;
195
213
  }
196
214
 
197
- if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c"))) {
198
- this.dispose();
199
- this.onClose();
200
- return;
201
- }
202
-
203
215
  if (matchesKey(data, Key.shift("tab"))) {
204
216
  this.activeTab = (this.activeTab - 1 + TAB_COUNT) % TAB_COUNT;
205
217
  this.invalidate();
@@ -442,20 +454,12 @@ export class GSDVisualizerOverlay {
442
454
  const content: string[] = [];
443
455
 
444
456
  // Tab bar
445
- const tabs = TAB_LABELS.map((label, i) => {
446
- let displayLabel = label;
447
- // Show filter indicator on active tab with filter
448
- if (i === this.activeTab && this.filterText) {
449
- displayLabel += " \u2731";
450
- }
451
- // Show captures badge
452
- if (i === 8 && this.data?.captures?.pendingCount) {
453
- displayLabel += ` (${this.data.captures.pendingCount})`;
454
- }
457
+ const tabEntries = buildTabBarEntries(this.activeTab, this.filterText, this.data?.captures?.pendingCount);
458
+ const tabs = tabEntries.map((entry, i) => {
455
459
  if (i === this.activeTab) {
456
- return th.fg("accent", `[${displayLabel}]`);
460
+ return th.fg("accent", `[${entry.label}]`);
457
461
  }
458
- return th.fg("dim", `[${displayLabel}]`);
462
+ return th.fg("dim", `[${entry.label}]`);
459
463
  });
460
464
  content.push(" " + tabs.join(" "));
461
465
  content.push("");