@bastani/atomic 0.8.1 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/builtin/intercom/config.ts +3 -4
  3. package/dist/builtin/intercom/index.ts +6 -6
  4. package/dist/builtin/intercom/package.json +1 -1
  5. package/dist/builtin/mcp/agent-dir.ts +11 -2
  6. package/dist/builtin/mcp/cli.js +12 -6
  7. package/dist/builtin/mcp/config.ts +31 -22
  8. package/dist/builtin/mcp/package.json +1 -1
  9. package/dist/builtin/subagents/package.json +1 -1
  10. package/dist/builtin/subagents/src/agents/agents.ts +63 -23
  11. package/dist/builtin/subagents/src/agents/skills.ts +21 -21
  12. package/dist/builtin/subagents/src/extension/index.ts +9 -8
  13. package/dist/builtin/subagents/src/runs/shared/run-history.ts +13 -10
  14. package/dist/builtin/subagents/src/runs/shared/subagent-prompt-runtime.ts +3 -3
  15. package/dist/builtin/subagents/src/shared/artifacts.ts +18 -17
  16. package/dist/builtin/subagents/src/shared/types.ts +4 -4
  17. package/dist/builtin/web-access/config-paths.ts +11 -0
  18. package/dist/builtin/web-access/exa.ts +3 -2
  19. package/dist/builtin/web-access/gemini-api.ts +2 -1
  20. package/dist/builtin/web-access/gemini-search.ts +2 -1
  21. package/dist/builtin/web-access/gemini-web-config.ts +2 -1
  22. package/dist/builtin/web-access/github-extract.ts +2 -1
  23. package/dist/builtin/web-access/index.ts +11 -8
  24. package/dist/builtin/web-access/package.json +1 -1
  25. package/dist/builtin/web-access/perplexity.ts +2 -1
  26. package/dist/builtin/web-access/video-extract.ts +2 -1
  27. package/dist/builtin/web-access/youtube-extract.ts +2 -1
  28. package/dist/builtin/workflows/builtin/deep-research-codebase.ts +4 -0
  29. package/dist/builtin/workflows/builtin/open-claude-design.ts +39 -22
  30. package/dist/builtin/workflows/builtin/ralph.ts +7 -0
  31. package/dist/builtin/workflows/package.json +1 -1
  32. package/dist/builtin/workflows/skills/workflow/SKILL.md +28 -20
  33. package/dist/builtin/workflows/skills/workflow/references/design-checklist.md +8 -4
  34. package/dist/builtin/workflows/skills/workflow/references/running-workflows.md +52 -23
  35. package/dist/builtin/workflows/skills/workflow/references/sdk-authoring.md +41 -12
  36. package/dist/builtin/workflows/src/extension/config-loader.ts +13 -14
  37. package/dist/builtin/workflows/src/extension/discovery.ts +4 -6
  38. package/dist/builtin/workflows/src/extension/index.ts +675 -524
  39. package/dist/builtin/workflows/src/extension/runtime.ts +40 -16
  40. package/dist/builtin/workflows/src/extension/wiring.ts +3 -0
  41. package/dist/builtin/workflows/src/extension/workflow-schema.ts +43 -33
  42. package/dist/builtin/workflows/src/runs/foreground/executor.ts +34 -10
  43. package/dist/builtin/workflows/src/shared/types.ts +1 -5
  44. package/dist/builtin/workflows/src/tui/graph-view.ts +245 -75
  45. package/dist/builtin/workflows/src/tui/overlay-adapter.ts +23 -0
  46. package/dist/builtin/workflows/src/tui/stage-chat-view.ts +259 -149
  47. package/dist/builtin/workflows/src/tui/status-helpers.ts +3 -3
  48. package/dist/builtin/workflows/src/tui/store-widget-installer.ts +99 -10
  49. package/dist/builtin/workflows/src/tui/switcher.ts +4 -5
  50. package/dist/builtin/workflows/src/tui/workflow-attach-pane.ts +29 -0
  51. package/dist/cli/args.d.ts.map +1 -1
  52. package/dist/cli/args.js +11 -8
  53. package/dist/cli/args.js.map +1 -1
  54. package/dist/config.d.ts +21 -0
  55. package/dist/config.d.ts.map +1 -1
  56. package/dist/config.js +59 -4
  57. package/dist/config.js.map +1 -1
  58. package/dist/core/agent-session.d.ts +1 -1
  59. package/dist/core/agent-session.d.ts.map +1 -1
  60. package/dist/core/agent-session.js +2 -2
  61. package/dist/core/agent-session.js.map +1 -1
  62. package/dist/core/auth-storage.d.ts +3 -1
  63. package/dist/core/auth-storage.d.ts.map +1 -1
  64. package/dist/core/auth-storage.js +31 -8
  65. package/dist/core/auth-storage.js.map +1 -1
  66. package/dist/core/extensions/runner.d.ts.map +1 -1
  67. package/dist/core/extensions/runner.js +9 -0
  68. package/dist/core/extensions/runner.js.map +1 -1
  69. package/dist/core/extensions/types.d.ts +11 -0
  70. package/dist/core/extensions/types.d.ts.map +1 -1
  71. package/dist/core/extensions/types.js.map +1 -1
  72. package/dist/core/model-registry.d.ts +3 -2
  73. package/dist/core/model-registry.d.ts.map +1 -1
  74. package/dist/core/model-registry.js +25 -8
  75. package/dist/core/model-registry.js.map +1 -1
  76. package/dist/core/package-manager.d.ts +3 -0
  77. package/dist/core/package-manager.d.ts.map +1 -1
  78. package/dist/core/package-manager.js +97 -58
  79. package/dist/core/package-manager.js.map +1 -1
  80. package/dist/core/resource-loader.d.ts +1 -0
  81. package/dist/core/resource-loader.d.ts.map +1 -1
  82. package/dist/core/resource-loader.js +37 -36
  83. package/dist/core/resource-loader.js.map +1 -1
  84. package/dist/core/sdk.d.ts +5 -4
  85. package/dist/core/sdk.d.ts.map +1 -1
  86. package/dist/core/sdk.js +2 -2
  87. package/dist/core/sdk.js.map +1 -1
  88. package/dist/core/settings-manager.d.ts +7 -1
  89. package/dist/core/settings-manager.d.ts.map +1 -1
  90. package/dist/core/settings-manager.js +29 -8
  91. package/dist/core/settings-manager.js.map +1 -1
  92. package/dist/core/system-prompt.d.ts +1 -1
  93. package/dist/core/system-prompt.d.ts.map +1 -1
  94. package/dist/core/system-prompt.js.map +1 -1
  95. package/dist/core/telemetry.d.ts.map +1 -1
  96. package/dist/core/telemetry.js +2 -2
  97. package/dist/core/telemetry.js.map +1 -1
  98. package/dist/core/timings.d.ts.map +1 -1
  99. package/dist/core/timings.js +2 -2
  100. package/dist/core/timings.js.map +1 -1
  101. package/dist/core/tools/index.d.ts +1 -0
  102. package/dist/core/tools/index.d.ts.map +1 -1
  103. package/dist/core/tools/index.js +8 -0
  104. package/dist/core/tools/index.js.map +1 -1
  105. package/dist/core/tools/todos.d.ts.map +1 -1
  106. package/dist/core/tools/todos.js +3 -3
  107. package/dist/core/tools/todos.js.map +1 -1
  108. package/dist/index.d.ts +2 -2
  109. package/dist/index.d.ts.map +1 -1
  110. package/dist/index.js +2 -2
  111. package/dist/index.js.map +1 -1
  112. package/dist/main.d.ts.map +1 -1
  113. package/dist/main.js +6 -6
  114. package/dist/main.js.map +1 -1
  115. package/dist/modes/interactive/components/atomic-banner.d.ts +4 -0
  116. package/dist/modes/interactive/components/atomic-banner.d.ts.map +1 -0
  117. package/dist/modes/interactive/components/atomic-banner.js +34 -0
  118. package/dist/modes/interactive/components/atomic-banner.js.map +1 -0
  119. package/dist/modes/interactive/components/chat-message-renderer.d.ts +99 -0
  120. package/dist/modes/interactive/components/chat-message-renderer.d.ts.map +1 -0
  121. package/dist/modes/interactive/components/chat-message-renderer.js +450 -0
  122. package/dist/modes/interactive/components/chat-message-renderer.js.map +1 -0
  123. package/dist/modes/interactive/components/chat-transcript.d.ts +69 -0
  124. package/dist/modes/interactive/components/chat-transcript.d.ts.map +1 -0
  125. package/dist/modes/interactive/components/chat-transcript.js +183 -0
  126. package/dist/modes/interactive/components/chat-transcript.js.map +1 -0
  127. package/dist/modes/interactive/components/footer.d.ts +16 -4
  128. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  129. package/dist/modes/interactive/components/footer.js +110 -137
  130. package/dist/modes/interactive/components/footer.js.map +1 -1
  131. package/dist/modes/interactive/components/index.d.ts +2 -0
  132. package/dist/modes/interactive/components/index.d.ts.map +1 -1
  133. package/dist/modes/interactive/components/index.js +2 -0
  134. package/dist/modes/interactive/components/index.js.map +1 -1
  135. package/dist/modes/interactive/interactive-mode.d.ts +9 -0
  136. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  137. package/dist/modes/interactive/interactive-mode.js +192 -137
  138. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  139. package/dist/modes/interactive/theme/catppuccin-mocha.json +5 -5
  140. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  141. package/dist/modes/rpc/rpc-mode.js +11 -0
  142. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  143. package/dist/utils/tools-manager.d.ts.map +1 -1
  144. package/dist/utils/tools-manager.js +2 -2
  145. package/dist/utils/tools-manager.js.map +1 -1
  146. package/dist/utils/version-check.d.ts.map +1 -1
  147. package/dist/utils/version-check.js +2 -2
  148. package/dist/utils/version-check.js.map +1 -1
  149. package/package.json +1 -1
