@bastani/atomic 0.9.3-alpha.1 → 0.9.3-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/builtin/cursor/CHANGELOG.md +21 -0
  3. package/dist/builtin/cursor/README.md +2 -1
  4. package/dist/builtin/cursor/package.json +2 -2
  5. package/dist/builtin/cursor/src/cursor-models-raw.json +2 -9
  6. package/dist/builtin/cursor/src/model-mapper.ts +14 -3
  7. package/dist/builtin/cursor/src/proto/protobuf-codec-base64.ts +22 -0
  8. package/dist/builtin/cursor/src/proto/protobuf-codec-request.ts +53 -13
  9. package/dist/builtin/cursor/src/proto/protobuf-codec-wire.ts +24 -7
  10. package/dist/builtin/cursor/src/proto/protobuf-codec.ts +3 -2
  11. package/dist/builtin/cursor/src/stream.ts +5 -11
  12. package/dist/builtin/cursor/src/transport-types.ts +3 -0
  13. package/dist/builtin/cursor/src/transport.ts +1 -0
  14. package/dist/builtin/intercom/CHANGELOG.md +6 -0
  15. package/dist/builtin/intercom/package.json +1 -1
  16. package/dist/builtin/mcp/CHANGELOG.md +6 -0
  17. package/dist/builtin/mcp/package.json +1 -1
  18. package/dist/builtin/subagents/CHANGELOG.md +15 -0
  19. package/dist/builtin/subagents/package.json +1 -1
  20. package/dist/builtin/subagents/src/extension/fanout-child.ts +1 -0
  21. package/dist/builtin/subagents/src/extension/index.ts +6 -3
  22. package/dist/builtin/subagents/src/extension/schemas.ts +0 -5
  23. package/dist/builtin/subagents/src/runs/background/async-job-tracker.ts +1 -4
  24. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-single.ts +15 -1
  25. package/dist/builtin/subagents/src/runs/foreground/subagent-executor.ts +35 -1
  26. package/dist/builtin/subagents/src/runs/shared/subagent-prompt-runtime.ts +4 -2
  27. package/dist/builtin/subagents/src/shared/types-async.ts +1 -0
  28. package/dist/builtin/subagents/src/slash/prompt-template-bridge.ts +27 -5
  29. package/dist/builtin/subagents/src/tui/render-layout.ts +27 -4
  30. package/dist/builtin/subagents/src/tui/render-result-animation.ts +22 -31
  31. package/dist/builtin/subagents/src/tui/render-result-compact.ts +6 -6
  32. package/dist/builtin/subagents/src/tui/render-result.ts +20 -19
  33. package/dist/builtin/subagents/src/tui/render-status-progress.ts +3 -3
  34. package/dist/builtin/subagents/src/tui/render-widget.ts +46 -7
  35. package/dist/builtin/subagents/src/tui/render.ts +2 -2
  36. package/dist/builtin/web-access/CHANGELOG.md +6 -0
  37. package/dist/builtin/web-access/package.json +1 -1
  38. package/dist/builtin/workflows/CHANGELOG.md +49 -0
  39. package/dist/builtin/workflows/README.md +1 -1
  40. package/dist/builtin/workflows/package.json +1 -1
  41. package/dist/builtin/workflows/src/authoring.d.ts +1 -1
  42. package/dist/builtin/workflows/src/durable/backend.ts +343 -0
  43. package/dist/builtin/workflows/src/durable/child-primitive.ts +79 -0
  44. package/dist/builtin/workflows/src/durable/dbos-backend.ts +421 -0
  45. package/dist/builtin/workflows/src/durable/dbos-envelope.ts +171 -0
  46. package/dist/builtin/workflows/src/durable/factory.ts +96 -0
  47. package/dist/builtin/workflows/src/durable/file-backend.ts +433 -0
  48. package/dist/builtin/workflows/src/durable/index.ts +73 -0
  49. package/dist/builtin/workflows/src/durable/resume-catalog.ts +217 -0
  50. package/dist/builtin/workflows/src/durable/resume-runtime.ts +299 -0
  51. package/dist/builtin/workflows/src/durable/scoped-backend.ts +171 -0
  52. package/dist/builtin/workflows/src/durable/stage-primitive.ts +284 -0
  53. package/dist/builtin/workflows/src/durable/tool-primitive.ts +180 -0
  54. package/dist/builtin/workflows/src/durable/types.ts +168 -0
  55. package/dist/builtin/workflows/src/durable/ui-primitive.ts +96 -0
  56. package/dist/builtin/workflows/src/engine/options.ts +3 -0
  57. package/dist/builtin/workflows/src/engine/primitives/parallel.ts +2 -2
  58. package/dist/builtin/workflows/src/engine/primitives/task.ts +4 -4
  59. package/dist/builtin/workflows/src/engine/primitives/ui.ts +22 -8
  60. package/dist/builtin/workflows/src/engine/primitives/workflow.ts +8 -0
  61. package/dist/builtin/workflows/src/engine/run-durable-finalize.ts +69 -0
  62. package/dist/builtin/workflows/src/engine/run-durable-stage-session.ts +31 -0
  63. package/dist/builtin/workflows/src/engine/run.ts +148 -6
  64. package/dist/builtin/workflows/src/engine/runtime.ts +8 -2
  65. package/dist/builtin/workflows/src/extension/extension-factory.ts +6 -12
  66. package/dist/builtin/workflows/src/extension/extension-lifecycle.ts +5 -1
  67. package/dist/builtin/workflows/src/extension/extension-runtime-state.ts +3 -0
  68. package/dist/builtin/workflows/src/extension/runtime.ts +48 -9
  69. package/dist/builtin/workflows/src/extension/workflow-run-control-command.ts +143 -4
  70. package/dist/builtin/workflows/src/runs/background/quit.ts +61 -0
  71. package/dist/builtin/workflows/src/runs/background/status.ts +1 -0
  72. package/dist/builtin/workflows/src/runs/foreground/executor-direct-helpers.ts +5 -5
  73. package/dist/builtin/workflows/src/runs/foreground/executor-stage-call.ts +74 -33
  74. package/dist/builtin/workflows/src/runs/foreground/executor-stage-context.ts +20 -1
  75. package/dist/builtin/workflows/src/runs/foreground/executor-stage-factory.ts +8 -7
  76. package/dist/builtin/workflows/src/runs/foreground/executor-stage-replay.ts +1 -0
  77. package/dist/builtin/workflows/src/runs/foreground/executor-stage-types.ts +1 -1
  78. package/dist/builtin/workflows/src/runs/foreground/executor-types.ts +19 -2
  79. package/dist/builtin/workflows/src/runs/foreground/stage-runner-context.ts +4 -0
  80. package/dist/builtin/workflows/src/runs/foreground/stage-runner-controller.ts +10 -10
  81. package/dist/builtin/workflows/src/runs/foreground/stage-runner-options.ts +5 -1
  82. package/dist/builtin/workflows/src/runs/foreground/stage-runner-send-user-message.ts +25 -0
  83. package/dist/builtin/workflows/src/runs/foreground/stage-runner-types.ts +3 -0
  84. package/dist/builtin/workflows/src/shared/authoring-contract-stage.d.ts +16 -0
  85. package/dist/builtin/workflows/src/shared/authoring-contract-stage.ts +20 -0
  86. package/dist/builtin/workflows/src/shared/authoring-contract-ui.d.ts +23 -1
  87. package/dist/builtin/workflows/src/shared/authoring-contract-ui.ts +30 -1
  88. package/dist/builtin/workflows/src/shared/store-public-types.ts +6 -2
  89. package/dist/builtin/workflows/src/shared/store-run-methods.ts +12 -6
  90. package/dist/builtin/workflows/src/shared/types.ts +55 -0
  91. package/dist/builtin/workflows/src/tui/graph-view-constants.ts +1 -1
  92. package/dist/builtin/workflows/src/tui/graph-view-graph-render.ts +41 -0
  93. package/dist/builtin/workflows/src/tui/graph-view-input.ts +82 -24
  94. package/dist/builtin/workflows/src/tui/graph-view-render.ts +7 -0
  95. package/dist/builtin/workflows/src/tui/graph-view-state.ts +22 -2
  96. package/dist/builtin/workflows/src/tui/graph-view-types.ts +4 -5
  97. package/dist/builtin/workflows/src/tui/overlay-adapter.ts +9 -11
  98. package/dist/builtin/workflows/src/tui/stage-chat-view-footer-status.ts +9 -3
  99. package/dist/builtin/workflows/src/tui/stage-chat-view-input.ts +11 -2
  100. package/dist/builtin/workflows/src/tui/stage-chat-view-live-events.ts +35 -0
  101. package/dist/builtin/workflows/src/tui/stage-chat-view-state.ts +51 -17
  102. package/dist/builtin/workflows/src/tui/stage-chat-view-status.ts +36 -0
  103. package/dist/builtin/workflows/src/tui/stage-chat-view-types.ts +5 -1
  104. package/dist/builtin/workflows/src/tui/stage-chat-view.ts +3 -1
  105. package/dist/builtin/workflows/src/tui/status-list.ts +14 -2
  106. package/dist/builtin/workflows/src/tui/widget.ts +23 -8
  107. package/dist/builtin/workflows/src/tui/workflow-attach-pane-types.ts +5 -4
  108. package/dist/builtin/workflows/src/tui/workflow-attach-pane.ts +8 -8
  109. package/dist/builtin/workflows/src/tui/workflow-resume-selector.ts +151 -0
  110. package/dist/core/extensions/loader-virtual-modules.d.ts.map +1 -1
  111. package/dist/core/extensions/loader-virtual-modules.js +47 -30
  112. package/dist/core/extensions/loader-virtual-modules.js.map +1 -1
  113. package/dist/core/messages.d.ts +1 -0
  114. package/dist/core/messages.d.ts.map +1 -1
  115. package/dist/core/messages.js +46 -1
  116. package/dist/core/messages.js.map +1 -1
  117. package/dist/core/sdk.d.ts.map +1 -1
  118. package/dist/core/sdk.js +12 -0
  119. package/dist/core/sdk.js.map +1 -1
  120. package/dist/core/session-manager-core.d.ts +15 -7
  121. package/dist/core/session-manager-core.d.ts.map +1 -1
  122. package/dist/core/session-manager-core.js +20 -9
  123. package/dist/core/session-manager-core.js.map +1 -1
  124. package/dist/core/session-manager-entries.d.ts +2 -2
  125. package/dist/core/session-manager-entries.d.ts.map +1 -1
  126. package/dist/core/session-manager-entries.js +9 -3
  127. package/dist/core/session-manager-entries.js.map +1 -1
  128. package/dist/core/session-manager-history.d.ts.map +1 -1
  129. package/dist/core/session-manager-history.js +2 -1
  130. package/dist/core/session-manager-history.js.map +1 -1
  131. package/dist/core/session-manager-list.d.ts +3 -3
  132. package/dist/core/session-manager-list.d.ts.map +1 -1
  133. package/dist/core/session-manager-list.js +27 -8
  134. package/dist/core/session-manager-list.js.map +1 -1
  135. package/dist/core/session-manager-storage.d.ts +3 -1
  136. package/dist/core/session-manager-storage.d.ts.map +1 -1
  137. package/dist/core/session-manager-storage.js +55 -12
  138. package/dist/core/session-manager-storage.js.map +1 -1
  139. package/dist/core/session-manager-tool-dependencies.d.ts +10 -0
  140. package/dist/core/session-manager-tool-dependencies.d.ts.map +1 -0
  141. package/dist/core/session-manager-tool-dependencies.js +133 -0
  142. package/dist/core/session-manager-tool-dependencies.js.map +1 -0
  143. package/dist/core/session-manager-types.d.ts +22 -0
  144. package/dist/core/session-manager-types.d.ts.map +1 -1
  145. package/dist/core/session-manager-types.js.map +1 -1
  146. package/dist/core/session-manager.d.ts +2 -2
  147. package/dist/core/session-manager.d.ts.map +1 -1
  148. package/dist/core/session-manager.js +1 -1
  149. package/dist/core/session-manager.js.map +1 -1
  150. package/dist/modes/interactive/components/chat-session-host-runtime.d.ts +1 -0
  151. package/dist/modes/interactive/components/chat-session-host-runtime.d.ts.map +1 -1
  152. package/dist/modes/interactive/components/chat-session-host-runtime.js +12 -0
  153. package/dist/modes/interactive/components/chat-session-host-runtime.js.map +1 -1
  154. package/dist/modes/interactive/components/chat-session-host-terminal-cleanup.d.ts +4 -0
  155. package/dist/modes/interactive/components/chat-session-host-terminal-cleanup.d.ts.map +1 -0
  156. package/dist/modes/interactive/components/chat-session-host-terminal-cleanup.js +131 -0
  157. package/dist/modes/interactive/components/chat-session-host-terminal-cleanup.js.map +1 -0
  158. package/dist/modes/interactive/components/chat-session-host.d.ts +2 -0
  159. package/dist/modes/interactive/components/chat-session-host.d.ts.map +1 -1
  160. package/dist/modes/interactive/components/chat-session-host.js +7 -1
  161. package/dist/modes/interactive/components/chat-session-host.js.map +1 -1
  162. package/dist/modes/interactive/components/chat-transcript.d.ts.map +1 -1
  163. package/dist/modes/interactive/components/chat-transcript.js +15 -4
  164. package/dist/modes/interactive/components/chat-transcript.js.map +1 -1
  165. package/dist/modes/interactive/components/tool-execution.d.ts +3 -0
  166. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  167. package/dist/modes/interactive/components/tool-execution.js +26 -0
  168. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  169. package/docs/compaction.md +2 -0
  170. package/docs/models.md +1 -1
  171. package/docs/providers.md +2 -1
  172. package/docs/session-format.md +6 -0
  173. package/docs/sessions.md +6 -0
  174. package/docs/workflows.md +105 -3
  175. package/package.json +4 -3
