@yaag/extension 0.1.4 → 0.2.1
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/package.json +5 -4
- package/src/background-status.ts +94 -0
- package/src/cli-child.ts +3 -0
- package/src/fake-extension-ui.ts +6 -3
- package/src/fake-tui.ts +1 -1
- package/src/index.ts +10 -7
- package/src/process-liveness.ts +51 -0
- package/src/run-details.ts +4 -2
- package/src/run-foreground.ts +107 -60
- package/src/run-id.ts +24 -0
- package/src/run-record.ts +91 -0
- package/src/run-registry.ts +153 -16
- package/src/run-restore.ts +43 -0
- package/src/run-settlement.ts +1 -1
- package/src/run-store.ts +103 -0
- package/src/run-tool-test-support.ts +4 -4
- package/src/run-tool.ts +38 -30
- package/src/run-tree-component.ts +2 -1
- package/src/run-tree-host.ts +4 -5
- package/src/run-trees.ts +4 -4
- package/src/runs-dir.ts +18 -0
- package/src/spawn-run.ts +3 -1
- package/src/status-tool.ts +39 -4
- package/src/stop-tool.ts +41 -3
- package/src/test-tui-context.ts +7 -1
- package/src/yaag-command.ts +12 -19
- package/src/background-widget.ts +0 -107
- package/src/run-foreground-view.ts +0 -95
package/src/spawn-run.ts
CHANGED
|
@@ -38,6 +38,8 @@ export interface SpawnRunOptions extends StartRunOptions {
|
|
|
38
38
|
export interface RunHandle {
|
|
39
39
|
readonly outcome: Promise<RunOutcome>;
|
|
40
40
|
readonly stop: () => void;
|
|
41
|
+
/** The child's pid, when the spawn produced one. */
|
|
42
|
+
readonly pid?: number | undefined;
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
/**
|
|
@@ -65,7 +67,7 @@ export function startRun(options: StartRunOptions): RunHandle {
|
|
|
65
67
|
options.onProgress?.(summary, event, sequence);
|
|
66
68
|
}),
|
|
67
69
|
]).then(([ended]) => ({ ...ended, summary }));
|
|
68
|
-
return { outcome, stop: child.stop };
|
|
70
|
+
return { outcome, stop: child.stop, pid: child.pid };
|
|
69
71
|
}
|
|
70
72
|
|
|
71
73
|
/**
|
package/src/status-tool.ts
CHANGED
|
@@ -2,7 +2,8 @@ import type { AgentToolResult, ToolDefinition } from "@earendil-works/pi-coding-
|
|
|
2
2
|
import type { RunSummary } from "@yaag/runtime";
|
|
3
3
|
import { renderSnapshot, TreeState } from "@yaag/tui";
|
|
4
4
|
import { Type } from "typebox";
|
|
5
|
-
import type {
|
|
5
|
+
import type { PersistedRunState } from "./run-record.ts";
|
|
6
|
+
import type { RegisteredRun, RestoredRun, RunRegistry } from "./run-registry.ts";
|
|
6
7
|
import { toUsage } from "./usage.ts";
|
|
7
8
|
|
|
8
9
|
const parameters = Type.Object({
|
|
@@ -25,10 +26,15 @@ export interface StatusToolOptions {
|
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
const DESCRIPTION = [
|
|
28
|
-
"Show a snapshot of a Run started
|
|
29
|
+
"Show a snapshot of a Run started by yaag_run.",
|
|
29
30
|
"",
|
|
30
31
|
"With an id, returns that Run's live snapshot or its retained final snapshot.",
|
|
31
|
-
"
|
|
32
|
+
"Run records outlive the Host Session, so an id from an earlier session still",
|
|
33
|
+
"resolves: a Run whose process died is reported as interrupted, and a Run whose",
|
|
34
|
+
"process is still alive is reported as orphaned.",
|
|
35
|
+
"",
|
|
36
|
+
"Without an id, lists this session's Runs plus every orphaned or interrupted",
|
|
37
|
+
"Run left by an earlier session.",
|
|
32
38
|
].join("\n");
|
|
33
39
|
|
|
34
40
|
/**
|
|
@@ -50,7 +56,7 @@ export function createStatusTool(
|
|
|
50
56
|
parameters,
|
|
51
57
|
async execute(_id, params) {
|
|
52
58
|
if (params.id === undefined) return overview(registry.runs, now, width);
|
|
53
|
-
const found = registry.
|
|
59
|
+
const found = await registry.recall(params.id);
|
|
54
60
|
if (found.state === "unknown") throw unknownId(params.id, registry.knownIds);
|
|
55
61
|
const details = detailsFor(found);
|
|
56
62
|
return {
|
|
@@ -85,6 +91,7 @@ function overview(
|
|
|
85
91
|
|
|
86
92
|
function detailsFor(run: RegisteredRun): SnapshotDetails {
|
|
87
93
|
if (run.state === "live") return { id: run.run.id, summary: run.run.summary };
|
|
94
|
+
if (run.state === "restored") return restoredDetails(run.run);
|
|
88
95
|
const { outcome } = run.run;
|
|
89
96
|
return {
|
|
90
97
|
id: run.run.id,
|
|
@@ -95,6 +102,34 @@ function detailsFor(run: RegisteredRun): SnapshotDetails {
|
|
|
95
102
|
};
|
|
96
103
|
}
|
|
97
104
|
|
|
105
|
+
/**
|
|
106
|
+
* A Run of an earlier Host Session. Its persisted state is stated in the
|
|
107
|
+
* Result region, because the folded Summary alone cannot say that nobody owns
|
|
108
|
+
* the Run any more (ticket 02).
|
|
109
|
+
*/
|
|
110
|
+
function restoredDetails(run: RestoredRun): SnapshotDetails {
|
|
111
|
+
const { record } = run;
|
|
112
|
+
const state = `Run ${record.id} is ${record.state} (started ${record.startedAt}).`;
|
|
113
|
+
const alive =
|
|
114
|
+
record.state === "orphaned" && record.process !== null
|
|
115
|
+
? ` Its process ${record.process.pid} is still running; stop it with yaag_stop.`
|
|
116
|
+
: "";
|
|
117
|
+
const detail = `${alive}${resumeHint(record.state, run.summary.artifact)}`;
|
|
118
|
+
const result = record.outcome?.kind === "fulfilled" ? `\n${record.outcome.result}` : "";
|
|
119
|
+
return { id: record.id, summary: run.summary, result: `${state}${detail}${result}` };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Names the Checkpoint an unowned Run left behind (ADR-0031). The Run
|
|
124
|
+
* republishes it at each Ask boundary, so it exists whenever the Run reached
|
|
125
|
+
* one, for a dead Run and for a still-running orphan alike.
|
|
126
|
+
*/
|
|
127
|
+
function resumeHint(state: PersistedRunState, artifact: string | null): string {
|
|
128
|
+
if (artifact === null) return "";
|
|
129
|
+
if (state !== "interrupted" && state !== "orphaned") return "";
|
|
130
|
+
return ` Resume it from its checkpoint: ${artifact}.`;
|
|
131
|
+
}
|
|
132
|
+
|
|
98
133
|
function snapshot(
|
|
99
134
|
details: SnapshotDetails,
|
|
100
135
|
now: () => number,
|
package/src/stop-tool.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import type { AgentToolResult, ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { RunSummary } from "@yaag/runtime";
|
|
3
3
|
import { Type } from "typebox";
|
|
4
|
-
import
|
|
4
|
+
import { isProcessAlive } from "./process-liveness.ts";
|
|
5
|
+
import type { RestoredRun, RunRegistry } from "./run-registry.ts";
|
|
5
6
|
import { toUsage } from "./usage.ts";
|
|
6
7
|
|
|
7
8
|
/** What the stop result carries for rendering — never shown to the model. */
|
|
@@ -18,6 +19,9 @@ const DESCRIPTION = [
|
|
|
18
19
|
"",
|
|
19
20
|
"The Run's Agents are reaped, and the report says what it got through and what",
|
|
20
21
|
"it spent before it stopped.",
|
|
22
|
+
"",
|
|
23
|
+
"An orphaned Run left behind by an earlier Host Session is stopped too: its",
|
|
24
|
+
"process group is killed, and the report holds its last persisted Summary.",
|
|
21
25
|
].join("\n");
|
|
22
26
|
|
|
23
27
|
/** What a stopped Run got through and what it cost, from the fold (ADR-0006). */
|
|
@@ -62,6 +66,38 @@ function agentStateReport(agent: RunSummary["agents"][string]): string {
|
|
|
62
66
|
}
|
|
63
67
|
}
|
|
64
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Ends a Run nobody owns any more. There is no reap ladder to run — its pipes
|
|
71
|
+
* belong to a dead Host Session — so the process group is killed outright
|
|
72
|
+
* (ADR-0008). The pid is re-probed first, because a record can name a pid that
|
|
73
|
+
* a different process has since inherited (ticket 02).
|
|
74
|
+
*/
|
|
75
|
+
function stopOrphan(run: RestoredRun): AgentToolResult<StopDetails> {
|
|
76
|
+
const { record } = run;
|
|
77
|
+
if (record.state !== "orphaned" || record.process === null)
|
|
78
|
+
throw new Error(`yaag_stop: Run ${record.id} has already finished`);
|
|
79
|
+
if (!isProcessAlive(record.process))
|
|
80
|
+
throw new Error(`yaag_stop: Run ${record.id} is no longer running`);
|
|
81
|
+
killGroup(record.process.pid);
|
|
82
|
+
return {
|
|
83
|
+
content: [{ type: "text", text: stopReport(record.id, record.summary) }],
|
|
84
|
+
details: { summary: record.summary },
|
|
85
|
+
usage: toUsage(record.summary),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function killGroup(pid: number): void {
|
|
90
|
+
try {
|
|
91
|
+
process.kill(-pid, "SIGKILL");
|
|
92
|
+
} catch {
|
|
93
|
+
try {
|
|
94
|
+
process.kill(pid, "SIGKILL");
|
|
95
|
+
} catch {
|
|
96
|
+
// The process ended between the liveness probe and the signal.
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
65
101
|
function toError(reason: unknown): Error {
|
|
66
102
|
return reason instanceof Error ? reason : new Error(String(reason));
|
|
67
103
|
}
|
|
@@ -82,10 +118,12 @@ export function createStopTool(
|
|
|
82
118
|
description: DESCRIPTION,
|
|
83
119
|
parameters,
|
|
84
120
|
async execute(_id, params) {
|
|
85
|
-
const status = registry.
|
|
121
|
+
const status = await registry.recall(params.id);
|
|
86
122
|
switch (status.state) {
|
|
87
123
|
case "unknown":
|
|
88
124
|
throw new Error(`yaag_stop: no Run with id ${params.id}`);
|
|
125
|
+
case "restored":
|
|
126
|
+
return stopOrphan(status.run);
|
|
89
127
|
case "finished":
|
|
90
128
|
throw new Error(`yaag_stop: Run ${params.id} has already finished`);
|
|
91
129
|
case "live": {
|
package/src/test-tui-context.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* A tui-mode ExtensionContext double for the
|
|
2
|
+
* A tui-mode ExtensionContext double for the `/yaag` view and the inline
|
|
3
|
+
* frames of a blocking Run.
|
|
3
4
|
*
|
|
4
5
|
* `ui.custom` invokes the factory synchronously against a fake TUI, so a test
|
|
5
6
|
* can feed key bytes and read rendered frames. `TestExtensionContext` still
|
|
@@ -62,6 +63,11 @@ export class TestTuiContext implements ExtensionContext {
|
|
|
62
63
|
return this.ui.widgets;
|
|
63
64
|
}
|
|
64
65
|
|
|
66
|
+
/** The latest footer segment text per key. */
|
|
67
|
+
get statuses(): ReadonlyMap<string, string | undefined> {
|
|
68
|
+
return this.ui.statuses;
|
|
69
|
+
}
|
|
70
|
+
|
|
65
71
|
/** Scripted answers for `ui.select`, consumed in order. */
|
|
66
72
|
get selections(): string[] {
|
|
67
73
|
return this.ui.selections;
|
package/src/yaag-command.ts
CHANGED
|
@@ -1,26 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The `/yaag` command (
|
|
3
|
-
* interactive tree
|
|
4
|
-
* surface.
|
|
2
|
+
* The `/yaag` command (ADR-0008): pick a Run of this session and open the
|
|
3
|
+
* interactive tree over it.
|
|
5
4
|
*
|
|
6
5
|
* It replaces `/yaag-peek`'s Run→Agent→menu picker chain: every surviving
|
|
7
6
|
* action lives in the tree's per-node menu, and the view stays read-only apart
|
|
8
|
-
* from the Run's own reap-ladder stop.
|
|
9
|
-
* live
|
|
10
|
-
*
|
|
7
|
+
* from the Run's own reap-ladder stop. `esc` at tree level closes the view,
|
|
8
|
+
* live or settled, and a live Run keeps executing after that; `ctrl+q` opens
|
|
9
|
+
* the two-way Stop Run prompt.
|
|
11
10
|
*/
|
|
12
11
|
import type {
|
|
13
12
|
ExtensionContext,
|
|
14
13
|
ExtensionUIContext,
|
|
15
14
|
RegisteredCommand,
|
|
16
15
|
} from "@earendil-works/pi-coding-agent";
|
|
17
|
-
import {
|
|
18
|
-
createRunTreeView,
|
|
19
|
-
type RunTreeViewHost,
|
|
20
|
-
type RunViewExit,
|
|
21
|
-
type RunViewResult,
|
|
22
|
-
TreeState,
|
|
23
|
-
} from "@yaag/tui";
|
|
16
|
+
import { createRunTreeView, type RunTreeViewHost, type RunViewResult, TreeState } from "@yaag/tui";
|
|
24
17
|
import { type PickableRun, pickableRuns, runFromLabel, runPickerLabel } from "./run-picker.ts";
|
|
25
18
|
import type { RunRegistry, RunSettlement } from "./run-registry.ts";
|
|
26
19
|
import { observedSettlement, viewResult } from "./run-settlement.ts";
|
|
@@ -102,17 +95,17 @@ async function openRun(
|
|
|
102
95
|
// neither the view nor the renderer.
|
|
103
96
|
let live = true;
|
|
104
97
|
try {
|
|
105
|
-
await ctx.ui.custom<
|
|
98
|
+
await ctx.ui.custom<void>((tui, _theme, keybindings, done) => {
|
|
106
99
|
const host: RunTreeViewHost = {
|
|
107
100
|
...createRunTreeHost({ ui: ctx.ui, tui, keybindings, state }),
|
|
108
101
|
stop: () => {
|
|
109
102
|
if (found.state === "live") found.run.stop();
|
|
110
103
|
},
|
|
111
|
-
// The Run
|
|
112
|
-
|
|
113
|
-
done,
|
|
104
|
+
// The Run keeps executing after the view closes; there is nothing to
|
|
105
|
+
// hand over, because a Run of this session is already addressable.
|
|
106
|
+
done: () => done(),
|
|
114
107
|
};
|
|
115
|
-
const view = createRunTreeView({ state, host, label: run.id
|
|
108
|
+
const view = createRunTreeView({ state, host, label: run.id });
|
|
116
109
|
if (found.state === "finished")
|
|
117
110
|
settle(state, view, found.run.outcome, options.registry, run.id);
|
|
118
111
|
else if (found.state === "live") {
|
|
@@ -120,7 +113,7 @@ async function openRun(
|
|
|
120
113
|
if (id === run.id && live) view.touch();
|
|
121
114
|
});
|
|
122
115
|
// A view opened while live must reach the settled phase itself, or `esc`
|
|
123
|
-
// would keep live behavior after the Run ended (
|
|
116
|
+
// would keep live behavior after the Run ended (ADR-0008).
|
|
124
117
|
const onSettled = (settlement: RunSettlement): void => {
|
|
125
118
|
if (!live) return;
|
|
126
119
|
settle(state, view, settlement, options.registry, run.id);
|
package/src/background-widget.ts
DELETED
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The single combined inline widget for every live background Run (spec §1).
|
|
3
|
-
*
|
|
4
|
-
* One pi widget key holds every Run, so a Run starting or ending costs no
|
|
5
|
-
* extra scrollback. The frame is a `string[]`, which pi never routes input to,
|
|
6
|
-
* so the widget consumes no key by construction.
|
|
7
|
-
*/
|
|
8
|
-
import { renderRunsWidget, type WidgetRun } from "@yaag/tui";
|
|
9
|
-
import type { RunRegistry } from "./run-registry.ts";
|
|
10
|
-
import type { RunTreeStore } from "./run-trees.ts";
|
|
11
|
-
|
|
12
|
-
/** The one pi widget key; every Run shares it. */
|
|
13
|
-
export const WIDGET_KEY = "yaag-runs";
|
|
14
|
-
|
|
15
|
-
/** The only pi capability the widget needs. */
|
|
16
|
-
export interface WidgetSurface {
|
|
17
|
-
setWidget(key: string, lines: string[] | undefined): void;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* What the combined widget needs: the Run registry it reads live Runs from,
|
|
22
|
-
* the projection store it reads their `TreeState` from, and the render budget.
|
|
23
|
-
*/
|
|
24
|
-
export interface BackgroundWidgetOptions {
|
|
25
|
-
readonly registry: RunRegistry;
|
|
26
|
-
readonly store: RunTreeStore;
|
|
27
|
-
readonly now?: () => number;
|
|
28
|
-
readonly width?: number;
|
|
29
|
-
readonly maxLines?: number;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** The widget controller: bind it once, then refresh it on every occurrence. */
|
|
33
|
-
export interface BackgroundWidget {
|
|
34
|
-
/**
|
|
35
|
-
* Binds the pi UI surface, once the Host Session has one, and draws the
|
|
36
|
-
* current frame. A `WidgetSurface.setWidget()` that throws propagates here.
|
|
37
|
-
*/
|
|
38
|
-
bind(surface: WidgetSurface): void;
|
|
39
|
-
/**
|
|
40
|
-
* Redraws the frame; drops the redraw before `bind`, and skips an unchanged
|
|
41
|
-
* frame. A bound `WidgetSurface.setWidget()` that throws propagates here.
|
|
42
|
-
*/
|
|
43
|
-
refresh(): void;
|
|
44
|
-
/**
|
|
45
|
-
* Unsubscribes from the store and clears the widget. A bound
|
|
46
|
-
* `WidgetSurface.setWidget()` that throws propagates here.
|
|
47
|
-
*/
|
|
48
|
-
dispose(): void;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Builds the combined widget over this session's Run registry and projections.
|
|
53
|
-
*
|
|
54
|
-
* An unbound controller drops every refresh, and a Run without a projection is
|
|
55
|
-
* skipped. Construction itself never throws, but `bind()`, `refresh()`, and
|
|
56
|
-
* `dispose()` call the caller's `WidgetSurface.setWidget()` and propagate
|
|
57
|
-
* whatever it throws; the store subscription also propagates it into the
|
|
58
|
-
* `ingest()` that caused the redraw.
|
|
59
|
-
*/
|
|
60
|
-
export function createBackgroundWidget(options: BackgroundWidgetOptions): BackgroundWidget {
|
|
61
|
-
const now = options.now ?? Date.now;
|
|
62
|
-
const width = options.width ?? 100;
|
|
63
|
-
let surface: WidgetSurface | undefined;
|
|
64
|
-
let drawn: readonly string[] | undefined;
|
|
65
|
-
|
|
66
|
-
const entries = (): readonly WidgetRun[] => {
|
|
67
|
-
const runs: WidgetRun[] = [];
|
|
68
|
-
for (const run of options.registry.live) {
|
|
69
|
-
if (options.store.kind(run.id) !== "background") continue;
|
|
70
|
-
const state = options.store.get(run.id);
|
|
71
|
-
if (state !== undefined) runs.push({ id: run.id, state });
|
|
72
|
-
}
|
|
73
|
-
return runs;
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
const refresh = (): void => {
|
|
77
|
-
if (surface === undefined) return;
|
|
78
|
-
const lines = renderRunsWidget(entries(), {
|
|
79
|
-
now: now(),
|
|
80
|
-
width,
|
|
81
|
-
...(options.maxLines === undefined ? {} : { maxLines: options.maxLines }),
|
|
82
|
-
});
|
|
83
|
-
if (drawn !== undefined && sameLines(drawn, lines)) return;
|
|
84
|
-
drawn = lines;
|
|
85
|
-
surface.setWidget(WIDGET_KEY, lines.length === 0 ? undefined : [...lines]);
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
const unsubscribe = options.store.subscribe(() => refresh());
|
|
89
|
-
|
|
90
|
-
return {
|
|
91
|
-
bind(bound: WidgetSurface): void {
|
|
92
|
-
surface = bound;
|
|
93
|
-
drawn = undefined;
|
|
94
|
-
refresh();
|
|
95
|
-
},
|
|
96
|
-
refresh,
|
|
97
|
-
dispose(): void {
|
|
98
|
-
unsubscribe();
|
|
99
|
-
surface?.setWidget(WIDGET_KEY, undefined);
|
|
100
|
-
surface = undefined;
|
|
101
|
-
},
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function sameLines(left: readonly string[], right: readonly string[]): boolean {
|
|
106
|
-
return left.length === right.length && left.every((line, index) => line === right[index]);
|
|
107
|
-
}
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Opens the interactive Run tree for a blocking `yaag_run` in tui mode
|
|
3
|
-
* (spec §4), and adapts the Host Session's capabilities to
|
|
4
|
-
* `RunTreeViewHost`.
|
|
5
|
-
*
|
|
6
|
-
* The host surface is read-only except for the Run's own reap-ladder stop and
|
|
7
|
-
* the detach request: nothing here can prompt an Agent (ADR — a Peek observes,
|
|
8
|
-
* it never sends).
|
|
9
|
-
*/
|
|
10
|
-
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
11
|
-
import {
|
|
12
|
-
createRunTreeView,
|
|
13
|
-
type RunTreeView,
|
|
14
|
-
type RunTreeViewHost,
|
|
15
|
-
type RunViewExit,
|
|
16
|
-
type TreeState,
|
|
17
|
-
} from "@yaag/tui";
|
|
18
|
-
import { createRunTreeHost } from "./run-tree-host.ts";
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* What `openRunTreeView` needs to adapt one Run to the interactive view.
|
|
22
|
-
*
|
|
23
|
-
* The caller must supply a tui-mode `ctx` with UI; `openRunTreeView` throws
|
|
24
|
-
* when `ctx.ui` is absent, because there is no surface to open.
|
|
25
|
-
*/
|
|
26
|
-
export interface OpenRunTreeViewOptions {
|
|
27
|
-
readonly ctx: ExtensionContext;
|
|
28
|
-
/** Created and ingested from Run start, so no early Ask is missed. */
|
|
29
|
-
readonly state: TreeState;
|
|
30
|
-
readonly id: string;
|
|
31
|
-
/** Runs the reap ladder for this Run (ADR-0008). */
|
|
32
|
-
stop(): void;
|
|
33
|
-
/** Converts the Run to a background Run. */
|
|
34
|
-
onDetach(): void;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** The opened view: its exit promise, plus the handles the tool drives. */
|
|
38
|
-
export interface OpenedRunTreeView {
|
|
39
|
-
/** Resolves when the reader dismisses or detaches the view. */
|
|
40
|
-
readonly exit: Promise<RunViewExit>;
|
|
41
|
-
/** The Run settled; freeze the tree and show the Result region. */
|
|
42
|
-
settle(result: Parameters<RunTreeView["settle"]>[0]): void;
|
|
43
|
-
/** A pushed fd 3 update landed in the TreeState; redraw. */
|
|
44
|
-
touch(): void;
|
|
45
|
-
/** Closes the view from the host's side, e.g. on an abort. */
|
|
46
|
-
close(exit: RunViewExit): void;
|
|
47
|
-
dispose(): void;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Opens the view through `ctx.ui.custom()`.
|
|
52
|
-
*
|
|
53
|
-
* The returned `exit` promise always settles: the reader's `q`/detach resolves
|
|
54
|
-
* it, and `close` resolves it for a host-side abort, so a tool call can never
|
|
55
|
-
* hang on the view.
|
|
56
|
-
*/
|
|
57
|
-
export function openRunTreeView(options: OpenRunTreeViewOptions): OpenedRunTreeView {
|
|
58
|
-
const state = options.state;
|
|
59
|
-
let view: RunTreeView | undefined;
|
|
60
|
-
let finish: ((exit: RunViewExit) => void) | undefined;
|
|
61
|
-
let pendingExit: RunViewExit | undefined;
|
|
62
|
-
let pendingSettle: Parameters<RunTreeView["settle"]>[0] | undefined;
|
|
63
|
-
|
|
64
|
-
const exit = options.ctx.ui.custom<RunViewExit>((tui, _theme, keybindings, done) => {
|
|
65
|
-
finish = done;
|
|
66
|
-
const host: RunTreeViewHost = {
|
|
67
|
-
...createRunTreeHost({ ui: options.ctx.ui, tui, keybindings, state }),
|
|
68
|
-
stop: options.stop,
|
|
69
|
-
detach: options.onDetach,
|
|
70
|
-
done,
|
|
71
|
-
};
|
|
72
|
-
view = createRunTreeView({ state, host, label: options.id, surface: "foreground" });
|
|
73
|
-
if (pendingSettle !== undefined) view.settle(pendingSettle);
|
|
74
|
-
if (pendingExit !== undefined) done(pendingExit);
|
|
75
|
-
return view;
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
return {
|
|
79
|
-
exit,
|
|
80
|
-
settle(result): void {
|
|
81
|
-
if (view === undefined) pendingSettle = result;
|
|
82
|
-
else view.settle(result);
|
|
83
|
-
},
|
|
84
|
-
touch(): void {
|
|
85
|
-
view?.touch();
|
|
86
|
-
},
|
|
87
|
-
close(kind): void {
|
|
88
|
-
if (finish === undefined) pendingExit = kind;
|
|
89
|
-
else finish(kind);
|
|
90
|
-
},
|
|
91
|
-
dispose(): void {
|
|
92
|
-
view?.dispose();
|
|
93
|
-
},
|
|
94
|
-
};
|
|
95
|
-
}
|