shariq-pi-extensions 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +94 -0
- package/THIRD_PARTY_NOTICES.md +11 -0
- package/docs/ARCHITECTURE.md +54 -0
- package/docs/DEVELOPMENT.md +88 -0
- package/docs/EXTENSIONS.md +89 -0
- package/extensions/antigravity-provider/LICENSE +21 -0
- package/extensions/antigravity-provider/README.md +23 -0
- package/extensions/antigravity-provider/antigravity/cloud-code-assist.ts +519 -0
- package/extensions/antigravity-provider/antigravity/index.ts +67 -0
- package/extensions/antigravity-provider/antigravity/models.ts +184 -0
- package/extensions/antigravity-provider/antigravity/oauth.ts +499 -0
- package/extensions/antigravity-provider/index.ts +3 -0
- package/extensions/ask-user/README.md +21 -0
- package/extensions/ask-user/index.ts +417 -0
- package/extensions/background-terminals/README.md +43 -0
- package/extensions/background-terminals/index.ts +400 -0
- package/extensions/background-terminals/src/manager.ts +480 -0
- package/extensions/background-terminals/src/output-buffer.ts +110 -0
- package/extensions/background-terminals/src/presentation.ts +74 -0
- package/extensions/background-terminals/src/types.ts +47 -0
- package/extensions/background-terminals/src/ui.ts +478 -0
- package/extensions/context-usage/LICENSE +21 -0
- package/extensions/context-usage/README.md +20 -0
- package/extensions/context-usage/index.ts +2006 -0
- package/extensions/context-usage/lib/ansi.ts +60 -0
- package/extensions/context-usage/lib/boundary.ts +27 -0
- package/extensions/context-usage/lib/chat.ts +190 -0
- package/extensions/context-usage/lib/config.ts +30 -0
- package/extensions/context-usage/lib/fmt.ts +59 -0
- package/extensions/context-usage/lib/style.ts +260 -0
- package/extensions/copy-all/README.md +15 -0
- package/extensions/copy-all/index.ts +72 -0
- package/extensions/factory-provider/API_KEYS.md +17 -0
- package/extensions/factory-provider/AUTH.md +15 -0
- package/extensions/factory-provider/README.md +90 -0
- package/extensions/factory-provider/REFRESH_MODELS.md +185 -0
- package/extensions/factory-provider/UPDATING.md +97 -0
- package/extensions/factory-provider/factory/api-keys.ts +407 -0
- package/extensions/factory-provider/factory/auth.ts +132 -0
- package/extensions/factory-provider/factory/constants.ts +15 -0
- package/extensions/factory-provider/factory/dashboard.ts +363 -0
- package/extensions/factory-provider/factory/droid.ts +84 -0
- package/extensions/factory-provider/factory/limits.ts +342 -0
- package/extensions/factory-provider/factory/models.ts +283 -0
- package/extensions/factory-provider/factory/responses.ts +331 -0
- package/extensions/factory-provider/factory/warm-theme.ts +53 -0
- package/extensions/factory-provider/factory/websocket.ts +130 -0
- package/extensions/factory-provider/index.ts +204 -0
- package/extensions/factory-provider/scripts/audit-reasoning-efforts.py +84 -0
- package/extensions/firecrawl-web/README.md +31 -0
- package/extensions/firecrawl-web/auth.ts +92 -0
- package/extensions/firecrawl-web/client.ts +238 -0
- package/extensions/firecrawl-web/index.ts +96 -0
- package/extensions/firecrawl-web/output.ts +125 -0
- package/extensions/git-info/README.md +25 -0
- package/extensions/git-info/index.ts +162 -0
- package/extensions/git-info/src/git.ts +206 -0
- package/extensions/git-info/src/ui.ts +291 -0
- package/extensions/goal/README.md +44 -0
- package/extensions/goal/constants.ts +4 -0
- package/extensions/goal/extension.ts +1064 -0
- package/extensions/goal/index.ts +2 -0
- package/extensions/goal/types.ts +56 -0
- package/extensions/goal/ui.ts +334 -0
- package/extensions/orchestration/README.md +58 -0
- package/extensions/orchestration/dashboard.ts +192 -0
- package/extensions/orchestration/engine.ts +706 -0
- package/extensions/orchestration/index.ts +243 -0
- package/extensions/orchestration/prompts.ts +124 -0
- package/extensions/orchestration/protocol.ts +116 -0
- package/extensions/orchestration/settings-ui.ts +41 -0
- package/extensions/orchestration/settings.ts +59 -0
- package/extensions/orchestration/storage.ts +49 -0
- package/extensions/orchestration/types.ts +105 -0
- package/extensions/performance-status/README.md +5 -0
- package/extensions/performance-status/index.ts +171 -0
- package/extensions/pi-memory/README.md +53 -0
- package/extensions/pi-memory/index.ts +326 -0
- package/extensions/pi-memory/src/config.ts +75 -0
- package/extensions/pi-memory/src/database.ts +617 -0
- package/extensions/pi-memory/src/extraction.ts +178 -0
- package/extensions/pi-memory/src/project.ts +80 -0
- package/extensions/pi-memory/src/projection.ts +59 -0
- package/extensions/pi-memory/src/redaction.ts +60 -0
- package/extensions/pi-memory/src/retrieval.ts +30 -0
- package/extensions/pi-memory/src/service.ts +194 -0
- package/extensions/pi-memory/src/session.ts +146 -0
- package/extensions/pi-memory/src/types.ts +82 -0
- package/extensions/shared/README.md +18 -0
- package/extensions/shared/activity-status.ts +31 -0
- package/extensions/shared/child-session.ts +150 -0
- package/extensions/shared/context-utilization.ts +47 -0
- package/extensions/shared/dashboard-state.ts +99 -0
- package/extensions/shared/tool-call-timeout.ts +104 -0
- package/extensions/shared/tui-dashboard.ts +139 -0
- package/extensions/shell-shortcuts/README.md +17 -0
- package/extensions/shell-shortcuts/features/exit-alias.ts +10 -0
- package/extensions/shell-shortcuts/index.ts +6 -0
- package/extensions/subagents/README.md +89 -0
- package/extensions/subagents/index.ts +1386 -0
- package/extensions/subagents/src/backend.ts +69 -0
- package/extensions/subagents/src/backends/pi.ts +781 -0
- package/extensions/subagents/src/catalog.ts +85 -0
- package/extensions/subagents/src/config.ts +241 -0
- package/extensions/subagents/src/context.ts +57 -0
- package/extensions/subagents/src/coordinator.ts +64 -0
- package/extensions/subagents/src/domain.ts +294 -0
- package/extensions/subagents/src/format.ts +74 -0
- package/extensions/subagents/src/manager.ts +813 -0
- package/extensions/subagents/src/prompt.ts +94 -0
- package/extensions/subagents/src/result-delivery.ts +20 -0
- package/extensions/subagents/src/runtime.ts +48 -0
- package/extensions/subagents/src/ui/takeover.ts +695 -0
- package/extensions/subagents/src/ui/transcript.ts +182 -0
- package/extensions/subagents/src/worktree.ts +175 -0
- package/extensions/web-fetch/README.md +13 -0
- package/extensions/web-fetch/index.ts +441 -0
- package/package.json +117 -0
- package/skills/background-terminals/SKILL.md +42 -0
- package/skills/orchestration/SKILL.md +33 -0
- package/skills/subagents/SKILL.md +89 -0
- package/themes/ember-warm-dark.json +85 -0
|
@@ -0,0 +1,695 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Takeover UI for subagents (ported from v1, rendering from the synchronous
|
|
3
|
+
* SubagentReadModel instead of live pi sessions):
|
|
4
|
+
* - SubagentDashboard: full popup (overlay) listing all subagents.
|
|
5
|
+
* - TakeoverView: full interactive view of one subagent with an input line
|
|
6
|
+
* to steer/continue it.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type {
|
|
10
|
+
ExtensionCommandContext,
|
|
11
|
+
KeybindingsManager,
|
|
12
|
+
Theme,
|
|
13
|
+
} from "@earendil-works/pi-coding-agent";
|
|
14
|
+
import type { Component, Focusable, TUI } from "@earendil-works/pi-tui";
|
|
15
|
+
import { Input, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
16
|
+
import { formatElapsed, latestText, type SubagentSnapshot } from "../domain.ts";
|
|
17
|
+
import { contextPercent, formatContextUtilization } from "../format.ts";
|
|
18
|
+
import {
|
|
19
|
+
joinSides,
|
|
20
|
+
meter,
|
|
21
|
+
oneLine,
|
|
22
|
+
padLine,
|
|
23
|
+
stateLabel,
|
|
24
|
+
viewportSlice,
|
|
25
|
+
} from "../../../shared/tui-dashboard.ts";
|
|
26
|
+
import type { SubagentReadModel } from "../manager.ts";
|
|
27
|
+
import { buildTranscriptLines } from "./transcript.ts";
|
|
28
|
+
|
|
29
|
+
function configuredKeys(
|
|
30
|
+
keybindings: KeybindingsManager,
|
|
31
|
+
binding: Parameters<KeybindingsManager["getKeys"]>[0],
|
|
32
|
+
) {
|
|
33
|
+
return keybindings.getKeys(binding).join("/") || "unbound";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function statusGlyph(snap: SubagentSnapshot, theme: Theme): string {
|
|
37
|
+
switch (snap.status) {
|
|
38
|
+
case "running":
|
|
39
|
+
return theme.fg("warning", "■");
|
|
40
|
+
case "done":
|
|
41
|
+
return theme.fg("success", "■");
|
|
42
|
+
case "error":
|
|
43
|
+
return theme.fg("error", "■");
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function statusWord(snap: SubagentSnapshot, theme: Theme): string {
|
|
48
|
+
switch (snap.status) {
|
|
49
|
+
case "running":
|
|
50
|
+
return theme.fg("warning", "running");
|
|
51
|
+
case "done":
|
|
52
|
+
return theme.fg("success", "done");
|
|
53
|
+
case "error":
|
|
54
|
+
return theme.fg("error", "failed");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// --- Entry point ---------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
export async function openSubagentPicker(
|
|
61
|
+
ctx: ExtensionCommandContext,
|
|
62
|
+
view: SubagentReadModel,
|
|
63
|
+
) {
|
|
64
|
+
const selection: DashboardSelection = { index: 0 };
|
|
65
|
+
|
|
66
|
+
while (true) {
|
|
67
|
+
if (view.size() === 0) {
|
|
68
|
+
ctx.ui.notify("No subagents", "info");
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const picked = await ctx.ui.custom<string | null>(
|
|
73
|
+
(tui, theme, keybindings, done) =>
|
|
74
|
+
new SubagentDashboard(tui, theme, keybindings, view, selection, done),
|
|
75
|
+
{
|
|
76
|
+
overlay: true,
|
|
77
|
+
overlayOptions: { anchor: "center", width: "100%", maxHeight: "100%" },
|
|
78
|
+
},
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
if (!picked) return;
|
|
82
|
+
if (!view.get(picked)) continue;
|
|
83
|
+
|
|
84
|
+
await openSubagentTakeover(ctx, view, picked);
|
|
85
|
+
// After leaving the takeover view, fall back to the dashboard.
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function openSubagentTakeover(
|
|
90
|
+
ctx: ExtensionCommandContext,
|
|
91
|
+
view: SubagentReadModel,
|
|
92
|
+
id: string,
|
|
93
|
+
) {
|
|
94
|
+
if (!view.get(id)) {
|
|
95
|
+
ctx.ui.notify(`Subagent ${id} is no longer tracked`, "warning");
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
await ctx.ui.custom<null>(
|
|
99
|
+
(tui, theme, keybindings, done) =>
|
|
100
|
+
new TakeoverView(tui, theme, keybindings, id, view, done),
|
|
101
|
+
{
|
|
102
|
+
overlay: true,
|
|
103
|
+
overlayOptions: { anchor: "center", width: "100%", maxHeight: "100%" },
|
|
104
|
+
},
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export async function openPeerMessageViewer(
|
|
109
|
+
ctx: ExtensionCommandContext,
|
|
110
|
+
view: SubagentReadModel,
|
|
111
|
+
) {
|
|
112
|
+
if (view.peerMessages().length === 0) {
|
|
113
|
+
ctx.ui.notify("No peer messages yet", "info");
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
await ctx.ui.custom<null>(
|
|
117
|
+
(tui, theme, keybindings, done) => {
|
|
118
|
+
const unsubscribe = view.subscribe(() => tui.requestRender());
|
|
119
|
+
let closed = false;
|
|
120
|
+
const close = () => {
|
|
121
|
+
if (closed) return;
|
|
122
|
+
closed = true;
|
|
123
|
+
unsubscribe();
|
|
124
|
+
done(null);
|
|
125
|
+
};
|
|
126
|
+
return {
|
|
127
|
+
handleInput(data: string) {
|
|
128
|
+
if (
|
|
129
|
+
keybindings.matches(data, "tui.select.cancel") ||
|
|
130
|
+
keybindings.matches(data, "tui.select.confirm")
|
|
131
|
+
) close();
|
|
132
|
+
},
|
|
133
|
+
render(width: number) {
|
|
134
|
+
const messages = view.peerMessages().slice(-Math.max(1, (tui.terminal.rows || 30) - 5));
|
|
135
|
+
const lines = [theme.fg("accent", theme.bold("Peer coordination")), theme.fg("border", "─".repeat(width))];
|
|
136
|
+
for (const message of messages) {
|
|
137
|
+
const time = new Date(message.sentAt).toLocaleTimeString();
|
|
138
|
+
const state = message.status === "delivered"
|
|
139
|
+
? theme.fg("success", "✓")
|
|
140
|
+
: theme.fg("error", "✗");
|
|
141
|
+
const sender = message.senderId
|
|
142
|
+
? `${message.sender} (${message.senderId})`
|
|
143
|
+
: message.sender;
|
|
144
|
+
lines.push(truncateToWidth(`${state} ${time} ${sender} → ${message.targetTitle} (${message.targetId})`, width));
|
|
145
|
+
lines.push(truncateToWidth(` ${message.message}`, width));
|
|
146
|
+
if (message.error) lines.push(truncateToWidth(theme.fg("error", ` ${message.error}`), width));
|
|
147
|
+
}
|
|
148
|
+
lines.push(theme.fg("border", "─".repeat(width)));
|
|
149
|
+
lines.push(truncateToWidth(theme.fg("dim", "Enter/Esc close"), width));
|
|
150
|
+
return lines;
|
|
151
|
+
},
|
|
152
|
+
invalidate() {},
|
|
153
|
+
dispose() {
|
|
154
|
+
if (!closed) unsubscribe();
|
|
155
|
+
closed = true;
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
},
|
|
159
|
+
{ overlay: true, overlayOptions: { anchor: "center", width: "100%", maxHeight: "100%" } },
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// --- Dashboard (fullscreen overlay) ----------------------------------------------
|
|
164
|
+
|
|
165
|
+
export interface DashboardSelection {
|
|
166
|
+
id?: string;
|
|
167
|
+
index: number;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function reconcileDashboardSelection(
|
|
171
|
+
selection: DashboardSelection,
|
|
172
|
+
subs: ReadonlyArray<Pick<SubagentSnapshot, "id">>,
|
|
173
|
+
) {
|
|
174
|
+
const stableIndex = selection.id
|
|
175
|
+
? subs.findIndex((snap) => snap.id === selection.id)
|
|
176
|
+
: -1;
|
|
177
|
+
selection.index =
|
|
178
|
+
stableIndex >= 0
|
|
179
|
+
? stableIndex
|
|
180
|
+
: Math.min(Math.max(0, selection.index), Math.max(0, subs.length - 1));
|
|
181
|
+
selection.id = subs[selection.index]?.id;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export class SubagentDashboard implements Component {
|
|
185
|
+
private tui: TUI;
|
|
186
|
+
private theme: Theme;
|
|
187
|
+
private keybindings: KeybindingsManager;
|
|
188
|
+
private view: SubagentReadModel;
|
|
189
|
+
private selection: DashboardSelection;
|
|
190
|
+
private done: (value: string | null) => void;
|
|
191
|
+
|
|
192
|
+
private closed = false;
|
|
193
|
+
private ticker: ReturnType<typeof setInterval>;
|
|
194
|
+
private unsubChange: () => void;
|
|
195
|
+
|
|
196
|
+
constructor(
|
|
197
|
+
tui: TUI,
|
|
198
|
+
theme: Theme,
|
|
199
|
+
keybindings: KeybindingsManager,
|
|
200
|
+
view: SubagentReadModel,
|
|
201
|
+
selection: DashboardSelection,
|
|
202
|
+
done: (value: string | null) => void,
|
|
203
|
+
) {
|
|
204
|
+
this.tui = tui;
|
|
205
|
+
this.theme = theme;
|
|
206
|
+
this.keybindings = keybindings;
|
|
207
|
+
this.view = view;
|
|
208
|
+
this.selection = selection;
|
|
209
|
+
this.done = done;
|
|
210
|
+
// Elapsed times, token counts, and statuses tick along at 1Hz.
|
|
211
|
+
this.ticker = setInterval(() => this.tui.requestRender(), 1000);
|
|
212
|
+
this.ticker.unref?.();
|
|
213
|
+
this.unsubChange = view.subscribe(() => this.tui.requestRender());
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private subs(): ReadonlyArray<SubagentSnapshot> {
|
|
217
|
+
return this.view.list();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
private cleanup() {
|
|
221
|
+
if (this.closed) return false;
|
|
222
|
+
this.closed = true;
|
|
223
|
+
clearInterval(this.ticker);
|
|
224
|
+
this.unsubChange();
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private close(result: string | null) {
|
|
229
|
+
if (this.cleanup()) this.done(result);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
dispose(): void {
|
|
233
|
+
this.cleanup();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
handleInput(data: string): void {
|
|
237
|
+
const subs = this.subs();
|
|
238
|
+
reconcileDashboardSelection(this.selection, subs);
|
|
239
|
+
|
|
240
|
+
if (this.keybindings.matches(data, "tui.select.cancel")) {
|
|
241
|
+
this.close(null);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
if (this.keybindings.matches(data, "tui.select.confirm")) {
|
|
245
|
+
const snap = subs[this.selection.index];
|
|
246
|
+
if (snap) this.close(snap.id);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (this.keybindings.matches(data, "tui.select.up") || data === "k") {
|
|
250
|
+
if (subs.length > 0) {
|
|
251
|
+
this.selection.index =
|
|
252
|
+
(this.selection.index - 1 + subs.length) % subs.length;
|
|
253
|
+
this.selection.id = subs[this.selection.index]?.id;
|
|
254
|
+
this.tui.requestRender();
|
|
255
|
+
}
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (this.keybindings.matches(data, "tui.select.down") || data === "j") {
|
|
259
|
+
if (subs.length > 0) {
|
|
260
|
+
this.selection.index = (this.selection.index + 1) % subs.length;
|
|
261
|
+
this.selection.id = subs[this.selection.index]?.id;
|
|
262
|
+
this.tui.requestRender();
|
|
263
|
+
}
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (data === "x") {
|
|
267
|
+
const snap = subs[this.selection.index];
|
|
268
|
+
if (snap && snap.status === "running") this.view.requestAbort(snap.id);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
private pad(text: string, width: number): string {
|
|
274
|
+
const truncated = truncateToWidth(text, width);
|
|
275
|
+
return truncated + " ".repeat(Math.max(0, width - visibleWidth(truncated)));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
private borderSegment(width: number, title: string): string {
|
|
279
|
+
const theme = this.theme;
|
|
280
|
+
const label = title
|
|
281
|
+
? ` ${truncateToWidth(title, Math.max(0, width - 3))} `
|
|
282
|
+
: "";
|
|
283
|
+
const labelWidth = visibleWidth(label);
|
|
284
|
+
return (
|
|
285
|
+
theme.fg("border", "─") +
|
|
286
|
+
(label ? theme.fg("text", label) : "") +
|
|
287
|
+
theme.fg("border", "─".repeat(Math.max(0, width - 1 - labelWidth)))
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
render(width: number): string[] {
|
|
292
|
+
const theme = this.theme;
|
|
293
|
+
const subs = this.subs();
|
|
294
|
+
reconcileDashboardSelection(this.selection, subs);
|
|
295
|
+
|
|
296
|
+
const rows = this.tui.terminal.rows || 30;
|
|
297
|
+
const bodyHeight = Math.max(7, rows - 6);
|
|
298
|
+
const innerWidth = Math.max(1, width - 2);
|
|
299
|
+
const running = subs.filter((snap) => snap.status === "running").length;
|
|
300
|
+
const done = subs.filter((snap) => snap.status === "done").length;
|
|
301
|
+
const failed = subs.filter((snap) => snap.status === "error").length;
|
|
302
|
+
const selected = subs[this.selection.index];
|
|
303
|
+
|
|
304
|
+
const headerLeft = theme.fg("accent", theme.bold("Subagent operations"));
|
|
305
|
+
const counts = [
|
|
306
|
+
running > 0 ? stateLabel(theme, "warning", `${running} running`) : "",
|
|
307
|
+
done > 0 ? stateLabel(theme, "success", `${done} done`) : "",
|
|
308
|
+
failed > 0 ? stateLabel(theme, "error", `${failed} failed`) : "",
|
|
309
|
+
].filter(Boolean).join(theme.fg("dim", " · "));
|
|
310
|
+
const lines = [joinSides(` ${headerLeft}`, `${counts || theme.fg("muted", "idle")} `, width)];
|
|
311
|
+
|
|
312
|
+
lines.push(
|
|
313
|
+
theme.fg("border", "╭") +
|
|
314
|
+
this.borderSegment(innerWidth, `${subs.length} agent${subs.length === 1 ? "" : "s"} · live control`) +
|
|
315
|
+
theme.fg("border", "╮"),
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
const divider = theme.fg("border", "│");
|
|
319
|
+
if (width >= 100 && selected) {
|
|
320
|
+
const leftWidth = Math.max(38, Math.floor((innerWidth - 1) * 0.48));
|
|
321
|
+
const rightWidth = Math.max(20, innerWidth - leftWidth - 1);
|
|
322
|
+
const list = this.renderRows(subs, leftWidth, bodyHeight, true);
|
|
323
|
+
const detail = this.renderSelected(selected, rightWidth, bodyHeight);
|
|
324
|
+
for (let row = 0; row < bodyHeight; row++) {
|
|
325
|
+
lines.push(
|
|
326
|
+
divider +
|
|
327
|
+
padLine(list[row] ?? "", leftWidth) +
|
|
328
|
+
theme.fg("borderMuted", "│") +
|
|
329
|
+
padLine(detail[row] ?? "", rightWidth) +
|
|
330
|
+
divider,
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
} else {
|
|
334
|
+
const list = this.renderRows(subs, innerWidth, bodyHeight, false);
|
|
335
|
+
for (let row = 0; row < bodyHeight; row++) {
|
|
336
|
+
lines.push(divider + padLine(list[row] ?? "", innerWidth) + divider);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
lines.push(
|
|
341
|
+
theme.fg("border", "╰") +
|
|
342
|
+
theme.fg("border", "─".repeat(innerWidth)) +
|
|
343
|
+
theme.fg("border", "╯"),
|
|
344
|
+
);
|
|
345
|
+
lines.push(
|
|
346
|
+
truncateToWidth(
|
|
347
|
+
theme.fg(
|
|
348
|
+
"dim",
|
|
349
|
+
` ${configuredKeys(this.keybindings, "tui.select.up")}/${configuredKeys(this.keybindings, "tui.select.down")}/jk select · ${configuredKeys(this.keybindings, "tui.select.confirm")} take over · x abort running · ${configuredKeys(this.keybindings, "tui.select.cancel")} close`,
|
|
350
|
+
),
|
|
351
|
+
width,
|
|
352
|
+
),
|
|
353
|
+
);
|
|
354
|
+
return lines;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
private renderRows(
|
|
358
|
+
subs: ReadonlyArray<SubagentSnapshot>,
|
|
359
|
+
width: number,
|
|
360
|
+
height: number,
|
|
361
|
+
compact: boolean,
|
|
362
|
+
): string[] {
|
|
363
|
+
const theme = this.theme;
|
|
364
|
+
const out: string[] = [];
|
|
365
|
+
const viewport = viewportSlice(subs, this.selection.index, height);
|
|
366
|
+
|
|
367
|
+
for (let row = 0; row < viewport.items.length; row++) {
|
|
368
|
+
const snap = viewport.items[row];
|
|
369
|
+
const index = viewport.start + row;
|
|
370
|
+
const selected = index === this.selection.index;
|
|
371
|
+
const marker = selected ? theme.fg("accent", "❯") : " ";
|
|
372
|
+
const title = selected
|
|
373
|
+
? theme.fg("accent", oneLine(snap.title))
|
|
374
|
+
: theme.fg("text", oneLine(snap.title));
|
|
375
|
+
const left = ` ${marker} ${statusGlyph(snap, theme)} ${title} ${theme.fg("dim", snap.id)}`;
|
|
376
|
+
const utilization = formatContextUtilization(snap.usage);
|
|
377
|
+
const rightParts = compact
|
|
378
|
+
? [theme.fg("muted", formatElapsed(snap)), statusWord(snap, theme)]
|
|
379
|
+
: [
|
|
380
|
+
theme.fg("muted", snap.meta.modelLabel ?? "?"),
|
|
381
|
+
...(utilization ? [theme.fg("muted", utilization)] : []),
|
|
382
|
+
theme.fg("muted", formatElapsed(snap)),
|
|
383
|
+
statusWord(snap, theme),
|
|
384
|
+
];
|
|
385
|
+
const line = joinSides(left, `${rightParts.join(theme.fg("dim", " · "))} `, width);
|
|
386
|
+
out.push(selected ? theme.bg("selectedBg", padLine(line, width)) : line);
|
|
387
|
+
}
|
|
388
|
+
if (viewport.start > 0 && out.length > 0) {
|
|
389
|
+
out[0] = truncateToWidth(theme.fg("dim", ` ↑ ${viewport.start} more`), width);
|
|
390
|
+
}
|
|
391
|
+
const below = subs.length - viewport.start - viewport.items.length;
|
|
392
|
+
if (below > 0 && out.length > 0) {
|
|
393
|
+
out[out.length - 1] = truncateToWidth(theme.fg("dim", ` ↓ ${below} more`), width);
|
|
394
|
+
}
|
|
395
|
+
return out;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
private renderSelected(
|
|
399
|
+
snap: SubagentSnapshot,
|
|
400
|
+
width: number,
|
|
401
|
+
height: number,
|
|
402
|
+
): string[] {
|
|
403
|
+
const theme = this.theme;
|
|
404
|
+
const state = snap.status === "running" ? "warning" : snap.status === "done" ? "success" : "error";
|
|
405
|
+
const percent = contextPercent(snap.usage);
|
|
406
|
+
const activeTools = snap.liveTools.filter((tool) => !tool.done);
|
|
407
|
+
const lines: string[] = [
|
|
408
|
+
` ${stateLabel(theme, state, snap.status === "error" ? "failed" : snap.status)} ${theme.fg("accent", theme.bold(oneLine(snap.title)))}`,
|
|
409
|
+
` ${theme.fg("dim", snap.id)} ${theme.fg("muted", `· ${formatElapsed(snap)} · ${snap.turns} turn${snap.turns === 1 ? "" : "s"}`)}`,
|
|
410
|
+
"",
|
|
411
|
+
` ${theme.fg("muted", "MODEL")} ${theme.fg("text", snap.meta.modelLabel ?? "inherit")}`,
|
|
412
|
+
` ${theme.fg("muted", "PROFILE")} ${theme.fg("text", snap.meta.agentType ?? "general-purpose")}${snap.meta.persona ? theme.fg("dim", ` · ${snap.meta.persona}`) : ""}`,
|
|
413
|
+
` ${theme.fg("muted", "ACCESS")} ${theme.fg("text", snap.meta.capability ?? "all")}${snap.meta.isolation === "worktree" ? theme.fg("warning", " · worktree") : ""}`,
|
|
414
|
+
` ${theme.fg("muted", "SOURCE")} ${theme.fg("text", snap.meta.origin === "btw" ? "user aside" : "model delegation")}`,
|
|
415
|
+
];
|
|
416
|
+
if (percent !== undefined) {
|
|
417
|
+
lines.push("", ` ${theme.fg("muted", "CONTEXT")} ${meter(theme, percent, 100, Math.max(12, width - 12), percent >= 85 ? "warning" : "active")}`);
|
|
418
|
+
}
|
|
419
|
+
lines.push("");
|
|
420
|
+
if (activeTools.length > 0) {
|
|
421
|
+
lines.push(` ${theme.fg("muted", "NOW")} ${theme.fg("toolTitle", oneLine(activeTools[0]!.name))}${activeTools.length > 1 ? theme.fg("dim", ` +${activeTools.length - 1}`) : ""}`);
|
|
422
|
+
} else if (snap.status === "running") {
|
|
423
|
+
lines.push(` ${theme.fg("muted", "NOW")} ${theme.fg("warning", "thinking / responding")}`);
|
|
424
|
+
} else {
|
|
425
|
+
lines.push(` ${theme.fg("muted", "NOW")} ${theme.fg("dim", "settled")}`);
|
|
426
|
+
}
|
|
427
|
+
if (snap.queued.length > 0) {
|
|
428
|
+
lines.push(` ${theme.fg("muted", "QUEUE")} ${theme.fg("warning", `${snap.queued.length} message${snap.queued.length === 1 ? "" : "s"}`)}`);
|
|
429
|
+
}
|
|
430
|
+
lines.push("", ` ${theme.fg("muted", "LATEST")}`);
|
|
431
|
+
const latest = oneLine(latestText(snap) || snap.errorText || "No output yet.");
|
|
432
|
+
const preview = wrapTextWithAnsi(latest, Math.max(10, width - 2)).slice(0, Math.max(1, height - lines.length - 3));
|
|
433
|
+
lines.push(...preview.map((line) => ` ${theme.fg(snap.errorText ? "error" : "toolOutput", line)}`));
|
|
434
|
+
lines.push("", truncateToWidth(` ${theme.fg("dim", snap.cwd)}`, width));
|
|
435
|
+
return lines.slice(0, height);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
invalidate(): void {}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// --- Takeover view ------------------------------------------------------------
|
|
442
|
+
|
|
443
|
+
const TRANSCRIPT_SCROLL_STEP = 6;
|
|
444
|
+
|
|
445
|
+
export class TakeoverView implements Component, Focusable {
|
|
446
|
+
private tui: TUI;
|
|
447
|
+
private theme: Theme;
|
|
448
|
+
private keybindings: KeybindingsManager;
|
|
449
|
+
private id: string;
|
|
450
|
+
private view: SubagentReadModel;
|
|
451
|
+
private done: (value: null) => void;
|
|
452
|
+
|
|
453
|
+
private input = new Input();
|
|
454
|
+
/** Scroll offset in lines from the bottom of the transcript. 0 = pinned to bottom. */
|
|
455
|
+
private scrollOffset = 0;
|
|
456
|
+
private unsubscribe: () => void;
|
|
457
|
+
private renderTimer?: ReturnType<typeof setTimeout>;
|
|
458
|
+
private ticker: ReturnType<typeof setInterval>;
|
|
459
|
+
private transcriptCacheKey = "";
|
|
460
|
+
private transcriptCache: string[] = [];
|
|
461
|
+
private closed = false;
|
|
462
|
+
|
|
463
|
+
private _focused = false;
|
|
464
|
+
get focused(): boolean {
|
|
465
|
+
return this._focused;
|
|
466
|
+
}
|
|
467
|
+
set focused(value: boolean) {
|
|
468
|
+
this._focused = value;
|
|
469
|
+
this.input.focused = value;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
constructor(
|
|
473
|
+
tui: TUI,
|
|
474
|
+
theme: Theme,
|
|
475
|
+
keybindings: KeybindingsManager,
|
|
476
|
+
id: string,
|
|
477
|
+
view: SubagentReadModel,
|
|
478
|
+
done: (value: null) => void,
|
|
479
|
+
) {
|
|
480
|
+
this.tui = tui;
|
|
481
|
+
this.theme = theme;
|
|
482
|
+
this.keybindings = keybindings;
|
|
483
|
+
this.id = id;
|
|
484
|
+
this.view = view;
|
|
485
|
+
this.done = done;
|
|
486
|
+
this.unsubscribe = view.subscribeTo(id, () => this.scheduleRender());
|
|
487
|
+
// Elapsed time in the header ticks along at 1Hz.
|
|
488
|
+
this.ticker = setInterval(() => this.tui.requestRender(), 1000);
|
|
489
|
+
this.ticker.unref?.();
|
|
490
|
+
this.input.onSubmit = (value: string) => {
|
|
491
|
+
const text = value.trim();
|
|
492
|
+
if (!text) return;
|
|
493
|
+
this.input.setValue("");
|
|
494
|
+
this.view.requestSend(this.id, text);
|
|
495
|
+
this.scrollOffset = 0;
|
|
496
|
+
this.tui.requestRender();
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
private snap(): SubagentSnapshot | undefined {
|
|
501
|
+
return this.view.get(this.id);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
private scheduleRender() {
|
|
505
|
+
if (this.renderTimer) return;
|
|
506
|
+
// Streaming can emit an event per token. Limit terminal repaints so this
|
|
507
|
+
// view cannot starve input handling or make the child look frozen.
|
|
508
|
+
this.renderTimer = setTimeout(() => {
|
|
509
|
+
this.renderTimer = undefined;
|
|
510
|
+
if (!this.closed) this.tui.requestRender();
|
|
511
|
+
}, 50);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
private cleanup() {
|
|
515
|
+
if (this.closed) return false;
|
|
516
|
+
this.closed = true;
|
|
517
|
+
this.unsubscribe();
|
|
518
|
+
clearInterval(this.ticker);
|
|
519
|
+
if (this.renderTimer) clearTimeout(this.renderTimer);
|
|
520
|
+
this.renderTimer = undefined;
|
|
521
|
+
return true;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
private close() {
|
|
525
|
+
if (this.cleanup()) this.done(null);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
dispose(): void {
|
|
529
|
+
this.cleanup();
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
handleInput(data: string): void {
|
|
533
|
+
if (this.keybindings.matches(data, "app.clear")) {
|
|
534
|
+
const snap = this.snap();
|
|
535
|
+
if (snap?.status === "running") this.view.requestAbort(this.id);
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
if (
|
|
539
|
+
this.keybindings.matches(data, "app.interrupt") ||
|
|
540
|
+
this.keybindings.matches(data, "tui.select.cancel")
|
|
541
|
+
) {
|
|
542
|
+
this.close();
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
if (this.keybindings.matches(data, "tui.editor.cursorUp")) {
|
|
546
|
+
this.scrollOffset += TRANSCRIPT_SCROLL_STEP;
|
|
547
|
+
this.tui.requestRender();
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
if (this.keybindings.matches(data, "tui.editor.cursorDown")) {
|
|
551
|
+
this.scrollOffset = Math.max(
|
|
552
|
+
0,
|
|
553
|
+
this.scrollOffset - TRANSCRIPT_SCROLL_STEP,
|
|
554
|
+
);
|
|
555
|
+
this.tui.requestRender();
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
if (this.keybindings.matches(data, "tui.editor.pageUp")) {
|
|
559
|
+
this.scrollOffset += this.viewportHeight();
|
|
560
|
+
this.tui.requestRender();
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
if (this.keybindings.matches(data, "tui.editor.pageDown")) {
|
|
564
|
+
this.scrollOffset = Math.max(
|
|
565
|
+
0,
|
|
566
|
+
this.scrollOffset - this.viewportHeight(),
|
|
567
|
+
);
|
|
568
|
+
this.tui.requestRender();
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
this.input.handleInput(data);
|
|
572
|
+
this.tui.requestRender();
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
private transcriptLines(snap: SubagentSnapshot, width: number): string[] {
|
|
576
|
+
const live = snap.liveAssistant;
|
|
577
|
+
const toolVersion = snap.liveTools
|
|
578
|
+
.map((tool) => `${tool.toolId}:${tool.done ? 1 : 0}:${tool.isError ? 1 : 0}:${tool.outputPreview ?? ""}`)
|
|
579
|
+
.join("|");
|
|
580
|
+
const key = [
|
|
581
|
+
width,
|
|
582
|
+
snap.transcript.length,
|
|
583
|
+
live?.text.length ?? 0,
|
|
584
|
+
live?.thinking.length ?? 0,
|
|
585
|
+
toolVersion,
|
|
586
|
+
live?.text.slice(-80) ?? "",
|
|
587
|
+
live?.thinking.slice(-80) ?? "",
|
|
588
|
+
snap.queued.map((message) => `${message.kind}:${message.text}`).join("|"),
|
|
589
|
+
snap.status,
|
|
590
|
+
].join(":");
|
|
591
|
+
if (key !== this.transcriptCacheKey) {
|
|
592
|
+
this.transcriptCacheKey = key;
|
|
593
|
+
this.transcriptCache = buildTranscriptLines(snap, width, this.theme);
|
|
594
|
+
}
|
|
595
|
+
return this.transcriptCache;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
private viewportHeight(): number {
|
|
599
|
+
const rows = this.tui.terminal.rows || 30;
|
|
600
|
+
// Header, metadata, input, borders, and hints consume eight rows.
|
|
601
|
+
return Math.max(6, rows - 9);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
render(width: number): string[] {
|
|
605
|
+
const theme = this.theme;
|
|
606
|
+
const border = theme.fg("borderAccent", "─".repeat(Math.max(1, width)));
|
|
607
|
+
const lines: string[] = [];
|
|
608
|
+
const snap = this.snap();
|
|
609
|
+
|
|
610
|
+
if (!snap) {
|
|
611
|
+
lines.push(border);
|
|
612
|
+
lines.push(theme.fg("dim", `${this.id} is no longer tracked`));
|
|
613
|
+
lines.push(border);
|
|
614
|
+
return lines;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
lines.push(border);
|
|
618
|
+
const utilization = formatContextUtilization(snap.usage);
|
|
619
|
+
const header =
|
|
620
|
+
`${statusGlyph(snap, theme)} ` +
|
|
621
|
+
theme.fg("accent", theme.bold(`${snap.id} · ${snap.title}`)) +
|
|
622
|
+
theme.fg("muted", ` · ${snap.status} · ${formatElapsed(snap)}`) +
|
|
623
|
+
theme.fg("dim", ` · ${snap.backend}: ${snap.meta.modelLabel ?? "?"}`) +
|
|
624
|
+
(utilization ? theme.fg("dim", ` · ${utilization}`) : "");
|
|
625
|
+
lines.push(truncateToWidth(header, width));
|
|
626
|
+
const percent = contextPercent(snap.usage);
|
|
627
|
+
const activeTools = snap.liveTools.filter((tool) => !tool.done);
|
|
628
|
+
const metaLeft =
|
|
629
|
+
` ${theme.fg("muted", "MODEL")} ${theme.fg("text", snap.meta.modelLabel ?? "inherit")}` +
|
|
630
|
+
theme.fg("dim", ` · ${snap.meta.capability ?? "all"}${snap.meta.isolation === "worktree" ? " · worktree" : ""}`);
|
|
631
|
+
const metaRight = percent === undefined
|
|
632
|
+
? activeTools.length > 0
|
|
633
|
+
? theme.fg("toolTitle", oneLine(activeTools[0]!.name))
|
|
634
|
+
: ""
|
|
635
|
+
: `${meter(theme, percent, 100, Math.min(24, Math.max(10, Math.floor(width / 4))), percent >= 85 ? "warning" : "active")}`;
|
|
636
|
+
lines.push(joinSides(metaLeft, `${metaRight} `, width));
|
|
637
|
+
lines.push(border);
|
|
638
|
+
|
|
639
|
+
// Fixed-height transcript viewport. Error and scroll status consume rows
|
|
640
|
+
// inside the viewport so streaming/scrolling never changes overlay height.
|
|
641
|
+
const transcript = this.transcriptLines(snap, width);
|
|
642
|
+
const viewport = this.viewportHeight();
|
|
643
|
+
const errorRows = snap.errorText ? 1 : 0;
|
|
644
|
+
const scrollRows = this.scrollOffset > 0 ? 1 : 0;
|
|
645
|
+
const transcriptCapacity = Math.max(1, viewport - errorRows - scrollRows);
|
|
646
|
+
const maxOffset = Math.max(0, transcript.length - transcriptCapacity);
|
|
647
|
+
if (this.scrollOffset > maxOffset) this.scrollOffset = maxOffset;
|
|
648
|
+
|
|
649
|
+
const body: string[] = [];
|
|
650
|
+
if (snap.errorText) {
|
|
651
|
+
body.push(
|
|
652
|
+
truncateToWidth(theme.fg("error", `error: ${snap.errorText}`), width),
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
const capacity = Math.max(
|
|
657
|
+
1,
|
|
658
|
+
viewport - body.length - (this.scrollOffset > 0 ? 1 : 0),
|
|
659
|
+
);
|
|
660
|
+
const end = transcript.length - this.scrollOffset;
|
|
661
|
+
const visible = transcript.slice(Math.max(0, end - capacity), end);
|
|
662
|
+
if (visible.length === 0) body.push(theme.fg("dim", "(no output yet)"));
|
|
663
|
+
else body.push(...visible);
|
|
664
|
+
|
|
665
|
+
if (this.scrollOffset > 0) {
|
|
666
|
+
body.push(
|
|
667
|
+
truncateToWidth(
|
|
668
|
+
theme.fg("dim", `... ${this.scrollOffset} lines below · ↓/pgdn`),
|
|
669
|
+
width,
|
|
670
|
+
),
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
while (body.length < viewport) body.push("");
|
|
674
|
+
lines.push(...body.slice(0, viewport));
|
|
675
|
+
|
|
676
|
+
lines.push(border);
|
|
677
|
+
lines.push(...this.input.render(width));
|
|
678
|
+
lines.push(
|
|
679
|
+
truncateToWidth(
|
|
680
|
+
theme.fg(
|
|
681
|
+
"dim",
|
|
682
|
+
`${configuredKeys(this.keybindings, "tui.input.submit")} send · ${configuredKeys(this.keybindings, "app.interrupt")} back · ${configuredKeys(this.keybindings, "app.clear")} abort run · ${configuredKeys(this.keybindings, "tui.editor.cursorUp")}/${configuredKeys(this.keybindings, "tui.editor.cursorDown")} scroll · ${configuredKeys(this.keybindings, "tui.editor.pageUp")}/${configuredKeys(this.keybindings, "tui.editor.pageDown")} page`,
|
|
683
|
+
),
|
|
684
|
+
width,
|
|
685
|
+
),
|
|
686
|
+
);
|
|
687
|
+
lines.push(border);
|
|
688
|
+
return lines;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
invalidate(): void {
|
|
692
|
+
this.transcriptCacheKey = "";
|
|
693
|
+
this.input.invalidate();
|
|
694
|
+
}
|
|
695
|
+
}
|