@bastani/atomic 0.9.19-alpha.7 → 0.9.19-alpha.9

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.
@@ -69012,9 +69012,9 @@ function renderDetailHintRows(detail, width, theme) {
69012
69012
  }
69013
69013
  function formatTime(ms) {
69014
69014
  const d = new Date(ms);
69015
- const hh = String(d.getUTCHours()).padStart(2, "0");
69016
- const mm = String(d.getUTCMinutes()).padStart(2, "0");
69017
- const ss = String(d.getUTCSeconds()).padStart(2, "0");
69015
+ const hh = String(d.getHours()).padStart(2, "0");
69016
+ const mm = String(d.getMinutes()).padStart(2, "0");
69017
+ const ss = String(d.getSeconds()).padStart(2, "0");
69018
69018
  return `${hh}:${mm}:${ss}`;
69019
69019
  }
69020
69020
 
@@ -72599,7 +72599,7 @@ class GraphCanvas {
72599
72599
 
72600
72600
  // dist/builtin/workflows/src/tui/layout.ts
72601
72601
  var NODE_W = 24;
72602
- var NODE_H = 6;
72602
+ var NODE_H = 5;
72603
72603
  function computeLayout(stages, opts = {}) {
72604
72604
  const colGap = opts.colGap ?? 4;
72605
72605
  const rowGap = opts.rowGap ?? 3;
@@ -72682,17 +72682,255 @@ function computeLayout(stages, opts = {}) {
72682
72682
  rowIndex.set(ids[i], i);
72683
72683
  }
72684
72684
  }
72685
+ const heights = new Map(stages.map((stage) => [stage.id, opts.nodeHeight?.(stage) ?? NODE_H]));
72686
+ const verticalOffsets = new Map;
72687
+ const horizontalOffsets = new Map;
72688
+ let depthY = 0;
72689
+ for (const depth of [...colGroups.keys()].sort((a, b) => a - b)) {
72690
+ const ids = colGroups.get(depth);
72691
+ verticalOffsets.set(depth, depthY);
72692
+ let siblingY = 0;
72693
+ let tallest = 0;
72694
+ for (const id of ids) {
72695
+ const height = heights.get(id);
72696
+ horizontalOffsets.set(id, siblingY);
72697
+ siblingY += height + rowGap;
72698
+ tallest = Math.max(tallest, height);
72699
+ }
72700
+ depthY += tallest + rowGap;
72701
+ }
72685
72702
  const nodes = [];
72686
72703
  for (const s of stages) {
72687
72704
  const col = colMap.get(s.id) ?? 0;
72688
72705
  const row = rowIndex.get(s.id) ?? 0;
72689
72706
  const x = orientation === "vertical" ? row * (NODE_W + colGap) + (centreShift.get(col) ?? 0) : col * (NODE_W + colGap);
72690
- const y = orientation === "vertical" ? col * (NODE_H + rowGap) : row * (NODE_H + rowGap);
72691
- nodes.push({ stage: s, col, row, x, y });
72707
+ const y = orientation === "vertical" ? verticalOffsets.get(col) : horizontalOffsets.get(s.id);
72708
+ nodes.push({ stage: s, col, row, x, y, ...opts.nodeHeight ? { height: heights.get(s.id) } : {} });
72692
72709
  }
72693
72710
  return nodes;
72694
72711
  }
72695
72712
 
