@esso0428/pi-subagents 0.15.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/CHANGELOG.md +638 -0
- package/CONTRIBUTING.md +68 -0
- package/LICENSE +21 -0
- package/README.md +745 -0
- package/SECURITY.md +95 -0
- package/dist/agent-manager.d.ts +144 -0
- package/dist/agent-manager.js +542 -0
- package/dist/agent-runner.d.ts +212 -0
- package/dist/agent-runner.js +850 -0
- package/dist/agent-types.d.ts +67 -0
- package/dist/agent-types.js +168 -0
- package/dist/context.d.ts +12 -0
- package/dist/context.js +56 -0
- package/dist/cross-extension-rpc.d.ts +46 -0
- package/dist/cross-extension-rpc.js +76 -0
- package/dist/custom-agents.d.ts +17 -0
- package/dist/custom-agents.js +156 -0
- package/dist/default-agents.d.ts +7 -0
- package/dist/default-agents.js +122 -0
- package/dist/enabled-models.d.ts +49 -0
- package/dist/enabled-models.js +145 -0
- package/dist/env.d.ts +6 -0
- package/dist/env.js +28 -0
- package/dist/group-join.d.ts +32 -0
- package/dist/group-join.js +116 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +2209 -0
- package/dist/invocation-config.d.ts +22 -0
- package/dist/invocation-config.js +15 -0
- package/dist/memory.d.ts +53 -0
- package/dist/memory.js +165 -0
- package/dist/model-resolver.d.ts +19 -0
- package/dist/model-resolver.js +80 -0
- package/dist/nico-overrides.d.ts +53 -0
- package/dist/nico-overrides.js +169 -0
- package/dist/output-file.d.ts +24 -0
- package/dist/output-file.js +101 -0
- package/dist/prompts.d.ts +32 -0
- package/dist/prompts.js +73 -0
- package/dist/schedule-store.d.ts +38 -0
- package/dist/schedule-store.js +155 -0
- package/dist/schedule.d.ts +109 -0
- package/dist/schedule.js +338 -0
- package/dist/settings.d.ts +141 -0
- package/dist/settings.js +162 -0
- package/dist/skill-loader.d.ts +24 -0
- package/dist/skill-loader.js +93 -0
- package/dist/status-note.d.ts +13 -0
- package/dist/status-note.js +24 -0
- package/dist/types.d.ts +197 -0
- package/dist/types.js +5 -0
- package/dist/ui/agent-widget.d.ts +160 -0
- package/dist/ui/agent-widget.js +484 -0
- package/dist/ui/conversation-viewer.d.ts +57 -0
- package/dist/ui/conversation-viewer.js +354 -0
- package/dist/ui/fleet-list.d.ts +106 -0
- package/dist/ui/fleet-list.js +345 -0
- package/dist/ui/schedule-menu.d.ts +16 -0
- package/dist/ui/schedule-menu.js +95 -0
- package/dist/ui/viewer-keys.d.ts +20 -0
- package/dist/ui/viewer-keys.js +17 -0
- package/dist/usage.d.ts +50 -0
- package/dist/usage.js +49 -0
- package/dist/worktree.d.ts +45 -0
- package/dist/worktree.js +160 -0
- package/examples/agent-tool-description.md +42 -0
- package/package.json +56 -0
- package/src/agent-manager.ts +631 -0
- package/src/agent-runner.ts +1014 -0
- package/src/agent-types.ts +202 -0
- package/src/context.ts +58 -0
- package/src/cross-extension-rpc.ts +122 -0
- package/src/custom-agents.ts +167 -0
- package/src/default-agents.ts +126 -0
- package/src/enabled-models.ts +180 -0
- package/src/env.ts +33 -0
- package/src/group-join.ts +141 -0
- package/src/index.ts +2400 -0
- package/src/invocation-config.ts +40 -0
- package/src/memory.ts +179 -0
- package/src/model-resolver.ts +100 -0
- package/src/nico-overrides.ts +235 -0
- package/src/output-file.ts +110 -0
- package/src/prompts.ts +99 -0
- package/src/schedule-store.ts +153 -0
- package/src/schedule.ts +365 -0
- package/src/settings.ts +288 -0
- package/src/skill-loader.ts +102 -0
- package/src/status-note.ts +25 -0
- package/src/types.ts +208 -0
- package/src/ui/agent-widget.ts +566 -0
- package/src/ui/conversation-viewer.ts +362 -0
- package/src/ui/fleet-list.ts +380 -0
- package/src/ui/schedule-menu.ts +104 -0
- package/src/ui/viewer-keys.ts +39 -0
- package/src/usage.ts +60 -0
- package/src/worktree.ts +191 -0
- package/vitest.config.ts +18 -0
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* conversation-viewer.ts — Live conversation overlay for viewing agent sessions.
|
|
3
|
+
*
|
|
4
|
+
* Displays a scrollable, live-updating view of an agent's conversation.
|
|
5
|
+
* Subscribes to session events for real-time streaming updates.
|
|
6
|
+
*/
|
|
7
|
+
import { Input, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
8
|
+
import { extractText } from "../context.js";
|
|
9
|
+
import { getLifetimeTotal, getSessionContextPercent } from "../usage.js";
|
|
10
|
+
import { buildInvocationTags, describeActivity, fgPreservingNestedStyles, formatDuration, formatSessionTokens, getDisplayName, getPromptModeLabel } from "./agent-widget.js";
|
|
11
|
+
import { createViewerKeys } from "./viewer-keys.js";
|
|
12
|
+
/** Base lines consumed by chrome: top border + header + header sep + footer sep + footer + bottom border. */
|
|
13
|
+
const CHROME_LINES_BASE = 6;
|
|
14
|
+
const MIN_VIEWPORT = 3;
|
|
15
|
+
/** Height ceiling shared by the overlay's `maxHeight` and the viewer's internal viewport cap. */
|
|
16
|
+
export const VIEWPORT_HEIGHT_PCT = 70;
|
|
17
|
+
export class ConversationViewer {
|
|
18
|
+
tui;
|
|
19
|
+
session;
|
|
20
|
+
record;
|
|
21
|
+
activity;
|
|
22
|
+
theme;
|
|
23
|
+
done;
|
|
24
|
+
onStop;
|
|
25
|
+
onSteer;
|
|
26
|
+
scrollOffset = 0;
|
|
27
|
+
autoScroll = true;
|
|
28
|
+
unsubscribe;
|
|
29
|
+
lastInnerW = 0;
|
|
30
|
+
closed = false;
|
|
31
|
+
/** Two-press confirm guard for the stop key, so a stray key can't kill the agent. */
|
|
32
|
+
stopArmed = false;
|
|
33
|
+
keys;
|
|
34
|
+
/** Steering composer — present while the user is typing a message to the agent. */
|
|
35
|
+
composer;
|
|
36
|
+
constructor(tui, session, record, activity, theme, done,
|
|
37
|
+
/** Abort the agent shown here. Omitted → no stop affordance (e.g. read-only history). */
|
|
38
|
+
onStop,
|
|
39
|
+
/** User keybindings from `ctx.ui.custom()`. Omitted → hardcoded defaults. */
|
|
40
|
+
keybindings,
|
|
41
|
+
/** Send a steering message to the agent. Omitted → no compose affordance. */
|
|
42
|
+
onSteer) {
|
|
43
|
+
this.tui = tui;
|
|
44
|
+
this.session = session;
|
|
45
|
+
this.record = record;
|
|
46
|
+
this.activity = activity;
|
|
47
|
+
this.theme = theme;
|
|
48
|
+
this.done = done;
|
|
49
|
+
this.onStop = onStop;
|
|
50
|
+
this.onSteer = onSteer;
|
|
51
|
+
this.keys = createViewerKeys(keybindings);
|
|
52
|
+
this.unsubscribe = session.subscribe(() => {
|
|
53
|
+
if (this.closed)
|
|
54
|
+
return;
|
|
55
|
+
this.tui.requestRender();
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
handleInput(data) {
|
|
59
|
+
// While composing a steer message, the input owns all keys (Enter sends,
|
|
60
|
+
// Esc cancels — both wired in openComposer()). Editing keys flow through.
|
|
61
|
+
if (this.composer) {
|
|
62
|
+
this.composer.handleInput(data);
|
|
63
|
+
this.tui.requestRender();
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (matchesKey(data, "escape") || matchesKey(data, "q")) {
|
|
67
|
+
this.closed = true;
|
|
68
|
+
this.done(undefined);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
// Enter opens the steering composer (only while the agent can still be
|
|
72
|
+
// steered) — then type + Enter sends, Esc or an empty submit returns. When
|
|
73
|
+
// not steerable, fall through so the key still disarms a pending stop.
|
|
74
|
+
if (matchesKey(data, "enter") && this.canSteer()) {
|
|
75
|
+
this.stopArmed = false;
|
|
76
|
+
this.openComposer();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
// Stop/abort the agent (only while it can still be stopped). Two-press:
|
|
80
|
+
// first "x" arms, second confirms — any other key disarms.
|
|
81
|
+
if (matchesKey(data, "x")) {
|
|
82
|
+
if (this.isStoppable()) {
|
|
83
|
+
if (this.stopArmed) {
|
|
84
|
+
this.stopArmed = false;
|
|
85
|
+
this.onStop?.();
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
this.stopArmed = true;
|
|
89
|
+
}
|
|
90
|
+
this.tui.requestRender();
|
|
91
|
+
}
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (this.stopArmed)
|
|
95
|
+
this.stopArmed = false;
|
|
96
|
+
const totalLines = this.buildContentLines(this.lastInnerW).length;
|
|
97
|
+
const viewportHeight = this.viewportHeight();
|
|
98
|
+
const maxScroll = Math.max(0, totalLines - viewportHeight);
|
|
99
|
+
if (this.keys.scrollUp(data)) {
|
|
100
|
+
this.scrollOffset = Math.max(0, this.scrollOffset - 1);
|
|
101
|
+
this.autoScroll = this.scrollOffset >= maxScroll;
|
|
102
|
+
}
|
|
103
|
+
else if (this.keys.scrollDown(data)) {
|
|
104
|
+
this.scrollOffset = Math.min(maxScroll, this.scrollOffset + 1);
|
|
105
|
+
this.autoScroll = this.scrollOffset >= maxScroll;
|
|
106
|
+
}
|
|
107
|
+
else if (this.keys.pageUp(data)) {
|
|
108
|
+
this.scrollOffset = Math.max(0, this.scrollOffset - viewportHeight);
|
|
109
|
+
this.autoScroll = false;
|
|
110
|
+
}
|
|
111
|
+
else if (this.keys.pageDown(data)) {
|
|
112
|
+
this.scrollOffset = Math.min(maxScroll, this.scrollOffset + viewportHeight);
|
|
113
|
+
this.autoScroll = this.scrollOffset >= maxScroll;
|
|
114
|
+
}
|
|
115
|
+
else if (matchesKey(data, "home")) {
|
|
116
|
+
this.scrollOffset = 0;
|
|
117
|
+
this.autoScroll = false;
|
|
118
|
+
}
|
|
119
|
+
else if (matchesKey(data, "end")) {
|
|
120
|
+
this.scrollOffset = maxScroll;
|
|
121
|
+
this.autoScroll = true;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
render(width) {
|
|
125
|
+
if (width < 6)
|
|
126
|
+
return []; // too narrow for any meaningful rendering
|
|
127
|
+
const th = this.theme;
|
|
128
|
+
const innerW = width - 4; // border + padding
|
|
129
|
+
this.lastInnerW = innerW;
|
|
130
|
+
const lines = [];
|
|
131
|
+
const pad = (s, len) => {
|
|
132
|
+
const vis = visibleWidth(s);
|
|
133
|
+
return s + " ".repeat(Math.max(0, len - vis));
|
|
134
|
+
};
|
|
135
|
+
const row = (content) => th.fg("border", "│") + " " + truncateToWidth(pad(content, innerW), innerW, "...", true) + " " + th.fg("border", "│");
|
|
136
|
+
const hrTop = th.fg("border", `╭${"─".repeat(width - 2)}╮`);
|
|
137
|
+
const hrBot = th.fg("border", `╰${"─".repeat(width - 2)}╯`);
|
|
138
|
+
const hrMid = row(th.fg("dim", "─".repeat(innerW)));
|
|
139
|
+
// Header
|
|
140
|
+
lines.push(hrTop);
|
|
141
|
+
const name = getDisplayName(this.record.type);
|
|
142
|
+
const modeLabel = getPromptModeLabel(this.record.type);
|
|
143
|
+
const modeTag = modeLabel ? ` ${th.fg("dim", `(${modeLabel})`)}` : "";
|
|
144
|
+
const statusIcon = this.record.status === "running"
|
|
145
|
+
? th.fg("accent", "●")
|
|
146
|
+
: this.record.status === "completed"
|
|
147
|
+
? th.fg("success", "✓")
|
|
148
|
+
: this.record.status === "error"
|
|
149
|
+
? th.fg("error", "✗")
|
|
150
|
+
: th.fg("dim", "○");
|
|
151
|
+
const duration = formatDuration(this.record.startedAt, this.record.completedAt);
|
|
152
|
+
const headerParts = [duration];
|
|
153
|
+
const toolUses = this.activity?.toolUses ?? this.record.toolUses;
|
|
154
|
+
if (toolUses > 0)
|
|
155
|
+
headerParts.unshift(`${toolUses} tool${toolUses === 1 ? "" : "s"}`);
|
|
156
|
+
const tokens = getLifetimeTotal(this.activity?.lifetimeUsage);
|
|
157
|
+
if (tokens > 0) {
|
|
158
|
+
const percent = getSessionContextPercent(this.activity?.session);
|
|
159
|
+
headerParts.push(formatSessionTokens(tokens, percent, th, this.record.compactionCount));
|
|
160
|
+
}
|
|
161
|
+
lines.push(row(`${statusIcon} ${th.bold(name)}${modeTag} ${th.fg("muted", this.record.description)} ${th.fg("dim", "·")} ${fgPreservingNestedStyles(th, "dim", headerParts.join(" · "))}`));
|
|
162
|
+
const invocationLine = this.invocationLine();
|
|
163
|
+
if (invocationLine)
|
|
164
|
+
lines.push(row(invocationLine));
|
|
165
|
+
lines.push(hrMid);
|
|
166
|
+
// Content area — rebuild every render (live data, no cache needed)
|
|
167
|
+
const contentLines = this.buildContentLines(innerW);
|
|
168
|
+
const viewportHeight = this.viewportHeight();
|
|
169
|
+
const maxScroll = Math.max(0, contentLines.length - viewportHeight);
|
|
170
|
+
if (this.autoScroll) {
|
|
171
|
+
this.scrollOffset = maxScroll;
|
|
172
|
+
}
|
|
173
|
+
const visibleStart = Math.min(this.scrollOffset, maxScroll);
|
|
174
|
+
const visible = contentLines.slice(visibleStart, visibleStart + viewportHeight);
|
|
175
|
+
for (let i = 0; i < viewportHeight; i++) {
|
|
176
|
+
lines.push(row(visible[i] ?? ""));
|
|
177
|
+
}
|
|
178
|
+
// Footer
|
|
179
|
+
lines.push(hrMid);
|
|
180
|
+
if (this.composer) {
|
|
181
|
+
// Composer row: the Input renders its own `> ` prompt and cursor.
|
|
182
|
+
lines.push(row(this.composer.render(innerW)[0] ?? ""));
|
|
183
|
+
const composeHint = th.fg("dim", "Enter send · Esc cancel");
|
|
184
|
+
const composeLeft = th.fg("accent", "✎ steer");
|
|
185
|
+
const composeGap = Math.max(1, innerW - visibleWidth(composeLeft) - visibleWidth(composeHint));
|
|
186
|
+
lines.push(row(composeLeft + " ".repeat(composeGap) + composeHint));
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
// Actions on the left, navigation on the right. The scroll hint keeps its
|
|
190
|
+
// full key list so the less-obvious bindings stay discoverable; it leads
|
|
191
|
+
// the right group so "Esc close" is the only part that truncates first.
|
|
192
|
+
const sep = th.fg("dim", " · ");
|
|
193
|
+
const actions = [];
|
|
194
|
+
if (this.canSteer())
|
|
195
|
+
actions.push(th.fg("dim", "Enter steer"));
|
|
196
|
+
if (this.isStoppable()) {
|
|
197
|
+
actions.push(this.stopArmed ? th.fg("error", "x again to STOP") : th.fg("dim", "x stop"));
|
|
198
|
+
}
|
|
199
|
+
const footerRight = th.fg("dim", "↑↓ scroll · PgUp/PgDn or Shift+↑↓ · Esc close");
|
|
200
|
+
// Prepend the line-count/scroll-% readout only when there's spare width —
|
|
201
|
+
// it's the first thing dropped so it never crowds out the hints.
|
|
202
|
+
const scrollPct = contentLines.length <= viewportHeight
|
|
203
|
+
? "100%"
|
|
204
|
+
: `${Math.round(((visibleStart + viewportHeight) / contentLines.length) * 100)}%`;
|
|
205
|
+
const count = th.fg("dim", `${contentLines.length} lines · ${scrollPct}`);
|
|
206
|
+
const withCount = [count, ...actions].join(sep);
|
|
207
|
+
const footerLeft = visibleWidth(withCount) + visibleWidth(footerRight) + 1 <= innerW
|
|
208
|
+
? withCount
|
|
209
|
+
: actions.join(sep);
|
|
210
|
+
const footerGap = Math.max(1, innerW - visibleWidth(footerLeft) - visibleWidth(footerRight));
|
|
211
|
+
lines.push(row(footerLeft + " ".repeat(footerGap) + footerRight));
|
|
212
|
+
}
|
|
213
|
+
lines.push(hrBot);
|
|
214
|
+
return lines;
|
|
215
|
+
}
|
|
216
|
+
/** Stoppable only when a stop handler exists and the agent is still active. */
|
|
217
|
+
isStoppable() {
|
|
218
|
+
return !!this.onStop && (this.record.status === "running" || this.record.status === "queued");
|
|
219
|
+
}
|
|
220
|
+
/** Steerable only when a steer handler exists and the agent is still active. */
|
|
221
|
+
canSteer() {
|
|
222
|
+
return !!this.onSteer && (this.record.status === "running" || this.record.status === "queued");
|
|
223
|
+
}
|
|
224
|
+
/** Open the inline steering composer and route subsequent input to it. */
|
|
225
|
+
openComposer() {
|
|
226
|
+
const input = new Input();
|
|
227
|
+
input.focused = true;
|
|
228
|
+
input.onSubmit = (value) => {
|
|
229
|
+
const message = value.trim();
|
|
230
|
+
this.composer = undefined;
|
|
231
|
+
if (message)
|
|
232
|
+
this.onSteer?.(message);
|
|
233
|
+
this.tui.requestRender();
|
|
234
|
+
};
|
|
235
|
+
input.onEscape = () => {
|
|
236
|
+
this.composer = undefined;
|
|
237
|
+
this.tui.requestRender();
|
|
238
|
+
};
|
|
239
|
+
this.composer = input;
|
|
240
|
+
this.tui.requestRender();
|
|
241
|
+
}
|
|
242
|
+
invalidate() { }
|
|
243
|
+
dispose() {
|
|
244
|
+
this.closed = true;
|
|
245
|
+
if (this.unsubscribe) {
|
|
246
|
+
this.unsubscribe();
|
|
247
|
+
this.unsubscribe = undefined;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
// ---- Private ----
|
|
251
|
+
viewportHeight() {
|
|
252
|
+
// Cap mirrors the overlay's maxHeight — otherwise the viewer would render
|
|
253
|
+
// more lines than the overlay shows and clip the footer.
|
|
254
|
+
const maxRows = Math.floor((this.tui.terminal.rows * VIEWPORT_HEIGHT_PCT) / 100);
|
|
255
|
+
return Math.max(MIN_VIEWPORT, maxRows - this.chromeLines());
|
|
256
|
+
}
|
|
257
|
+
chromeLines() {
|
|
258
|
+
// The composer adds one row above the footer hint while it's open.
|
|
259
|
+
return CHROME_LINES_BASE + (this.invocationLine() ? 1 : 0) + (this.composer ? 1 : 0);
|
|
260
|
+
}
|
|
261
|
+
invocationLine() {
|
|
262
|
+
const { modelName, tags } = buildInvocationTags(this.record.invocation);
|
|
263
|
+
const parts = modelName ? [modelName, ...tags] : tags;
|
|
264
|
+
if (parts.length === 0)
|
|
265
|
+
return undefined;
|
|
266
|
+
return this.theme.fg("dim", ` ↳ ${parts.join(" · ")}`);
|
|
267
|
+
}
|
|
268
|
+
buildContentLines(width) {
|
|
269
|
+
if (width <= 0)
|
|
270
|
+
return [];
|
|
271
|
+
const th = this.theme;
|
|
272
|
+
const messages = this.session.messages;
|
|
273
|
+
const lines = [];
|
|
274
|
+
if (messages.length === 0) {
|
|
275
|
+
lines.push(th.fg("dim", "(waiting for first message...)"));
|
|
276
|
+
return lines;
|
|
277
|
+
}
|
|
278
|
+
let needsSeparator = false;
|
|
279
|
+
for (const msg of messages) {
|
|
280
|
+
if (msg.role === "user") {
|
|
281
|
+
const text = typeof msg.content === "string"
|
|
282
|
+
? msg.content
|
|
283
|
+
: extractText(msg.content);
|
|
284
|
+
if (!text.trim())
|
|
285
|
+
continue;
|
|
286
|
+
if (needsSeparator)
|
|
287
|
+
lines.push(th.fg("dim", "───"));
|
|
288
|
+
lines.push(th.fg("accent", "[User]"));
|
|
289
|
+
for (const line of wrapTextWithAnsi(text.trim(), width)) {
|
|
290
|
+
lines.push(line);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
else if (msg.role === "assistant") {
|
|
294
|
+
const textParts = [];
|
|
295
|
+
const toolCalls = [];
|
|
296
|
+
for (const c of msg.content) {
|
|
297
|
+
if (c.type === "text" && c.text)
|
|
298
|
+
textParts.push(c.text);
|
|
299
|
+
else if (c.type === "toolCall") {
|
|
300
|
+
toolCalls.push(c.name ?? c.toolName ?? "unknown");
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if (needsSeparator)
|
|
304
|
+
lines.push(th.fg("dim", "───"));
|
|
305
|
+
lines.push(th.bold("[Assistant]"));
|
|
306
|
+
if (textParts.length > 0) {
|
|
307
|
+
for (const line of wrapTextWithAnsi(textParts.join("\n").trim(), width)) {
|
|
308
|
+
lines.push(line);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
for (const name of toolCalls) {
|
|
312
|
+
lines.push(truncateToWidth(th.fg("muted", ` [Tool: ${name}]`), width));
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
else if (msg.role === "toolResult") {
|
|
316
|
+
const text = extractText(msg.content);
|
|
317
|
+
const truncated = text.length > 500 ? text.slice(0, 500) + "... (truncated)" : text;
|
|
318
|
+
if (!truncated.trim())
|
|
319
|
+
continue;
|
|
320
|
+
if (needsSeparator)
|
|
321
|
+
lines.push(th.fg("dim", "───"));
|
|
322
|
+
lines.push(th.fg("dim", "[Result]"));
|
|
323
|
+
for (const line of wrapTextWithAnsi(truncated.trim(), width)) {
|
|
324
|
+
lines.push(th.fg("dim", line));
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
else if (msg.role === "bashExecution") {
|
|
328
|
+
const bash = msg;
|
|
329
|
+
if (needsSeparator)
|
|
330
|
+
lines.push(th.fg("dim", "───"));
|
|
331
|
+
lines.push(truncateToWidth(th.fg("muted", ` $ ${bash.command}`), width));
|
|
332
|
+
if (bash.output?.trim()) {
|
|
333
|
+
const out = bash.output.length > 500
|
|
334
|
+
? bash.output.slice(0, 500) + "... (truncated)"
|
|
335
|
+
: bash.output;
|
|
336
|
+
for (const line of wrapTextWithAnsi(out.trim(), width)) {
|
|
337
|
+
lines.push(th.fg("dim", line));
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
needsSeparator = true;
|
|
345
|
+
}
|
|
346
|
+
// Streaming indicator for running agents
|
|
347
|
+
if (this.record.status === "running" && this.activity) {
|
|
348
|
+
const act = describeActivity(this.activity.activeTools, this.activity.responseText);
|
|
349
|
+
lines.push("");
|
|
350
|
+
lines.push(truncateToWidth(th.fg("accent", "▍ ") + th.fg("dim", act), width));
|
|
351
|
+
}
|
|
352
|
+
return lines.map(l => truncateToWidth(l, width));
|
|
353
|
+
}
|
|
354
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fleet-list.ts — Claude Code-style "FleetView" list rendered below the editor.
|
|
3
|
+
*
|
|
4
|
+
* Shows `main` + each running/queued subagent as a navigable list. Pressing ↓ (or
|
|
5
|
+
* ←) at an empty prompt activates the list; ↑/↓ move the selection (filled ● marker),
|
|
6
|
+
* Enter opens the selected agent's live conversation overlay, Esc returns to the prompt.
|
|
7
|
+
* A viewer stays open when its agent finishes; finished agents linger briefly in the list.
|
|
8
|
+
*
|
|
9
|
+
* Mechanics (see plan): the list is a `belowEditor` widget (render-only), and ALL key
|
|
10
|
+
* handling goes through `onTerminalInput` — which fires before the focused editor and
|
|
11
|
+
* can `consume` keys — gated on `getEditorText() === ""` so normal typing is untouched.
|
|
12
|
+
*/
|
|
13
|
+
import type { AgentManager } from "../agent-manager.js";
|
|
14
|
+
import { type AgentActivity, type Theme } from "./agent-widget.js";
|
|
15
|
+
/** Minimal UI surface the FleetView needs from `ctx.ui` (structural subset). */
|
|
16
|
+
export type FleetUICtx = {
|
|
17
|
+
setWidget(key: string, content: undefined | ((tui: any, theme: Theme) => {
|
|
18
|
+
render(width: number): string[];
|
|
19
|
+
invalidate(): void;
|
|
20
|
+
dispose?(): void;
|
|
21
|
+
}), options?: {
|
|
22
|
+
placement?: "aboveEditor" | "belowEditor";
|
|
23
|
+
}): void;
|
|
24
|
+
onTerminalInput(handler: (data: string) => {
|
|
25
|
+
consume?: boolean;
|
|
26
|
+
data?: string;
|
|
27
|
+
} | undefined): () => void;
|
|
28
|
+
getEditorText(): string;
|
|
29
|
+
notify(message: string, type?: "info" | "warning" | "error"): void;
|
|
30
|
+
custom<T>(factory: (tui: any, theme: Theme, keybindings: any, done: (result: T) => void) => {
|
|
31
|
+
render(width: number): string[];
|
|
32
|
+
invalidate(): void;
|
|
33
|
+
dispose?(): void;
|
|
34
|
+
}, options?: {
|
|
35
|
+
overlay?: boolean;
|
|
36
|
+
overlayOptions?: unknown;
|
|
37
|
+
onHandle?: (handle: unknown) => void;
|
|
38
|
+
}): Promise<T>;
|
|
39
|
+
};
|
|
40
|
+
/** `11s` — integer seconds, no decimal/suffix (matches Claude Code, unlike formatMs). */
|
|
41
|
+
export declare function formatFleetElapsed(ms: number): string;
|
|
42
|
+
/** `↓ 13.1k tokens` — down-arrow prefix, compact magnitude, plural "tokens". */
|
|
43
|
+
export declare function formatFleetTokens(count: number): string;
|
|
44
|
+
export declare class FleetList {
|
|
45
|
+
private manager;
|
|
46
|
+
private agentActivity;
|
|
47
|
+
private ui;
|
|
48
|
+
private tui;
|
|
49
|
+
private inputUnsub;
|
|
50
|
+
private widgetRegistered;
|
|
51
|
+
private timer;
|
|
52
|
+
private enabled;
|
|
53
|
+
/** Whether arrow keys currently navigate the list (vs. flow to the editor). */
|
|
54
|
+
private active;
|
|
55
|
+
/** 0 = `main`, 1..N = subagents. */
|
|
56
|
+
private selectedIndex;
|
|
57
|
+
/** Set while a conversation overlay is open; calling it closes the overlay. */
|
|
58
|
+
private viewerClose;
|
|
59
|
+
private viewingAgentId;
|
|
60
|
+
constructor(manager: AgentManager, agentActivity: Map<string, AgentActivity>);
|
|
61
|
+
setEnabled(enabled: boolean): void;
|
|
62
|
+
/** Capture the UI context and (re)register the global input handler. */
|
|
63
|
+
setUICtx(ui: FleetUICtx): void;
|
|
64
|
+
/** Ensure the re-render timer is running (called when an agent spawns). */
|
|
65
|
+
ensureTimer(): void;
|
|
66
|
+
/**
|
|
67
|
+
* Called when an agent finishes. The viewer (if open on it) stays open so the
|
|
68
|
+
* final output remains readable, and the row lingers in the list — just refresh.
|
|
69
|
+
*/
|
|
70
|
+
onAgentFinished(_id: string): void;
|
|
71
|
+
dispose(): void;
|
|
72
|
+
/** Re-register/refresh the below-editor widget; clears it when no agents remain. */
|
|
73
|
+
update(): void;
|
|
74
|
+
/**
|
|
75
|
+
* Agents shown in the list, ordered earliest-launched first so the ones you
|
|
76
|
+
* started sooner sit at the top. Every row is openable (has a session), so Enter
|
|
77
|
+
* never dead-ends. Included: running/queued, plus the agent currently being
|
|
78
|
+
* viewed, plus recently-finished ones (they linger briefly before dropping out).
|
|
79
|
+
* Pending agents with no session yet are hidden until they start.
|
|
80
|
+
* (`listAgents()` is newest-first, so we re-sort.)
|
|
81
|
+
*/
|
|
82
|
+
private agentRecords;
|
|
83
|
+
private roster;
|
|
84
|
+
private clampSelection;
|
|
85
|
+
/** Returns `{consume:true}` to swallow a key, or undefined to let it through. */
|
|
86
|
+
handleKey(data: string): {
|
|
87
|
+
consume?: boolean;
|
|
88
|
+
data?: string;
|
|
89
|
+
} | undefined;
|
|
90
|
+
/**
|
|
91
|
+
* True when pi's prompt editor owns the keyboard. pi's editor is an `Editor`
|
|
92
|
+
* subclass (CustomEditor) while every dialog/selector is not, and the loader
|
|
93
|
+
* aliases pi-tui to pi's own copy, so `instanceof` is a reliable identity
|
|
94
|
+
* check. `focusedComponent` is TUI-private (no public accessor), hence the
|
|
95
|
+
* best-effort peek: unknowable focus (no tui seen yet, nothing focused)
|
|
96
|
+
* counts as the editor so activation keeps working.
|
|
97
|
+
*/
|
|
98
|
+
private editorHasFocus;
|
|
99
|
+
private deactivate;
|
|
100
|
+
private openSelected;
|
|
101
|
+
/** Reset overlay state and return to the list (on close, auto-close, or error). */
|
|
102
|
+
private clearViewer;
|
|
103
|
+
private renderBar;
|
|
104
|
+
private bullet;
|
|
105
|
+
private renderAgentRow;
|
|
106
|
+
}
|