@bastani/atomic 0.8.6-0 → 0.8.7-0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/builtin/intercom/package.json +1 -1
  3. package/dist/builtin/mcp/package.json +1 -1
  4. package/dist/builtin/subagents/package.json +1 -1
  5. package/dist/builtin/web-access/package.json +1 -1
  6. package/dist/builtin/workflows/builtin/ralph.ts +368 -52
  7. package/dist/builtin/workflows/package.json +1 -1
  8. package/dist/builtin/workflows/src/extension/index.ts +30 -2
  9. package/dist/builtin/workflows/src/runs/background/status.ts +6 -0
  10. package/dist/builtin/workflows/src/runs/foreground/executor.ts +2 -4
  11. package/dist/builtin/workflows/src/runs/foreground/stage-runner.ts +5 -5
  12. package/dist/builtin/workflows/src/shared/store-types.ts +8 -0
  13. package/dist/builtin/workflows/src/shared/store.ts +39 -4
  14. package/dist/builtin/workflows/src/shared/timing.ts +48 -0
  15. package/dist/builtin/workflows/src/tui/chat-surface-message.ts +21 -2
  16. package/dist/builtin/workflows/src/tui/graph-view.ts +17 -18
  17. package/dist/builtin/workflows/src/tui/inline-form-card.ts +2 -2
  18. package/dist/builtin/workflows/src/tui/inline-form-editor.ts +18 -15
  19. package/dist/builtin/workflows/src/tui/inputs-picker.ts +24 -22
  20. package/dist/builtin/workflows/src/tui/node-card.ts +3 -5
  21. package/dist/builtin/workflows/src/tui/prompt-card.ts +11 -11
  22. package/dist/builtin/workflows/src/tui/run-detail.ts +4 -6
  23. package/dist/builtin/workflows/src/tui/session-confirm.ts +93 -8
  24. package/dist/builtin/workflows/src/tui/session-picker.ts +10 -15
  25. package/dist/builtin/workflows/src/tui/stage-chat-view.ts +93 -22
  26. package/dist/builtin/workflows/src/tui/status-list.ts +4 -6
  27. package/dist/builtin/workflows/src/tui/text-helpers.ts +7 -1
  28. package/dist/builtin/workflows/src/tui/widget.ts +2 -1
  29. package/dist/builtin/workflows/src/tui/workflow-attach-pane.ts +2 -1
  30. package/package.json +1 -1
@@ -16,13 +16,14 @@
16
16
  * Behaviour:
17
17
  * - **Idle** stage (empty transcript, not streaming, not settled): welcome
18
18
  * panel describing the attached stage. Enter sends `handle.prompt(text)`.
19
- * - **Running** stage with a live stream: Enter calls `handle.steer(text)`
20
- * (interrupt mid-turn). Ctrl+F always queues a follow-up via
21
- * `handle.followUp(text)`.
19
+ * - **Running** stage with a live stream: Enter queues a Pi-style steering
20
+ * message (interrupt mid-turn) without adding a premature transcript row.
21
+ * Ctrl+F queues a follow-up the same way.
22
22
  * - **Escape** mirrors the main coding-agent chat interrupt path for active
23
23
  * live stages: it requests a controlled pause/abort while keeping the
24
24
  * composer active. While paused, Enter calls `handle.resume(text)`.
25
- * - **Ctrl+D** detaches (back to graph); **Escape** closes the popup when idle.
25
+ * - **Ctrl+D** detaches (back to graph), or closes the popup while paused;
26
+ * **Escape** closes the popup when idle.
26
27
  * - **Blocked** stage: keystrokes absorbed; BLOCKED banner names the
27
28
  * upstream awaiter.
28
29
  * - **Settled** stage with a live handle remains a normal chat session:
@@ -65,6 +66,7 @@ import type {
65
66
  } from "@earendil-works/pi-tui";
66
67
  import type { Store } from "../shared/store.js";
67
68
  import type { StageNotice, StageSnapshot } from "../shared/store-types.js";
69
+ import { elapsedStageMs } from "../shared/timing.js";
68
70
  import type { GraphTheme } from "./graph-theme.js";
69
71
  import type { StageControlHandle } from "../runs/foreground/stage-control-registry.js";
70
72
  import { BOLD, RESET, hexBg, hexToAnsi, lerpColor } from "./color-utils.js";