72713
+ // dist/builtin/workflows/src/tui/node-card.ts
72714
+ import { stripTerminalSequences } from "@earendil-works/pi-tui";
72715
+ function pulseT(phase) {
72716
+ return (Math.sin(phase * Math.PI * 2 - Math.PI / 2) + 1) / 2;
72717
+ }
72718
+ function queuedBadgeCount(count) {
72719
+ if (typeof count !== "number" || !Number.isFinite(count))
72720
+ return 0;
72721
+ return Math.max(0, Math.trunc(count));
72722
+ }
72723
+ function queuedBadgeText(count) {
72724
+ return `✉ ${count} queued`;
72725
+ }
72726
+ function pickBorder(status, focused, phase, theme) {
72727
+ switch (status) {
72728
+ case "running":
72729
+ if (focused)
72730
+ return theme.warning;
72731
+ return lerpColor(theme.borderDim, theme.warning, pulseT(phase));
72732
+ case "paused":
72733
+ return theme.warning;
72734
+ case "awaiting_input":
72735
+ if (focused)
72736
+ return theme.info;
72737
+ return lerpColor(theme.borderDim, theme.info, pulseT(phase));
72738
+ case "completed":
72739
+ return theme.success;
72740
+ case "failed":
72741
+ return theme.error;
72742
+ case "blocked":
72743
+ return theme.dim;
72744
+ case "skipped":
72745
+ return theme.dim;
72746
+ default:
72747
+ return focused ? theme.borderActive : theme.borderDim;
72748
+ }
72749
+ }
72750
+ function durationColor(status, theme) {
72751
+ switch (status) {
72752
+ case "running":
72753
+ return theme.warning;
72754
+ case "paused":
72755
+ return theme.warning;
72756
+ case "awaiting_input":
72757
+ return theme.info;
72758
+ case "completed":
72759
+ return theme.success;
72760
+ case "failed":
72761
+ return theme.error;
72762
+ default:
72763
+ return theme.dim;
72764
+ }
72765
+ }
72766
+ function blockedBadgeText(stage, stages, width) {
72767
+ const base = "↑ blocked";
72768
+ const blockedBy = stage.blockedByStageId;
72769
+ if (!blockedBy)
72770
+ return base;
72771
+ const upstream = stages?.find((s) => s.id === blockedBy)?.name ?? blockedBy;
72772
+ const withUpstream = `${base} by ${upstream}`;
72773
+ if (visibleWidth(withUpstream) <= width)
72774
+ return withUpstream;
72775
+ return base;
72776
+ }
72777
+ function durationText(stage) {
72778
+ const elapsed = elapsedStageMs(stage);
72779
+ return elapsed === undefined ? "—" : fmtDuration(elapsed);
72780
+ }
72781
+ function metaText(stage) {
72782
+ return stage.topologyState === "unavailable" ? "topology unavailable" : "";
72783
+ }
72784
+ function modelText(stage, innerWidth) {
72785
+ const model = stage.model;
72786
+ if (model === undefined || model === "")
72787
+ return "";
72788
+ const slash = model.lastIndexOf("/");
72789
+ const short = slash >= 0 ? model.slice(slash + 1) : model;
72790
+ const level = stage.thinkingLevel;
72791
+ const showLevel = level !== undefined && level !== "" && level !== "off";
72792
+ const suffix = showLevel ? ` · ${level}` : "";
72793
+ const full = `${short}${suffix}`;
72794
+ if (visibleWidth(full) <= innerWidth)
72795
+ return full;
72796
+ const modelSuffix = short.endsWith("-fast") ? "-fast" : "";
72797
+ const name = modelSuffix ? short.slice(0, -modelSuffix.length) : short;
72798
+ const room = Math.max(1, innerWidth - visibleWidth(modelSuffix + suffix));
72799
+ return `${truncateToWidth2(name, room, "…")}${modelSuffix}${suffix}`;
72800
+ }
72801
+ function workflowChildRunRows(stage, width) {
72802
+ const child = stage.workflowChild ?? stage.workflowChildRun;
72803
+ if (child === undefined)
72804
+ return [];
72805
+ return wrapIdentifierLines(child.runId, Math.max(1, width), "run ", "").map((row) => `${row.prefix}${row.chunk}`);
72806
+ }
72807
+ function workflowChildMetaText(stage) {
72808
+ const completed = stage.workflowChild;
72809
+ if (completed !== undefined) {
72810
+ const outputCount = completed.outputCount ?? Object.keys(completed.outputs).length;
72811
+ return outputCount === 1 ? "1 out" : `${outputCount} outs`;
72812
+ }
72813
+ if (stage.workflowChildRun !== undefined)
72814
+ return "live";
72815
+ return;
72816
+ }
72817
+ function joinCompactStatusMeta(status, meta, width) {
72818
+ const candidates = [`${status} · ${meta}`, `${status} ·${meta}`, `${status}· ${meta}`, `${status}·${meta}`];
72819
+ return candidates.find((candidate) => visibleWidth(candidate) <= width) ?? status;
72820
+ }
72821
+ function nodeCardHeight(stage, opts = {}) {
72822
+ const queuedRows = queuedBadgeCount(opts.queuedMessageCount) > 0 ? 1 : 0;
72823
+ const modelRows = stage.model !== undefined && stage.model !== "" ? 1 : 0;
72824
+ let bodyRows;
72825
+ if (stage.status === "awaiting_input") {
72826
+ bodyRows = 2 + modelRows + queuedRows;
72827
+ } else if (stage.workflowChild !== undefined || stage.workflowChildRun !== undefined) {
72828
+ const innerWidth = Math.max(2, (opts.width ?? NODE_W) - 2);
72829
+ bodyRows = workflowChildRunRows(stage, innerWidth).length + 1 + queuedRows;
72830
+ } else {
72831
+ bodyRows = 2 + modelRows + (metaText(stage) ? 1 : 0) + queuedRows;
72832
+ }
72833
+ return Math.max(NODE_H, bodyRows + 2);
72834
+ }
72835
+ function statusLabel(status) {
72836
+ switch (status) {
72837
+ case "awaiting_input":
72838
+ return "awaiting input";
72839
+ case "completed":
72840
+ return "complete";
72841
+ default:
72842
+ return status.replace(/_/g, " ");
72843
+ }
72844
+ }
72845
+ function truncate(s, maxWidth) {
72846
+ if (maxWidth <= 0)
72847
+ return "";
72848
+ if (visibleWidth(s) <= maxWidth)
72849
+ return s;
72850
+ return truncateToWidth2(s, maxWidth, "…");
72851
+ }
72852
+ function centreColored(content, width, fg, bg, opts = {}) {
72853
+ const safe = truncate(content, width);
72854
+ const safeWidth = visibleWidth(safe);
72855
+ const pad = Math.max(0, width - safeWidth);
72856
+ const left = Math.max(0, Math.floor(pad / 2));
72857
+ const right = Math.max(0, pad - left);
72858
+ const bold = opts.bold ? BOLD : "";
72859
+ return `${bg}${" ".repeat(left)}` + `${hexToAnsi(fg)}${bold}${safe}${RESET}` + `${bg}${" ".repeat(right)}`;
72860
+ }
72861
+ function buildTitleSlot(name, innerWidth, focused, theme, cardBg, compact = false) {
72862
+ const maxName = Math.max(2, compact ? innerWidth - 1 : innerWidth - 4);
72863
+ const safeName = stripTerminalSequences(truncate(name, maxName));
72864
+ if (focused) {
72865
+ const tabText = compact ? safeName : ` ${safeName} `;
72866
+ const styled = `${paint(tabText, theme.surface, {
72867
+ bg: theme.accent,
72868
+ bold: true
72869
+ })}${cardBg}`;
72870
+ return { slot: styled, visibleWidth: visibleWidth(tabText) };
72871
+ }
72872
+ const titleRaw = compact ? safeName : ` ${safeName} `;
72873
+ const styled = `${BOLD}${titleRaw}${RESET}${cardBg}`;
72874
+ return { slot: styled, visibleWidth: visibleWidth(titleRaw) };
72875
+ }
72876
+ function renderNodeCard(stage, opts) {
72877
+ const width = opts.width ?? NODE_W;
72878
+ const height = opts.height ?? nodeCardHeight(stage, opts);
72879
+ const focused = opts.focused ?? false;
72880
+ const phase = opts.pulsePhase ?? 0;
72881
+ const theme = opts.theme;
72882
+ const borderHex = pickBorder(stage.status, focused, phase, theme);
72883
+ const bc = hexToAnsi(borderHex);
72884
+ const bg = DEFAULT_BG;
72885
+ const innerWidth = Math.max(2, width - 2);
72886
+ const child = stage.workflowChild ?? stage.workflowChildRun;
72887
+ const { slot: titleSlot, visibleWidth: titleVisibleWidth } = buildTitleSlot(child === undefined ? stage.name : `↳ ${child.workflow}`, innerWidth, focused, theme, bg, child !== undefined);
72888
+ const titleStart = Math.max(1, Math.floor((innerWidth - titleVisibleWidth) / 2));
72889
+ const titleEnd = titleStart + titleVisibleWidth;
72890
+ const topMiddle = `${bc}${"─".repeat(titleStart)}` + `${titleSlot}${bc}` + `${"─".repeat(Math.max(0, innerWidth - titleEnd))}`;
72891
+ const top = `${bg}${bc}╭${topMiddle}╮${RESET}`;
72892
+ const bottom = `${bg}${bc}╰${"─".repeat(innerWidth)}╯${RESET}`;
72893
+ const bodyText = stage.nodeKind === "tool" ? "durable tool" : stage.status === "blocked" ? blockedBadgeText(stage, opts.stages, innerWidth) : durationText(stage);
72894
+ const bodyHex = durationColor(stage.status, theme);
72895
+ const statusText = `${statusIcon(stage.status)} ${stage.toolStatus ?? statusLabel(stage.status)}`;
72896
+ const queuedCount = queuedBadgeCount(opts.queuedMessageCount);
72897
+ const statusLine = `${bg}${bc}│${RESET}` + centreColored(statusText, innerWidth, bodyHex, bg, {
72898
+ bold: stage.status === "running" || stage.status === "awaiting_input"
72899
+ }) + `${bg}${bc}│${RESET}`;
72900
+ const durLine = `${bg}${bc}│${RESET}` + centreColored(bodyText, innerWidth, bodyHex, bg, {
72901
+ bold: stage.status === "blocked"
72902
+ }) + `${bg}${bc}│${RESET}`;
72903
+ const contentRows = Math.max(0, height - 2);
72904
+ const metaLine = `${bg}${bc}│${RESET}${centreColored(metaText(stage), innerWidth, theme.dim, bg)}${bg}${bc}│${RESET}`;
72905
+ const model = modelText(stage, innerWidth);
72906
+ const modelLine = `${bg}${bc}│${RESET}${centreColored(model, innerWidth, theme.textMuted, bg)}${bg}${bc}│${RESET}`;
72907
+ const childRunLines = workflowChildRunRows(stage, innerWidth).map((row) => `${bg}${bc}│${RESET}${centreColored(row, innerWidth, theme.dim, bg)}${bg}${bc}│${RESET}`);
72908
+ const childMeta = workflowChildMetaText(stage);
72909
+ const childSummary = childMeta === undefined ? undefined : joinCompactStatusMeta(statusText, childMeta, innerWidth);
72910
+ const childSummaryLine = childSummary === undefined ? undefined : `${bg}${bc}│${RESET}` + centreColored(childSummary, innerWidth, bodyHex, bg, {
72911
+ bold: stage.status === "running" || stage.status === "awaiting_input"
72912
+ }) + `${bg}${bc}│${RESET}`;
72913
+ const queuedLines = queuedCount > 0 ? [
72914
+ `${bg}${bc}│${RESET}` + centreColored(queuedBadgeText(queuedCount), innerWidth, theme.info, bg, { bold: true }) + `${bg}${bc}│${RESET}`
72915
+ ] : [];
72916
+ const modelLines = model === "" ? [] : [modelLine];
72917
+ const interior = stage.status === "awaiting_input" ? [
72918
+ ...modelLines,
72919
+ statusLine,
72920
+ ...model === "" && queuedCount === 0 ? [
72921
+ `${bg}${bc}│${RESET}` + centreColored("waiting for response", innerWidth, theme.info, bg) + `${bg}${bc}│${RESET}`
72922
+ ] : [],
72923
+ ...queuedLines,
72924
+ `${bg}${bc}│${RESET}` + centreColored("↵ enter to respond", innerWidth, theme.dim, bg) + `${bg}${bc}│${RESET}`
72925
+ ] : childSummaryLine === undefined ? [durLine, ...modelLines, statusLine, ...metaText(stage) ? [metaLine] : [], ...queuedLines] : [...childRunLines, childSummaryLine, ...queuedLines];
72926
+ while (interior.length < contentRows) {
72927
+ interior.push(`${bg}${bc}│${RESET}${bg}${" ".repeat(innerWidth)}${bg}${bc}│${RESET}`);
72928
+ }
72929
+ if (interior.length > contentRows)
72930
+ interior.length = contentRows;
72931
+ return [top, ...interior, bottom];
72932
+ }
72933
+
72696
72934
  // dist/builtin/workflows/src/tui/prompt-card-select.ts
