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

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/intercom",
3
- "version": "0.9.19-alpha.7",
3
+ "version": "0.9.19-alpha.8",
4
4
  "private": true,
5
5
  "description": "Atomic extension providing a private coordination channel between parent and child agent sessions. Fork of: https://github.com/nicobailon/pi-intercom",
6
6
  "contributors": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/mcp",
3
- "version": "0.9.19-alpha.7",
3
+ "version": "0.9.19-alpha.8",
4
4
  "private": true,
5
5
  "description": "Atomic extension that adapts MCP (Model Context Protocol) servers into the coding agent. Fork of: https://github.com/nicobailon/pi-mcp-adapter",
6
6
  "contributors": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/subagents",
3
- "version": "0.9.19-alpha.7",
3
+ "version": "0.9.19-alpha.8",
4
4
  "private": true,
5
5
  "description": "Atomic extension for delegating tasks to subagents with parallel execution. Fork of: https://github.com/nicobailon/pi-subagents",
6
6
  "contributors": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/web-access",
3
- "version": "0.9.19-alpha.7",
3
+ "version": "0.9.19-alpha.8",
4
4
  "private": true,
5
5
  "description": "Atomic extension for web search, URL fetching, GitHub repo cloning, PDF/video extraction. Fork of: https://github.com/nicobailon/pi-web-access",