@@ -16,6 +16,8 @@ export abstract class GraphViewGraphRenderer extends GraphViewRenderHelpers {
16
16
  protected _renderGraph(width: number): string[] {
17
17
  const run = this._getCurrentRun();
18
18
  if (!run || this.cachedLayout.length === 0) {
19
+ this.lastGraphViewport = null;
20
+ this.graphNodeHitRects = [];
19
21
  const dim = hexToAnsi(this.graphTheme.dim);
20
22
  return [
21
23
  this._centerCanvasContent(
@@ -40,6 +42,7 @@ export abstract class GraphViewGraphRenderer extends GraphViewRenderHelpers {
40
42
  );
41
43
  const viewportWidth = Math.max(1, width - leftMargin);
42
44
  const fullCanvasWidth = Math.max(canvasWidth, viewportWidth);
45
+ this.lastGraphViewport = { leftMargin, viewportWidth };
43
46
  this._clampGraphHorizontalScroll(fullCanvasWidth, viewportWidth);
44
47
  if (this.pendingEnsureFocusedVisible) {
45
48
  this._scrollFocusedColumnIntoView(viewportWidth, fullCanvasWidth);
@@ -129,6 +132,44 @@ export abstract class GraphViewGraphRenderer extends GraphViewRenderHelpers {
129
132
  });
130
133
  }
131
134
 
135
+ protected _recordGraphNodeHitRects(
136
+ graphStartRow: number,
137
+ visibleRowCount: number,
138
+ ): void {
139
+ const viewport = this.lastGraphViewport;
140
+ if (!viewport || visibleRowCount <= 0) {
141
+ this.graphNodeHitRects = [];
142
+ return;
143
+ }
144
+
145
+ const visibleTop = graphStartRow;
146
+ const visibleBottom = graphStartRow + visibleRowCount;
147
+ const viewportLeft = viewport.leftMargin;
148
+ const viewportRight = viewport.leftMargin + viewport.viewportWidth;
149
+ const rects: typeof this.graphNodeHitRects = [];
150
+
151
+ for (let index = 0; index < this.cachedLayout.length; index++) {
152
+ const node = this.cachedLayout[index]!;
153
+ const top = graphStartRow + node.y - this.graphScrollOffset;
154
+ const bottom = top + NODE_H;
155
+ const left = viewport.leftMargin + node.x - this.graphScrollColOffset;
156
+ const right = left + NODE_W;
157
+ const clippedTop = Math.max(visibleTop, top);
158
+ const clippedBottom = Math.min(visibleBottom, bottom);
159
+ const clippedLeft = Math.max(viewportLeft, left);
160
+ const clippedRight = Math.min(viewportRight, right);
161
+ if (clippedTop >= clippedBottom || clippedLeft >= clippedRight) continue;
162
+ rects.push({
163
+ index,
164
+ top: clippedTop,
165
+ bottom: clippedBottom,
166
+ left: clippedLeft,
167
+ right: clippedRight,
168
+ });
169
+ }
170
+ this.graphNodeHitRects = rects;
171
+ }
172
+
132
173
  protected _visibleGraphLines(
133
174
  graphLines: string[],
134
175
  frameWidth: number,
@@ -11,6 +11,13 @@ import {
11
11
  import { filterStages, type SwitcherState } from "./switcher.js";
12
12
  import { Key, matchesKey } from "./text-helpers.js";
13
13
 
14
+ interface SgrMouseEvent {
15
+ buttonCode: number;
16
+ col: number;
17
+ row: number;
18
+ final: "M" | "m";
19
+ }
20
+
14
21
  /** Keyboard, mouse, switcher, prompt, and focus navigation handling. */
15
22
  export abstract class GraphViewInputController extends GraphViewRenderer {
16
23
  /** Returns true if consumed. */
@@ -80,6 +87,15 @@ export abstract class GraphViewInputController extends GraphViewRenderer {
80
87
  return true;
81
88
  }
82
89
 
90
+ const clickedNodeIndex = this._graphNodeIndexForClick(data);
91
+ if (clickedNodeIndex !== undefined) {
92
+ if (clickedNodeIndex !== null) {
93
+ this._setFocusedIndex(clickedNodeIndex);
94
+ this._activateFocusedNode();
95
+ }
96
+ return true;
97
+ }
98
+
83
99
  // Vertical-graph navigation: up/down step between depth levels
84
100
  // (col), left/right step between siblings at the same depth (row).
85
101
  // j/k preserved as a flat-order fallback for muscle memory.
@@ -115,12 +131,7 @@ export abstract class GraphViewInputController extends GraphViewRenderer {
115
131
  return true;
116
132
  }
117
133
  if (matchesKey(data, Key.enter)) {
118
- // Enter attaches the popup interior to the focused stage. The
119
- // attach shell swaps in the stage-chat view without remounting
120
- // the overlay; without a callback, fall back to the legacy
121
- // expand/collapse toggle so non-attach hosts still work.
122
- if (this._attachFocusedStage()) return true;
123
- this.detailsExpanded = !this.detailsExpanded;
134
+ this._activateFocusedNode();
124
135
  return true;
125
136
  }
126
137
  // `ctrl+d` detaches the whole popup (host hides the overlay). This
@@ -133,16 +144,13 @@ export abstract class GraphViewInputController extends GraphViewRenderer {
133
144
  }
134
145
  return true;
135
146
  }
136
- // `q` kills the active run (no confirm). `h` hides the pane via
137
- // the overlay's setHidden() flag (not unmount); Escape/Ctrl+C closes.
147
+ // `q` quits/detaches the orchestrator view without authoritatively
148
+ // killing the workflow. The workflow remains resumable via
149
+ // `/workflow resume`; use `/workflow kill` for non-resumable disposal.
138
150
  if (matchesKey(data, "q")) {
139
151
  const run = this._getCurrentRun();
140
- const targetRunId = this._focusedStageTarget()?.runId ?? run?.id;
141
- const targetRun = targetRunId !== undefined
142
- ? this.currentSnapshot?.runs.find((candidate) => candidate.id === targetRunId)
143
- : undefined;
144
- if (targetRun && targetRun.endedAt === undefined && this.onKill) {
145
- this.onKill(targetRun.id);
152
+ if (run && run.endedAt === undefined && this.onQuit) {
153
+ this.onQuit(run.id);
146
154
  }
147
155
  this.onClose?.();
148
156
  return true;
@@ -265,6 +273,15 @@ export abstract class GraphViewInputController extends GraphViewRenderer {
265
273
  return true;
266
274
  }
267
275
 
276
+ private _activateFocusedNode(): void {
277
+ // Enter and direct node clicks attach the popup interior to the focused
278
+ // stage. The attach shell swaps in the stage-chat view without remounting
279
+ // the overlay; without a callback, fall back to the legacy expand/collapse
280
+ // toggle so non-attach hosts still work.
281
+ if (this._attachFocusedStage()) return;
282
+ this.detailsExpanded = !this.detailsExpanded;
283
+ }
284
+
268
285
  private _attachFocusedStage(): boolean {
269
286
  if (!this.onStageAttach) return false;
270
287
  const node = this.cachedLayout[this.focusedIndex];
@@ -278,13 +295,6 @@ export abstract class GraphViewInputController extends GraphViewRenderer {
278
295
  return true;
279
296
  }
280
297
 
281
- private _focusedStageTarget(): { runId: string; stageId: string } | undefined {
282
- const node = this.cachedLayout[this.focusedIndex];
283
- if (!node) return undefined;
284
- const target = expandedStageTarget(this.expandedGraph, node.stage.id);
285
- return target ? { runId: target.runId, stageId: target.stageId } : undefined;
286
- }
287
-
288
298
  private _setFocusedIndex(index: number): void {
289
299
  const max = Math.max(0, this.cachedLayout.length - 1);
290
300
  const next = Math.max(0, Math.min(index, max));
@@ -298,10 +308,55 @@ export abstract class GraphViewInputController extends GraphViewRenderer {
298
308
  this.graphScrollOffset = Math.max(0, this.graphScrollOffset + deltaRows);
299
309
  }
300
310
 
311
+ private _graphNodeIndexForClick(data: string): number | null | undefined {
312
+ const click = this._sgrLeftMousePress(data);
313
+ if (!click) return undefined;
314
+ if (this.mode !== "overlay") return undefined;
315
+ if (this.cachedLayout.length === 0) return null;
316
+
317
+ for (const rect of this.graphNodeHitRects) {
318
+ if (
319
+ click.row >= rect.top &&
320
+ click.row < rect.bottom &&
321
+ click.col >= rect.left &&
322
+ click.col < rect.right
323
+ ) {
324
+ return rect.index;
325
+ }
326
+ }
327
+ return null;
328
+ }
329
+
330
+ private _parseSgrMouse(data: string): SgrMouseEvent | null {
331
+ const sgr = data.match(/^\x1b\[<(\d+);(\d+);(\d+)([Mm])$/);
332
+ if (!sgr) return null;
333
+ const oneBasedCol = Number.parseInt(sgr[2]!, 10);
334
+ const oneBasedRow = Number.parseInt(sgr[3]!, 10);
335
+ const final = sgr[4];
336
+ if (oneBasedCol < 1 || oneBasedRow < 1) return null;
337
+ if (final !== "M" && final !== "m") return null;
338
+ return {
339
+ buttonCode: Number.parseInt(sgr[1]!, 10),
340
+ col: oneBasedCol - 1,
341
+ row: oneBasedRow - 1,
342
+ final,
343
+ };
344
+ }
345
+
346
+ private _sgrLeftMousePress(data: string): { col: number; row: number } | null {
347
+ const sgr = this._parseSgrMouse(data);
348
+ if (!sgr || sgr.final !== "M") return null;
349
+ const buttonCode = sgr.buttonCode;
350
+ if ((buttonCode & 64) !== 0 || (buttonCode & 32) !== 0 || (buttonCode & 3) !== 0) {
351
+ return null;
352
+ }
353
+ return { col: sgr.col, row: sgr.row };
354
+ }
355
+
301
356
  private _mouseWheelDeltaRows(data: string): number {
302
- const sgr = data.match(/^\x1b\[<(\d+);\d+;\d+M$/);
303
- if (sgr) {
304
- return this._wheelDeltaForButtonCode(Number.parseInt(sgr[1]!, 10));
357
+ const sgr = this._parseSgrMouse(data);
358
+ if (sgr && sgr.final === "M") {
359
+ return this._wheelDeltaForButtonCode(sgr.buttonCode);
305
360
  }
306
361
  if (data.startsWith("\x1b[M") && data.length >= 6) {
307
362
  return this._wheelDeltaForButtonCode(data.charCodeAt(3) - 32);
@@ -330,4 +385,7 @@ export abstract class GraphViewInputController extends GraphViewRenderer {
330
385
  get _graphScrollOffset(): number {
331
386
  return this.graphScrollOffset;
332
387
  }
388
+ get _graphScrollColOffset(): number {
389
+ return this.graphScrollColOffset;
390
+ }
333
391
  }
@@ -37,6 +37,7 @@ export abstract class GraphViewRenderer extends GraphViewGraphRenderer {
37
37
 
38
38
  protected _renderOverlay(width: number): string[] {
39
39
  const frameWidth = Math.max(40, width);
40
+ this.lastOverlayFrameWidth = frameWidth;
40
41
  const lines: string[] = [];
41
42
  const run = this._getCurrentRun();
42
43
 
@@ -58,6 +59,10 @@ export abstract class GraphViewRenderer extends GraphViewGraphRenderer {
58
59
  frameWidth,
59
60
  bodyTarget,
60
61
  );
62
+ this._recordGraphNodeHitRects(
63
+ this._overlayVerticalMarginRows() + 3 + visibleGraph.topPad,
64
+ visibleGraph.lines.length,
65
+ );
61
66
  for (let i = 0; i < visibleGraph.topPad; i++)
62
67
  lines.push(this._blankRow(frameWidth));
63
68
  for (const line of visibleGraph.lines) {
@@ -78,6 +83,8 @@ export abstract class GraphViewRenderer extends GraphViewGraphRenderer {
78
83
  }
79
84
 
80
85
  protected _renderEmptyState(width: number): string[] {
86
+ this.graphNodeHitRects = [];
87
+ this.lastGraphViewport = null;
81
88
  const t = this.graphTheme;
82
89
  const muted = hexToAnsi(t.textMuted);
83
90
  const dim = hexToAnsi(t.dim);
@@ -34,6 +34,19 @@ export interface GraphStageCounts {
34
34
  skipped: number;
35
35
  }
36
36
 
37
+ interface GraphNodeHitRect {
38
+ index: number;
39
+ top: number;
40
+ bottom: number;
41
+ left: number;
42
+ right: number;
43
+ }
44
+
45
+ interface GraphViewportGeometry {
46
+ leftMargin: number;
47
+ viewportWidth: number;
48
+ }
49
+
37
50
  /** Expansion, focus, prompt, and store-backed layout state for GraphView. */
38
51
  export abstract class GraphViewState {
39
52
  protected mode: GraphViewMode;
@@ -41,7 +54,7 @@ export abstract class GraphViewState {
41
54
  protected store: Store;
42
55
  protected graphTheme: GraphTheme;
43
56
  protected onClose?: () => void;
44
- protected onKill?: (runId: string) => void;
57
+ protected onQuit?: (runId: string) => void;
45
58
  protected onHide?: () => void;
46
59
  protected onPromptResolve?: (runId: string, promptId: string, response: unknown) => void;
47
60
  protected onStageAttach?: (runId: string, stageId: string) => void;
@@ -64,6 +77,9 @@ export abstract class GraphViewState {
64
77
  protected currentSnapshot: StoreSnapshot | null = null;
65
78
  protected graphScrollOffset = 0;
66
79
  protected graphScrollColOffset = 0;
80
+ protected graphNodeHitRects: GraphNodeHitRect[] = [];
81
+ protected lastGraphViewport: GraphViewportGeometry | null = null;
82
+ protected lastOverlayFrameWidth = 80;
67
83
  protected pendingEnsureFocusedVisible = true;
68
84
  protected lastAutoFocusedAwaitingInputKey: string | null = null;
69
85
 
@@ -77,7 +93,7 @@ export abstract class GraphViewState {
77
93
  this.store = opts.store;
78
94
  this.graphTheme = opts.graphTheme;
79
95
  this.onClose = opts.onClose;
80
- this.onKill = opts.onKill;
96
+ this.onQuit = opts.onQuit;
81
97
  this.onHide = opts.onHide;
82
98
  this.onPromptResolve = opts.onPromptResolve;
83
99
  this.onStageAttach = opts.onStageAttach;
@@ -121,6 +137,8 @@ export abstract class GraphViewState {
121
137
  this.focusedIndex = 0;
122
138
  this.graphScrollOffset = 0;
123
139
  this.graphScrollColOffset = 0;
140
+ this.graphNodeHitRects = [];
141
+ this.lastGraphViewport = null;
124
142
  this.pendingEnsureFocusedVisible = true;
125
143
  this.promptState = null;
126
144
  return;
@@ -130,6 +148,8 @@ export abstract class GraphViewState {
130
148
  const graphStages = this._graphStages(run);
131
149
  const nextLayout = computeLayout(graphStages, { orientation: "vertical" });
132
150
  this.cachedLayout = nextLayout;
151
+ this.graphNodeHitRects = [];
152
+ this.lastGraphViewport = null;
133
153
 
134
154
  let focusNeedsReveal = this.pendingEnsureFocusedVisible;
135
155
  // One-shot: if the host passed `initialFocusedStageId`, snap the
@@ -10,12 +10,11 @@ export interface GraphViewOpts {
10
10
  graphTheme: GraphTheme;
11
11
  onClose?: () => void;
12
12
  /**
13
- * Invoked when the user presses `q` inside the pane on an in-flight
14
- * run. Fires immediately (no confirm) per the toggle-driven UX:
15
- * `h` hides without quitting; `q` is reserved for terminating the
16
- * current run.
13
+ * Invoked when the user presses `q` inside the pane. This quits/detaches
14
+ * the orchestrator view and leaves the workflow resumable; it must not use
15
+ * the `/workflow kill` terminal path.
17
16
  */
18
- onKill?: (runId: string) => void;
17
+ onQuit?: (runId: string) => void;
19
18
  /**
20
19
  * Invoked when the user presses `h` inside the pane. Hides without
21
20
  * unmounting (overlay-adapter calls `setHidden(true)`). Re-open via
@@ -21,8 +21,7 @@ import type { ChatMessageRenderOptions, ReadonlyFooterDataProvider } from "@bast
21
21
  import { WorkflowAttachPane } from "./workflow-attach-pane.js";
22
22
  import { WORKFLOW_STATUS_KEY } from "./workflow-status.js";
23
23
  import { deriveGraphThemeFromPiTheme } from "./graph-theme.js";
24
- import { killRun as defaultKillRun } from "../runs/background/status.js";
25
- import { cancellationRegistry } from "../runs/background/cancellation-registry.js";
24
+ import { quitRun as defaultQuitRun } from "../runs/background/quit.js";
26
25
  import { stageControlRegistry as defaultStageControlRegistry } from "../runs/foreground/stage-control-registry.js";
27
26
  import type { StageControlRegistry } from "../runs/foreground/stage-control-registry.js";
28
27
  import type { StageUiBroker } from "../shared/stage-ui-broker.js";
@@ -100,8 +99,8 @@ const FULLSCREEN_OVERLAY_OPTIONS: PiOverlayOptions = {
100
99
  margin: 0,
101
100
  };
102
101
 
103
- const MOUSE_SCROLL_TRACKING_ON = "\x1b[?1000h\x1b[?1006h";
104
- const MOUSE_SCROLL_TRACKING_OFF = "\x1b[?1006l\x1b[?1000l";
102
+ const MOUSE_SCROLL_TRACKING_ON = "\x1b[?1000h\x1b[?1002h\x1b[?1006h";
103
+ const MOUSE_SCROLL_TRACKING_OFF = "\x1b[?1006l\x1b[?1002l\x1b[?1000l";
105
104
  const MAIN_CHAT_INPUT_STATUS_KEY = `${WORKFLOW_STATUS_KEY}:main-chat-input`;
106
105
  const MAIN_CHAT_INPUT_STATUS = "Main chat needs input — exit graph to answer.";
107
106
 
@@ -119,11 +118,10 @@ export interface BuildGraphOverlayAdapterOpts {
119
118
  /** Broker used to route stage-local custom UI into attached stage chats. */
120
119
  stageUiBroker?: StageUiBroker;
121
120
  /**
122
- * Kill hook used by graph-mode `q`. The extension factory supplies this so
123
- * persistence can record a terminal event while retaining the run for
124
- * inspection.
121
+ * Quit hook used by graph-mode `q`. This is intentionally distinct from
122
+ * `/workflow kill`: panel quit leaves durable-progress runs resumable.
125
123
  */
126
- onKillRun?: (runId: string) => void;
124
+ onQuitRun?: (runId: string) => void;
127
125
  /** Optional clock injection for deterministic attach-pane transition tests. */
128
126
  now?: () => number;
129
127
  }
@@ -135,8 +133,8 @@ export function buildGraphOverlayAdapter(
135
133
  ): GraphOverlayPort {
136
134
  const registry = buildOpts.stageControlRegistry ?? defaultStageControlRegistry;
137
135
  const stageUiBroker = buildOpts.stageUiBroker;
138
- const killRun = buildOpts.onKillRun ?? ((id: string): void => {
139
- defaultKillRun(id, { store, cancellation: cancellationRegistry });
136
+ const quitRun = buildOpts.onQuitRun ?? ((id: string): void => {
137
+ defaultQuitRun(id, { store, stageControlRegistry: registry });
140
138
  });
141
139
  let currentView: WorkflowAttachPane | null = null;
142
140
  // pi-tui returns an OverlayHandle via `options.onHandle`. We hold onto
@@ -329,7 +327,7 @@ export function buildGraphOverlayAdapter(
329
327
  uiStatus,
330
328
  onClose: finish,
331
329
  onHide: hideMounted,
332
- onKill: killRun,
330
+ onQuit: quitRun,
333
331
  initialAttachStageId: stageId,
334
332
  piTui: tui,
335
333
  piTheme: theme,
@@ -10,7 +10,10 @@ import {
10
10
  trailingWidgetBorderChar,
11
11
  widgetHintTargetLineIndex,
12
12
  } from "./stage-chat-view-render-helpers.js";
13
- import type { StageChatViewContext } from "./stage-chat-view-types.js";
13
+ import {
14
+ STAGE_CHAT_MOUSE_SCROLL_TOGGLE_LABEL,
15
+ type StageChatViewContext,
16
+ } from "./stage-chat-view-types.js";
14
17
  import type { StageSnapshot } from "../shared/store-types.js";
15
18
  import { truncateToWidth, visibleWidth } from "./text-helpers.js";
16
19
 
@@ -101,10 +104,13 @@ function mergeOrchestratorReturnHintIntoLine(
101
104
  width: number,
102
105
  options: { preserveTrailingBorder?: boolean; rightMargin?: number } = {},
103
106
  ): string {
104
- const plain = "ctrl+d returns to orchestrator panel";
107
+ const copyModeState = ctx.mouseScrollCaptureEnabled ? "off" : "on";
108
+ const plain = `ctrl+d graph · ${STAGE_CHAT_MOUSE_SCROLL_TOGGLE_LABEL} copy mode ${copyModeState}`;
105
109
  const styled =
106
110
  paint("ctrl+d", ctx.theme.text, { bold: true }) +
107
- paint(" returns to orchestrator panel", ctx.theme.textMuted);
111
+ paint(" graph · ", ctx.theme.textMuted) +
112
+ paint(STAGE_CHAT_MOUSE_SCROLL_TOGGLE_LABEL, ctx.theme.text, { bold: true }) +
113
+ paint(` copy mode ${copyModeState}`, ctx.theme.textMuted);
108
114
  const trailingBorder = options.preserveTrailingBorder === true
109
115
  ? trailingWidgetBorderChar(line)
110
116
  : "";
@@ -17,9 +17,9 @@ import { releaseMountedCustomUi } from "./stage-chat-view-custom-ui.js";
17
17
  import {
18
18
  canSubmitPrompt,
19
19
  currentStage,
20
+ isAbortableStreamingSession,
20
21
  isBlocked,
21
22
  isReadOnlyArchive,
22
- isStreaming,
23
23
  promptPageSize,
24
24
  recordCurrentPromptDraft,
25
25
  resolvePromptResponse,
@@ -30,6 +30,11 @@ export function handleStageChatInput(
30
30
  ctx: StageChatViewContext,
31
31
  data: string,
32
32
  ): boolean {
33
+ if (matchesKey(data, Key.ctrl("t"))) {
34
+ ctx.mouseScrollCaptureEnabled = !ctx.mouseScrollCaptureEnabled;
35
+ ctx.requestRender?.();
36
+ return true;
37
+ }
33
38
  if (ctx.mountedCustomUi) {
34
39
  return handleMountedCustomUiInput(ctx, data);
35
40
  }
@@ -54,12 +59,16 @@ export function handleStageChatInput(
54
59
  if (ctx.chatHost.handleScrollInput(data)) return true;
55
60
  if (matchesKey(data, Key.escape)) {
56
61
  if (
57
- isStreaming(ctx) ||
62
+ ctx.chatHost.isCompacting() ||
58
63
  ctx.chatHost.isBashRunning() ||
59
64
  ctx.chatHost.isEditingBashCommand()
60
65
  ) {
61
66
  return ctx.chatHost.handleInput(data);
62
67
  }
68
+ if (isAbortableStreamingSession(ctx)) {
69
+ void ctx.chatHost.interrupt();
70
+ return true;
71
+ }
63
72
  ctx.onClose();
64
73
  return true;
65
74
  }
@@ -0,0 +1,35 @@
1
+ import type { AgentSessionEvent } from "@bastani/atomic";
2
+ import type { StageChatViewContext } from "./stage-chat-view-types.js";
3
+ import { isTerminalStageChatState } from "./stage-chat-view-status.js";
4
+
5
+ export function applyStageChatLiveHandleEvent(
6
+ ctx: StageChatViewContext,
7
+ event: AgentSessionEvent,
8
+ ): void {
9
+ ctx.chatHost.applyAgentEvent(event);
10
+ if (!shouldCleanupAfterLiveEvent(ctx, event)) return;
11
+ const hadAnimationTick = ctx.chatHost.hasAnimationTick();
12
+ ctx.chatHost.clearBusyForTerminalWorkflowStage();
13
+ if (hadAnimationTick !== ctx.chatHost.hasAnimationTick()) ctx.requestRender?.();
14
+ }
15
+
16
+ function shouldCleanupAfterLiveEvent(
17
+ ctx: StageChatViewContext,
18
+ event: AgentSessionEvent,
19
+ ): boolean {
20
+ if (!isToolExecutionLiveEvent(event)) return false;
21
+ if (ctx.chatHost.isStreaming()) return false;
22
+ return isCurrentRunOrStageTerminal(ctx);
23
+ }
24
+
25
+ function isCurrentRunOrStageTerminal(ctx: StageChatViewContext): boolean {
26
+ return (
27
+ isTerminalStageChatState(ctx.lastObservedRunStatus) ||
28
+ isTerminalStageChatState(ctx.lastObservedStageStatus)
29
+ );
30
+ }
31
+
32
+ function isToolExecutionLiveEvent(event: AgentSessionEvent): boolean {
33
+ const type = String((event as { type?: unknown }).type ?? "");
34
+ return type === "tool_execution_start" || type === "tool_execution_update";
35
+ }
@@ -1,10 +1,6 @@
1
- import {
2
- ChatSessionHost,
3
- type ChatSessionHostStyle,
4
- } from "@bastani/atomic";
5
- import { Editor } from "@earendil-works/pi-tui";
6
- import type { EditorComponent } from "@earendil-works/pi-tui";
7
- import type { PendingPrompt, StageSnapshot } from "../shared/store-types.js";
1
+ import { ChatSessionHost, type ChatSessionHostStyle } from "@bastani/atomic";
2
+ import { Editor, type EditorComponent } from "@earendil-works/pi-tui";
3
+ import type { PendingPrompt, RunSnapshot, StageSnapshot } from "../shared/store-types.js";
8
4
  import { stageUiBroker } from "../shared/stage-ui-broker.js";
9
5
  import { resolveStageChatViewportRows } from "./stage-chat-layout.js";
10
6
  import { createPromptCardState } from "./prompt-card.js";
@@ -33,7 +29,13 @@ import {
33
29
  type StageChatViewOpts,
34
30
  } from "./stage-chat-view-types.js";
35
31
  import { noticeRow, noticeSummary } from "./stage-chat-view-transcript.js";
32
+ import { applyStageChatLiveHandleEvent } from "./stage-chat-view-live-events.js";
36
33
  import { hexToAnsi, RESET } from "./color-utils.js";
34
+ import {
35
+ isTerminalOrNonStreamingStageChatStatus,
36
+ isTerminalStageChatState,
37
+ isTerminalStageChatTransition,
38
+ } from "./stage-chat-view-status.js";
37
39
 
38
40
  export function initializeStageChatView(
39
41
  ctx: StageChatViewContext,
@@ -66,6 +68,9 @@ export function initializeStageChatView(
66
68
  ctx.promptScrollOffset = 0;
67
69
  ctx.promptMaxScroll = 0;
68
70
  ctx.localPaused = false;
71
+ ctx.mouseScrollCaptureEnabled = true;
72
+ ctx.lastObservedStageStatus = undefined;
73
+ ctx.lastObservedRunStatus = undefined;
69
74
  ctx.seenNoticeIds = new Set<string>();
70
75
  ctx._unsubscribeStore = null;
71
76
  ctx._unsubscribeHandle = null;
@@ -82,16 +87,18 @@ export function initializeStageChatView(
82
87
  });
83
88
 
84
89
  snapshotMessagesFromHandle(ctx);
85
- const initialStage = currentStage(ctx);
90
+ const initialRun = currentRun(ctx);
91
+ const initialStage = initialRun?.stages.find((s) => s.id === ctx.stageId);
92
+ ctx.lastObservedRunStatus = initialRun?.status;
93
+ ctx.lastObservedStageStatus = initialStage?.status;
86
94
  snapshotMessagesFromSessionFile(ctx, initialStage);
87
95
  absorbStageNotices(ctx, initialStage);
88
96
  syncPromptState(ctx, initialStage?.pendingPrompt);
97
+ if (isTerminalStageChatState(initialRun?.status) || isTerminalStageChatState(initialStage?.status)) ctx.chatHost.clearBusyForTerminalWorkflowStage();
89
98
  ctx._unsubscribeStore = ctx.store.subscribe(() => handleStoreUpdate(ctx));
90
99
 
91
100
  if (ctx.handle) {
92
- ctx._unsubscribeHandle = ctx.handle.subscribe((event) => {
93
- ctx.chatHost.applyAgentEvent(event);
94
- });
101
+ ctx._unsubscribeHandle = ctx.handle.subscribe((event) => applyStageChatLiveHandleEvent(ctx, event));
95
102
  }
96
103
  ctx.chatHost.syncAnimationTick();
97
104
  }
@@ -171,7 +178,7 @@ function createChatHost(
171
178
  isBashRunning: () => liveHandle(ctx)?.agentSession?.isBashRunning === true,
172
179
  requestRender: opts.requestRender,
173
180
  getAgentSession: () => liveHandle(ctx)?.agentSession,
174
- isStreaming: () => liveHandle(ctx)?.isStreaming === true,
181
+ isStreaming: () => isLiveHandleStreaming(ctx),
175
182
  isPaused: () => isPaused(ctx),
176
183
  isDisabled: () => isBlocked(ctx) || !liveHandle(ctx),
177
184
  tui: opts.piTui,
@@ -200,7 +207,10 @@ function chatHostStyle(ctx: StageChatViewContext): ChatSessionHostStyle {
200
207
  }
201
208
 
202
209
  function handleStoreUpdate(ctx: StageChatViewContext): void {
203
- const stage = currentStage(ctx);
210
+ const run = currentRun(ctx);
211
+ const stage = run?.stages.find((s) => s.id === ctx.stageId);
212
+ const currentRunStatus = run?.status;
213
+ const currentStageStatus = stage?.status;
204
214
  let changed = false;
205
215
  if (stage && stage.status === "paused" && !ctx.localPaused) {
206
216
  ctx.localPaused = true;
@@ -215,8 +225,15 @@ function handleStoreUpdate(ctx: StageChatViewContext): void {
215
225
  if (promptChanged && ctx.promptState && canSubmitPrompt(ctx, ctx.promptState.prompt.id)) {
216
226
  ctx.requestFocus?.();
217
227
  }
228
+ if (isTerminalStageChatTransition(ctx.lastObservedStageStatus, currentStageStatus) || isTerminalStageChatTransition(ctx.lastObservedRunStatus, currentRunStatus)) {
229
+ ctx.chatHost.clearBusyForTerminalWorkflowStage();
230
+ changed = true;
231
+ }
232
+ ctx.lastObservedRunStatus = currentRunStatus;
233
+ ctx.lastObservedStageStatus = currentStageStatus;
234
+ const hadAnimationTick = ctx.chatHost.hasAnimationTick();
218
235
  ctx.chatHost.syncAnimationTick();
219
- if (changed) ctx.requestRender?.();
236
+ if (changed || hadAnimationTick !== ctx.chatHost.hasAnimationTick()) ctx.requestRender?.();
220
237
  }
221
238
 
222
239
  function snapshotMessagesFromHandle(ctx: StageChatViewContext): void {
@@ -256,10 +273,12 @@ function absorbStageNotices(
256
273
  return changed;
257
274
  }
258
275
 
276
+ export function currentRun(ctx: StageChatViewContext): RunSnapshot | undefined {
277
+ return ctx.store.snapshot().runs.find((r) => r.id === ctx.runId);
278
+ }
279
+
259
280
  export function currentStage(ctx: StageChatViewContext): StageSnapshot | undefined {
260
- const snap = ctx.store.snapshot();
261
- const run = snap.runs.find((r) => r.id === ctx.runId);
262
- return run?.stages.find((s) => s.id === ctx.stageId);
281
+ return currentRun(ctx)?.stages.find((s) => s.id === ctx.stageId);
263
282
  }
264
283
 
265
284
  export function syncPromptState(
@@ -386,14 +405,29 @@ export function viewLineCount(ctx: StageChatViewContext): number {
386
405
  return resolveStageChatViewportRows(reported, VIEW_LINE_COUNT);
387
406
  }
388
407
 
408
+ export { isTerminalOrNonStreamingStageChatStatus } from "./stage-chat-view-status.js";
409
+
389
410
  export function liveHandle(ctx: StageChatViewContext) {
390
411
  return ctx.handle?.isDisposed === true ? undefined : ctx.handle;
391
412
  }
392
413
 
414
+ export function isLiveHandleStreaming(ctx: StageChatViewContext): boolean {
415
+ const handle = liveHandle(ctx);
416
+ if (!handle) return false;
417
+ if (isTerminalOrNonStreamingStageChatStatus(currentRun(ctx)?.status)) return false;
418
+ if (isTerminalOrNonStreamingStageChatStatus(currentStage(ctx)?.status)) return false;
419
+ if (isTerminalOrNonStreamingStageChatStatus(handle.status)) return false;
420
+ return handle.isStreaming === true;
421
+ }
422
+
393
423
  export function isStreaming(ctx: StageChatViewContext): boolean {
394
424
  return ctx.chatHost.isStreaming();
395
425
  }
396
426
 
427
+ export function isAbortableStreamingSession(ctx: StageChatViewContext): boolean {
428
+ return isLiveHandleStreaming(ctx) || liveHandle(ctx)?.agentSession?.isStreaming === true;
429
+ }
430
+
397
431
  export function isBlocked(ctx: StageChatViewContext): boolean {
398
432
  return currentStage(ctx)?.status === "blocked";
399
433
  }
@@ -0,0 +1,36 @@
1
+ const TERMINAL_OR_NON_STREAMING_STAGE_CHAT_STATUSES = new Set<string>([
2
+ "success",
3
+ "complete",
4
+ "completed",
5
+ "failure",
6
+ "failed",
7
+ "error",
8
+ "cancellation",
9
+ "cancelled",
10
+ "canceled",
11
+ "paused",
12
+ "detached",
13
+ "killed",
14
+ "stopped",
15
+ "no-longer-running",
16
+ "skipped",
17
+ "blocked",
18
+ ]);
19
+
20
+ export function isTerminalOrNonStreamingStageChatStatus(
21
+ status: string | undefined,
22
+ ): boolean {
23
+ return status !== undefined && TERMINAL_OR_NON_STREAMING_STAGE_CHAT_STATUSES.has(status);
24
+ }
25
+
26
+ export function isTerminalStageChatTransition(
27
+ previousStatus: string | undefined,
28
+ currentStatus: string | undefined,
29
+ ): boolean {
30
+ return !isTerminalOrNonStreamingStageChatStatus(previousStatus) &&
31
+ isTerminalOrNonStreamingStageChatStatus(currentStatus);
32
+ }
33
+
34
+ export function isTerminalStageChatState(status: string | undefined): boolean {
35
+ return isTerminalOrNonStreamingStageChatStatus(status);
36
+ }