@bastani/atomic 0.8.6 → 0.8.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/builtin/intercom/package.json +1 -1
- package/dist/builtin/mcp/package.json +1 -1
- package/dist/builtin/subagents/package.json +1 -1
- package/dist/builtin/web-access/package.json +1 -1
- package/dist/builtin/workflows/builtin/ralph.ts +368 -52
- package/dist/builtin/workflows/package.json +1 -1
- package/dist/builtin/workflows/src/extension/index.ts +30 -2
- package/dist/builtin/workflows/src/runs/background/status.ts +6 -0
- package/dist/builtin/workflows/src/runs/foreground/executor.ts +2 -4
- package/dist/builtin/workflows/src/runs/foreground/stage-runner.ts +5 -5
- package/dist/builtin/workflows/src/shared/store-types.ts +8 -0
- package/dist/builtin/workflows/src/shared/store.ts +39 -4
- package/dist/builtin/workflows/src/shared/timing.ts +48 -0
- package/dist/builtin/workflows/src/tui/chat-surface-message.ts +21 -2
- package/dist/builtin/workflows/src/tui/graph-view.ts +17 -18
- package/dist/builtin/workflows/src/tui/inline-form-card.ts +2 -2
- package/dist/builtin/workflows/src/tui/inline-form-editor.ts +18 -15
- package/dist/builtin/workflows/src/tui/inputs-picker.ts +24 -22
- package/dist/builtin/workflows/src/tui/node-card.ts +3 -5
- package/dist/builtin/workflows/src/tui/prompt-card.ts +11 -11
- package/dist/builtin/workflows/src/tui/run-detail.ts +4 -6
- package/dist/builtin/workflows/src/tui/session-confirm.ts +93 -8
- package/dist/builtin/workflows/src/tui/session-picker.ts +10 -15
- package/dist/builtin/workflows/src/tui/stage-chat-view.ts +93 -22
- package/dist/builtin/workflows/src/tui/status-list.ts +4 -6
- package/dist/builtin/workflows/src/tui/text-helpers.ts +7 -1
- package/dist/builtin/workflows/src/tui/widget.ts +2 -1
- package/dist/builtin/workflows/src/tui/workflow-attach-pane.ts +2 -1
- 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
|
|
20
|
-
* (interrupt mid-turn)
|
|
21
|
-
*
|
|
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)
|
|
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
|
|
953
|
-
this.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
1070
|
-
this.
|
|
1071
|
-
|
|
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 (
|
|
1126
|
+
if (isPaused) {
|
|
1075
1127
|
await this._resume(text);
|
|
1076
1128
|
return;
|
|
1077
1129
|
}
|
|
1078
1130
|
if (mode === "followUp") {
|
|
1079
|
-
await this.
|
|
1131
|
+
await this._queueFollowUp(text);
|
|
1080
1132
|
return;
|
|
1081
1133
|
}
|
|
1082
|
-
if (
|
|
1083
|
-
await this.
|
|
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
|
|
1464
|
-
const
|
|
1465
|
-
|
|
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
|
|
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
|
-
|
|
268
|
-
|
|
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
|
|
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
|