72697
72935
  import { SelectList, truncateToWidth as truncateToWidth3 } from "@earendil-works/pi-tui";
72698
72936
  function createPromptSelectList(state, theme, maxVisible = 5) {
@@ -73558,9 +73796,25 @@ class GraphViewState {
73558
73796
  _needsAnimationTick() {
73559
73797
  return this.promptState !== null || this.hasAnimatingStages;
73560
73798
  }
73799
+ _nodeHeight(stage) {
73800
+ const baseHeight = nodeCardHeight(stage);
73801
+ if (!this.getStageQueuedMessageCount || nodeCardHeight(stage, { queuedMessageCount: 1 }) === baseHeight) {
73802
+ return baseHeight;
73803
+ }
73804
+ return nodeCardHeight(stage, { queuedMessageCount: this._stageQueuedMessageCount(stage) });
73805
+ }
73806
+ _refreshQueuedNodeHeights() {
73807
+ if (!this.getStageQueuedMessageCount)
73808
+ return;
73809
+ if (!this.cachedLayout.some((node) => (node.height ?? NODE_H) !== this._nodeHeight(node.stage)))
73810
+ return;
73811
+ this.lastBuiltSnapshotVersion = null;
73812
+ this._rebuildLayout();
73813
+ }
73561
73814
  _rebuildLayout() {
73562
73815
  const version = this.currentSnapshot?.version ?? null;
73563
73816
  if (version !== null && version === this.lastBuiltSnapshotVersion) {
73817
+ this._refreshQueuedNodeHeights();
73564
73818
  return;
73565
73819
  }
73566
73820
  const run = this._getCurrentRun();
@@ -73587,6 +73841,8 @@ class GraphViewState {
73587
73841
  const next = graphStages[index];
73588
73842
  if (!next || node.stage.id !== next.id || node.stage.nodeKind !== next.nodeKind)
73589
73843
  return false;
73844
+ if ((node.height ?? NODE_H) !== this._nodeHeight(next))
73845
+ return false;
73590
73846
  if (node.stage.parentIds.length !== next.parentIds.length)
73591
73847
  return false;
73592
73848
  return node.stage.parentIds.every((parentId, parentIndex) => parentId === next.parentIds[parentIndex]);
@@ -73602,7 +73858,10 @@ class GraphViewState {
73602
73858
  this.lastBuiltSnapshotVersion = version;
73603
73859
  return;
73604
73860
  }
73605
- const nextLayout = computeLayout(graphStages, { orientation: "vertical" });
73861
+ const nextLayout = computeLayout(graphStages, {
73862
+ orientation: "vertical",
73863
+ nodeHeight: (stage) => this._nodeHeight(stage)
73864
+ });
73606
73865
  this.cachedLayout = nextLayout;
73607
73866
  this.cachedDisplayStages = nextLayout.map((node) => node.stage);
73608
73867
  this.cachedRenderGeometry = this._buildRenderGeometry(nextLayout);
@@ -73621,13 +73880,15 @@ class GraphViewState {
73621
73880
  for (let index = 0;index < layout.length; index++) {
73622
73881
  const node = layout[index];
73623
73882
  canvasWidth = Math.max(canvasWidth, node.x + NODE_W);
73624
- totalRows = Math.max(totalRows, node.y + NODE_H);
73883
+ const bottom = node.y + (node.height ?? NODE_H);
73884
+ totalRows = Math.max(totalRows, bottom);
73625
73885
  nodeByStageId.set(node.stage.id, node);
73626
73886
  const band = bandsByTop.get(node.y);
73627
- if (band)
73887
+ if (band) {
73628
73888
  band.nodeIndices.push(index);
73629
- else
73630
- bandsByTop.set(node.y, { top: node.y, bottom: node.y + NODE_H, nodeIndices: [index] });
73889
+ band.bottom = Math.max(band.bottom, bottom);
73890
+ } else
73891
+ bandsByTop.set(node.y, { top: node.y, bottom, nodeIndices: [index] });
73631
73892
  }
73632
73893
  const edges = [];
73633
73894
  for (const node of layout) {
@@ -73640,9 +73901,10 @@ class GraphViewState {
73640
73901
  edges.push({
73641
73902
  parentX: parent.x,
73642
73903
  parentY: parent.y,
73904
+ parentHeight: parent.height ?? NODE_H,
73643
73905
  childX: node.x,
73644
73906
  childY: node.y,
73645
- top: parent.y + NODE_H,
73907
+ top: parent.y + (parent.height ?? NODE_H),
73646
73908
  bottom: node.y,
73647
73909
  left: Math.min(parentCol, childCol),
73648
73910
  right: Math.max(parentCol, childCol) + 1
@@ -73978,10 +74240,10 @@ class GraphViewRenderHelpers extends GraphViewState {
73978
74240
  }
73979
74241
  this._clampGraphHorizontalScroll(totalCols, viewportCols);
73980
74242
  }
73981
- _plotEdge(canvas, px, py, cx, cy, color) {
74243
+ _plotEdge(canvas, px, py, cx, cy, color, parentHeight = NODE_H) {
73982
74244
  const parentCol = px + Math.floor(NODE_W / 2);
73983
74245
  const childCol = cx + Math.floor(NODE_W / 2);
73984
- const parentExitRow = py + NODE_H;
74246
+ const parentExitRow = py + parentHeight;
73985
74247
  const childEntryRow = cy - 1;
73986
74248
  if (childEntryRow < parentExitRow)
73987
74249
  return;
@@ -74040,214 +74302,6 @@ class GraphViewRenderHelpers extends GraphViewState {
74040
74302
  }
74041
74303
  }
74042
74304
 
74043
- // dist/builtin/workflows/src/tui/node-card.ts
74044
- import { stripTerminalSequences } from "@earendil-works/pi-tui";
74045
- function pulseT(phase) {
74046
- return (Math.sin(phase * Math.PI * 2 - Math.PI / 2) + 1) / 2;
74047
- }
74048
- function queuedBadgeCount(count) {
74049
- if (typeof count !== "number" || !Number.isFinite(count))
74050
- return 0;
74051
- return Math.max(0, Math.trunc(count));
74052
- }
74053
- function queuedBadgeText(count) {
74054
- return `✉ ${count} queued`;
74055
- }
74056
- function pickBorder(status, focused, phase, theme) {
74057
- switch (status) {
74058
- case "running":
74059
- if (focused)
74060
- return theme.warning;
74061
- return lerpColor(theme.borderDim, theme.warning, pulseT(phase));
74062
- case "paused":
74063
- return theme.warning;
74064
- case "awaiting_input":
74065
- if (focused)
74066
- return theme.info;
74067
- return lerpColor(theme.borderDim, theme.info, pulseT(phase));
74068
- case "completed":
74069
- return theme.success;
74070
- case "failed":
74071
- return theme.error;
74072
- case "blocked":
74073
- return theme.dim;
74074
- case "skipped":
74075
- return theme.dim;
74076
- default:
74077
- return focused ? theme.borderActive : theme.borderDim;
74078
- }
74079
- }
74080
- function durationColor(status, theme) {
74081
- switch (status) {
74082
- case "running":
74083
- return theme.warning;
74084
- case "paused":
74085
- return theme.warning;
74086
- case "awaiting_input":
74087
- return theme.info;
74088
- case "completed":
74089
- return theme.success;
74090
- case "failed":
74091
- return theme.error;
74092
- default:
74093
- return theme.dim;
74094
- }
74095
- }
74096
- function blockedBadgeText(stage, stages, width) {
74097
- const base = "↑ blocked";
74098
- const blockedBy = stage.blockedByStageId;
74099
- if (!blockedBy)
74100
- return base;
74101
- const upstream = stages?.find((s) => s.id === blockedBy)?.name ?? blockedBy;
74102
- const withUpstream = `${base} by ${upstream}`;
74103
- if (visibleWidth(withUpstream) <= width)
74104
- return withUpstream;
74105
- return base;
74106
- }
74107
- function durationText(stage) {
74108
- const elapsed = elapsedStageMs(stage);
74109
- return elapsed === undefined ? "—" : fmtDuration(elapsed);
74110
- }
74111
- function metaText(stage) {
74112
- if (stage.topologyState === "unavailable")
74113
- return "topology unavailable";
74114
- const deps = stage.parentIds.length;
74115
- const dependencyText = deps === 0 ? "root" : deps === 1 ? "1 dep" : `${deps} deps`;
74116
- return dependencyText;
74117
- }
74118
- function modelText(stage, innerWidth) {
74119
- const model = stage.model;
74120
- if (model === undefined || model === "")
74121
- return "—";
74122
- const slash = model.lastIndexOf("/");
74123
- const short = slash >= 0 ? model.slice(slash + 1) : model;
74124
- const level = stage.thinkingLevel;
74125
- const showLevel = level !== undefined && level !== "" && level !== "off";
74126
- const suffix = showLevel ? ` · ${level}` : "";
74127
- const full = `${short}${suffix}`;
74128
- if (visibleWidth(full) <= innerWidth)
74129
- return full;
74130
- const modelSuffix = short.endsWith("-fast") ? "-fast" : "";
74131
- const name = modelSuffix ? short.slice(0, -modelSuffix.length) : short;
74132
- const room = Math.max(1, innerWidth - visibleWidth(modelSuffix + suffix));
74133
- return `${truncateToWidth2(name, room, "…")}${modelSuffix}${suffix}`;
74134
- }
74135
- function workflowChildRunRows(stage, width) {
74136
- const child = stage.workflowChild ?? stage.workflowChildRun;
74137
- if (child === undefined)
74138
- return [];
74139
- return wrapIdentifierLines(child.runId, Math.max(1, width), "run ", "").map((row) => `${row.prefix}${row.chunk}`);
74140
- }
74141
- function workflowChildMetaText(stage) {
74142
- const completed = stage.workflowChild;
74143
- if (completed !== undefined) {
74144
- const outputCount = completed.outputCount ?? Object.keys(completed.outputs).length;
74145
- return outputCount === 1 ? "1 out" : `${outputCount} outs`;
74146
- }
74147
- if (stage.workflowChildRun !== undefined)
74148
- return "live";
74149
- return;
74150
- }
74151
- function joinCompactStatusMeta(status, meta, width) {
74152
- const candidates = [`${status} · ${meta}`, `${status} ·${meta}`, `${status}· ${meta}`, `${status}·${meta}`];
74153
- return candidates.find((candidate) => visibleWidth(candidate) <= width) ?? meta;
74154
- }
74155
- function statusLabel(status) {
74156
- switch (status) {
74157
- case "awaiting_input":
74158
- return "awaiting input";
74159
- case "completed":
74160
- return "complete";
74161
- default:
74162
- return status.replace(/_/g, " ");
74163
- }
74164
- }
74165
- function truncate(s, maxWidth) {
74166
- if (maxWidth <= 0)
74167
- return "";
74168
- if (visibleWidth(s) <= maxWidth)
74169
- return s;
74170
- return truncateToWidth2(s, maxWidth, "…");
74171
- }
74172
- function centreColored(content, width, fg, bg, opts = {}) {
74173
- const safe = truncate(content, width);
74174
- const safeWidth = visibleWidth(safe);
74175
- const pad = Math.max(0, width - safeWidth);
74176
- const left = Math.max(0, Math.floor(pad / 2));
74177
- const right = Math.max(0, pad - left);
74178
- const bold = opts.bold ? BOLD : "";
74179
- return `${bg}${" ".repeat(left)}` + `${hexToAnsi(fg)}${bold}${safe}${RESET}` + `${bg}${" ".repeat(right)}`;
74180
- }
74181
- function buildTitleSlot(name, innerWidth, focused, theme, cardBg, compact = false) {
74182
- const maxName = Math.max(2, compact ? innerWidth - 1 : innerWidth - 4);
74183
- const safeName = stripTerminalSequences(truncate(name, maxName));
74184
- if (focused) {
74185
- const tabText = compact ? safeName : ` ${safeName} `;
74186
- const styled = `${paint(tabText, theme.surface, {
74187
- bg: theme.accent,
74188
- bold: true
74189
- })}${cardBg}`;
74190
- return { slot: styled, visibleWidth: visibleWidth(tabText) };
74191
- }
74192
- const titleRaw = compact ? safeName : ` ${safeName} `;
74193
- const styled = `${BOLD}${titleRaw}${RESET}${cardBg}`;
74194
- return { slot: styled, visibleWidth: visibleWidth(titleRaw) };
74195
- }
74196
- function renderNodeCard(stage, opts) {
74197
- const width = opts.width ?? NODE_W;
74198
- const height = opts.height ?? NODE_H;
74199
- const focused = opts.focused ?? false;
74200
- const phase = opts.pulsePhase ?? 0;
74201
- const theme = opts.theme;
74202
- const borderHex = pickBorder(stage.status, focused, phase, theme);
74203
- const bc = hexToAnsi(borderHex);
74204
- const bg = DEFAULT_BG;
74205
- const innerWidth = Math.max(2, width - 2);
74206
- const child = stage.workflowChild ?? stage.workflowChildRun;
74207
- const { slot: titleSlot, visibleWidth: titleVisibleWidth } = buildTitleSlot(child === undefined ? stage.name : `↳ ${child.workflow}`, innerWidth, focused, theme, bg, child !== undefined);
74208
- const titleStart = Math.max(1, Math.floor((innerWidth - titleVisibleWidth) / 2));
74209
- const titleEnd = titleStart + titleVisibleWidth;
74210
- const topMiddle = `${bc}${"─".repeat(titleStart)}` + `${titleSlot}${bc}` + `${"─".repeat(Math.max(0, innerWidth - titleEnd))}`;
74211
- const top = `${bg}${bc}╭${topMiddle}╮${RESET}`;
74212
- const bottom = `${bg}${bc}╰${"─".repeat(innerWidth)}╯${RESET}`;
74213
- const bodyText = stage.nodeKind === "tool" ? "durable tool" : stage.status === "blocked" ? blockedBadgeText(stage, opts.stages, innerWidth) : durationText(stage);
74214
- const bodyHex = durationColor(stage.status, theme);
74215
- const statusText = `${statusIcon(stage.status)} ${stage.toolStatus ?? statusLabel(stage.status)}`;
74216
- const statusLine = `${bg}${bc}│${RESET}` + centreColored(statusText, innerWidth, bodyHex, bg, {
74217
- bold: stage.status === "running" || stage.status === "awaiting_input"
74218
- }) + `${bg}${bc}│${RESET}`;
74219
- const durLine = `${bg}${bc}│${RESET}` + centreColored(bodyText, innerWidth, bodyHex, bg, {
74220
- bold: stage.status === "blocked"
74221
- }) + `${bg}${bc}│${RESET}`;
74222
- const contentRows = Math.max(0, height - 2);
74223
- const metaLine = `${bg}${bc}│${RESET}${centreColored(metaText(stage), innerWidth, theme.dim, bg)}${bg}${bc}│${RESET}`;
74224
- const modelLine = `${bg}${bc}│${RESET}${centreColored(modelText(stage, innerWidth), innerWidth, theme.textMuted, bg)}${bg}${bc}│${RESET}`;
74225
- const childRunLines = workflowChildRunRows(stage, innerWidth).map((row) => `${bg}${bc}│${RESET}${centreColored(row, innerWidth, theme.dim, bg)}${bg}${bc}│${RESET}`);
74226
- const queuedCount = queuedBadgeCount(opts.queuedMessageCount);
74227
- const childMeta = workflowChildMetaText(stage);
74228
- const childSummary = childMeta === undefined ? undefined : joinCompactStatusMeta(statusText, queuedCount > 0 ? queuedBadgeText(queuedCount) : childMeta, innerWidth);
74229
- const childSummaryLine = childSummary === undefined ? undefined : `${bg}${bc}│${RESET}` + centreColored(childSummary, innerWidth, bodyHex, bg, {
74230
- bold: stage.status === "running" || stage.status === "awaiting_input"
74231
- }) + `${bg}${bc}│${RESET}`;
74232
- const interior = stage.status === "awaiting_input" ? [
74233
- statusLine,
74234
- `${bg}${bc}│${RESET}` + centreColored("waiting for response", innerWidth, theme.info, bg) + `${bg}${bc}│${RESET}`,
74235
- `${bg}${bc}│${RESET}` + centreColored("↵ enter to respond", innerWidth, theme.dim, bg) + `${bg}${bc}│${RESET}`,
74236
- modelLine
74237
- ] : childSummaryLine === undefined ? [durLine, statusLine, modelLine, metaLine] : [...childRunLines, childSummaryLine];
74238
- const preferredBadgeRow = stage.status === "awaiting_input" ? 1 : childSummaryLine === undefined ? interior.length - 1 : -1;
74239
- while (interior.length < contentRows) {
74240
- interior.push(`${bg}${bc}│${RESET}${bg}${" ".repeat(innerWidth)}${bg}${bc}│${RESET}`);
74241
- }
74242
- if (interior.length > contentRows)
74243
- interior.length = contentRows;
74244
- if (queuedCount > 0 && interior.length > 0 && preferredBadgeRow >= 0) {
74245
- const badgeRow = preferredBadgeRow < interior.length ? preferredBadgeRow : interior.length - 1;
74246
- interior[badgeRow] = `${bg}${bc}│${RESET}` + centreColored(queuedBadgeText(queuedCount), innerWidth, theme.info, bg, { bold: true }) + `${bg}${bc}│${RESET}`;
74247
- }
74248
- return [top, ...interior, bottom];
74249
- }
74250
-
74251
74305
  // dist/builtin/workflows/src/tui/graph-view-graph-render.ts
74252
74306
  class GraphViewGraphRenderer extends GraphViewRenderHelpers {
74253
74307
  _renderGraph(width, viewportTop, viewportRows, renderRun) {
@@ -74292,7 +74346,7 @@ class GraphViewGraphRenderer extends GraphViewRenderHelpers {
74292
74346
  if (edge.bottom <= safeTop || edge.top >= viewportBottom || edge.right <= viewportLeft || edge.left >= viewportRight) {
74293
74347
  continue;
74294
74348
  }
74295
- this._plotEdge(edgeCanvas, edge.parentX, edge.parentY, edge.childX, edge.childY, edgeColor);
74349
+ this._plotEdge(edgeCanvas, edge.parentX, edge.parentY, edge.childX, edge.childY, edgeColor, edge.parentHeight);
74296
74350
  }
74297
74351
  const edgeLines = edgeCanvas.toLines();
74298
74352
  const placements = new Map;
@@ -74306,7 +74360,7 @@ class GraphViewGraphRenderer extends GraphViewRenderHelpers {
74306
74360
  continue;
74307
74361
  const cardLines = renderNodeCard(node.stage, {
74308
74362
  width: NODE_W,
74309
- height: NODE_H,
74363
+ height: node.height ?? NODE_H,
74310
74364
  focused: ni === this.focusedIndex,
74311
74365
  pulsePhase,
74312
74366
  theme: this.graphTheme,
@@ -74363,7 +74417,7 @@ class GraphViewGraphRenderer extends GraphViewRenderHelpers {
74363
74417
  for (const index of band.nodeIndices) {
74364
74418
  const node = this.cachedLayout[index];
74365
74419
  const top = graphStartRow + node.y - viewportTop;
74366
- const bottom = top + NODE_H;
74420
+ const bottom = top + (node.height ?? NODE_H);
74367
74421
  const left = viewport.leftMargin + node.x - this.graphScrollColOffset;
74368
74422
  const right = left + NODE_W;
74369
74423
  const clippedTop = Math.max(visibleTop, top);
@@ -75071,6 +75125,7 @@ class GraphViewRenderer extends GraphViewGraphRenderer {
75071
75125
  render(width) {
75072
75126
  if (this.mode === "widget")
75073
75127
  return this._renderWidget(width);
75128
+ this._refreshQueuedNodeHeights();
75074
75129
  return this._renderOverlay(width);
75075
75130
  }
75076
75131
  dispose() {
@@ -75183,8 +75238,8 @@ class GraphViewRenderer extends GraphViewGraphRenderer {
75183
75238
  let next = scrollView.scrollTop;
75184
75239
  if (node.y < next)
75185
75240
  next = node.y;
75186
- else if (node.y + NODE_H > next + viewportRows)
75187
- next = node.y + NODE_H - viewportRows;
75241
+ else if (node.y + (node.height ?? NODE_H) > next + viewportRows)
75242
+ next = node.y + (node.height ?? NODE_H) - viewportRows;
75188
75243
  scrollView.scrollTo(next);
75189
75244
  const graphInner = Math.max(1, width - 4);
75190
75245
  const leftMargin = Math.max(2, this.cachedRenderGeometry.canvasWidth <= graphInner ? Math.floor((graphInner - this.cachedRenderGeometry.canvasWidth) / 2) : 2);
@@ -79038,7 +79093,9 @@ function plainCollapsed(counts, activeTools) {
79038
79093
  const tools = activeTools > 0 ? ` · ${activeTools} tool${activeTools === 1 ? "" : "s"}` : "";
79039
79094
  return ` ▾ ${total} background · ${counts.active} ●${paused}${quit}${blocked}${tools}`;
79040
79095
  }
79041
- function buildThemedWidgetLines(snap, piTheme, width = 120, now = Date.now()) {
79096
+ function buildThemedWidgetLines(snap, piTheme, width = 120, now = Date.now(), layout) {
79097
+ if (layout)
79098
+ layout.runs = [];
79042
79099
  const display = selectDisplayRuns(snap, now);
79043
79100
  if (display.length === 0)
79044
79101
  return [];
@@ -79057,7 +79114,7 @@ function buildThemedWidgetLines(snap, piTheme, width = 120, now = Date.now()) {
79057
79114
  const activeTools = display.filter(isActive).reduce((total, run) => total + activeToolNodes(run).length, 0);
79058
79115
  if (width < COLLAPSED_BREAKPOINT_COLS) {
79059
79116
  return [
79060
- themed ? themedCollapsed(visibleCounts, activeTools, graphTheme) : plainCollapsed(visibleCounts, activeTools)
79117
+ truncateToWidth2(themed ? themedCollapsed(visibleCounts, activeTools, graphTheme) : plainCollapsed(visibleCounts, activeTools), width, "…")
79061
79118
  ];
79062
79119
  }
79063
79120
  const total = display.length;
@@ -79071,6 +79128,11 @@ function buildThemedWidgetLines(snap, piTheme, width = 120, now = Date.now()) {
79071
79128
  const run = display[i];
79072
79129
  const runLines = themed ? themedRunLines(run, now, graphTheme, snap.runs, width, expandGraph) : plainRunLines(run, now, snap.runs, width, expandGraph);
79073
79130
  body.push(...runLines);
79131
+ layout?.runs.push({
79132
+ id: run.id,
79133
+ start: body.length - runLines.length + 1,
79134
+ end: body.length + (i === display.length - 1 ? 2 : 1)
79135
+ });
79074
79136
  if (i < display.length - 1)
79075
79137
  body.push("");
79076
79138
  }
@@ -79083,7 +79145,95 @@ function buildThemedWidgetLines(snap, piTheme, width = 120, now = Date.now()) {
79083
79145
  });
79084
79146
  }
79085
79147
 
79148
+ // dist/builtin/workflows/src/tui/widget-viewport.ts
79149
+ import { ScrollableComponentViewport } from "@bastani/atomic";
79150
+ var WORKFLOW_WIDGET_MAX_ROWS = 10;
79151
+
79152
+ class WorkflowWidgetViewport {
79153
+ content;
79154
+ terminalRows;
79155
+ requestRender;
79156
+ getRunRows;
79157
+ viewport = new ScrollableComponentViewport;
79158
+ lines = [];
79159
+ scrollable = false;
79160
+ runRows = [];
79161
+ constructor(content, terminalRows, requestRender, getRunRows) {
79162
+ this.content = content;
79163
+ this.terminalRows = terminalRows;
79164
+ this.requestRender = requestRender;
79165
+ this.getRunRows = getRunRows;
79166
+ this.viewport.setComponents([{ render: () => this.lines, invalidate() {} }]);
79167
+ this.viewport.scrollTo(0);
79168
+ }
79169
+ scroll(direction) {
79170
+ if (!this.scrollable)
79171
+ return;
79172
+ this.viewport.scrollBy(direction);
79173
+ this.requestRender();
79174
+ }
79175
+ render(width) {
79176
+ const cap = Math.max(1, Math.min(WORKFLOW_WIDGET_MAX_ROWS, Math.floor(this.terminalRows() / 3)));
79177
+ const nextLines = this.content.render(width);
79178
+ const nextRunRows = this.getRunRows?.() ?? [];
79179
+ if (this.getRunRows && nextLines.length === 1 && nextRunRows.length === 0) {
79180
+ this.scrollable = false;
79181
+ return nextLines;
79182
+ }
79183
+ const previousLineCount = this.lines.length;
79184
+ const offset = this.viewport.getMaxScroll() - this.viewport.getScrollFromBottom();
79185
+ const anchoredOffset = this.getRunRows ? this.resolveAnchor(offset, nextRunRows) : undefined;
79186
+ this.lines = nextLines;
79187
+ this.runRows = nextRunRows;
79188
+ if (anchoredOffset !== undefined) {
79189
+ this.viewport.scrollTo(anchoredOffset);
79190
+ } else if (previousLineCount <= 1 || this.lines.length < previousLineCount && this.lines.length <= cap) {
79191
+ this.viewport.scrollTo(0);
79192
+ }
79193
+ this.scrollable = this.lines.length > 1;
79194
+ this.viewport.setVisibleRows(1);
79195
+ this.viewport.render(width);
79196
+ if (!this.scrollable)
79197
+ return this.lines;
79198
+ const first = this.viewport.getMaxScroll() - this.viewport.getScrollFromBottom() + 1;
79199
+ const visible = this.lines.slice(first - 1, first - 1 + Math.max(1, cap - 1));
79200
+ if (cap === 1)
79201
+ return visible;
79202
+ const hint = ` ${first}–${first + visible.length - 1}/${this.lines.length} · Alt+PgUp/PgDn scroll workflows`;
79203
+ return [...visible, truncateToWidth2(hint, width, "…")];
79204
+ }
79205
+ resolveAnchor(offset, next) {
79206
+ if (offset === 0 || next.length === 0)
79207
+ return 0;
79208
+ const index = this.runRows.findIndex((run) => offset >= run.start - 1 && offset < run.end);
79209
+ const anchor = this.runRows[index];
79210
+ if (!anchor)
79211
+ return 0;
79212
+ const byId = new Map(next.map((run) => [run.id, run]));
79213
+ const retained = byId.get(anchor.id);
79214
+ if (retained)
79215
+ return Math.min(retained.end - 1, retained.start + offset - anchor.start);
79216
+ const neighbours = [...this.runRows.slice(index + 1), ...this.runRows.slice(0, index).reverse()];
79217
+ for (const neighbour of neighbours) {
79218
+ const survivor = byId.get(neighbour.id);
79219
+ if (survivor)
79220
+ return survivor.start;
79221
+ }
79222
+ return 0;
79223
+ }
79224
+ invalidate() {
79225
+ this.content.invalidate?.();
79226
+ }
79227
+ dispose() {
79228
+ this.content.dispose?.();
79229
+ }
79230
+ }
79231
+
79086
79232
  // dist/builtin/workflows/src/tui/store-widget-installer.ts
79233
+ var widgetViewports = new WeakMap;
79234
+ function scrollStoreWidget(storeInstance, direction) {
79235
+ widgetViewports.get(storeInstance)?.scroll(direction);
79236
+ }
79087
79237
  var defaultTimerApi = {
79088
79238
  setTimeout: (handler, delayMs) => setTimeout(handler, delayMs),
79089
79239
  clearTimeout: (handle) => clearTimeout(handle)
@@ -79114,9 +79264,19 @@ function installStoreWidget(pi, storeInstance, timers = defaultTimerApi) {
79114
79264
  const setWidget = ui.setWidget;
79115
79265
  const requestRender = ui.requestRender;
79116
79266
  const onWidgetRelease = ui.onWidgetRelease;
79267
+ const layout = { runs: [] };
79117
79268
  const controller = installReactiveWidget({
79118
79269
  ui: {
79119
- setWidget: (key, factory, opts) => setWidget.call(ui, key, factory, opts),
79270
+ setWidget: (key, factory, opts) => {
79271
+ if (!factory)
79272
+ widgetViewports.delete(storeInstance);
79273
+ setWidget.call(ui, key, factory ? (tui, theme) => {
79274
+ const host = tui;
79275
+ const viewport = new WorkflowWidgetViewport(factory(tui, theme), () => host?.terminal?.rows ?? 30, () => requestRender ? requestRender.call(ui) : host?.requestRender?.(), () => layout.runs);
79276
+ widgetViewports.set(storeInstance, viewport);
79277
+ return viewport;
79278
+ } : undefined, opts);
79279
+ },
79120
79280
  ...requestRender ? { requestRender: () => requestRender.call(ui) } : {},
79121
79281
  ...onWidgetRelease ? { onWidgetRelease: (key, listener) => onWidgetRelease.call(ui, key, listener) } : {}
79122
79282
  },
@@ -79126,7 +79286,7 @@ function installStoreWidget(pi, storeInstance, timers = defaultTimerApi) {
79126
79286
  getSnapshot: () => liveWidgetSnapshot(storeInstance),
79127
79287
  subscribe: (listener) => subscribeStoreInvalidation(storeInstance, listener),
79128
79288
  getPreviewLines: (snap, now) => buildThemedWidgetLines(snap, undefined, 120, now),
79129
- render: (snap, { theme, width, now }) => buildThemedWidgetLines(snap, theme, width, now),
79289
+ render: (snap, { theme, width, now }) => buildThemedWidgetLines(snap, theme, width, now, layout),
79130
79290
  getNextRefreshDelayMs: (snap, now) => nextWidgetRefreshDelayMs(snap, now),
79131
79291
  requestRenderOnStateNoop: false,
79132
79292
  isStaleError: isStaleExtensionContextError4,
@@ -79134,7 +79294,10 @@ function installStoreWidget(pi, storeInstance, timers = defaultTimerApi) {
79134
79294
  reportWidgetFailure(ui, `Workflow progress widget could not mount: ${error.message}`);
79135
79295
  }
79136
79296
  });
79137
- return () => controller.dispose();
79297
+ return () => {
79298
+ widgetViewports.delete(storeInstance);
79299
+ controller.dispose();
79300
+ };
79138
79301
  }
79139
79302
  function installToolExecutionHooks(pi, storeInstance) {
79140
79303
  const eventBusOn = pi.events?.on;
@@ -117680,6 +117843,14 @@ function registerWorkflowShortcut(pi, overlay) {
117680
117843
  description: "Open workflow orchestrator pane",
117681
117844
  handler: openPane
117682
117845
  });
117846
+ pi.registerShortcut("alt+pageUp", {
117847
+ description: "Scroll background workflows up",
117848
+ handler: () => scrollStoreWidget(store, -1)
117849
+ });
117850
+ pi.registerShortcut("alt+pageDown", {
117851
+ description: "Scroll background workflows down",
117852
+ handler: () => scrollStoreWidget(store, 1)
117853
+ });
117683
117854
  }
117684
117855
  function registerIntercomControl(pi, intercomControlRef) {
117685
117856
  intercomControlRef.current = subscribeIntercomControl(pi, buildIntercomCallbacks({