@@ -37,7 +37,7 @@ import { renderNodeCard } from "./node-card.js";
37
37
  import { renderSwitcher, filterStages } from "./switcher.js";
38
38
  import { renderToasts, createToastManager } from "./toast.js";
39
39
  import { hexToAnsi, hexBg, RESET, BOLD } from "./color-utils.js";
40
- import { fmtDuration, statusIcon, statusColor } from "./status-helpers.js";
40
+ import { fmtDuration } from "./status-helpers.js";
41
41
  import { GraphCanvas } from "./graph-canvas.js";
42
42
  import {
43
43
  createPromptCardState,
@@ -156,6 +156,7 @@ const ANIMATION_TICK_MS = 100;
156
156
  * eased lerp inside `pickBorder` traces one full breath per cycle.
157
157
  */
158
158
  const PULSE_PERIOD_MS = 2000;
159
+ const GRAPH_SCROLL_STEP_ROWS = 4;
159
160
 
160
161
  export class GraphView implements Component {
161
162
  private mode: GraphViewMode;
@@ -182,6 +183,9 @@ export class GraphView implements Component {
182
183
  private detailsExpanded = true;
183
184
  private cachedLayout: LayoutNode[] = [];
184
185
  private currentSnapshot: StoreSnapshot | null = null;
186
+ private graphScrollOffset = 0;
187
+ private graphScrollColOffset = 0;
188
+ private pendingEnsureFocusedVisible = true;
185
189
 
186
190
  private _intervalId: ReturnType<typeof setInterval> | null = null;
187
191
  private _lastGTime: number | null = null;
@@ -231,10 +235,19 @@ export class GraphView implements Component {
231
235
  const run = this._getCurrentRun();
232
236
  if (!run) {
233
237
  this.cachedLayout = [];
238
+ this.focusedIndex = 0;
239
+ this.graphScrollOffset = 0;
240
+ this.graphScrollColOffset = 0;
241
+ this.pendingEnsureFocusedVisible = true;
234
242
  this.promptState = null;
235
243
  return;
236
244
  }
237
- this.cachedLayout = computeLayout(run.stages, { orientation: "vertical" });
245
+
246
+ const previousFocusedStageId = this.cachedLayout[this.focusedIndex]?.stage.id;
247
+ const nextLayout = computeLayout(run.stages, { orientation: "vertical" });
248
+ this.cachedLayout = nextLayout;
249
+
250
+ let focusNeedsReveal = this.pendingEnsureFocusedVisible;
238
251
  // One-shot: if the host passed `initialFocusedStageId`, snap the
239
252
  // cursor to that stage now that the layout exists. The attach shell
240
253
  // uses this when swapping back from chat mode so the focus lands on
@@ -243,9 +256,30 @@ export class GraphView implements Component {
243
256
  const idx = this.cachedLayout.findIndex(
244
257
  (n) => n.stage.id === this.initialFocusedStageId,
245
258
  );
246
- if (idx >= 0) this.focusedIndex = idx;
259
+ if (idx >= 0 && idx !== this.focusedIndex) {
260
+ this.focusedIndex = idx;
261
+ focusNeedsReveal = true;
262
+ }
247
263
  this.initialFocusedStageId = undefined;
264
+ } else if (previousFocusedStageId !== undefined) {
265
+ const idx = this.cachedLayout.findIndex(
266
+ (n) => n.stage.id === previousFocusedStageId,
267
+ );
268
+ if (idx >= 0 && idx !== this.focusedIndex) {
269
+ this.focusedIndex = idx;
270
+ focusNeedsReveal = true;
271
+ }
272
+ }
273
+
274
+ if (this.cachedLayout.length === 0) {
275
+ this.focusedIndex = 0;
276
+ this.graphScrollOffset = 0;
277
+ this.graphScrollColOffset = 0;
278
+ } else if (this.focusedIndex >= this.cachedLayout.length) {
279
+ this.focusedIndex = this.cachedLayout.length - 1;
280
+ focusNeedsReveal = true;
248
281
  }
282
+ this.pendingEnsureFocusedVisible = focusNeedsReveal;
249
283
  this._syncPromptState(run.pendingPrompt);
250
284
  }
251
285
 
@@ -344,13 +378,16 @@ export class GraphView implements Component {
344
378
  // stage panel — status colour on each card carries that signal.
345
379
  const graphLines = this._renderGraph(frameWidth);
346
380
  const bodyTarget = this._overlayBodyRows(this._overlayLineCount());
347
- // Vertically centre the graph in the body band.
348
- const topPad = Math.max(
349
- 0,
350
- Math.floor((bodyTarget - graphLines.length) / 2),
381
+ const visibleGraph = this._visibleGraphLines(
382
+ graphLines,
383
+ frameWidth,
384
+ bodyTarget,
351
385
  );
352
- for (let i = 0; i < topPad; i++) lines.push(this._blankRow(frameWidth));
353
- for (const line of graphLines) {
386
+ // Vertically centre short graphs; tall graphs are clipped to the
387
+ // scroll window managed by keyboard focus and mouse wheel events.
388
+ for (let i = 0; i < visibleGraph.topPad; i++)
389
+ lines.push(this._blankRow(frameWidth));
390
+ for (const line of visibleGraph.lines) {
354
391
  lines.push(this._canvasRow(line, frameWidth));
355
392
  }
356
393
  while (lines.length < 3 + bodyTarget)
@@ -367,9 +404,11 @@ export class GraphView implements Component {
367
404
  const insertAt = 4; // beneath the header chrome band
368
405
  for (let i = 0; i < switcherLines.length; i++) {
369
406
  const lineIdx = insertAt + i;
370
- const padded = this._padCanvas(switcherLines[i]!, frameWidth);
371
- if (lineIdx < lines.length) lines[lineIdx] = padded;
372
- else lines.push(padded);
407
+ const overlay = this._padCanvas(switcherLines[i]!, switcherWidth);
408
+ const base = lines[lineIdx] ?? this._blankRow(frameWidth);
409
+ const merged = this._overlayInline(base, overlay, 0, frameWidth);
410
+ if (lineIdx < lines.length) lines[lineIdx] = merged;
411
+ else lines.push(merged);
373
412
  }
374
413
  }
375
414
 
@@ -466,13 +505,21 @@ export class GraphView implements Component {
466
505
  (max, node) => Math.max(max, node.x + NODE_W),
467
506
  0,
468
507
  );
469
- if (canvasWidth > graphInner || this.cachedLayout.length > 12) {
470
- return this._renderCompactGraph(run.stages, graphInner);
508
+ // Centre the whole graph horizontally when it fits; otherwise keep a
509
+ // small gutter and reveal focused nodes by horizontally scrolling the
510
+ // graph canvas. Do not switch to a compact list: the orchestrator pane
511
+ // should always preserve the node-card graph view.
512
+ const leftMargin = Math.max(
513
+ 2,
514
+ canvasWidth <= graphInner ? Math.floor((graphInner - canvasWidth) / 2) : 2,
515
+ );
516
+ const viewportWidth = Math.max(1, width - leftMargin);
517
+ const fullCanvasWidth = Math.max(canvasWidth, viewportWidth);
518
+ this._clampGraphHorizontalScroll(fullCanvasWidth, viewportWidth);
519
+ if (this.pendingEnsureFocusedVisible) {
520
+ this._scrollFocusedColumnIntoView(viewportWidth, fullCanvasWidth);
471
521
  }
472
522
 
473
- // Centre the whole graph horizontally in the available canvas.
474
- const leftMargin = Math.max(2, Math.floor((graphInner - canvasWidth) / 2));
475
-
476
523
  // Pulse phase ∈ [0, 1) derived from wall-clock time so cards lerp
477
524
  // their border colour on the same beat regardless of how often
478
525
  // render() fires. The animation tick (`ANIMATION_TICK_MS`) only
@@ -545,17 +592,106 @@ export class GraphView implements Component {
545
592
  composed.push(this._composeRow(edgeRowChars, cards, edgeColor));
546
593
  }
547
594
 
548
- // Pad each row out to the full overlay width with the body bg so
549
- // nothing leaks through to the terminal default — cards/edges only
550
- // paint the cells they occupy; everywhere else needs explicit bg.
595
+ // Pad the full graph canvas, then crop a horizontal viewport when the
596
+ // fan-out is wider than the terminal. Cards/edges only paint cells they
597
+ // occupy; everywhere else needs explicit bg so default terminal colours
598
+ // never leak through.
551
599
  const bg = hexBg(this.graphTheme.bg);
552
600
  const leftPad = `${bg}${" ".repeat(leftMargin)}${RESET}`;
553
601
  return composed.map((line) => {
554
- const inner = this._padCanvas(line, width - leftMargin);
555
- return `${leftPad}${inner}`;
602
+ const full = this._padCanvas(line, fullCanvasWidth);
603
+ const cells = this._splitVisible(full);
604
+ const sliced = this._sliceVisible(
605
+ cells,
606
+ this.graphScrollColOffset,
607
+ this.graphScrollColOffset + viewportWidth,
608
+ );
609
+ return `${leftPad}${this._padCanvas(sliced, viewportWidth)}`;
556
610
  });
557
611
  }
558
612
 
613
+ private _visibleGraphLines(
614
+ graphLines: string[],
615
+ frameWidth: number,
616
+ bodyRows: number,
617
+ ): { lines: string[]; topPad: number } {
618
+ if (graphLines.length <= bodyRows) {
619
+ this.graphScrollOffset = 0;
620
+ this.pendingEnsureFocusedVisible = false;
621
+ return {
622
+ lines: graphLines,
623
+ topPad: Math.max(0, Math.floor((bodyRows - graphLines.length) / 2)),
624
+ };
625
+ }
626
+
627
+ this._clampGraphScroll(graphLines.length, bodyRows);
628
+ if (this.pendingEnsureFocusedVisible) {
629
+ this._scrollFocusedIntoView(frameWidth, bodyRows, graphLines.length);
630
+ this.pendingEnsureFocusedVisible = false;
631
+ }
632
+ this._clampGraphScroll(graphLines.length, bodyRows);
633
+ return {
634
+ lines: graphLines.slice(
635
+ this.graphScrollOffset,
636
+ this.graphScrollOffset + bodyRows,
637
+ ),
638
+ topPad: 0,
639
+ };
640
+ }
641
+
642
+ private _clampGraphScroll(totalRows: number, bodyRows: number): void {
643
+ const maxOffset = Math.max(0, totalRows - bodyRows);
644
+ this.graphScrollOffset = Math.max(
645
+ 0,
646
+ Math.min(maxOffset, this.graphScrollOffset),
647
+ );
648
+ }
649
+
650
+ private _clampGraphHorizontalScroll(totalCols: number, viewportCols: number): void {
651
+ const maxOffset = Math.max(0, totalCols - viewportCols);
652
+ this.graphScrollColOffset = Math.max(
653
+ 0,
654
+ Math.min(maxOffset, this.graphScrollColOffset),
655
+ );
656
+ }
657
+
658
+ private _scrollFocusedColumnIntoView(
659
+ viewportCols: number,
660
+ totalCols: number,
661
+ ): void {
662
+ const node = this.cachedLayout[this.focusedIndex];
663
+ if (!node) return;
664
+ const start = node.x;
665
+ const end = node.x + NODE_W - 1;
666
+ if (start < this.graphScrollColOffset) {
667
+ this.graphScrollColOffset = start;
668
+ } else if (end >= this.graphScrollColOffset + viewportCols) {
669
+ this.graphScrollColOffset = end - viewportCols + 1;
670
+ }
671
+ this._clampGraphHorizontalScroll(totalCols, viewportCols);
672
+ }
673
+
674
+ private _scrollFocusedIntoView(
675
+ frameWidth: number,
676
+ bodyRows: number,
677
+ totalRows: number,
678
+ ): void {
679
+ const range = this._focusedGraphRowRange(frameWidth);
680
+ if (!range) return;
681
+ if (range.start < this.graphScrollOffset) {
682
+ this.graphScrollOffset = range.start;
683
+ } else if (range.end >= this.graphScrollOffset + bodyRows) {
684
+ this.graphScrollOffset = range.end - bodyRows + 1;
685
+ }
686
+ this._clampGraphScroll(totalRows, bodyRows);
687
+ }
688
+
689
+ private _focusedGraphRowRange(frameWidth: number): { start: number; end: number } | null {
690
+ const node = this.cachedLayout[this.focusedIndex];
691
+ if (!node) return null;
692
+ return { start: node.y, end: node.y + NODE_H - 1 };
693
+ }
694
+
559
695
  /**
560
696
  * Plot a parent → child edge for the vertical orientation. The edge
561
697
  * exits from the parent's bottom-centre, runs through a horizontal
@@ -790,45 +926,6 @@ export class GraphView implements Component {
790
926
  return out;
791
927
  }
792
928
 
793
- private _renderCompactGraph(
794
- stages: readonly StageSnapshot[],
795
- width: number,
796
- ): string[] {
797
- const t = this.graphTheme;
798
- const dim = hexToAnsi(t.dim);
799
- const muted = hexToAnsi(t.textMuted);
800
- const accent = hexToAnsi(t.accent);
801
- const byId = new Map(stages.map((stage) => [stage.id, stage]));
802
-
803
- const lines: string[] = [];
804
- for (let i = 0; i < this.cachedLayout.length; i++) {
805
- const node = this.cachedLayout[i]!;
806
- const stage = node.stage;
807
- const focused = i === this.focusedIndex;
808
- const parents = stage.parentIds
809
- .map((id) => byId.get(id)?.name ?? id)
810
- .filter((name) => name.length > 0);
811
- const parentLabel = parents.length > 0 ? ` ← ${parents.join(", ")}` : "";
812
- const blockedBy = stage.blockedByStageId
813
- ? (byId.get(stage.blockedByStageId)?.name ?? stage.blockedByStageId)
814
- : "";
815
- const blockedLabel = blockedBy ? ` · blocked by ${blockedBy}` : "";
816
- const dur = this._duration(stage);
817
- const cursor = focused ? `${accent}❯${RESET}` : ` `;
818
- const sc = hexToAnsi(statusColor(stage.status, t));
819
- const nameStyled = focused
820
- ? `${accent}${BOLD}${stage.name}${RESET}`
821
- : `${hexToAnsi(t.text)}${stage.name}${RESET}`;
822
- const meta = `${dim}${stage.status}${dur ? ` · ${dur}` : ""}${blockedLabel}${parentLabel}${RESET}`;
823
- const line = ` ${cursor} ${sc}${statusIcon(stage.status)}${RESET} ${nameStyled} ${meta}`;
824
- lines.push(truncateToWidth(line, width, "…", true));
825
- }
826
- if (lines.length === 0) {
827
- lines.push(` ${muted}(no stages)${RESET}`);
828
- }
829
- return lines;
830
- }
831
-
832
929
  // -------------------------------------------------------------------------
833
930
  // Chrome / canvas / section helpers
834
931
  // -------------------------------------------------------------------------
@@ -927,6 +1024,30 @@ export class GraphView implements Component {
927
1024
  return `${bg}${" ".repeat(leftPad)}${RESET}${cardLine}${bg}${" ".repeat(rightPadLen)}${RESET}`;
928
1025
  }
929
1026
 
1027
+ /** Overlay a fixed-width panel on a row while preserving graph cells
1028
+ * outside the panel bounds. Used by the stage switcher so the picker
1029
+ * does not erase nodes to its right. */
1030
+ private _overlayInline(
1031
+ base: string,
1032
+ overlay: string,
1033
+ leftPad: number,
1034
+ totalWidth: number,
1035
+ ): string {
1036
+ const baseCells = this._splitVisible(base);
1037
+ const overlayWidth = Math.min(
1038
+ Math.max(0, totalWidth - leftPad),
1039
+ visibleWidth(overlay),
1040
+ );
1041
+ const left = this._sliceVisible(baseCells, 0, leftPad);
1042
+ const panel = truncateToWidth(overlay, overlayWidth, "", true);
1043
+ const right = this._sliceVisible(
1044
+ baseCells,
1045
+ leftPad + overlayWidth,
1046
+ totalWidth,
1047
+ );
1048
+ return `${left}${panel}${right}`;
1049
+ }
1050
+
930
1051
  private _duration(stage: StageSnapshot): string {
931
1052
  if (stage.durationMs != null) return fmtDuration(stage.durationMs);
932
1053
  if (stage.startedAt != null)
@@ -1006,6 +1127,11 @@ export class GraphView implements Component {
1006
1127
 
1007
1128
  private _handleGraphInput(data: string): boolean {
1008
1129
  const stageCount = this.cachedLayout.length;
1130
+ const wheelDeltaRows = this._mouseWheelDeltaRows(data);
1131
+ if (wheelDeltaRows !== 0) {
1132
+ this._scrollGraphBy(wheelDeltaRows);
1133
+ return true;
1134
+ }
1009
1135
 
1010
1136
  // Vertical-graph navigation: up/down step between depth levels
1011
1137
  // (col), left/right step between siblings at the same depth (row).
@@ -1019,17 +1145,17 @@ export class GraphView implements Component {
1019
1145
  if (matchesKey(data, "left") || data === "\x1b[D")
1020
1146
  return this._moveBySibling(-1);
1021
1147
  if (matchesKey(data, "j")) {
1022
- this.focusedIndex = Math.min(this.focusedIndex + 1, stageCount - 1);
1148
+ this._setFocusedIndex(Math.min(this.focusedIndex + 1, stageCount - 1));
1023
1149
  return true;
1024
1150
  }
1025
1151
  if (matchesKey(data, "k")) {
1026
- this.focusedIndex = Math.max(this.focusedIndex - 1, 0);
1152
+ this._setFocusedIndex(Math.max(this.focusedIndex - 1, 0));
1027
1153
  return true;
1028
1154
  }
1029
1155
  if (matchesKey(data, "g")) {
1030
1156
  const now = Date.now();
1031
1157
  if (this._lastGTime != null && now - this._lastGTime < 500) {
1032
- this.focusedIndex = 0;
1158
+ this._setFocusedIndex(0);
1033
1159
  this._lastGTime = null;
1034
1160
  } else {
1035
1161
  this._lastGTime = now;
@@ -1046,14 +1172,7 @@ export class GraphView implements Component {
1046
1172
  // attach shell swaps in the stage-chat view without remounting
1047
1173
  // the overlay; without a callback, fall back to the legacy
1048
1174
  // expand/collapse toggle so non-attach hosts still work.
1049
- if (this.onStageAttach) {
1050
- const node = this.cachedLayout[this.focusedIndex];
1051
- const run = this._getCurrentRun();
1052
- if (node && run) {
1053
- this.onStageAttach(run.id, node.stage.id);
1054
- return true;
1055
- }
1056
- }
1175
+ if (this._attachFocusedStage()) return true;
1057
1176
  this.detailsExpanded = !this.detailsExpanded;
1058
1177
  return true;
1059
1178
  }
@@ -1103,7 +1222,14 @@ export class GraphView implements Component {
1103
1222
  const idx = this.cachedLayout.findIndex(
1104
1223
  (n) => n.stage.id === selected.id,
1105
1224
  );
1106
- if (idx !== -1) this.focusedIndex = idx;
1225
+ if (idx !== -1) {
1226
+ this._setFocusedIndex(idx);
1227
+ // Selecting from the `/` switcher should complete the same
1228
+ // action as pressing Enter on a graph node: jump straight
1229
+ // into that stage's chat when the attach shell is present.
1230
+ this.switcherOpen = false;
1231
+ if (this._attachFocusedStage()) return true;
1232
+ }
1107
1233
  }
1108
1234
  this.switcherOpen = false;
1109
1235
  return true;
@@ -1180,7 +1306,7 @@ export class GraphView implements Component {
1180
1306
  bestDist = d;
1181
1307
  }
1182
1308
  }
1183
- this.focusedIndex = best.i;
1309
+ this._setFocusedIndex(best.i);
1184
1310
  return true;
1185
1311
  }
1186
1312
 
@@ -1200,10 +1326,51 @@ export class GraphView implements Component {
1200
1326
  if (pos === -1) return true;
1201
1327
  const next = siblings[pos + step];
1202
1328
  if (!next) return true;
1203
- this.focusedIndex = next.i;
1329
+ this._setFocusedIndex(next.i);
1330
+ return true;
1331
+ }
1332
+
1333
+ private _attachFocusedStage(): boolean {
1334
+ if (!this.onStageAttach) return false;
1335
+ const node = this.cachedLayout[this.focusedIndex];
1336
+ const run = this._getCurrentRun();
1337
+ if (!node || !run) return false;
1338
+ this.onStageAttach(run.id, node.stage.id);
1204
1339
  return true;
1205
1340
  }
1206
1341
 
1342
+ private _setFocusedIndex(index: number): void {
1343
+ const max = Math.max(0, this.cachedLayout.length - 1);
1344
+ const next = Math.max(0, Math.min(index, max));
1345
+ if (next === this.focusedIndex) return;
1346
+ this.focusedIndex = next;
1347
+ this.pendingEnsureFocusedVisible = true;
1348
+ }
1349
+
1350
+ private _scrollGraphBy(deltaRows: number): void {
1351
+ this.pendingEnsureFocusedVisible = false;
1352
+ this.graphScrollOffset = Math.max(0, this.graphScrollOffset + deltaRows);
1353
+ }
1354
+
1355
+ private _mouseWheelDeltaRows(data: string): number {
1356
+ const sgr = data.match(/^\x1b\[<(\d+);\d+;\d+M$/);
1357
+ if (sgr) {
1358
+ return this._wheelDeltaForButtonCode(Number.parseInt(sgr[1]!, 10));
1359
+ }
1360
+ if (data.startsWith("\x1b[M") && data.length >= 6) {
1361
+ return this._wheelDeltaForButtonCode(data.charCodeAt(3) - 32);
1362
+ }
1363
+ return 0;
1364
+ }
1365
+
1366
+ private _wheelDeltaForButtonCode(code: number): number {
1367
+ if ((code & 64) === 0) return 0;
1368
+ const direction = code & 3;
1369
+ if (direction === 0) return -GRAPH_SCROLL_STEP_ROWS;
1370
+ if (direction === 1) return GRAPH_SCROLL_STEP_ROWS;
1371
+ return 0;
1372
+ }
1373
+
1207
1374
  // ---- test seams ----
1208
1375
  get _focusedIndex(): number {
1209
1376
  return this.focusedIndex;
@@ -1214,4 +1381,7 @@ export class GraphView implements Component {
1214
1381
  get _switcherState(): SwitcherState {
1215
1382
  return this.switcherState;
1216
1383
  }
1384
+ get _graphScrollOffset(): number {
1385
+ return this.graphScrollOffset;
1386
+ }
1217
1387
  }
@@ -16,6 +16,7 @@
16
16
  */
17
17
 
18
18
  import type { Store } from "../shared/store.js";
19
+ import type { ChatMessageRenderOptions } from "@bastani/atomic";
19
20
  import { WorkflowAttachPane } from "./workflow-attach-pane.js";
20
21
  import { deriveGraphThemeFromPiTheme } from "./graph-theme.js";
21
22
  import { killRun } from "../runs/background/status.js";
@@ -34,9 +35,12 @@ import type {
34
35
  PiTheme,
35
36
  } from "../extension/wiring.js";
36
37
 
38
+ export type OverlayChatRenderSettings = Partial<Omit<ChatMessageRenderOptions, "ui" | "cwd" | "markdownTheme">>;
39
+
37
40
  export interface OverlayUISurface {
38
41
  custom?: PiCustomOverlayFunction;
39
42
  getEditorComponent?: () => PiEditorFactory | undefined;
43
+ getChatRenderSettings?: () => OverlayChatRenderSettings | undefined;
40
44
  }
41
45
 
42
46
  export interface OverlayPiSurface {
@@ -86,6 +90,14 @@ const FULLSCREEN_OVERLAY_OPTIONS: PiOverlayOptions = {
86
90
  margin: 0,
87
91
  };
88
92
 
93
+ const MOUSE_SCROLL_TRACKING_ON = "\x1b[?1000h\x1b[?1006h";
94
+ const MOUSE_SCROLL_TRACKING_OFF = "\x1b[?1006l\x1b[?1000l";
95
+
96
+ function setMouseScrollTracking(enabled: boolean): void {
97
+ if (!process.stdout.isTTY) return;
98
+ process.stdout.write(enabled ? MOUSE_SCROLL_TRACKING_ON : MOUSE_SCROLL_TRACKING_OFF);
99
+ }
100
+
89
101
  export interface BuildGraphOverlayAdapterOpts {
90
102
  /**
91
103
  * Live stage-control registry threaded through to the attach shell.
@@ -110,6 +122,7 @@ export function buildGraphOverlayAdapter(
110
122
  let finishMounted: (() => void) | null = null;
111
123
 
112
124
  function close(): void {
125
+ setMouseScrollTracking(false);
113
126
  currentHandle?.hide();
114
127
  finishMounted?.();
115
128
  currentView?.dispose();
@@ -136,6 +149,7 @@ export function buildGraphOverlayAdapter(
136
149
  * running and can be re-attached.
137
150
  */
138
151
  function hideMounted(): void {
152
+ setMouseScrollTracking(false);
139
153
  if (currentHandle) {
140
154
  currentHandle.setHidden(true);
141
155
  currentHandle.unfocus();
@@ -164,6 +178,7 @@ export function buildGraphOverlayAdapter(
164
178
  },
165
179
  invalidate: () => tui.requestRender?.(),
166
180
  dispose: () => {
181
+ setMouseScrollTracking(false);
167
182
  unsubscribe();
168
183
  view.dispose();
169
184
  },
@@ -177,6 +192,7 @@ export function buildGraphOverlayAdapter(
177
192
  ): void {
178
193
  // Already mounted but hidden — flip visibility without remounting.
179
194
  if (mounted && currentHandle?.isHidden()) {
195
+ setMouseScrollTracking(currentView?.wantsMouseScrollTracking() ?? true);
180
196
  currentHandle.setHidden(false);
181
197
  currentHandle.focus();
182
198
  return;
@@ -198,6 +214,7 @@ export function buildGraphOverlayAdapter(
198
214
  const finish = (): void => {
199
215
  if (settled) return;
200
216
  settled = true;
217
+ setMouseScrollTracking(false);
201
218
  currentView?.dispose();
202
219
  currentView = null;
203
220
  currentHandle = null;
@@ -221,6 +238,7 @@ export function buildGraphOverlayAdapter(
221
238
  piTheme: theme,
222
239
  piKeybindings: keybindings,
223
240
  piEditorFactory: ui?.getEditorComponent?.(),
241
+ getChatRenderSettings: ui?.getChatRenderSettings,
224
242
  // Pi-tui owns terminal dimensions; thread its row count down
225
243
  // so the overlay frame fills the actual viewport rather than
226
244
  // a hard-coded 32-row rectangle. Returning `undefined` keeps
@@ -236,6 +254,7 @@ export function buildGraphOverlayAdapter(
236
254
  if (currentHandle?.isHidden() === true) return;
237
255
  tui.requestRender?.();
238
256
  },
257
+ setMouseScrollTracking,
239
258
  } as ConstructorParameters<typeof WorkflowAttachPane>[0] & {
240
259
  piTui?: PiCustomOverlayFactoryTui;
241
260
  piTheme?: PiTheme;
@@ -244,6 +263,7 @@ export function buildGraphOverlayAdapter(
244
263
  currentView = view;
245
264
  finishMounted = finish;
246
265
  mounted = true;
266
+ setMouseScrollTracking(view.wantsMouseScrollTracking());
247
267
  return makeComponent(view, tui);
248
268
  };
249
269
 
@@ -262,6 +282,9 @@ export function buildGraphOverlayAdapter(
262
282
  // no scroll-pollution).
263
283
  if (mounted && currentHandle) {
264
284
  const nowHidden = !currentHandle.isHidden();
285
+ setMouseScrollTracking(
286
+ nowHidden ? false : currentView?.wantsMouseScrollTracking() ?? true,
287
+ );
265
288
  currentHandle.setHidden(nowHidden);
266
289
  if (!nowHidden) currentHandle.focus();
267
290
  return;