@@ -86,7 +88,7 @@ export interface StageChatViewOpts {
86
88
  * inspect-only (settled stage with no live handle).
87
89
  */
88
90
  handle?: StageControlHandle;
89
- /** Called when the user presses Ctrl+D (back to graph). */
91
+ /** Called when the user presses Ctrl+D outside a paused stage (back to graph). */
90
92
  onDetach: () => void;
91
93
  /** Called when the user presses Escape (close the whole popup). */
92
94
  onClose: () => void;
@@ -199,6 +201,10 @@ export class StageChatView implements Component, Focusable {
199
201
  private workingMessage: string | undefined;
200
202
  /** User rows optimistically appended by this embedded editor, de-duped on SDK echo. */
201
203
  private optimisticUserSignatures = new Set<string>();
204
+ /** Pending steering messages emitted by AgentSession queue updates. */
205
+ private pendingSteeringMessages: readonly string[] = [];
206
+ /** Pending follow-up messages emitted by AgentSession queue updates. */
207
+ private pendingFollowUpMessages: readonly string[] = [];
202
208
  /** Chat-mode repaint driver for Pi-style loaders/spinners. */
203
209
  private animationTimer: ReturnType<typeof setInterval> | undefined;
204
210
  /** Coalesces high-frequency SDK deltas while the fixed overlay is streaming. */
@@ -391,6 +397,13 @@ export class StageChatView implements Component, Focusable {
391
397
  this.workingMessage = undefined;
392
398
  return true;
393
399
 
400
+ case "queue_update": {
401
+ const queue = event as Extract<AgentSessionEvent, { type: "queue_update" }>;
402
+ this.pendingSteeringMessages = queue.steering;
403
+ this.pendingFollowUpMessages = queue.followUp;
404
+ return true;
405
+ }
406
+
394
407
  // Compatibility with older/headless shims that predate the SDK's
395
408
  // tool_execution_* events. Project these shims into coding-agent's live
396
409
  // controller rather than maintaining a second workflow tool renderer.
@@ -533,6 +546,7 @@ export class StageChatView implements Component, Focusable {
533
546
 
534
547
  const headerLines = this._renderHeader(w, stage);
535
548
  const sepLines = [this._sepRule(w)];
549
+ const pendingLines = this._renderPendingMessages(w);
536
550
  const workingLines = this._renderWorkingStatus(w, stage, { streaming });
537
551
  const usageLines = this._renderUsage(w);
538
552
  const editorLines = this._renderEditor(w, blocked);
@@ -541,6 +555,7 @@ export class StageChatView implements Component, Focusable {
541
555
  const fixed =
542
556
  HEADER_ROWS +
543
557
  SEP_ROWS +
558
+ pendingLines.length +
544
559
  workingLines.length +
545
560
  usageLines.length +
546
561
  editorLines.length +
@@ -556,6 +571,7 @@ export class StageChatView implements Component, Focusable {
556
571
  ...headerLines,
557
572
  ...sepLines,
558
573
  ...bodyLines,
574
+ ...pendingLines,
559
575
  ...workingLines,
560
576
  ...usageLines,
561
577
  ...editorLines,
@@ -915,6 +931,36 @@ export class StageChatView implements Component, Focusable {
915
931
  }).render(width);
916
932
  }
917
933
 
934
+ private _renderPendingMessages(width: number): string[] {
935
+ if (
936
+ this.pendingSteeringMessages.length === 0 &&
937
+ this.pendingFollowUpMessages.length === 0
938
+ ) {
939
+ return [];
940
+ }
941
+ const lines = [this._blank(width)];
942
+ for (const message of this.pendingSteeringMessages) {
943
+ lines.push(...this._pendingMessageLine(width, "Steering", message));
944
+ }
945
+ for (const message of this.pendingFollowUpMessages) {
946
+ lines.push(...this._pendingMessageLine(width, "Follow-up", message));
947
+ }
948
+ return lines;
949
+ }
950
+
951
+ private _pendingMessageLine(
952
+ width: number,
953
+ label: "Steering" | "Follow-up",
954
+ message: string,
955
+ ): string[] {
956
+ const text = `${label}: ${message}`;
957
+ return new Text(
958
+ paint(truncateToWidth(text, Math.max(1, width - 2)), this.theme.dim),
959
+ 1,
960
+ 0,
961
+ ).render(width);
962
+ }
963
+
918
964
  private _renderUsage(width: number): string[] {
919
965
  const agentSession = this.handle?.agentSession;
920
966
  if (!agentSession) return [];
@@ -949,8 +995,9 @@ export class StageChatView implements Component, Focusable {
949
995
  if (this.bodyViewport.handleInput(data)) {
950
996
  return true;
951
997
  }
952
- if (data === "\x04") {
953
- this.onDetach();
998
+ if (matchesKey(data, "ctrl+d")) {
999
+ if (this._isPaused()) this.onClose();
1000
+ else this.onDetach();
954
1001
  return true;
955
1002
  }
956
1003
  if (matchesKey(data, "escape")) {
@@ -961,12 +1008,12 @@ export class StageChatView implements Component, Focusable {
961
1008
  }
962
1009
  return true;
963
1010
  }
964
- if (data === "\x03") {
1011
+ if (matchesKey(data, "ctrl+c")) {
965
1012
  this.onClose();
966
1013
  return true;
967
1014
  }
968
1015
  const blocked = this._isBlocked();
969
- if (data === "\x06") {
1016
+ if (matchesKey(data, "ctrl+f")) {
970
1017
  if (blocked) return true;
971
1018
  void this._submit("followUp");
972
1019
  return true;
@@ -976,12 +1023,12 @@ export class StageChatView implements Component, Focusable {
976
1023
  this.editor.handleInput(data);
977
1024
  return true;
978
1025
  }
979
- if (data === "\r" || data === "\n") {
1026
+ if (matchesKey(data, "enter")) {
980
1027
  if (blocked) return true;
981
1028
  void this._submit("auto");
982
1029
  return true;
983
1030
  }
984
- if (data === "\x7f" || data === "\b") {
1031
+ if (matchesKey(data, "backspace")) {
985
1032
  if (blocked) return true;
986
1033
  this.inputBuffer = this.inputBuffer.slice(0, -1);
987
1034
  return true;
@@ -1066,26 +1113,33 @@ export class StageChatView implements Component, Focusable {
1066
1113
  this.requestRender?.();
1067
1114
  return;
1068
1115
  }
1069
- this.liveChat.appendUserText(text);
1070
- this.bodyViewport.scrollToBottom();
1071
- this.optimisticUserSignatures.add(userMessageSignature(text));
1116
+ const isPaused = this._isPaused();
1117
+ const isStreaming = this._isStreaming();
1118
+ const shouldAppendOptimisticUser = mode === "auto" && !isStreaming;
1119
+ if (shouldAppendOptimisticUser) {
1120
+ this.liveChat.appendUserText(text);
1121
+ this.bodyViewport.scrollToBottom();
1122
+ this.optimisticUserSignatures.add(userMessageSignature(text));
1123
+ }
1072
1124
  this.requestRender?.();
1073
1125
  try {
1074
- if (this._isPaused()) {
1126
+ if (isPaused) {
1075
1127
  await this._resume(text);
1076
1128
  return;
1077
1129
  }
1078
1130
  if (mode === "followUp") {
1079
- await this.handle.followUp(text);
1131
+ await this._queueFollowUp(text);
1080
1132
  return;
1081
1133
  }
1082
- if (this.handle.isStreaming) {
1083
- await this.handle.steer(text);
1134
+ if (isStreaming) {
1135
+ await this._queueSteer(text);
1084
1136
  } else {
1085
1137
  this.sdkBusy = true;
1086
1138
  this._syncAnimationTick();
1087
1139
  await this.handle.ensureAttached();
1088
1140
  await this.handle.prompt(text);
1141
+ this.sdkBusy = false;
1142
+ this._syncAnimationTick();
1089
1143
  }
1090
1144
  } catch (err) {
1091
1145
  this.sdkBusy = false;
@@ -1095,6 +1149,24 @@ export class StageChatView implements Component, Focusable {
1095
1149
  }
1096
1150
  }
1097
1151
 
1152
+ private async _queueSteer(text: string): Promise<void> {
1153
+ const agentSession = this.handle?.agentSession;
1154
+ if (agentSession?.isStreaming) {
1155
+ await agentSession.prompt(text, { streamingBehavior: "steer" });
1156
+ return;
1157
+ }
1158
+ await this.handle?.steer(text);
1159
+ }
1160
+
1161
+ private async _queueFollowUp(text: string): Promise<void> {
1162
+ const agentSession = this.handle?.agentSession;
1163
+ if (agentSession?.isStreaming) {
1164
+ await agentSession.prompt(text, { streamingBehavior: "followUp" });
1165
+ return;
1166
+ }
1167
+ await this.handle?.followUp(text);
1168
+ }
1169
+
1098
1170
  invalidate(): void {
1099
1171
  // Stateless render reads directly from snapshot + handle.
1100
1172
  }
@@ -1460,10 +1532,9 @@ function tailStreamingText(text: string): string {
1460
1532
  }
1461
1533
 
1462
1534
  function stageDurationText(stage: StageSnapshot | undefined): string {
1463
- if (!stage?.startedAt) return "";
1464
- const end = stage.endedAt ?? Date.now();
1465
- const ms = Math.max(0, end - stage.startedAt);
1466
- return formatDuration(ms);
1535
+ if (!stage) return "";
1536
+ const elapsed = elapsedStageMs(stage);
1537
+ return elapsed === undefined ? "" : formatDuration(elapsed);
1467
1538
  }
1468
1539
 
1469
1540
  function formatDuration(ms: number): string {
@@ -27,6 +27,7 @@
27
27
  */
28
28
 
29
29
  import type { RunSnapshot, StageSnapshot, StageStatus } from "../shared/store-types.js";
30
+ import { elapsedRunMs, elapsedStageMs } from "../shared/timing.js";
30
31
  import type { GraphTheme } from "./graph-theme.js";
31
32
  import { fmtDuration } from "./status-helpers.js";
32
33
  import {
@@ -211,7 +212,7 @@ function runCardMeta(run: RunSnapshot, now: number): string {
211
212
  const ago = run.endedAt !== undefined
212
213
  ? `${fmtDuration(now - run.endedAt)} ago`
213
214
  : run.startedAt != null
214
- ? fmtDuration(now - run.startedAt)
215
+ ? fmtDuration(elapsedRunMs(run, now))
215
216
  : undefined;
216
217
 
217
218
  if (run.status === "running") {
@@ -264,11 +265,8 @@ function lastStageDuration(run: RunSnapshot, now: number): string | undefined {
264
265
  }
265
266
 
266
267
  function stageDurationString(stage: StageSnapshot, now: number): string | undefined {
267
- if (stage.durationMs !== undefined) return fmtDuration(stage.durationMs);
268
- if (stage.startedAt !== undefined && stage.endedAt === undefined) {
269
- return fmtDuration(now - stage.startedAt);
270
- }
271
- return undefined;
268
+ const elapsed = elapsedStageMs(stage, now);
269
+ return elapsed === undefined ? undefined : fmtDuration(elapsed);
272
270
  }
273
271
 
274
272
  function stageCells(run: RunSnapshot): Array<{ status: StageStatus }> {
@@ -4,6 +4,7 @@ import {
4
4
  visibleWidth,
5
5
  type KeyId,
6
6
  } from "@earendil-works/pi-tui";
7
+ import { decodePrintableKey as piDecodePrintableKey } from "@earendil-works/pi-tui/dist/keys.js";
7
8
 
8
9
  export { visibleWidth };
9
10
 
@@ -35,7 +36,12 @@ export function truncateToWidth(
35
36
 
36
37
  /** Use pi-tui's key parser/matcher while preserving the local string API. */
37
38
  export function matchesKey(data: string, key: string): boolean {
38
- return piMatchesKey(data, key as KeyId);
39
+ return data === key || piMatchesKey(data, key as KeyId);
40
+ }
41
+
42
+ /** Decode CSI-u / Kitty printable-key sequences emitted by terminals such as VSCode. */
43
+ export function decodePrintableKey(data: string): string | undefined {
44
+ return piDecodePrintableKey(data);
39
45
  }
40
46
 
41
47
  export function sliceColumns(
@@ -29,6 +29,7 @@ import type {
29
29
  StoreSnapshot,
30
30
  RunSnapshot,
31
31
  } from "../shared/store-types.js";
32
+ import { elapsedRunMs } from "../shared/timing.js";
32
33
  import type { PiTheme } from "./store-widget-installer.js";
33
34
  import { renderBandHeader } from "./header.js";
34
35
  import type { BandBadge } from "./header.js";
@@ -166,7 +167,7 @@ function elapsedLabel(run: RunSnapshot, now: number): string {
166
167
  if (run.status === "killed") return `killed · ${ago} ago`;
167
168
  return `${run.status} · ${ago} ago`;
168
169
  }
169
- if (run.startedAt != null) return formatDuration(now - run.startedAt);
170
+ if (run.startedAt != null) return formatDuration(elapsedRunMs(run, now));
170
171
  return "";
171
172
  }
172
173
 
@@ -5,7 +5,8 @@
5
5
  * between the orchestrator `GraphView` and a stage-scoped
6
6
  * `StageChatView`. Pressing Enter on a graph node attaches the popup
7
7
  * to that node's chat; Ctrl+D in chat mode swaps back to graph mode
8
- * with the same node still focused (see ui/attach-mockup.html).
8
+ * with the same node still focused (see ui/attach-mockup.html), except
9
+ * paused stage chats close the pane to mirror shell EOF semantics.
9
10
  *
10
11
  * The shell never remounts the overlay — it only flips a `mode`
11
12
  * field and re-renders, so the popup stays in pi-tui's overlay layer
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/atomic",
3
- "version": "0.8.6-0",
3
+ "version": "0.8.7-0",
4
4
  "description": "Atomic coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "atomicConfig": {