@bastani/atomic 0.8.1 → 0.8.2-0
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.
- package/CHANGELOG.md +6 -0
- package/dist/builtin/intercom/config.ts +3 -4
- package/dist/builtin/intercom/index.ts +6 -6
- package/dist/builtin/intercom/package.json +1 -1
- package/dist/builtin/mcp/agent-dir.ts +11 -2
- package/dist/builtin/mcp/cli.js +12 -6
- package/dist/builtin/mcp/config.ts +31 -22
- package/dist/builtin/mcp/package.json +1 -1
- package/dist/builtin/subagents/package.json +1 -1
- package/dist/builtin/subagents/src/agents/agents.ts +63 -23
- package/dist/builtin/subagents/src/agents/skills.ts +21 -21
- package/dist/builtin/subagents/src/extension/index.ts +9 -8
- package/dist/builtin/subagents/src/runs/shared/run-history.ts +13 -10
- package/dist/builtin/subagents/src/runs/shared/subagent-prompt-runtime.ts +3 -3
- package/dist/builtin/subagents/src/shared/artifacts.ts +18 -17
- package/dist/builtin/subagents/src/shared/types.ts +4 -4
- package/dist/builtin/web-access/config-paths.ts +11 -0
- package/dist/builtin/web-access/exa.ts +3 -2
- package/dist/builtin/web-access/gemini-api.ts +2 -1
- package/dist/builtin/web-access/gemini-search.ts +2 -1
- package/dist/builtin/web-access/gemini-web-config.ts +2 -1
- package/dist/builtin/web-access/github-extract.ts +2 -1
- package/dist/builtin/web-access/index.ts +11 -8
- package/dist/builtin/web-access/package.json +1 -1
- package/dist/builtin/web-access/perplexity.ts +2 -1
- package/dist/builtin/web-access/video-extract.ts +2 -1
- package/dist/builtin/web-access/youtube-extract.ts +2 -1
- package/dist/builtin/workflows/builtin/deep-research-codebase.ts +4 -0
- package/dist/builtin/workflows/builtin/open-claude-design.ts +39 -22
- package/dist/builtin/workflows/builtin/ralph.ts +7 -0
- package/dist/builtin/workflows/package.json +1 -1
- package/dist/builtin/workflows/skills/workflow/SKILL.md +28 -20
- package/dist/builtin/workflows/skills/workflow/references/design-checklist.md +8 -4
- package/dist/builtin/workflows/skills/workflow/references/running-workflows.md +52 -23
- package/dist/builtin/workflows/skills/workflow/references/sdk-authoring.md +41 -12
- package/dist/builtin/workflows/src/extension/config-loader.ts +13 -14
- package/dist/builtin/workflows/src/extension/discovery.ts +4 -6
- package/dist/builtin/workflows/src/extension/index.ts +675 -524
- package/dist/builtin/workflows/src/extension/runtime.ts +40 -16
- package/dist/builtin/workflows/src/extension/wiring.ts +3 -0
- package/dist/builtin/workflows/src/extension/workflow-schema.ts +43 -33
- package/dist/builtin/workflows/src/runs/foreground/executor.ts +34 -10
- package/dist/builtin/workflows/src/shared/types.ts +1 -5
- package/dist/builtin/workflows/src/tui/graph-view.ts +245 -75
- package/dist/builtin/workflows/src/tui/overlay-adapter.ts +23 -0
- package/dist/builtin/workflows/src/tui/stage-chat-view.ts +259 -149
- package/dist/builtin/workflows/src/tui/status-helpers.ts +3 -3
- package/dist/builtin/workflows/src/tui/store-widget-installer.ts +99 -10
- package/dist/builtin/workflows/src/tui/switcher.ts +4 -5
- package/dist/builtin/workflows/src/tui/workflow-attach-pane.ts +29 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +11 -8
- package/dist/cli/args.js.map +1 -1
- package/dist/config.d.ts +21 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +59 -4
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session.d.ts +1 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +2 -2
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/auth-storage.d.ts +3 -1
- package/dist/core/auth-storage.d.ts.map +1 -1
- package/dist/core/auth-storage.js +31 -8
- package/dist/core/auth-storage.js.map +1 -1
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +9 -0
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +11 -0
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/model-registry.d.ts +3 -2
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +25 -8
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/package-manager.d.ts +3 -0
- package/dist/core/package-manager.d.ts.map +1 -1
- package/dist/core/package-manager.js +97 -58
- package/dist/core/package-manager.js.map +1 -1
- package/dist/core/resource-loader.d.ts +1 -0
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +37 -36
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/sdk.d.ts +5 -4
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +2 -2
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/settings-manager.d.ts +7 -1
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +29 -8
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/system-prompt.d.ts +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/telemetry.d.ts.map +1 -1
- package/dist/core/telemetry.js +2 -2
- package/dist/core/telemetry.js.map +1 -1
- package/dist/core/timings.d.ts.map +1 -1
- package/dist/core/timings.js +2 -2
- package/dist/core/timings.js.map +1 -1
- package/dist/core/tools/index.d.ts +1 -0
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +8 -0
- package/dist/core/tools/index.js.map +1 -1
- package/dist/core/tools/todos.d.ts.map +1 -1
- package/dist/core/tools/todos.js +3 -3
- package/dist/core/tools/todos.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +6 -6
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/atomic-banner.d.ts +4 -0
- package/dist/modes/interactive/components/atomic-banner.d.ts.map +1 -0
- package/dist/modes/interactive/components/atomic-banner.js +34 -0
- package/dist/modes/interactive/components/atomic-banner.js.map +1 -0
- package/dist/modes/interactive/components/chat-message-renderer.d.ts +99 -0
- package/dist/modes/interactive/components/chat-message-renderer.d.ts.map +1 -0
- package/dist/modes/interactive/components/chat-message-renderer.js +450 -0
- package/dist/modes/interactive/components/chat-message-renderer.js.map +1 -0
- package/dist/modes/interactive/components/chat-transcript.d.ts +69 -0
- package/dist/modes/interactive/components/chat-transcript.d.ts.map +1 -0
- package/dist/modes/interactive/components/chat-transcript.js +183 -0
- package/dist/modes/interactive/components/chat-transcript.js.map +1 -0
- package/dist/modes/interactive/components/footer.d.ts +16 -4
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +110 -137
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/index.d.ts +2 -0
- package/dist/modes/interactive/components/index.d.ts.map +1 -1
- package/dist/modes/interactive/components/index.js +2 -0
- package/dist/modes/interactive/components/index.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +9 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +192 -137
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/theme/catppuccin-mocha.json +5 -5
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +11 -0
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/utils/tools-manager.d.ts.map +1 -1
- package/dist/utils/tools-manager.js +2 -2
- package/dist/utils/tools-manager.js.map +1 -1
- package/dist/utils/version-check.d.ts.map +1 -1
- package/dist/utils/version-check.js +2 -2
- package/dist/utils/version-check.js.map +1 -1
- 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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
381
|
+
const visibleGraph = this._visibleGraphLines(
|
|
382
|
+
graphLines,
|
|
383
|
+
frameWidth,
|
|
384
|
+
bodyTarget,
|
|
351
385
|
);
|
|
352
|
-
|
|
353
|
-
|
|
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
|
|
371
|
-
|
|
372
|
-
|
|
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
|
-
|
|
470
|
-
|
|
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
|
|
549
|
-
//
|
|
550
|
-
//
|
|
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
|
|
555
|
-
|
|
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.
|
|
1148
|
+
this._setFocusedIndex(Math.min(this.focusedIndex + 1, stageCount - 1));
|
|
1023
1149
|
return true;
|
|
1024
1150
|
}
|
|
1025
1151
|
if (matchesKey(data, "k")) {
|
|
1026
|
-
this.
|
|
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.
|
|
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.
|
|
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)
|
|
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.
|
|
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.
|
|
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;
|