@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
|
@@ -1173,10 +1173,19 @@ function factory(pi: ExtensionAPI): void {
|
|
|
1173
1173
|
// noopOverlay returned when pi.ui?.custom is absent (degraded runtime).
|
|
1174
1174
|
const overlay: GraphOverlayPort = buildGraphOverlayAdapter(pi, store, {
|
|
1175
1175
|
onKillRun: (runId) => {
|
|
1176
|
-
|
|
1176
|
+
const run = store.runs().find((r) => r.id === runId);
|
|
1177
|
+
const result = destroyRun(runId, {
|
|
1177
1178
|
cancellation: cancellationRegistry,
|
|
1178
1179
|
persistence: persistenceRef.current,
|
|
1179
1180
|
});
|
|
1181
|
+
if (run && result.ok) {
|
|
1182
|
+
emitChatSurface(pi, {
|
|
1183
|
+
kind: "killed",
|
|
1184
|
+
run,
|
|
1185
|
+
previousStatus: result.previousStatus,
|
|
1186
|
+
wasInFlight: result.wasInFlight,
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1180
1189
|
},
|
|
1181
1190
|
});
|
|
1182
1191
|
|
|
@@ -1453,6 +1462,14 @@ function factory(pi: ExtensionAPI): void {
|
|
|
1453
1462
|
cancellation: cancellationRegistry,
|
|
1454
1463
|
persistence: persistenceRef.current,
|
|
1455
1464
|
});
|
|
1465
|
+
if (killed.ok) {
|
|
1466
|
+
emitChatSurface(pi, {
|
|
1467
|
+
kind: "killed",
|
|
1468
|
+
run,
|
|
1469
|
+
previousStatus: killed.previousStatus,
|
|
1470
|
+
wasInFlight: killed.wasInFlight,
|
|
1471
|
+
});
|
|
1472
|
+
}
|
|
1456
1473
|
print(
|
|
1457
1474
|
killed.ok
|
|
1458
1475
|
? `Run ${killed.runId.slice(0, 8)} killed and removed.`
|
|
@@ -1632,6 +1649,14 @@ function factory(pi: ExtensionAPI): void {
|
|
|
1632
1649
|
persistence: persistenceRef.current,
|
|
1633
1650
|
});
|
|
1634
1651
|
if (result.ok) {
|
|
1652
|
+
if (run) {
|
|
1653
|
+
emitChatSurface(pi, {
|
|
1654
|
+
kind: "killed",
|
|
1655
|
+
run,
|
|
1656
|
+
previousStatus: result.previousStatus,
|
|
1657
|
+
wasInFlight: result.wasInFlight,
|
|
1658
|
+
});
|
|
1659
|
+
}
|
|
1635
1660
|
print(
|
|
1636
1661
|
`Run ${result.runId.slice(0, 8)} killed and removed (was ${result.previousStatus}).`,
|
|
1637
1662
|
);
|
|
@@ -1696,9 +1721,12 @@ function factory(pi: ExtensionAPI): void {
|
|
|
1696
1721
|
stageId = byName.id;
|
|
1697
1722
|
}
|
|
1698
1723
|
overlay.open(runId, overlaySurfaceFromContext(ctx), stageId);
|
|
1724
|
+
const attachedStage = stageId ? run?.stages.find((s) => s.id === stageId) : undefined;
|
|
1699
1725
|
print(
|
|
1700
1726
|
stageId
|
|
1701
|
-
?
|
|
1727
|
+
? attachedStage?.status === "paused"
|
|
1728
|
+
? `Attached to ${runId.slice(0, 8)} stage ${stageId.slice(0, 8)}. ctrl+d close · esc close.`
|
|
1729
|
+
: `Attached to ${runId.slice(0, 8)} stage ${stageId.slice(0, 8)}. ctrl+d return to graph · esc close.`
|
|
1702
1730
|
: `Attached to ${runId.slice(0, 8)}. ↵ chat · ctrl+d detach.`,
|
|
1703
1731
|
);
|
|
1704
1732
|
return true;
|
|
@@ -70,6 +70,9 @@ export interface RunDetail {
|
|
|
70
70
|
readonly startedAt: number;
|
|
71
71
|
readonly endedAt?: number;
|
|
72
72
|
readonly durationMs?: number;
|
|
73
|
+
readonly pausedDurationMs?: number;
|
|
74
|
+
readonly pausedAt?: number;
|
|
75
|
+
readonly resumedAt?: number;
|
|
73
76
|
readonly inputs: Readonly<Record<string, unknown>>;
|
|
74
77
|
readonly stages: readonly RunSnapshot["stages"][number][];
|
|
75
78
|
readonly result?: Record<string, unknown>;
|
|
@@ -433,6 +436,9 @@ export function inspectRun(
|
|
|
433
436
|
startedAt: copy.startedAt,
|
|
434
437
|
endedAt: copy.endedAt,
|
|
435
438
|
durationMs: copy.durationMs,
|
|
439
|
+
pausedDurationMs: copy.pausedDurationMs,
|
|
440
|
+
pausedAt: copy.pausedAt,
|
|
441
|
+
resumedAt: copy.resumedAt,
|
|
436
442
|
inputs: copy.inputs,
|
|
437
443
|
stages: copy.stages,
|
|
438
444
|
result: copy.result,
|
|
@@ -51,6 +51,7 @@ import {
|
|
|
51
51
|
type WorktreeSetup,
|
|
52
52
|
} from "../shared/worktree.js";
|
|
53
53
|
import { store as defaultStore } from "../../shared/store.js";
|
|
54
|
+
import { elapsedStageMs } from "../../shared/timing.js";
|
|
54
55
|
import {
|
|
55
56
|
appendRunStart,
|
|
56
57
|
appendStageStart,
|
|
@@ -1520,10 +1521,7 @@ export async function run<TInputs extends Record<string, unknown>>(
|
|
|
1520
1521
|
throw err;
|
|
1521
1522
|
} finally {
|
|
1522
1523
|
stageSnapshot.endedAt = Date.now();
|
|
1523
|
-
stageSnapshot.durationMs =
|
|
1524
|
-
stageSnapshot.startedAt !== undefined
|
|
1525
|
-
? stageSnapshot.endedAt - stageSnapshot.startedAt
|
|
1526
|
-
: undefined;
|
|
1524
|
+
stageSnapshot.durationMs = elapsedStageMs(stageSnapshot, stageSnapshot.endedAt);
|
|
1527
1525
|
|
|
1528
1526
|
const finalModelMeta = innerCtx.__modelFallbackMeta();
|
|
1529
1527
|
if (finalModelMeta.model !== undefined) stageSnapshot.model = finalModelMeta.model;
|
|
@@ -174,7 +174,7 @@ type MessageWithTextContent = {
|
|
|
174
174
|
readonly content?: string | readonly TextLikeContent[];
|
|
175
175
|
};
|
|
176
176
|
|
|
177
|
-
function
|
|
177
|
+
function extractMessageText(message: AgentSession["messages"][number]): string {
|
|
178
178
|
const { content } = message as MessageWithTextContent;
|
|
179
179
|
if (typeof content === "string") return content;
|
|
180
180
|
if (Array.isArray(content)) {
|
|
@@ -186,11 +186,11 @@ function extractAssistantText(message: AgentSession["messages"][number]): string
|
|
|
186
186
|
return "";
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
-
function
|
|
189
|
+
function lastOutputTextFromMessages(messages: AgentSession["messages"]): string | undefined {
|
|
190
190
|
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
191
191
|
const message = messages[index];
|
|
192
|
-
if (!message || message.role !== "assistant") continue;
|
|
193
|
-
const text =
|
|
192
|
+
if (!message || (message.role !== "assistant" && message.role !== "toolResult")) continue;
|
|
193
|
+
const text = extractMessageText(message).trim();
|
|
194
194
|
if (text) return text;
|
|
195
195
|
}
|
|
196
196
|
return undefined;
|
|
@@ -217,7 +217,7 @@ function lastAssistantTextFromSession(
|
|
|
217
217
|
if (!activeSession) return fallback;
|
|
218
218
|
const direct = activeSession.getLastAssistantText?.();
|
|
219
219
|
if (direct !== undefined && direct.trim()) return direct;
|
|
220
|
-
return
|
|
220
|
+
return lastOutputTextFromMessages(activeSession.messages) ?? direct ?? fallback;
|
|
221
221
|
}
|
|
222
222
|
|
|
223
223
|
const DEFAULT_MAX_OUTPUT_BYTES = 200 * 1024;
|
|
@@ -101,6 +101,8 @@ export interface StageSnapshot {
|
|
|
101
101
|
attachable?: boolean;
|
|
102
102
|
/** True while a user pane is actively attached to this stage. */
|
|
103
103
|
attached?: boolean;
|
|
104
|
+
/** Milliseconds spent paused across completed pause intervals. */
|
|
105
|
+
pausedDurationMs?: number;
|
|
104
106
|
/** Timestamp set when a controlled pause begins; cleared on resume. */
|
|
105
107
|
pausedAt?: number;
|
|
106
108
|
/** Timestamp recorded on the most recent resume from a paused state. */
|
|
@@ -116,6 +118,12 @@ export interface RunSnapshot {
|
|
|
116
118
|
startedAt: number;
|
|
117
119
|
endedAt?: number;
|
|
118
120
|
durationMs?: number;
|
|
121
|
+
/** Milliseconds spent paused across completed pause intervals. */
|
|
122
|
+
pausedDurationMs?: number;
|
|
123
|
+
/** Timestamp set when a controlled pause begins; cleared on resume. */
|
|
124
|
+
pausedAt?: number;
|
|
125
|
+
/** Timestamp recorded on the most recent resume from a paused state. */
|
|
126
|
+
resumedAt?: number;
|
|
119
127
|
result?: Record<string, unknown>;
|
|
120
128
|
error?: string;
|
|
121
129
|
/**
|
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
RunStatus,
|
|
14
14
|
WorkflowNotice,
|
|
15
15
|
} from "./store-types.js";
|
|
16
|
+
import { accumulatePausedDurationMs, elapsedRunMs } from "./timing.js";
|
|
16
17
|
|
|
17
18
|
/** Statuses that represent a terminal run state — cannot be overwritten. */
|
|
18
19
|
const TERMINAL_STATUSES: ReadonlySet<RunStatus> = new Set(["completed", "failed", "killed"]);
|
|
@@ -263,6 +264,14 @@ export function createStore(): Store {
|
|
|
263
264
|
if (!existing) return;
|
|
264
265
|
existing.status = stage.status;
|
|
265
266
|
existing.endedAt = stage.endedAt;
|
|
267
|
+
if (existing.endedAt !== undefined && existing.pausedAt !== undefined) {
|
|
268
|
+
existing.pausedDurationMs = accumulatePausedDurationMs(
|
|
269
|
+
existing.pausedDurationMs,
|
|
270
|
+
existing.pausedAt,
|
|
271
|
+
existing.endedAt,
|
|
272
|
+
);
|
|
273
|
+
existing.pausedAt = undefined;
|
|
274
|
+
}
|
|
266
275
|
existing.durationMs = stage.durationMs;
|
|
267
276
|
existing.result = stage.result;
|
|
268
277
|
existing.error = stage.error;
|
|
@@ -283,7 +292,15 @@ export function createStore(): Store {
|
|
|
283
292
|
if (TERMINAL_STATUSES.has(run.status)) return false;
|
|
284
293
|
run.status = status;
|
|
285
294
|
run.endedAt = Date.now();
|
|
286
|
-
run.
|
|
295
|
+
if (run.pausedAt !== undefined) {
|
|
296
|
+
run.pausedDurationMs = accumulatePausedDurationMs(
|
|
297
|
+
run.pausedDurationMs,
|
|
298
|
+
run.pausedAt,
|
|
299
|
+
run.endedAt,
|
|
300
|
+
);
|
|
301
|
+
run.pausedAt = undefined;
|
|
302
|
+
}
|
|
303
|
+
run.durationMs = elapsedRunMs(run, run.endedAt);
|
|
287
304
|
if (status === "completed" && result !== undefined) {
|
|
288
305
|
run.result = result;
|
|
289
306
|
}
|
|
@@ -547,8 +564,16 @@ export function createStore(): Store {
|
|
|
547
564
|
const stage = findStage(run, stageId);
|
|
548
565
|
if (!stage) return false;
|
|
549
566
|
if (stage.status !== "paused" && stage.status !== "blocked") return false;
|
|
567
|
+
const resumedTs = resumedAt ?? Date.now();
|
|
550
568
|
stage.status = "running";
|
|
551
|
-
stage.
|
|
569
|
+
if (stage.startedAt !== undefined) {
|
|
570
|
+
stage.pausedDurationMs = accumulatePausedDurationMs(
|
|
571
|
+
stage.pausedDurationMs,
|
|
572
|
+
stage.pausedAt,
|
|
573
|
+
resumedTs,
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
stage.resumedAt = resumedTs;
|
|
552
577
|
stage.pausedAt = undefined;
|
|
553
578
|
delete stage.blockedByStageId;
|
|
554
579
|
delete stage.awaitingInputSince;
|
|
@@ -557,23 +582,33 @@ export function createStore(): Store {
|
|
|
557
582
|
return true;
|
|
558
583
|
},
|
|
559
584
|
|
|
560
|
-
recordRunPaused(runId: string,
|
|
585
|
+
recordRunPaused(runId: string, pausedAt?: number): boolean {
|
|
561
586
|
const run = findRun(runId);
|
|
562
587
|
if (!run) return false;
|
|
563
588
|
if (TERMINAL_STATUSES.has(run.status)) return false;
|
|
564
589
|
if (run.status === "paused") return false;
|
|
565
590
|
run.status = "paused";
|
|
591
|
+
run.pausedAt = pausedAt ?? Date.now();
|
|
592
|
+
run.resumedAt = undefined;
|
|
566
593
|
_version++;
|
|
567
594
|
notify();
|
|
568
595
|
return true;
|
|
569
596
|
},
|
|
570
597
|
|
|
571
|
-
recordRunResumed(runId: string,
|
|
598
|
+
recordRunResumed(runId: string, resumedAt?: number): boolean {
|
|
572
599
|
const run = findRun(runId);
|
|
573
600
|
if (!run) return false;
|
|
574
601
|
if (TERMINAL_STATUSES.has(run.status)) return false;
|
|
575
602
|
if (run.status !== "paused") return false;
|
|
603
|
+
const resumedTs = resumedAt ?? Date.now();
|
|
576
604
|
run.status = "running";
|
|
605
|
+
run.pausedDurationMs = accumulatePausedDurationMs(
|
|
606
|
+
run.pausedDurationMs,
|
|
607
|
+
run.pausedAt,
|
|
608
|
+
resumedTs,
|
|
609
|
+
);
|
|
610
|
+
run.resumedAt = resumedTs;
|
|
611
|
+
run.pausedAt = undefined;
|
|
577
612
|
_version++;
|
|
578
613
|
notify();
|
|
579
614
|
return true;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
interface StageTimerSnapshot {
|
|
2
|
+
readonly startedAt?: number;
|
|
3
|
+
readonly durationMs?: number;
|
|
4
|
+
readonly pausedDurationMs?: number;
|
|
5
|
+
readonly pausedAt?: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface RunTimerSnapshot {
|
|
9
|
+
readonly startedAt: number;
|
|
10
|
+
readonly durationMs?: number;
|
|
11
|
+
readonly pausedDurationMs?: number;
|
|
12
|
+
readonly pausedAt?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function nonNegative(ms: number): number {
|
|
16
|
+
return Math.max(0, ms);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function elapsedFromStart(
|
|
20
|
+
startedAt: number,
|
|
21
|
+
now: number,
|
|
22
|
+
pausedDurationMs: number | undefined,
|
|
23
|
+
pausedAt: number | undefined,
|
|
24
|
+
): number {
|
|
25
|
+
const completedPausedSegment = pausedDurationMs ?? 0;
|
|
26
|
+
const activePausedSegment = pausedAt === undefined ? 0 : nonNegative(now - pausedAt);
|
|
27
|
+
return nonNegative(now - startedAt - completedPausedSegment - activePausedSegment);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function accumulatePausedDurationMs(
|
|
31
|
+
pausedDurationMs: number | undefined,
|
|
32
|
+
pausedAt: number | undefined,
|
|
33
|
+
resumedAt: number,
|
|
34
|
+
): number {
|
|
35
|
+
if (pausedAt === undefined) return pausedDurationMs ?? 0;
|
|
36
|
+
return (pausedDurationMs ?? 0) + nonNegative(resumedAt - pausedAt);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function elapsedStageMs(stage: StageTimerSnapshot, now = Date.now()): number | undefined {
|
|
40
|
+
if (stage.durationMs !== undefined) return nonNegative(stage.durationMs);
|
|
41
|
+
if (stage.startedAt === undefined) return undefined;
|
|
42
|
+
return elapsedFromStart(stage.startedAt, now, stage.pausedDurationMs, stage.pausedAt);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function elapsedRunMs(run: RunTimerSnapshot, now = Date.now()): number {
|
|
46
|
+
if (run.durationMs !== undefined) return nonNegative(run.durationMs);
|
|
47
|
+
return elapsedFromStart(run.startedAt, now, run.pausedDurationMs, run.pausedAt);
|
|
48
|
+
}
|
|
@@ -27,13 +27,14 @@
|
|
|
27
27
|
|
|
28
28
|
import type { ExtensionAPI } from "../extension/index.js";
|
|
29
29
|
import type { RunDetail } from "../runs/background/status.js";
|
|
30
|
-
import type { RunSnapshot } from "../shared/store-types.js";
|
|
30
|
+
import type { RunSnapshot, RunStatus } from "../shared/store-types.js";
|
|
31
31
|
import type { GraphTheme } from "./graph-theme.js";
|
|
32
32
|
import type { WorkflowListEntry } from "./workflow-list.js";
|
|
33
33
|
import { renderDispatchConfirm } from "./dispatch-confirm.js";
|
|
34
34
|
import { renderRunDetail } from "./run-detail.js";
|
|
35
35
|
import { renderStatusList } from "./status-list.js";
|
|
36
36
|
import { renderWorkflowList } from "./workflow-list.js";
|
|
37
|
+
import { renderWorkflowKilledNotice } from "./session-confirm.js";
|
|
37
38
|
|
|
38
39
|
/** Custom message type wired to {@link registerChatSurfaceRenderer}. */
|
|
39
40
|
export const CHAT_SURFACE_CUSTOM_TYPE = "workflows:chat-surface";
|
|
@@ -78,11 +79,20 @@ export interface DetailPayload {
|
|
|
78
79
|
detail: RunDetail;
|
|
79
80
|
}
|
|
80
81
|
|
|
82
|
+
/** Inline notice after a workflow run is destructively killed and removed. */
|
|
83
|
+
export interface KilledPayload {
|
|
84
|
+
kind: "killed";
|
|
85
|
+
run: RunSnapshot;
|
|
86
|
+
previousStatus: RunStatus;
|
|
87
|
+
wasInFlight: boolean;
|
|
88
|
+
}
|
|
89
|
+
|
|
81
90
|
export type ChatSurfacePayload =
|
|
82
91
|
| DispatchPayload
|
|
83
92
|
| StatusPayload
|
|
84
93
|
| ListPayload
|
|
85
|
-
| DetailPayload
|
|
94
|
+
| DetailPayload
|
|
95
|
+
| KilledPayload;
|
|
86
96
|
|
|
87
97
|
// ---------------------------------------------------------------------------
|
|
88
98
|
// Renderer registration
|
|
@@ -177,6 +187,7 @@ function describePayload(payload: ChatSurfacePayload): string {
|
|
|
177
187
|
case "status": return `status · ${payload.runs.length} run${payload.runs.length === 1 ? "" : "s"}`;
|
|
178
188
|
case "list": return `workflows · ${payload.entries.length} registered`;
|
|
179
189
|
case "detail": return `run detail · ${payload.detail.runId.slice(0, 8)}`;
|
|
190
|
+
case "killed": return `workflow killed · ${payload.run.id.slice(0, 8)}`;
|
|
180
191
|
}
|
|
181
192
|
}
|
|
182
193
|
|
|
@@ -220,5 +231,13 @@ function renderPayload(
|
|
|
220
231
|
// hint is unused — its surface mirrors the orchestrator panel
|
|
221
232
|
// (outline-pill chrome), not the flex chat band.
|
|
222
233
|
return renderRunDetail(payload.detail, { theme });
|
|
234
|
+
case "killed":
|
|
235
|
+
return renderWorkflowKilledNotice({
|
|
236
|
+
width,
|
|
237
|
+
theme,
|
|
238
|
+
run: payload.run,
|
|
239
|
+
previousStatus: payload.previousStatus,
|
|
240
|
+
wasInFlight: payload.wasInFlight,
|
|
241
|
+
}).join("\n");
|
|
223
242
|
}
|
|
224
243
|
}
|
|
@@ -30,6 +30,7 @@ import type {
|
|
|
30
30
|
StoreSnapshot,
|
|
31
31
|
RunSnapshot,
|
|
32
32
|
} from "../shared/store-types.js";
|
|
33
|
+
import { elapsedStageMs } from "../shared/timing.js";
|
|
33
34
|
import type { GraphTheme } from "./graph-theme.js";
|
|
34
35
|
import type { SwitcherState } from "./switcher.js";
|
|
35
36
|
import type { LayoutNode } from "./layout.js";
|
|
@@ -146,8 +147,8 @@ const OVERLAY_VERTICAL_MARGIN_ROWS = 1;
|
|
|
146
147
|
|
|
147
148
|
/**
|
|
148
149
|
* Animation tick period. Overlay re-renders fire on this cadence so
|
|
149
|
-
* duration counters
|
|
150
|
-
*
|
|
150
|
+
* duration counters tick from active elapsed time (freezing while paused)
|
|
151
|
+
* and the running-stage border lerps between `borderDim` and
|
|
151
152
|
* `warning` without a key press. The host-supplied `requestRender`
|
|
152
153
|
* gate prevents work while the overlay is hidden or unfocused.
|
|
153
154
|
*/
|
|
@@ -964,10 +965,8 @@ export class GraphView implements Component {
|
|
|
964
965
|
}
|
|
965
966
|
|
|
966
967
|
private _duration(stage: StageSnapshot): string {
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
return fmtDuration(Date.now() - stage.startedAt);
|
|
970
|
-
return "";
|
|
968
|
+
const elapsed = elapsedStageMs(stage);
|
|
969
|
+
return elapsed === undefined ? "" : fmtDuration(elapsed);
|
|
971
970
|
}
|
|
972
971
|
|
|
973
972
|
private _counts(run: RunSnapshot): {
|
|
@@ -1051,13 +1050,13 @@ export class GraphView implements Component {
|
|
|
1051
1050
|
// Vertical-graph navigation: up/down step between depth levels
|
|
1052
1051
|
// (col), left/right step between siblings at the same depth (row).
|
|
1053
1052
|
// j/k preserved as a flat-order fallback for muscle memory.
|
|
1054
|
-
if (matchesKey(data, "down")
|
|
1053
|
+
if (matchesKey(data, "down"))
|
|
1055
1054
|
return this._moveByDepth(+1);
|
|
1056
|
-
if (matchesKey(data, "up")
|
|
1055
|
+
if (matchesKey(data, "up"))
|
|
1057
1056
|
return this._moveByDepth(-1);
|
|
1058
|
-
if (matchesKey(data, "right")
|
|
1057
|
+
if (matchesKey(data, "right"))
|
|
1059
1058
|
return this._moveBySibling(+1);
|
|
1060
|
-
if (matchesKey(data, "left")
|
|
1059
|
+
if (matchesKey(data, "left"))
|
|
1061
1060
|
return this._moveBySibling(-1);
|
|
1062
1061
|
if (matchesKey(data, "j")) {
|
|
1063
1062
|
this._setFocusedIndex(Math.min(this.focusedIndex + 1, stageCount - 1));
|
|
@@ -1082,7 +1081,7 @@ export class GraphView implements Component {
|
|
|
1082
1081
|
this.switcherState = { query: "", selectedIndex: 0 };
|
|
1083
1082
|
return true;
|
|
1084
1083
|
}
|
|
1085
|
-
if (matchesKey(data, "enter")
|
|
1084
|
+
if (matchesKey(data, "enter")) {
|
|
1086
1085
|
// Enter attaches the popup interior to the focused stage. The
|
|
1087
1086
|
// attach shell swaps in the stage-chat view without remounting
|
|
1088
1087
|
// the overlay; without a callback, fall back to the legacy
|
|
@@ -1093,7 +1092,7 @@ export class GraphView implements Component {
|
|
|
1093
1092
|
}
|
|
1094
1093
|
// `ctrl+d` detaches the whole popup (host hides the overlay). This
|
|
1095
1094
|
// is the graph-mode counterpart of the in-chat back affordance.
|
|
1096
|
-
if (data
|
|
1095
|
+
if (matchesKey(data, "ctrl+d")) {
|
|
1097
1096
|
if (this.onDetach) {
|
|
1098
1097
|
this.onDetach();
|
|
1099
1098
|
} else if (this.onHide) {
|
|
@@ -1115,7 +1114,7 @@ export class GraphView implements Component {
|
|
|
1115
1114
|
this.onHide();
|
|
1116
1115
|
return true;
|
|
1117
1116
|
}
|
|
1118
|
-
if (matchesKey(data, "escape") || data
|
|
1117
|
+
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
|
|
1119
1118
|
this.onClose?.();
|
|
1120
1119
|
return true;
|
|
1121
1120
|
}
|
|
@@ -1126,11 +1125,11 @@ export class GraphView implements Component {
|
|
|
1126
1125
|
const run = this._getCurrentRun();
|
|
1127
1126
|
const stages = run?.stages ?? [];
|
|
1128
1127
|
|
|
1129
|
-
if (matchesKey(data, "escape")
|
|
1128
|
+
if (matchesKey(data, "escape")) {
|
|
1130
1129
|
this.switcherOpen = false;
|
|
1131
1130
|
return true;
|
|
1132
1131
|
}
|
|
1133
|
-
if (matchesKey(data, "enter")
|
|
1132
|
+
if (matchesKey(data, "enter")) {
|
|
1134
1133
|
const filtered = filterStages(stages, this.switcherState.query);
|
|
1135
1134
|
const selected = filtered[this.switcherState.selectedIndex];
|
|
1136
1135
|
if (selected) {
|
|
@@ -1149,7 +1148,7 @@ export class GraphView implements Component {
|
|
|
1149
1148
|
this.switcherOpen = false;
|
|
1150
1149
|
return true;
|
|
1151
1150
|
}
|
|
1152
|
-
if (matchesKey(data, "down")
|
|
1151
|
+
if (matchesKey(data, "down")) {
|
|
1153
1152
|
const filtered = filterStages(stages, this.switcherState.query);
|
|
1154
1153
|
this.switcherState = {
|
|
1155
1154
|
...this.switcherState,
|
|
@@ -1160,14 +1159,14 @@ export class GraphView implements Component {
|
|
|
1160
1159
|
};
|
|
1161
1160
|
return true;
|
|
1162
1161
|
}
|
|
1163
|
-
if (matchesKey(data, "up")
|
|
1162
|
+
if (matchesKey(data, "up")) {
|
|
1164
1163
|
this.switcherState = {
|
|
1165
1164
|
...this.switcherState,
|
|
1166
1165
|
selectedIndex: Math.max(this.switcherState.selectedIndex - 1, 0),
|
|
1167
1166
|
};
|
|
1168
1167
|
return true;
|
|
1169
1168
|
}
|
|
1170
|
-
if (data
|
|
1169
|
+
if (matchesKey(data, "backspace")) {
|
|
1171
1170
|
this.switcherState = {
|
|
1172
1171
|
query: this.switcherState.query.slice(0, -1),
|
|
1173
1172
|
selectedIndex: 0,
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* ╰──────────────────────────────────────────────────────────────────╯
|
|
26
26
|
* integer · optional · loop count
|
|
27
27
|
*
|
|
28
|
-
* ╭ EDIT ╮ tab next · shift+tab prev · ctrl+
|
|
28
|
+
* ╭ EDIT ╮ tab next · shift+tab prev · ctrl+x run · esc cancel
|
|
29
29
|
* │ EDIT │
|
|
30
30
|
* ╰──────╯
|
|
31
31
|
*
|
|
@@ -193,7 +193,7 @@ function renderFooterBand(theme: GraphTheme, width: number): string[] {
|
|
|
193
193
|
const hints: Array<{ key: string; label: string }> = [
|
|
194
194
|
{ key: "tab", label: "Next" },
|
|
195
195
|
{ key: "shift+tab", label: "Prev" },
|
|
196
|
-
{ key: "ctrl+
|
|
196
|
+
{ key: "ctrl+x", label: "Run" },
|
|
197
197
|
{ key: "esc", label: "Cancel" },
|
|
198
198
|
];
|
|
199
199
|
const sep = `${chromeBg} ${dim}·${RESET}${chromeBg} `;
|
|
@@ -16,13 +16,13 @@
|
|
|
16
16
|
* space — boolean toggle
|
|
17
17
|
* enter — newline (text) | otherwise next field
|
|
18
18
|
* printable ASCII — insert at caret (text/string/number)
|
|
19
|
-
* ctrl+
|
|
19
|
+
* ctrl+x — submit form (if valid)
|
|
20
20
|
* esc / ctrl+c — cancel form
|
|
21
21
|
*
|
|
22
22
|
* Editor-mode keys (cursor movement, word jumps, deletions) route through
|
|
23
23
|
* the Pi `KeybindingsManager` injected by the host at factory time, so any
|
|
24
24
|
* user-configured keybinding overrides surfaces here as well. Form-level
|
|
25
|
-
* keys (tab/shift+tab/ctrl+
|
|
25
|
+
* keys (tab/shift+tab/ctrl+x/esc/ctrl+c) stay as raw byte checks because
|
|
26
26
|
* they are workflow form contract, not Pi-configurable actions.
|
|
27
27
|
*
|
|
28
28
|
* On submit/cancel the editor calls back to the orchestrator which:
|
|
@@ -57,14 +57,14 @@ import {
|
|
|
57
57
|
wordLeft,
|
|
58
58
|
wordRight,
|
|
59
59
|
} from "./keybindings-adapter.js";
|
|
60
|
-
import { matchesKey, visibleWidth } from "./text-helpers.js";
|
|
60
|
+
import { decodePrintableKey, matchesKey, visibleWidth } from "./text-helpers.js";
|
|
61
61
|
|
|
62
62
|
export type FormEditorOutcome = "submit" | "cancel";
|
|
63
63
|
|
|
64
64
|
export interface InlineFormEditorOpts {
|
|
65
65
|
formId: string;
|
|
66
66
|
theme: GraphTheme;
|
|
67
|
-
/** Called when Ctrl+
|
|
67
|
+
/** Called when Ctrl+X passes validation or cancel fires. */
|
|
68
68
|
onExit: (outcome: FormEditorOutcome) => void;
|
|
69
69
|
/**
|
|
70
70
|
* Pi's `KeybindingsManager` injected as the third arg of the editor
|
|
@@ -433,14 +433,14 @@ export class InlineFormEditor implements PiEditorComponent {
|
|
|
433
433
|
// editor actions, so they stay as raw byte checks:
|
|
434
434
|
// esc (\x1b) — cancel form
|
|
435
435
|
// ctrl+c (\x03) — cancel form
|
|
436
|
-
// ctrl+
|
|
436
|
+
// ctrl+x — submit form
|
|
437
437
|
// tab (\t) — focus next field
|
|
438
438
|
// shift+tab (\x1b[Z) — focus previous field
|
|
439
|
-
if (data
|
|
439
|
+
if (matchesKey(data, "ctrl+c") || matchesKey(data, "escape")) {
|
|
440
440
|
this.opts.onExit("cancel");
|
|
441
441
|
return true;
|
|
442
442
|
}
|
|
443
|
-
if (matchesKey(data, "ctrl+
|
|
443
|
+
if (matchesKey(data, "ctrl+x")) {
|
|
444
444
|
if (this.allValid(state)) this.opts.onExit("submit");
|
|
445
445
|
else this.focusFirstInvalid(state);
|
|
446
446
|
return true;
|
|
@@ -475,7 +475,7 @@ export class InlineFormEditor implements PiEditorComponent {
|
|
|
475
475
|
state.rawText[field.name] = choices[(i - 1 + choices.length) % choices.length]!;
|
|
476
476
|
return true;
|
|
477
477
|
}
|
|
478
|
-
if (matchesAction(this.kb, data, "tui.editor.cursorRight") || data
|
|
478
|
+
if (matchesAction(this.kb, data, "tui.editor.cursorRight") || matchesKey(data, "space")) {
|
|
479
479
|
state.rawText[field.name] = choices[(i + 1) % choices.length]!;
|
|
480
480
|
return true;
|
|
481
481
|
}
|
|
@@ -499,7 +499,7 @@ export class InlineFormEditor implements PiEditorComponent {
|
|
|
499
499
|
state: InlineFormState,
|
|
500
500
|
): boolean {
|
|
501
501
|
if (
|
|
502
|
-
data
|
|
502
|
+
matchesKey(data, "space") ||
|
|
503
503
|
matchesAction(this.kb, data, "tui.editor.cursorLeft") ||
|
|
504
504
|
matchesAction(this.kb, data, "tui.editor.cursorRight")
|
|
505
505
|
) {
|
|
@@ -654,12 +654,15 @@ export class InlineFormEditor implements PiEditorComponent {
|
|
|
654
654
|
return true;
|
|
655
655
|
}
|
|
656
656
|
|
|
657
|
-
// Printable insertion —
|
|
658
|
-
//
|
|
659
|
-
//
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
657
|
+
// Printable insertion — accept raw graphemes and terminal-encoded
|
|
658
|
+
// printable keys (CSI-u / Kitty). VSCode's integrated terminal can emit
|
|
659
|
+
// printable keys as escape sequences when modifyOtherKeys is active.
|
|
660
|
+
// Numeric fields accept the same printable range as text; per-field
|
|
661
|
+
// validation catches non-numeric content at submit time.
|
|
662
|
+
const printable = decodePrintableKey(data) ?? data;
|
|
663
|
+
if (isPrintableGrapheme(printable)) {
|
|
664
|
+
state.rawText[name] = cur.slice(0, caret) + printable + cur.slice(caret);
|
|
665
|
+
state.caret = caret + printable.length;
|
|
663
666
|
return true;
|
|
664
667
|
}
|
|
665
668
|
return false;
|