pi-squire 0.20.0 → 0.20.2
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 +126 -61
- package/README.md +59 -90
- package/package.json +2 -2
- package/src/agents.ts +1 -1
- package/src/fork.ts +18 -1
- package/src/index.ts +7 -23
- package/src/navigator-view.ts +8 -75
- package/src/navigator.ts +29 -148
- package/src/result-sessions.ts +26 -0
- package/src/results.ts +2 -2
- package/src/tmux.ts +37 -0
- package/src/tracking.ts +3 -2
- package/src/tree-registry.ts +4 -0
- package/src/tree-view.ts +2 -1
- package/src/widget-activity-runtime.ts +10 -27
- package/src/widget-ancestors.ts +53 -41
- package/src/widget-current.ts +21 -20
- package/src/widget-data.ts +35 -2
- package/src/widget-focus.ts +21 -11
- package/src/widget-live.ts +7 -16
- package/src/widget-render.ts +72 -71
- package/src/widget-selection.ts +16 -7
- package/src/widget.ts +94 -53
- package/src/windows.ts +9 -1
- package/src/dashboard.ts +0 -144
- package/src/widget-live-process.ts +0 -31
- package/src/widget-summary.ts +0 -51
|
@@ -4,33 +4,20 @@ import type { TmuxClient } from "./tmux.ts";
|
|
|
4
4
|
import { ActivityCollector } from "./widget-activity.ts";
|
|
5
5
|
import { LIVE_LAUNCH_ENV, LiveActivityPublisher, liveLaunch } from "./widget-live.ts";
|
|
6
6
|
|
|
7
|
-
function observeLocator(
|
|
7
|
+
async function observeLocator(
|
|
8
8
|
publisher: LiveActivityPublisher,
|
|
9
9
|
id: string,
|
|
10
10
|
token: string,
|
|
11
11
|
tmux: TmuxClient,
|
|
12
12
|
ctx: ExtensionContext,
|
|
13
|
-
):
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
if (
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
publisher.publishLocator(locator, token, ctx.sessionManager.getSessionFile());
|
|
22
|
-
} catch {
|
|
23
|
-
/* Optional observation cannot block task processing. */
|
|
24
|
-
} finally {
|
|
25
|
-
reading = false;
|
|
26
|
-
}
|
|
27
|
-
};
|
|
28
|
-
void refresh();
|
|
29
|
-
const timer: ReturnType<typeof setInterval> = setInterval(() => {
|
|
30
|
-
void refresh();
|
|
31
|
-
}, 1000);
|
|
32
|
-
timer.unref();
|
|
33
|
-
return timer;
|
|
13
|
+
): Promise<void> {
|
|
14
|
+
try {
|
|
15
|
+
const locator = await tmux.inspectPane?.().catch(() => undefined);
|
|
16
|
+
if (matchesSessionOwner(id, process.pid, token))
|
|
17
|
+
publisher.publishLocator(locator, token, ctx.sessionManager.getSessionFile());
|
|
18
|
+
} catch {
|
|
19
|
+
/* Optional observation cannot block task processing. */
|
|
20
|
+
}
|
|
34
21
|
}
|
|
35
22
|
|
|
36
23
|
/** Observe raw lifecycle after a positive startup owner gate; never grants task/action authority. */
|
|
@@ -43,12 +30,8 @@ export function registerActivityRuntime(
|
|
|
43
30
|
const seed: string | undefined = process.env[LIVE_LAUNCH_ENV];
|
|
44
31
|
let collector: ActivityCollector | undefined;
|
|
45
32
|
let publisher: LiveActivityPublisher | undefined;
|
|
46
|
-
let timer: ReturnType<typeof setInterval> | undefined;
|
|
47
33
|
|
|
48
34
|
const stop = (): void => {
|
|
49
|
-
if (timer) clearInterval(timer);
|
|
50
|
-
timer = undefined;
|
|
51
|
-
|
|
52
35
|
collector?.clear();
|
|
53
36
|
collector = undefined;
|
|
54
37
|
publisher?.stop();
|
|
@@ -66,7 +49,7 @@ export function registerActivityRuntime(
|
|
|
66
49
|
collector = new ActivityCollector();
|
|
67
50
|
publisher = new LiveActivityPublisher(id, liveLaunch(seed, id));
|
|
68
51
|
publish();
|
|
69
|
-
if (tmux && ownerToken)
|
|
52
|
+
if (tmux && ownerToken) void observeLocator(publisher, id, ownerToken, tmux, ctx);
|
|
70
53
|
});
|
|
71
54
|
pi.on("tool_execution_start", (event) => {
|
|
72
55
|
collector?.start(event.toolCallId, event.toolName, event.parentToolCallId);
|
package/src/widget-ancestors.ts
CHANGED
|
@@ -4,20 +4,30 @@ import { type Identity, readIdentity } from "./identity.ts";
|
|
|
4
4
|
import { locateSession, recordedName } from "./results.ts";
|
|
5
5
|
import { matchesSessionOwner, sessionIsOwned } from "./session-owner.ts";
|
|
6
6
|
import type { PaneLocator, TmuxClient } from "./tmux.ts";
|
|
7
|
-
import {
|
|
7
|
+
import { readTreeDisplay, type TreeEntry } from "./tree-registry.ts";
|
|
8
8
|
import { LiveActivityReader, type LiveLocator } from "./widget-live.ts";
|
|
9
9
|
import { WidgetTitleMirrors } from "./widget-title.ts";
|
|
10
10
|
import { WidgetTitleFiles } from "./widget-title-file.ts";
|
|
11
11
|
import { listWindows } from "./windows.ts";
|
|
12
12
|
|
|
13
|
-
export type
|
|
13
|
+
export type WidgetIdentity = { identity: string; agent?: string };
|
|
14
|
+
export type Breadcrumb = {
|
|
15
|
+
key: string;
|
|
16
|
+
parentSessionId: string;
|
|
17
|
+
crumbs: WidgetIdentity[];
|
|
18
|
+
self: WidgetIdentity;
|
|
19
|
+
parentTitle?: string;
|
|
20
|
+
};
|
|
14
21
|
type Ancestor = { sessionId: string; identity?: Identity };
|
|
15
22
|
|
|
16
23
|
/** Durable parent ids only. A broken/cyclic chain refuses rather than inventing ancestry. */
|
|
17
24
|
export function ancestorSessions(ctx: ExtensionContext): Ancestor[] {
|
|
18
25
|
const session: string = ctx.sessionManager.getSessionId();
|
|
19
26
|
const own: Identity | undefined = readIdentity(session);
|
|
20
|
-
|
|
27
|
+
return own ? ancestorChain(session, own) : [];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function ancestorChain(session: string, own: Identity): Ancestor[] {
|
|
21
31
|
const found: Ancestor[] = [];
|
|
22
32
|
const seen: Set<string> = new Set([session]);
|
|
23
33
|
let child: Identity = own;
|
|
@@ -44,7 +54,7 @@ export function ancestorSessions(ctx: ExtensionContext): Ancestor[] {
|
|
|
44
54
|
throw new Error("ancestor identity chain does not reach root");
|
|
45
55
|
}
|
|
46
56
|
|
|
47
|
-
export class
|
|
57
|
+
export class BreadcrumbView {
|
|
48
58
|
private readonly titles: WidgetTitleFiles = new WidgetTitleFiles();
|
|
49
59
|
private readonly mirrors: WidgetTitleMirrors = new WidgetTitleMirrors();
|
|
50
60
|
|
|
@@ -53,33 +63,40 @@ export class AncestorView {
|
|
|
53
63
|
this.mirrors.clear();
|
|
54
64
|
}
|
|
55
65
|
|
|
56
|
-
async read(ctx: ExtensionContext, reader: LiveActivityReader): Promise<
|
|
57
|
-
const
|
|
66
|
+
async read(ctx: ExtensionContext, reader: LiveActivityReader): Promise<Breadcrumb | undefined> {
|
|
67
|
+
const session = ctx.sessionManager.getSessionId();
|
|
68
|
+
const own = readIdentity(session);
|
|
69
|
+
if (!own) return;
|
|
70
|
+
const ancestors = ancestorChain(session, own);
|
|
58
71
|
let entries: TreeEntry[] = [];
|
|
59
72
|
try {
|
|
60
|
-
|
|
73
|
+
entries = readTreeDisplay(ancestors[0].sessionId).entries;
|
|
61
74
|
} catch {
|
|
62
75
|
/* labels optional */
|
|
63
76
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
77
|
+
const crumbs = ancestors.map((ancestor): WidgetIdentity => {
|
|
78
|
+
const entry = entries.findLast((item) => item.sessionId === ancestor.sessionId);
|
|
79
|
+
const label = entry?.launch ? recordedName(entry.launch) : ancestor.identity?.agent;
|
|
80
|
+
return {
|
|
81
|
+
identity: ancestor.identity ? (label ?? ancestor.identity.agent) : "root",
|
|
82
|
+
agent: ancestor.identity?.agent,
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
const parent = ancestors[ancestors.length - 1];
|
|
86
|
+
const entry = entries.findLast((item) => item.sessionId === parent.sessionId);
|
|
87
|
+
const live = await reader.locator(parent.sessionId);
|
|
88
|
+
const path = live?.sessionFile ?? (entry?.launch ? locateSession(entry.launch, undefined) : undefined);
|
|
89
|
+
const self = entries.findLast((item) => item.sessionId === session);
|
|
90
|
+
return {
|
|
91
|
+
key: `ancestor:${parent.sessionId}`,
|
|
92
|
+
parentSessionId: parent.sessionId,
|
|
93
|
+
crumbs,
|
|
94
|
+
self: {
|
|
95
|
+
identity: self?.launch ? recordedName(self.launch) : own.agent,
|
|
96
|
+
agent: own.agent,
|
|
97
|
+
},
|
|
98
|
+
parentTitle: this.titles.read(path, parent.sessionId)?.name ?? this.mirrors.read(parent.sessionId),
|
|
99
|
+
};
|
|
83
100
|
}
|
|
84
101
|
}
|
|
85
102
|
|
|
@@ -95,20 +112,13 @@ function sameTarget(a: LiveLocator, b: LiveLocator): boolean {
|
|
|
95
112
|
}
|
|
96
113
|
|
|
97
114
|
/** Positive own-server + exact target-pane + id/name listing, not a launch snapshot. */
|
|
98
|
-
export async function verifyLiveWindow(tmux: TmuxClient, live: LiveLocator): Promise<
|
|
115
|
+
export async function verifyLiveWindow(tmux: TmuxClient, live: LiveLocator): Promise<PaneLocator | undefined> {
|
|
99
116
|
const own: PaneLocator | undefined = await tmux.inspectPane?.();
|
|
100
|
-
if (!own || !sameServer(own, live.locator)) return
|
|
117
|
+
if (!own || !sameServer(own, live.locator)) return undefined;
|
|
101
118
|
const pane: PaneLocator | undefined = await tmux.inspectPane?.(live.locator.paneId);
|
|
102
|
-
if (
|
|
103
|
-
!pane ||
|
|
104
|
-
pane.exited ||
|
|
105
|
-
!sameServer(pane, live.locator) ||
|
|
106
|
-
pane.windowId !== live.locator.windowId ||
|
|
107
|
-
pane.windowName !== live.locator.windowName
|
|
108
|
-
)
|
|
109
|
-
return false;
|
|
119
|
+
if (!pane || pane.exited || !sameServer(pane, live.locator)) return undefined;
|
|
110
120
|
const windows = await listWindows(tmux);
|
|
111
|
-
return
|
|
121
|
+
return windows?.some((window) => window.id === pane.windowId && window.name === pane.windowName) ? pane : undefined;
|
|
112
122
|
}
|
|
113
123
|
|
|
114
124
|
export async function switchAncestor(
|
|
@@ -122,21 +132,23 @@ export async function switchAncestor(
|
|
|
122
132
|
if (!ancestorSessions(ctx).some((ancestor) => ancestor.sessionId === sessionId))
|
|
123
133
|
throw new Error("target is not a current ancestor");
|
|
124
134
|
const live: LiveLocator | undefined = await reader.locator(sessionId);
|
|
125
|
-
|
|
135
|
+
const pane = live ? await verifyLiveWindow(tmux, live) : undefined;
|
|
136
|
+
if (!live || !pane) throw new Error("no verified live window for this ancestor");
|
|
126
137
|
assertActive();
|
|
127
138
|
const own = await tmux.ownSession();
|
|
128
139
|
const client: string | undefined = own ? await tmux.clientFor(own.name) : undefined;
|
|
129
140
|
if (!client) throw new Error("no tmux client attached to this session");
|
|
130
141
|
const current: LiveLocator | undefined = await reader.locator(sessionId);
|
|
131
142
|
assertActive();
|
|
132
|
-
|
|
143
|
+
const verified = current ? await verifyLiveWindow(tmux, current) : undefined;
|
|
144
|
+
if (!current || !sameTarget(live, current) || !verified || JSON.stringify(pane) !== JSON.stringify(verified))
|
|
133
145
|
throw new Error("ancestor window changed or is unavailable");
|
|
134
146
|
assertActive();
|
|
135
147
|
if (!ancestorSessions(ctx).some((ancestor) => ancestor.sessionId === sessionId))
|
|
136
148
|
throw new Error("ancestor identity changed");
|
|
137
149
|
if (!matchesSessionOwner(sessionId, current.pid, current.ownerToken) || !sessionIsOwned(sessionId))
|
|
138
150
|
throw new Error("ancestor owner changed or is unavailable");
|
|
139
|
-
await tmux.switchTo(client,
|
|
151
|
+
await tmux.switchTo(client, verified.windowId);
|
|
140
152
|
assertActive();
|
|
141
|
-
ctx.ui.notify(`delegate: switched to ancestor ${sessionId} (${
|
|
153
|
+
ctx.ui.notify(`delegate: switched to ancestor ${sessionId} (${verified.windowId})`, "info");
|
|
142
154
|
}
|
package/src/widget-current.ts
CHANGED
|
@@ -1,40 +1,41 @@
|
|
|
1
1
|
// Optional live-window overrides for known local children, never semantic rewrites.
|
|
2
2
|
import { isStarted, type Results, type StartedRecorded, sessionIdOf } from "./results.ts";
|
|
3
|
-
import type {
|
|
4
|
-
import type { LiveActivityReader
|
|
3
|
+
import type { PaneListing } from "./tmux.ts";
|
|
4
|
+
import type { LiveActivityReader } from "./widget-live.ts";
|
|
5
5
|
|
|
6
6
|
export async function currentChildWindows(
|
|
7
7
|
results: Results,
|
|
8
|
-
|
|
9
|
-
windows: TmuxWindow[] | undefined,
|
|
8
|
+
listing: PaneListing | undefined,
|
|
10
9
|
reader: LiveActivityReader,
|
|
11
10
|
): Promise<Map<string, { windowId: string; windowName: string }>> {
|
|
12
|
-
const found
|
|
13
|
-
if (!
|
|
14
|
-
const
|
|
11
|
+
const found = new Map<string, { windowId: string; windowName: string }>();
|
|
12
|
+
if (!listing?.server || !listing.windows.length) return found;
|
|
13
|
+
const server = listing.server;
|
|
14
|
+
const latest = new Map<string, StartedRecorded>(
|
|
15
15
|
results
|
|
16
16
|
.list()
|
|
17
17
|
.filter(isStarted)
|
|
18
18
|
.map((record) => [sessionIdOf(record), record]),
|
|
19
19
|
);
|
|
20
|
-
const candidates: StartedRecorded[] = [...latest.values()].filter((record) => !record.closed);
|
|
21
|
-
if (!candidates.length) return found;
|
|
22
|
-
const own: PaneLocator | undefined = await tmux.inspectPane().catch(() => undefined);
|
|
23
|
-
if (!own) return found;
|
|
24
20
|
await Promise.all(
|
|
25
|
-
|
|
26
|
-
|
|
21
|
+
[...latest.values()].map(async (record): Promise<void> => {
|
|
22
|
+
if (
|
|
23
|
+
record.closed ||
|
|
24
|
+
listing.windows.some(
|
|
25
|
+
(window) => window.id === record.windowId && window.name === record.windowName && !window.exited,
|
|
26
|
+
)
|
|
27
|
+
)
|
|
28
|
+
return;
|
|
29
|
+
const live = await reader.locator(sessionIdOf(record));
|
|
27
30
|
if (
|
|
28
31
|
!live ||
|
|
29
|
-
live.locator.socket !==
|
|
30
|
-
live.locator.serverPid !==
|
|
31
|
-
live.locator.serverStarted !==
|
|
32
|
+
live.locator.socket !== server.socket ||
|
|
33
|
+
live.locator.serverPid !== server.serverPid ||
|
|
34
|
+
live.locator.serverStarted !== server.serverStarted
|
|
32
35
|
)
|
|
33
36
|
return;
|
|
34
|
-
const pane
|
|
35
|
-
if (
|
|
36
|
-
if (windows.some((window) => window.id === pane.windowId && window.name === pane.windowName))
|
|
37
|
-
found.set(record.id, { windowId: pane.windowId, windowName: pane.windowName });
|
|
37
|
+
const pane = listing.panes.get(live.locator.paneId);
|
|
38
|
+
if (pane && !pane.exited) found.set(record.id, { windowId: pane.windowId, windowName: pane.windowName });
|
|
38
39
|
}),
|
|
39
40
|
);
|
|
40
41
|
return found;
|
package/src/widget-data.ts
CHANGED
|
@@ -12,7 +12,12 @@ export type DelegateRow = {
|
|
|
12
12
|
key: string;
|
|
13
13
|
sessionId: string;
|
|
14
14
|
launchId: string;
|
|
15
|
+
parentKey?: string;
|
|
16
|
+
depth?: number;
|
|
17
|
+
descendantCount?: number;
|
|
18
|
+
expanded?: boolean;
|
|
15
19
|
identity: string;
|
|
20
|
+
agent?: string;
|
|
16
21
|
title: string | undefined;
|
|
17
22
|
state: string;
|
|
18
23
|
running: boolean;
|
|
@@ -21,7 +26,6 @@ export type DelegateRow = {
|
|
|
21
26
|
/** Transcript mtime is a last-active hint, not proof of tool activity. */
|
|
22
27
|
lastActive: number | undefined;
|
|
23
28
|
activity?: Activity;
|
|
24
|
-
summary?: string;
|
|
25
29
|
};
|
|
26
30
|
|
|
27
31
|
/** All eligible rows remain available; only this viewport incurs metadata I/O. */
|
|
@@ -82,6 +86,7 @@ export class DirectDelegateView {
|
|
|
82
86
|
sessionDir: string | undefined,
|
|
83
87
|
viewport: MetadataViewport = { offset: 0, limit: 3 },
|
|
84
88
|
currentWindows: ReadonlyMap<string, { windowId: string; windowName: string }> = new Map(),
|
|
89
|
+
visibleKeys?: ReadonlySet<string>,
|
|
85
90
|
): DelegateRow[] {
|
|
86
91
|
const running: Set<string> = new Set(results.running().map((record) => record.id));
|
|
87
92
|
const entries: Entry[] = [];
|
|
@@ -100,7 +105,8 @@ export class DirectDelegateView {
|
|
|
100
105
|
...entries.filter(({ row }) => !row.running),
|
|
101
106
|
];
|
|
102
107
|
return ordered.map(({ record, row }, index) => {
|
|
103
|
-
if (index >= viewport.offset && index < viewport.offset + viewport.limit)
|
|
108
|
+
if (visibleKeys ? visibleKeys.has(row.key) : index >= viewport.offset && index < viewport.offset + viewport.limit)
|
|
109
|
+
this.enrich(record, row, sessionDir);
|
|
104
110
|
return row;
|
|
105
111
|
});
|
|
106
112
|
}
|
|
@@ -111,7 +117,9 @@ export class DirectDelegateView {
|
|
|
111
117
|
key: sessionId,
|
|
112
118
|
sessionId,
|
|
113
119
|
launchId: record.id,
|
|
120
|
+
parentKey: record.identity?.parent,
|
|
114
121
|
identity: recordedName(record),
|
|
122
|
+
agent: record.agent,
|
|
115
123
|
title: undefined,
|
|
116
124
|
lastActive: undefined,
|
|
117
125
|
state: running ? "running" : (record.result?.status ?? (record.gone ? "missing" : "unknown")),
|
|
@@ -145,3 +153,28 @@ export class DirectDelegateView {
|
|
|
145
153
|
row.lastActive = disk?.modified;
|
|
146
154
|
}
|
|
147
155
|
}
|
|
156
|
+
|
|
157
|
+
export function delegateTree(rows: DelegateRow[], viewer: string, expanded: ReadonlySet<string>): DelegateRow[] {
|
|
158
|
+
const children = new Map<string, DelegateRow[]>();
|
|
159
|
+
const keys = new Set(rows.map((row) => row.key));
|
|
160
|
+
for (const row of rows) {
|
|
161
|
+
const parent = row.parentKey && keys.has(row.parentKey) ? row.parentKey : viewer;
|
|
162
|
+
const siblings = children.get(parent) ?? [];
|
|
163
|
+
siblings.push(row);
|
|
164
|
+
children.set(parent, siblings);
|
|
165
|
+
}
|
|
166
|
+
const count = (key: string): number =>
|
|
167
|
+
(children.get(key) ?? []).reduce((total, child) => total + 1 + count(child.key), 0);
|
|
168
|
+
const visible: DelegateRow[] = [];
|
|
169
|
+
const walk = (parent: string, depth: number): void => {
|
|
170
|
+
for (const row of children.get(parent) ?? []) {
|
|
171
|
+
row.depth = depth;
|
|
172
|
+
row.descendantCount = count(row.key);
|
|
173
|
+
row.expanded = expanded.has(row.key);
|
|
174
|
+
visible.push(row);
|
|
175
|
+
if (expanded.has(row.key)) walk(row.key, depth + 1);
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
walk(viewer, 0);
|
|
179
|
+
return visible;
|
|
180
|
+
}
|
package/src/widget-focus.ts
CHANGED
|
@@ -29,10 +29,12 @@ type FocusOptions = {
|
|
|
29
29
|
selection: WidgetSelection;
|
|
30
30
|
changed(): void;
|
|
31
31
|
open(key: string): Promise<void>;
|
|
32
|
+
close(key: string): Promise<void>;
|
|
33
|
+
horizontal(direction: -1 | 1): void;
|
|
32
34
|
error(error: unknown): void;
|
|
33
35
|
};
|
|
34
36
|
|
|
35
|
-
/** Pseudo-focus
|
|
37
|
+
/** Pseudo-focus leaves real Pi focus with the prompt or its confirmation dialog. */
|
|
36
38
|
export class WidgetFocus {
|
|
37
39
|
private readonly options: FocusOptions;
|
|
38
40
|
private tui: unknown;
|
|
@@ -68,7 +70,7 @@ export class WidgetFocus {
|
|
|
68
70
|
suspend(suspended: boolean): void {
|
|
69
71
|
if (this.stopped) return;
|
|
70
72
|
this.suspended = suspended;
|
|
71
|
-
// Our own
|
|
73
|
+
// Our own confirmation keeps selection; unrelated prompts relinquish pseudo-focus.
|
|
72
74
|
if (suspended && !this.busy) this.options.selection.leave();
|
|
73
75
|
this.options.changed();
|
|
74
76
|
}
|
|
@@ -88,8 +90,7 @@ export class WidgetFocus {
|
|
|
88
90
|
|
|
89
91
|
private input(data: string): { consume: true } | undefined {
|
|
90
92
|
if (this.stopped || isKeyRelease(data)) return;
|
|
91
|
-
//
|
|
92
|
-
// let dashboard Escape abort a run or action letters leak into that editor.
|
|
93
|
+
// Pending actions consume keys only while the exact empty prompt still owns focus.
|
|
93
94
|
if (this.busy) return this.pendingInput();
|
|
94
95
|
if (this.suspended || promptState(this.tui, this.options.ui) !== "empty") {
|
|
95
96
|
this.leave();
|
|
@@ -98,11 +99,7 @@ export class WidgetFocus {
|
|
|
98
99
|
const selection: WidgetSelection = this.options.selection;
|
|
99
100
|
if (!selection.active) {
|
|
100
101
|
if (!matchesKey(data, "down") || !selection.enter()) return;
|
|
101
|
-
} else if (
|
|
102
|
-
else if (matchesKey(data, "up")) selection.move(-1);
|
|
103
|
-
else if (matchesKey(data, "escape")) selection.leave();
|
|
104
|
-
else if (matchesKey(data, "enter")) this.open();
|
|
105
|
-
else {
|
|
102
|
+
} else if (!this.navigate(data)) {
|
|
106
103
|
this.leave();
|
|
107
104
|
return;
|
|
108
105
|
}
|
|
@@ -110,17 +107,30 @@ export class WidgetFocus {
|
|
|
110
107
|
return { consume: true };
|
|
111
108
|
}
|
|
112
109
|
|
|
110
|
+
private navigate(data: string): boolean {
|
|
111
|
+
const selection = this.options.selection;
|
|
112
|
+
if (matchesKey(data, "down")) selection.move(1);
|
|
113
|
+
else if (matchesKey(data, "up")) selection.move(-1);
|
|
114
|
+
else if (matchesKey(data, "escape")) selection.leave();
|
|
115
|
+
else if (matchesKey(data, "left")) this.options.horizontal(-1);
|
|
116
|
+
else if (matchesKey(data, "right")) this.options.horizontal(1);
|
|
117
|
+
else if (matchesKey(data, "enter")) this.action("open");
|
|
118
|
+
else if (matchesKey(data, "x")) this.action("close");
|
|
119
|
+
else return false;
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
|
|
113
123
|
private pendingInput(): { consume: true } | undefined {
|
|
114
124
|
return promptState(this.tui, this.options.ui) === "empty" ? { consume: true } : undefined;
|
|
115
125
|
}
|
|
116
126
|
|
|
117
|
-
private open
|
|
127
|
+
private action(action: "open" | "close"): void {
|
|
118
128
|
const key: string | undefined = this.options.selection.selectedKey;
|
|
119
129
|
if (key === undefined) return;
|
|
120
130
|
this.busy = true; // Set before invoking async custom UI; repeated Enter cannot reenter.
|
|
121
131
|
void Promise.resolve()
|
|
122
132
|
.then(() => {
|
|
123
|
-
if (!this.stopped) return this.options
|
|
133
|
+
if (!this.stopped) return this.options[action](key);
|
|
124
134
|
})
|
|
125
135
|
.catch((error: unknown) => {
|
|
126
136
|
if (!this.stopped) this.options.error(error);
|
package/src/widget-live.ts
CHANGED
|
@@ -6,8 +6,8 @@ import { join } from "node:path";
|
|
|
6
6
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
7
7
|
import { matchesSessionOwner } from "./session-owner.ts";
|
|
8
8
|
import type { PaneLocator } from "./tmux.ts";
|
|
9
|
+
import { processAlive } from "./tree-lock.ts";
|
|
9
10
|
import { type Activity, activityText } from "./widget-activity.ts";
|
|
10
|
-
import { type Liveness, processLiveness, processStart } from "./widget-live-process.ts";
|
|
11
11
|
|
|
12
12
|
export const LIVE_LAUNCH_ENV = "PI_SQUIRE_LIVE_LAUNCH";
|
|
13
13
|
export const LIVE_MAX_BYTES = 8192;
|
|
@@ -29,7 +29,6 @@ type Snapshot = {
|
|
|
29
29
|
launchId?: string;
|
|
30
30
|
generation: string;
|
|
31
31
|
pid: number;
|
|
32
|
-
processStart: string;
|
|
33
32
|
revision: number;
|
|
34
33
|
observedAt: number;
|
|
35
34
|
activity: Activity | null;
|
|
@@ -97,7 +96,6 @@ function validSnapshot(value: Partial<Snapshot>, sessionId: string, generation:
|
|
|
97
96
|
(value.launchId === undefined || validString(value.launchId)) &&
|
|
98
97
|
Number.isSafeInteger(value.pid) &&
|
|
99
98
|
(value.pid ?? 0) > 0 &&
|
|
100
|
-
validString(value.processStart) &&
|
|
101
99
|
Number.isSafeInteger(value.revision) &&
|
|
102
100
|
(value.revision ?? -1) >= 0 &&
|
|
103
101
|
typeof value.observedAt === "number" &&
|
|
@@ -131,13 +129,17 @@ function snapshotFile(name: string, file: boolean): boolean {
|
|
|
131
129
|
return file && /^[a-f0-9-]{36}\.json$/.test(name);
|
|
132
130
|
}
|
|
133
131
|
|
|
134
|
-
type
|
|
132
|
+
type Liveness = "live" | "dead" | "unknown";
|
|
133
|
+
type Probe = (owner: { pid: number }) => Promise<Liveness>;
|
|
135
134
|
|
|
136
135
|
export class LiveActivityReader {
|
|
137
136
|
private readonly agentDir: string;
|
|
138
137
|
private readonly probe: Probe;
|
|
139
138
|
|
|
140
|
-
constructor(
|
|
139
|
+
constructor(
|
|
140
|
+
agentDir = getAgentDir(),
|
|
141
|
+
probe: Probe = async ({ pid }: { pid: number }): Promise<Liveness> => (processAlive(pid) ? "live" : "dead"),
|
|
142
|
+
) {
|
|
141
143
|
this.agentDir = agentDir;
|
|
142
144
|
this.probe = probe;
|
|
143
145
|
}
|
|
@@ -209,7 +211,6 @@ export class LiveActivityReader {
|
|
|
209
211
|
}
|
|
210
212
|
|
|
211
213
|
type PublisherOptions = {
|
|
212
|
-
start?: () => Promise<string | undefined>;
|
|
213
214
|
rename?: typeof rename;
|
|
214
215
|
};
|
|
215
216
|
|
|
@@ -221,7 +222,6 @@ export class LiveActivityPublisher {
|
|
|
221
222
|
private readonly dir: string;
|
|
222
223
|
private readonly path: string;
|
|
223
224
|
private readonly temporary: string;
|
|
224
|
-
private readonly start: Promise<string | undefined>;
|
|
225
225
|
private readonly replace: typeof rename;
|
|
226
226
|
private activity: Activity | null = null;
|
|
227
227
|
private locator: PaneLocator | undefined;
|
|
@@ -243,9 +243,6 @@ export class LiveActivityPublisher {
|
|
|
243
243
|
this.dir = liveSessionDir(sessionId);
|
|
244
244
|
this.path = join(this.dir, `${this.generation}.json`);
|
|
245
245
|
this.temporary = `${this.path}.tmp`;
|
|
246
|
-
this.start = (options.start ?? ((): Promise<string | undefined> => processStart(process.pid)))().catch(
|
|
247
|
-
() => undefined,
|
|
248
|
-
);
|
|
249
246
|
this.replace = options.rename ?? rename;
|
|
250
247
|
}
|
|
251
248
|
|
|
@@ -311,11 +308,6 @@ export class LiveActivityPublisher {
|
|
|
311
308
|
}
|
|
312
309
|
|
|
313
310
|
private async drain(): Promise<void> {
|
|
314
|
-
const start: string | undefined = await this.start;
|
|
315
|
-
if (!start || this.stopped) {
|
|
316
|
-
this.dirty = false;
|
|
317
|
-
return;
|
|
318
|
-
}
|
|
319
311
|
if (this.dirty && !this.stopped) {
|
|
320
312
|
this.dirty = false;
|
|
321
313
|
this.urgent = false;
|
|
@@ -326,7 +318,6 @@ export class LiveActivityPublisher {
|
|
|
326
318
|
launchId: this.launchId,
|
|
327
319
|
generation: this.generation,
|
|
328
320
|
pid: process.pid,
|
|
329
|
-
processStart: start,
|
|
330
321
|
revision: this.revision,
|
|
331
322
|
observedAt: this.observedAt,
|
|
332
323
|
activity: this.activity,
|