6
6
  "contributors": [
@@ -6,6 +6,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.9.19-alpha.8] - 2026-09-12
10
+
11
+ ### Fixed
12
+
13
+ - Workflow status detail now shows started and ended times in the system local timezone instead of UTC, preserving the compact `HH:mm:ss` display and elapsed durations ([#3008](https://github.com/bastani-inc/atomic/issues/3008)).
14
+
15
+ ### Changed
16
+
17
+ - Removed generated `root`, `dep`, and `deps` labels from workflow graph nodes and left model text blank when no model is set. Cards omit the extra padding row. Queued-message counts appear on a separate `✉ N queued` row, reusing empty space or expanding occupied cards without hiding status or model details. Dependency edges are unchanged.
18
+
9
19
  ## [0.9.19-alpha.7] - 2026-09-12
10
20
 
11
21
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/workflows",
3
- "version": "0.9.19-alpha.7",
3
+ "version": "0.9.19-alpha.8",
4
4
  "private": true,
5
5
  "description": "Atomic extension for multi-stage workflow authoring and execution.",
6
6
  "contributors": [
@@ -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);
@@ -122,7 +122,7 @@ If you copy a HIL workflow example into a headless session, it can pass dispatch
122
122
 
123
123
  <p align="center"><img src="../images/workflow-input-picker.png" alt="Workflow Input Picker" width="600" /></p>
124
124
 
125
- Graph node cards show each model stage's effective model and thinking level beneath its status, including after fallback and durable resume. Long model names are truncated first, preserving the complete thinking level and a canonical `-fast` model suffix. This suffix is model identity, not a separate fast-mode switch or proof of service tier. Thinking `off` is omitted; unresolved model identity shows `—`. Tool nodes retain their `durable tool` body, and the `BACKGROUND` summary is unchanged.
125
+ Graph node cards show each model stage's effective model and thinking level above its status, including after fallback and durable resume. Long model names are truncated first, preserving the complete thinking level and a canonical `-fast` model suffix. This suffix is model identity, not a separate fast-mode switch or proof of service tier. Thinking `off` is omitted; stages without a model show no model placeholder. Tool nodes retain their `durable tool` body, and the `BACKGROUND` summary is unchanged.
126
126
 
127
127
  ## Workflow Commands
128
128
 
@@ -159,7 +159,7 @@ Surface behavior:
159
159
  - **Graph vs. stage chat** - Use `connect` for the workflow graph. Use `attach` when you want a chat pane for a specific stage.
160
160
  - **Hierarchy chord** - `ctrl+x` is the workflow hierarchy chord: in an attached stage chat it means **return to graph**, and in the graph it means **return to main chat**. The workflow surface handles `ctrl+x` before configurable editor or tool actions, including while a composer draft, primitive prompt, custom question, stage switcher, or legacy prompt card owns input.
161
161
  - **Draft preservation** - Leaving a stage preserves unsent composer and prompt drafts and keeps pending custom questions unresolved so they reappear when you attach again.
162
- - **Queued-message survival** - Steering and follow-up entries queued from a stage chat live on the stage session, not on the pane. Detaching to the graph and reattaching rehydrates the pending `Steering:` / `Follow-up:` rows, and while you are detached the stage's graph node shows a `✉ N queued` badge so a pending message stays visible without attaching. The attached chat shows the pending text; the detached node shows only their count. Both read one projection that the stage handle keeps current from the session's complete `queue_update` snapshots, so rows and badge shrink together as the agent consumes entries. That projection is fed by the events rather than by a concrete Atomic `AgentSession`, so a stage backed by a custom `AgentSessionAdapter` keeps this behavior as long as it publishes ordinary `queue_update` events; each snapshot replaces the previous steering and follow-up lists rather than adding to them. A queue can also outlive the session holding it — a stage session that fails over to a fallback model hands its pending messages to the session replacing it, and a completed stage reopened as a post-mortem chat is restored holding whatever it was queued. Those messages were announced before the projection could reach the new session, so Atomic reads it once as it attaches and the rows and badge show them too.
162
+ - **Queued-message survival** - Steering and follow-up entries queued from a stage chat live on the stage session, not on the pane. Detaching to the graph and reattaching rehydrates the pending `Steering:` / `Follow-up:` rows, and while you are detached the stage's graph node shows a `✉ N queued` badge on its own row so a pending message stays visible without attaching. The attached chat shows the pending text; the detached node shows only their count. Both read one projection that the stage handle keeps current from the session's complete `queue_update` snapshots, so rows and badge shrink together as the agent consumes entries. That projection is fed by the events rather than by a concrete Atomic `AgentSession`, so a stage backed by a custom `AgentSessionAdapter` keeps this behavior as long as it publishes ordinary `queue_update` events; each snapshot replaces the previous steering and follow-up lists rather than adding to them. A queue can also outlive the session holding it — a stage session that fails over to a fallback model hands its pending messages to the session replacing it, and a completed stage reopened as a post-mortem chat is restored holding whatever it was queued. Those messages were announced before the projection could reach the new session, so Atomic reads it once as it attaches and the rows and badge show them too.
163
163
  - **Reserved keys** - `ctrl+d` and `q` do not navigate workflow surfaces; `ctrl+d` keeps its ordinary editor or prompt behavior where applicable, and `q` remains printable in text-owning prompts. Existing `esc`, `ctrl+c`, and graph `h` close/hide controls are unchanged.
164
164
  - **Wheel and trackpad** - While the workflow graph is active, vertical wheel/trackpad gestures pan it up and down, and horizontal gestures pan wide graphs left and right when the terminal exposes horizontal wheel events. Focused graph and stage-chat overlays receive those gestures through the fullscreen application route, so scrolling stays inside the active workflow surface instead of falling through to terminal or main-chat scrollback.
165
165
  - **Fullscreen mouse routing and selection** - A focused workflow graph or attached stage chat overlay receives wheel/trackpad and click input through the host's application-owned input route before the fullscreen viewport. Events the overlay does not consume fall through to pi-tui's viewport, while non-overlay focused components leave pi-tui's transcript scrolling, scrollbar interaction, and drag-selection path intact. Graph panning, stage-chat scrolling, node click-to-attach, and drag or multi-click selection therefore work without a separate selection mode. Copy uses OSC 52; terminals that refuse OSC 52 writes still support the modifier-drag bypass (Shift/Option, as provided by the terminal). `ctrl+t` is not a workflow control: focused workflow overlays leave it to the host `app.thinking.toggle` action, while inline tree selectors keep `app.tree.filter.noTools`.
@@ -170,6 +170,7 @@ Surface behavior:
170
170
  - **Run control** - Use `pause` and `resume` for resumable live work. Pause holds a stage's queued steering and follow-up items in place without dequeuing them or starting continuation; `resume` releases those items once in their existing per-queue order, but queue release alone does not start a model turn. `resume` on a non-paused run reopens the saved snapshot or overlay. Use `quit` to pause a live run gracefully while preserving it for `/workflow resume`. `/workflow pause` selects the active run by default, accepts a full run id or `--all`, and does not open a stage picker. Use the workflow tool's `stageId` for stage or tool-node targeting.
171
171
  - **Rediscovery** - Use `/workflow reload` after adding, editing, installing, or removing workflow resources or package manifest workflow entries and you want Atomic to rediscover them in-process ([Reloading workflow resources](/workflows/operations#reloading-workflow-resources)).
172
172
  - **Status listing** - `/workflow status` lists all retained active and terminal top-level runs by default; implementation-owned nested child runs are flattened into their parent workflow rather than listed separately. `/workflow status --all` is retained as a compatibility alias.
173
+ - **Status times** - `/workflow status <run-id>` displays `started` and `ended` in the system local timezone as `HH:mm:ss`. If they differ from your expected clock, check the timezone of the machine or container running Atomic and any inherited `TZ` environment setting. Elapsed durations and raw timestamps in structured results are unchanged.
173
174
 
174
175
  `/workflows` is the retained-run history alias for `/workflow resume`: with no id it opens the same mixed picker, but the resumable section lists only runs that the resume path can actually accept and the completed section is read-only inspection. A run with no durable checkpoint, missing/pruned artifacts, or explicit deletion is omitted from the resume picker; an explicit `/workflow resume <id>` still returns an explanatory error. It is intentionally different from `/workflow list`, which lists installed workflow definitions. See [`/workflow resume` — cross-session resume selector](#/workflow-resume-—-cross-session-resume-selector) for the full picker semantics.
175
176
 
@@ -464,7 +465,7 @@ Repeated, sibling, sequential, parallel, and multi-level child calls keep indepe
464
465
 
465
466
  ### `ctx.tool` — durable cached tool execution
466
467
 
467
- The `ctx.tool(name, args, fn, options?)` primitive runs arbitrary TypeScript code as a first-class durable graph node and caches the result durably. The node is non-attachable and has no stage chat controls, and its graph card body is the constant `durable tool` in every state status, timing, and dependency rows keep their own rows, and the card does not preview the result or error. In the graph viewer, focusing the node and pressing Enter, clicking it, or choosing it from the switcher opens a read-only host-style operator card from the snapshot: a status-tinted shaded rectangle with the same inner padding and header/body gap as the main-chat tool block, inset from the orchestrator header and footer bars, a `$ <tool-name>` call header, an optional short argument summary, and the result or error as its body. Running and completed call headers have no status marker; pending, failed, cached, and cancelled calls retain their quiet markers. It is collapsed by default and wraps the fully bounded result or error before showing its last visual rows, with `... (N earlier lines, ctrl+o Expand)` above the tail when the action is bound; the configured `app.tools.expand` action (`ctrl+o` by default) toggles the full bounded result or error and then a muted callback-source block when source exists. The graph statusline advertises the resolved expand key with `expand` or `collapse` alongside return-to-graph and scroll hints, including remapped keys, and omits that segment entirely when the action is unbound. The footer says `Took` for settled calls or `Elapsed` for running calls, using the same second-resolution duration as the main-chat tool block, with cached/replayed markers kept as a quiet suffix. The operator surface has no ARGS/RESULT/SOURCE/TIMING/MARKERS debug table and does not expose raw clock fields. Source capture uses `fn.toString()` at registration without re-executing the callback or reading a file. `↑`/`↓`, `PageUp`/`PageDown`, `Home`/`End`, the wheel, and the scrollbar all scroll the block, so a long payload stays readable on a keyboard-only session or a terminal without mouse reporting; Escape or `ctrl+x` returns to the graph. The message block is read-only and never offers chat attachment, steering, pause, or resume. Bounded payloads remain width-safe and mark truncation explicitly with `… [truncated]`; source tabs expand and control bytes become `\xNN`, while cyclic payloads, throwing `toJSON`, or throwing property getters render `<cycle>`, `<unserializable>`, or `<unreadable>` instead of crashing the view. The same cap applies to what the live run snapshot retains for a tool node, while durable checkpoints keep the exact output, raw-args `argsHash`, and replay behavior unchanged.
468
+ The `ctx.tool(name, args, fn, options?)` primitive runs arbitrary TypeScript code as a first-class durable graph node and caches the result durably. The node is non-attachable and has no stage chat controls, and its graph card body is the constant `durable tool` in every state. The card shows status separately and does not preview the result or error. In the graph viewer, focusing the node and pressing Enter, clicking it, or choosing it from the switcher opens a read-only host-style operator card from the snapshot: a status-tinted shaded rectangle with the same inner padding and header/body gap as the main-chat tool block, inset from the orchestrator header and footer bars, a `$ <tool-name>` call header, an optional short argument summary, and the result or error as its body. Running and completed call headers have no status marker; pending, failed, cached, and cancelled calls retain their quiet markers. It is collapsed by default and wraps the fully bounded result or error before showing its last visual rows, with `... (N earlier lines, ctrl+o Expand)` above the tail when the action is bound; the configured `app.tools.expand` action (`ctrl+o` by default) toggles the full bounded result or error and then a muted callback-source block when source exists. The graph statusline advertises the resolved expand key with `expand` or `collapse` alongside return-to-graph and scroll hints, including remapped keys, and omits that segment entirely when the action is unbound. The footer says `Took` for settled calls or `Elapsed` for running calls, using the same second-resolution duration as the main-chat tool block, with cached/replayed markers kept as a quiet suffix. The operator surface has no ARGS/RESULT/SOURCE/TIMING/MARKERS debug table and does not expose raw clock fields. Source capture uses `fn.toString()` at registration without re-executing the callback or reading a file. `↑`/`↓`, `PageUp`/`PageDown`, `Home`/`End`, the wheel, and the scrollbar all scroll the block, so a long payload stays readable on a keyboard-only session or a terminal without mouse reporting; Escape or `ctrl+x` returns to the graph. The message block is read-only and never offers chat attachment, steering, pause, or resume. Bounded payloads remain width-safe and mark truncation explicitly with `… [truncated]`; source tabs expand and control bytes become `\xNN`, while cyclic payloads, throwing `toJSON`, or throwing property getters render `<cycle>`, `<unserializable>`, or `<unreadable>` instead of crashing the view. The same cap applies to what the live run snapshot retains for a tool node, while durable checkpoints keep the exact output, raw-args `argsHash`, and replay behavior unchanged.
468
469
 
469
470
  When the workflow body fulfills but one or more admitted tool calls failed, Atomic promotes the first observed failure to the terminal run failure, regardless of admission order, and persists that selected tool-node identity for status inspection and lifecycle output. A direct uncaught `await ctx.tool(...)` rejection keeps the original error and persists its failed-node link through session and durable restore. First-event arbitration also preserves the selected node when concurrent failures throw the same object or primitive; unrelated later stage or body errors do not inherit a caught tool's origin. Tool admission remains open while author code can catch a failure and continue. Once the body settles and failure has won before any real cancellation, Atomic closes admission, cancels remaining non-failed tool nodes, waits for observed failed nodes to finish publication, and publishes the failed root without waiting for callbacks that ignore cancellation.
470
471
 
@@ -1,16 +1,16 @@
1
1
  {
2
2
  "name": "@bastani/atomic",
3
- "version": "0.9.19-alpha.7",
3
+ "version": "0.9.19-alpha.8",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bastani/atomic",
9
- "version": "0.9.19-alpha.7",
9
+ "version": "0.9.19-alpha.8",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
- "@bastani/atomic-natives": "0.9.19-alpha.7",
13
- "@bastani/pi-ai": "0.9.19-alpha.7",
12
+ "@bastani/atomic-natives": "0.9.19-alpha.8",
13
+ "@bastani/pi-ai": "0.9.19-alpha.8",
14
14
  "@dbos-inc/dbos-sdk": "4.25.14",
15
15
  "@earendil-works/pi-agent-core": "0.85.1",
16
16
  "@earendil-works/pi-client": "0.85.1",
@@ -518,18 +518,18 @@
518
518
  }
519
519
  },
520
520
  "node_modules/@bastani/atomic-natives": {
521
- "version": "0.9.19-alpha.7",
522
- "resolved": "https://registry.npmjs.org/@bastani/atomic-natives/-/atomic-natives-0.9.19-alpha.7.tgz",
521
+ "version": "0.9.19-alpha.8",
522
+ "resolved": "https://registry.npmjs.org/@bastani/atomic-natives/-/atomic-natives-0.9.19-alpha.8.tgz",
523
523
  "license": "MIT",
524
524
  "optionalDependencies": {
525
- "@bastani/atomic-natives-darwin-arm64": "0.9.19-alpha.7",
526
- "@bastani/atomic-natives-darwin-x64": "0.9.19-alpha.7",
527
- "@bastani/atomic-natives-linux-arm64-gnu": "0.9.19-alpha.7",
528
- "@bastani/atomic-natives-linux-arm64-musl": "0.9.19-alpha.7",
529
- "@bastani/atomic-natives-linux-x64-gnu": "0.9.19-alpha.7",
530
- "@bastani/atomic-natives-linux-x64-musl": "0.9.19-alpha.7",
531
- "@bastani/atomic-natives-win32-arm64-msvc": "0.9.19-alpha.7",
532
- "@bastani/atomic-natives-win32-x64-msvc": "0.9.19-alpha.7"
525
+ "@bastani/atomic-natives-darwin-arm64": "0.9.19-alpha.8",
526
+ "@bastani/atomic-natives-darwin-x64": "0.9.19-alpha.8",
527
+ "@bastani/atomic-natives-linux-arm64-gnu": "0.9.19-alpha.8",
528
+ "@bastani/atomic-natives-linux-arm64-musl": "0.9.19-alpha.8",
529
+ "@bastani/atomic-natives-linux-x64-gnu": "0.9.19-alpha.8",
530
+ "@bastani/atomic-natives-linux-x64-musl": "0.9.19-alpha.8",
531
+ "@bastani/atomic-natives-win32-arm64-msvc": "0.9.19-alpha.8",
532
+ "@bastani/atomic-natives-win32-x64-msvc": "0.9.19-alpha.8"
533
533
  },
534
534
  "engines": {
535
535
  "bun": ">=1.4.2",
@@ -537,8 +537,8 @@
537
537
  }
538
538
  },
539
539
  "node_modules/@bastani/atomic-natives-darwin-arm64": {
540
- "version": "0.9.19-alpha.7",
541
- "resolved": "https://registry.npmjs.org/@bastani/atomic-natives-darwin-arm64/-/atomic-natives-darwin-arm64-0.9.19-alpha.7.tgz",
540
+ "version": "0.9.19-alpha.8",
541
+ "resolved": "https://registry.npmjs.org/@bastani/atomic-natives-darwin-arm64/-/atomic-natives-darwin-arm64-0.9.19-alpha.8.tgz",
542
542
  "license": "MIT",
543
543
  "os": [
544
544
  "darwin"
@@ -549,8 +549,8 @@
549
549
  "optional": true
550
550
  },
551
551
  "node_modules/@bastani/atomic-natives-darwin-x64": {
552
- "version": "0.9.19-alpha.7",
553
- "resolved": "https://registry.npmjs.org/@bastani/atomic-natives-darwin-x64/-/atomic-natives-darwin-x64-0.9.19-alpha.7.tgz",
552
+ "version": "0.9.19-alpha.8",
553
+ "resolved": "https://registry.npmjs.org/@bastani/atomic-natives-darwin-x64/-/atomic-natives-darwin-x64-0.9.19-alpha.8.tgz",
554
554
  "license": "MIT",
555
555
  "os": [
556
556
  "darwin"
@@ -561,8 +561,8 @@
561
561
  "optional": true
562
562
  },
563
563
  "node_modules/@bastani/atomic-natives-linux-arm64-gnu": {
564
- "version": "0.9.19-alpha.7",
565
- "resolved": "https://registry.npmjs.org/@bastani/atomic-natives-linux-arm64-gnu/-/atomic-natives-linux-arm64-gnu-0.9.19-alpha.7.tgz",
564
+ "version": "0.9.19-alpha.8",
565
+ "resolved": "https://registry.npmjs.org/@bastani/atomic-natives-linux-arm64-gnu/-/atomic-natives-linux-arm64-gnu-0.9.19-alpha.8.tgz",
566
566
  "license": "MIT",
567
567
  "os": [
568
568
  "linux"
@@ -576,8 +576,8 @@
576
576
  "optional": true
577
577
  },
578
578
  "node_modules/@bastani/atomic-natives-linux-arm64-musl": {
579
- "version": "0.9.19-alpha.7",
580
- "resolved": "https://registry.npmjs.org/@bastani/atomic-natives-linux-arm64-musl/-/atomic-natives-linux-arm64-musl-0.9.19-alpha.7.tgz",
579
+ "version": "0.9.19-alpha.8",
580
+ "resolved": "https://registry.npmjs.org/@bastani/atomic-natives-linux-arm64-musl/-/atomic-natives-linux-arm64-musl-0.9.19-alpha.8.tgz",
581
581
  "license": "MIT",
582
582
  "os": [
583
583
  "linux"
@@ -591,8 +591,8 @@
591
591
  "optional": true
592
592
  },
593
593
  "node_modules/@bastani/atomic-natives-linux-x64-gnu": {
594
- "version": "0.9.19-alpha.7",
595
- "resolved": "https://registry.npmjs.org/@bastani/atomic-natives-linux-x64-gnu/-/atomic-natives-linux-x64-gnu-0.9.19-alpha.7.tgz",
594
+ "version": "0.9.19-alpha.8",
595
+ "resolved": "https://registry.npmjs.org/@bastani/atomic-natives-linux-x64-gnu/-/atomic-natives-linux-x64-gnu-0.9.19-alpha.8.tgz",
596
596
  "license": "MIT",
597
597
  "os": [
598
598
  "linux"
@@ -606,8 +606,8 @@
606
606
  "optional": true
607
607
  },
608
608
  "node_modules/@bastani/atomic-natives-linux-x64-musl": {
609
- "version": "0.9.19-alpha.7",
610
- "resolved": "https://registry.npmjs.org/@bastani/atomic-natives-linux-x64-musl/-/atomic-natives-linux-x64-musl-0.9.19-alpha.7.tgz",
609
+ "version": "0.9.19-alpha.8",
610
+ "resolved": "https://registry.npmjs.org/@bastani/atomic-natives-linux-x64-musl/-/atomic-natives-linux-x64-musl-0.9.19-alpha.8.tgz",
611
611
  "license": "MIT",
612
612
  "os": [
613
613
  "linux"
@@ -621,8 +621,8 @@
621
621
  "optional": true
622
622
  },
623
623
  "node_modules/@bastani/atomic-natives-win32-arm64-msvc": {
624
- "version": "0.9.19-alpha.7",
625
- "resolved": "https://registry.npmjs.org/@bastani/atomic-natives-win32-arm64-msvc/-/atomic-natives-win32-arm64-msvc-0.9.19-alpha.7.tgz",
624
+ "version": "0.9.19-alpha.8",
625
+ "resolved": "https://registry.npmjs.org/@bastani/atomic-natives-win32-arm64-msvc/-/atomic-natives-win32-arm64-msvc-0.9.19-alpha.8.tgz",
626
626
  "license": "MIT",
627
627
  "os": [
628
628
  "win32"
@@ -633,8 +633,8 @@
633
633
  "optional": true
634
634
  },
635
635
  "node_modules/@bastani/atomic-natives-win32-x64-msvc": {
636
- "version": "0.9.19-alpha.7",
637
- "resolved": "https://registry.npmjs.org/@bastani/atomic-natives-win32-x64-msvc/-/atomic-natives-win32-x64-msvc-0.9.19-alpha.7.tgz",
636
+ "version": "0.9.19-alpha.8",
637
+ "resolved": "https://registry.npmjs.org/@bastani/atomic-natives-win32-x64-msvc/-/atomic-natives-win32-x64-msvc-0.9.19-alpha.8.tgz",
638
638
  "license": "MIT",
639
639
  "os": [
640
640
  "win32"
@@ -645,8 +645,8 @@
645
645
  "optional": true
646
646
  },
647
647
  "node_modules/@bastani/pi-ai": {
648
- "version": "0.9.19-alpha.7",
649
- "resolved": "https://registry.npmjs.org/@bastani/pi-ai/-/pi-ai-0.9.19-alpha.7.tgz",
648
+ "version": "0.9.19-alpha.8",
649
+ "resolved": "https://registry.npmjs.org/@bastani/pi-ai/-/pi-ai-0.9.19-alpha.8.tgz",
650
650
  "license": "MIT",
651
651
  "dependencies": {
652
652
  "@anthropic-ai/sdk": "0.124.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/atomic",
3
- "version": "0.9.19-alpha.7",
3
+ "version": "0.9.19-alpha.8",
4
4
  "description": "Atomic coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "atomicConfig": {
@@ -79,8 +79,8 @@
79
79
  "prepublishOnly": "bun run clean && bun run build && bun run shrinkwrap"
80
80
  },
81
81
  "dependencies": {
82
- "@bastani/atomic-natives": "0.9.19-alpha.7",
83
- "@bastani/pi-ai": "0.9.19-alpha.7",
82
+ "@bastani/atomic-natives": "0.9.19-alpha.8",
83
+ "@bastani/pi-ai": "0.9.19-alpha.8",
84
84
  "@dbos-inc/dbos-sdk": "4.25.14",
85
85
  "@earendil-works/pi-agent-core": "0.85.1",
86
86
  "@earendil-works/pi-client": "0.85.1",