@giladbarnea/pi-user-agents 0.0.7 → 0.0.9
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/INTERNALS.md +11 -1
- package/PARSER_SPEC.md +10 -2
- package/README.md +26 -7
- package/agent-viewer.ts +107 -38
- package/attach.ts +35 -4
- package/command-line.ts +47 -5
- package/context-meter.ts +5 -1
- package/herdr.ts +92 -0
- package/index.ts +2 -1
- package/package.json +3 -3
- package/runner.ts +109 -15
- package/screenshots/below-editor-widget-2.png +0 -0
- package/screenshots/editor-with-autocomplete.png +0 -0
- package/screenshots/overlay-2.png +0 -0
- package/shared.ts +26 -0
- package/widget.ts +298 -63
package/widget.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as childProcess from "node:child_process";
|
|
1
2
|
import {
|
|
2
3
|
isKeyRelease,
|
|
3
4
|
matchesKey,
|
|
@@ -7,6 +8,7 @@ import {
|
|
|
7
8
|
import {
|
|
8
9
|
AgentViewer,
|
|
9
10
|
type AgentViewerAction,
|
|
11
|
+
type AgentViewerActions,
|
|
10
12
|
describeActivity,
|
|
11
13
|
isLiveAgent,
|
|
12
14
|
isRunningAgent,
|
|
@@ -20,17 +22,21 @@ import type {
|
|
|
20
22
|
AgentMessage,
|
|
21
23
|
AgentResultMessage,
|
|
22
24
|
CompletedAgent,
|
|
25
|
+
HerdrDelivery,
|
|
23
26
|
RebaseDelivery,
|
|
24
27
|
RunningAgent,
|
|
25
28
|
Theme,
|
|
26
29
|
UIContext,
|
|
27
30
|
} from "./shared.js";
|
|
28
31
|
import {
|
|
32
|
+
CONFIRMATION_WINDOW_MS,
|
|
29
33
|
contextLabel,
|
|
34
|
+
errorMessage,
|
|
30
35
|
formatMs,
|
|
31
36
|
TimedConfirmation,
|
|
32
37
|
formatToolUses,
|
|
33
38
|
formatTurns,
|
|
39
|
+
herdrPaneLabel,
|
|
34
40
|
logSteering,
|
|
35
41
|
mainContextLabel,
|
|
36
42
|
MAX_WIDGET_LINES,
|
|
@@ -46,12 +52,17 @@ type UserAgentWidgetEntry =
|
|
|
46
52
|
type CachedActivityPreview = {
|
|
47
53
|
source: AgentMessage | string | undefined;
|
|
48
54
|
status: RunningAgent["status"] | boolean;
|
|
55
|
+
herdrPane: string | undefined;
|
|
49
56
|
error: string | undefined;
|
|
50
57
|
width: number;
|
|
51
58
|
theme: Theme;
|
|
52
59
|
text: string;
|
|
53
60
|
};
|
|
54
61
|
|
|
62
|
+
type CopyTarget = "response" | "session-id";
|
|
63
|
+
|
|
64
|
+
const COPY_FEEDBACK_MS = 1500;
|
|
65
|
+
|
|
55
66
|
export class UserAgentWidget {
|
|
56
67
|
private ui: UIContext | undefined;
|
|
57
68
|
private frame = 0;
|
|
@@ -62,9 +73,16 @@ export class UserAgentWidget {
|
|
|
62
73
|
private active = false;
|
|
63
74
|
private selectedIndex = 0;
|
|
64
75
|
private viewerOpen = false;
|
|
76
|
+
private copied: { agentId: string; target: CopyTarget } | undefined;
|
|
77
|
+
private copiedTimer: ReturnType<typeof setTimeout> | undefined;
|
|
78
|
+
/** A transient per-row notice explaining why an action did nothing. */
|
|
79
|
+
private warning: { agentId: string; text: string } | undefined;
|
|
80
|
+
private warningTimer: ReturnType<typeof setTimeout> | undefined;
|
|
81
|
+
/** The agent whose herdr pane is being opened right now. */
|
|
82
|
+
private herdrPending: string | undefined;
|
|
65
83
|
private activityPreviews = new WeakMap<ViewableAgent, CachedActivityPreview>();
|
|
66
|
-
/** One confirmation slot for
|
|
67
|
-
private readonly
|
|
84
|
+
/** One confirmation slot for both destructive selected-row actions. */
|
|
85
|
+
private readonly confirmation = new TimedConfirmation<string>(() => this.update());
|
|
68
86
|
private readonly completedAgents: CompletedAgent[] = [];
|
|
69
87
|
|
|
70
88
|
constructor(
|
|
@@ -72,6 +90,7 @@ export class UserAgentWidget {
|
|
|
72
90
|
private readonly sendSquashedResult: (message: AgentResultMessage) => void,
|
|
73
91
|
private readonly announceDetached: (sessionId: string) => void,
|
|
74
92
|
private readonly rebaseDelivery: RebaseDelivery,
|
|
93
|
+
private readonly herdrDelivery: HerdrDelivery,
|
|
75
94
|
) {}
|
|
76
95
|
|
|
77
96
|
setUI(ui: UIContext): void {
|
|
@@ -81,7 +100,7 @@ export class UserAgentWidget {
|
|
|
81
100
|
this.activityPreviews = new WeakMap();
|
|
82
101
|
this.widgetRegistered = false;
|
|
83
102
|
this.tui = undefined;
|
|
84
|
-
this.
|
|
103
|
+
this.confirmation.cancel();
|
|
85
104
|
this.inputUnsub = ui.onTerminalInput((data) => this.handleKey(data));
|
|
86
105
|
}
|
|
87
106
|
|
|
@@ -90,7 +109,22 @@ export class UserAgentWidget {
|
|
|
90
109
|
resultMessage: AgentResultMessage,
|
|
91
110
|
options: { squashable: boolean },
|
|
92
111
|
): void {
|
|
93
|
-
this.
|
|
112
|
+
this.snapshot(agent, {
|
|
113
|
+
ok: resultMessage.details.ok,
|
|
114
|
+
pendingSquashMessage: options.squashable ? resultMessage : undefined,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** The row for an agent dispatched straight into a herdr pane: nothing ran here, so nothing squashes. */
|
|
119
|
+
addHerdrDispatch(agent: RunningAgent, paneId: string): void {
|
|
120
|
+
this.snapshot(agent, { ok: true, herdrPane: paneId });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private snapshot(
|
|
124
|
+
agent: RunningAgent,
|
|
125
|
+
outcome: { ok: boolean; pendingSquashMessage?: AgentResultMessage; herdrPane?: string },
|
|
126
|
+
): CompletedAgent {
|
|
127
|
+
const completed: CompletedAgent = {
|
|
94
128
|
id: agent.id,
|
|
95
129
|
sessionId: agent.sessionId,
|
|
96
130
|
command: agent.command,
|
|
@@ -98,8 +132,8 @@ export class UserAgentWidget {
|
|
|
98
132
|
task: agent.task,
|
|
99
133
|
dispatchBaseFingerprint: agent.dispatchBaseFingerprint,
|
|
100
134
|
mainContextState: agent.mainContextState,
|
|
101
|
-
pendingSquashMessage:
|
|
102
|
-
ok:
|
|
135
|
+
pendingSquashMessage: outcome.pendingSquashMessage,
|
|
136
|
+
ok: outcome.ok,
|
|
103
137
|
responseText: agent.responseText,
|
|
104
138
|
messages: structuredClone(transcriptMessages(agent)),
|
|
105
139
|
error: agent.error,
|
|
@@ -108,8 +142,11 @@ export class UserAgentWidget {
|
|
|
108
142
|
toolUses: agent.toolUses,
|
|
109
143
|
turnCount: agent.turnCount,
|
|
110
144
|
contextPercent: agent.session?.getContextUsage()?.percent ?? undefined,
|
|
111
|
-
|
|
145
|
+
herdrPane: outcome.herdrPane,
|
|
146
|
+
};
|
|
147
|
+
this.completedAgents.push(completed);
|
|
112
148
|
this.update();
|
|
149
|
+
return completed;
|
|
113
150
|
}
|
|
114
151
|
|
|
115
152
|
ensureTimer(): void {
|
|
@@ -166,7 +203,7 @@ export class UserAgentWidget {
|
|
|
166
203
|
this.tui = undefined;
|
|
167
204
|
this.active = false;
|
|
168
205
|
this.activityPreviews = new WeakMap();
|
|
169
|
-
this.
|
|
206
|
+
this.confirmation.cancel();
|
|
170
207
|
}
|
|
171
208
|
|
|
172
209
|
private clear(): void {
|
|
@@ -181,7 +218,7 @@ export class UserAgentWidget {
|
|
|
181
218
|
}
|
|
182
219
|
this.active = false;
|
|
183
220
|
this.selectedIndex = 0;
|
|
184
|
-
this.
|
|
221
|
+
this.confirmation.cancel();
|
|
185
222
|
}
|
|
186
223
|
|
|
187
224
|
private runningAgentsForWidget(): RunningAgent[] {
|
|
@@ -225,13 +262,13 @@ export class UserAgentWidget {
|
|
|
225
262
|
}
|
|
226
263
|
|
|
227
264
|
if (matchesKey(data, "down")) {
|
|
228
|
-
this.
|
|
265
|
+
this.confirmation.cancel();
|
|
229
266
|
this.selectedIndex = Math.min(entries.length - 1, this.selectedIndex + 1);
|
|
230
267
|
this.update();
|
|
231
268
|
return { consume: true };
|
|
232
269
|
}
|
|
233
270
|
if (matchesKey(data, "up")) {
|
|
234
|
-
this.
|
|
271
|
+
this.confirmation.cancel();
|
|
235
272
|
if (this.selectedIndex === 0) {
|
|
236
273
|
this.deactivate();
|
|
237
274
|
return { consume: true };
|
|
@@ -246,20 +283,44 @@ export class UserAgentWidget {
|
|
|
246
283
|
}
|
|
247
284
|
|
|
248
285
|
const selected = entries[this.selectedIndex];
|
|
286
|
+
if (!matchesKey(data, "d") && !matchesKey(data, "r")) this.confirmation.cancel();
|
|
249
287
|
if (
|
|
250
288
|
matchesKey(data, "ctrl+x") &&
|
|
251
289
|
selected?.kind === "running" &&
|
|
252
290
|
isLiveAgent(selected.agent)
|
|
253
291
|
) {
|
|
254
|
-
this.detachConfirmation.cancel();
|
|
255
292
|
this.interruptRunning(selected.agent);
|
|
256
293
|
return { consume: true };
|
|
257
294
|
}
|
|
258
295
|
if (matchesKey(data, "enter")) {
|
|
259
|
-
this.detachConfirmation.cancel();
|
|
260
296
|
if (selected) this.openSelected(selected.agent);
|
|
261
297
|
return { consume: true };
|
|
262
298
|
}
|
|
299
|
+
if (matchesKey(data, "c") && selected) {
|
|
300
|
+
const text =
|
|
301
|
+
isRunningAgent(selected.agent) || selected.agent.ok
|
|
302
|
+
? selected.agent.responseText
|
|
303
|
+
: (selected.agent.error ?? "");
|
|
304
|
+
this.copyToClipboard(selected.agent.id, text, "response");
|
|
305
|
+
return { consume: true };
|
|
306
|
+
}
|
|
307
|
+
if (matchesKey(data, "i") && selected) {
|
|
308
|
+
this.copyToClipboard(selected.agent.id, selected.agent.sessionId, "session-id");
|
|
309
|
+
return { consume: true };
|
|
310
|
+
}
|
|
311
|
+
if (matchesKey(data, "s")) {
|
|
312
|
+
if (selected && this.canSquashMainContext(selected.agent.id))
|
|
313
|
+
this.squashMainContext(selected.agent.id);
|
|
314
|
+
return { consume: true };
|
|
315
|
+
}
|
|
316
|
+
if (matchesKey(data, "r")) {
|
|
317
|
+
this.requestRebase(selected);
|
|
318
|
+
return { consume: true };
|
|
319
|
+
}
|
|
320
|
+
if (matchesKey(data, "h")) {
|
|
321
|
+
if (selected) this.requestHerdr(selected.agent.id);
|
|
322
|
+
return { consume: true };
|
|
323
|
+
}
|
|
263
324
|
if (matchesKey(data, "d") && selected) {
|
|
264
325
|
if (!this.confirmDetach(selected.agent)) return { consume: true };
|
|
265
326
|
if (selected.kind === "running") this.closeRunning(selected.agent);
|
|
@@ -272,15 +333,116 @@ export class UserAgentWidget {
|
|
|
272
333
|
}
|
|
273
334
|
|
|
274
335
|
private confirmDetach(target: ViewableAgent): boolean {
|
|
275
|
-
const confirmed = this.
|
|
336
|
+
const confirmed = this.confirmation.press(`detach:${target.id}`);
|
|
276
337
|
if (!confirmed) this.update();
|
|
277
338
|
return confirmed;
|
|
278
339
|
}
|
|
279
340
|
|
|
341
|
+
private requestRebase(selected: UserAgentWidgetEntry | undefined): void {
|
|
342
|
+
if (!selected) return;
|
|
343
|
+
const agentId = selected.agent.id;
|
|
344
|
+
const reason = this.rebaseBlockReason(agentId);
|
|
345
|
+
if (reason) {
|
|
346
|
+
this.confirmation.cancel();
|
|
347
|
+
this.showWarning(agentId, `Can't rebase: ${reason}`);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
if (!this.deliverableAgent(agentId)) {
|
|
351
|
+
this.confirmation.cancel();
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
if (
|
|
355
|
+
this.rebaseDetachCount(agentId) > 0 &&
|
|
356
|
+
!this.confirmation.press(`rebase:${agentId}`)
|
|
357
|
+
) {
|
|
358
|
+
this.update();
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
this.rebaseMainContext(agentId);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
private requestHerdr(agentId: string): void {
|
|
365
|
+
this.openInHerdr(agentId).catch((error) =>
|
|
366
|
+
this.showWarning(agentId, `Can't open in herdr: ${errorMessage(error)}`),
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** Hand the agent's session to a Pi in a new herdr pane, then return focus to the editor. */
|
|
371
|
+
async openInHerdr(agentId: string): Promise<void> {
|
|
372
|
+
const reason = this.herdrBlockReason(agentId);
|
|
373
|
+
if (reason) throw new Error(reason);
|
|
374
|
+
this.herdrPending = agentId;
|
|
375
|
+
this.update();
|
|
376
|
+
try {
|
|
377
|
+
// The pane comes first: a herdr failure here leaves the agent untouched. Pi starts in it
|
|
378
|
+
// only after the session is free here, so the file has one writer at a time.
|
|
379
|
+
const target = this.idleAgent(agentId) ?? this.completedAgents.find((agent) => agent.id === agentId);
|
|
380
|
+
if (!target) throw new Error("the agent has no row anymore");
|
|
381
|
+
const pane = await this.herdrDelivery.split(target);
|
|
382
|
+
const snapshot = await this.releaseForHerdr(agentId);
|
|
383
|
+
await pane.start();
|
|
384
|
+
snapshot.herdrPane = pane.paneId;
|
|
385
|
+
logSteering(agentId, "opened-in-herdr", { paneId: pane.paneId });
|
|
386
|
+
} finally {
|
|
387
|
+
this.herdrPending = undefined;
|
|
388
|
+
this.update();
|
|
389
|
+
}
|
|
390
|
+
this.deactivate();
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/** Why the agent cannot open in a herdr pane right now; undefined when it can. */
|
|
394
|
+
private herdrBlockReason(agentId: string): string | undefined {
|
|
395
|
+
if (!this.herdrDelivery.available()) return "Pi is not running inside a herdr pane";
|
|
396
|
+
if (this.herdrPending === agentId) return "a herdr pane is already opening";
|
|
397
|
+
const completed = this.completedAgents.find((agent) => agent.id === agentId);
|
|
398
|
+
if (completed?.herdrPane) return `already in ${herdrPaneLabel(completed.herdrPane)}`;
|
|
399
|
+
if (!completed && !this.idleAgent(agentId)) return "the agent is mid-turn (Ctrl+x interrupts it)";
|
|
400
|
+
return undefined;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/** End the agent's life here and keep its row as a snapshot; resolves once its session is disposed. */
|
|
404
|
+
private async releaseForHerdr(agentId: string): Promise<CompletedAgent> {
|
|
405
|
+
const idleAgent = this.idleAgent(agentId);
|
|
406
|
+
if (idleAgent) {
|
|
407
|
+
idleAgent.status = "posted";
|
|
408
|
+
this.retireRunning(idleAgent);
|
|
409
|
+
this.snapshot(idleAgent, { ok: true, pendingSquashMessage: idleAgent.pendingSquashMessage });
|
|
410
|
+
}
|
|
411
|
+
// The lifecycle disposes the child session asynchronously; another Pi may open the file only after that.
|
|
412
|
+
await [...this.runningAgents].find((agent) => agent.id === agentId)?.finished;
|
|
413
|
+
return this.completedAgents.find((agent) => agent.id === agentId)!;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
private copyToClipboard(agentId: string, text: string, target: CopyTarget): void {
|
|
417
|
+
const child = childProcess.execFile("pbcopy", (error) => {
|
|
418
|
+
if (error) return;
|
|
419
|
+
this.copied = { agentId, target };
|
|
420
|
+
if (this.copiedTimer) clearTimeout(this.copiedTimer);
|
|
421
|
+
this.update();
|
|
422
|
+
this.copiedTimer = setTimeout(() => {
|
|
423
|
+
this.copied = undefined;
|
|
424
|
+
this.copiedTimer = undefined;
|
|
425
|
+
this.update();
|
|
426
|
+
}, COPY_FEEDBACK_MS);
|
|
427
|
+
});
|
|
428
|
+
child.stdin?.end(text);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
private showWarning(agentId: string, text: string): void {
|
|
432
|
+
this.warning = { agentId, text };
|
|
433
|
+
if (this.warningTimer) clearTimeout(this.warningTimer);
|
|
434
|
+
this.warningTimer = setTimeout(() => {
|
|
435
|
+
this.warning = undefined;
|
|
436
|
+
this.warningTimer = undefined;
|
|
437
|
+
this.update();
|
|
438
|
+
}, CONFIRMATION_WINDOW_MS);
|
|
439
|
+
this.update();
|
|
440
|
+
}
|
|
441
|
+
|
|
280
442
|
private deactivate(): void {
|
|
281
443
|
this.active = false;
|
|
282
444
|
this.selectedIndex = 0;
|
|
283
|
-
this.
|
|
445
|
+
this.confirmation.cancel();
|
|
284
446
|
this.update();
|
|
285
447
|
}
|
|
286
448
|
|
|
@@ -316,21 +478,7 @@ export class UserAgentWidget {
|
|
|
316
478
|
void ui
|
|
317
479
|
.custom<AgentViewerAction>(
|
|
318
480
|
(tui, theme, _keybindings, done) =>
|
|
319
|
-
new AgentViewer(
|
|
320
|
-
tui,
|
|
321
|
-
agent,
|
|
322
|
-
theme,
|
|
323
|
-
done,
|
|
324
|
-
() => this.canSquashMainContext(agent.id),
|
|
325
|
-
() => this.squashMainContext(agent.id),
|
|
326
|
-
() => this.canRebaseMainContext(agent.id),
|
|
327
|
-
() => this.rebaseMainContext(agent.id),
|
|
328
|
-
() => this.rebaseBlockReason(agent.id),
|
|
329
|
-
() => this.rebaseDetachCount(agent.id),
|
|
330
|
-
() => {
|
|
331
|
-
if (isRunningAgent(agent)) this.interruptRunning(agent);
|
|
332
|
-
},
|
|
333
|
-
),
|
|
481
|
+
new AgentViewer(tui, agent, theme, done, this.viewerActions(agent)),
|
|
334
482
|
{ overlay: true, overlayOptions: { anchor: "center", width: "90%", maxHeight: "85%" } },
|
|
335
483
|
)
|
|
336
484
|
.then(
|
|
@@ -339,13 +487,34 @@ export class UserAgentWidget {
|
|
|
339
487
|
);
|
|
340
488
|
}
|
|
341
489
|
|
|
490
|
+
private viewerActions(agent: ViewableAgent): AgentViewerActions {
|
|
491
|
+
return {
|
|
492
|
+
canSquashMainContext: () => this.canSquashMainContext(agent.id),
|
|
493
|
+
squashMainContext: () => this.squashMainContext(agent.id),
|
|
494
|
+
canRebaseMainContext: () => this.canRebaseMainContext(agent.id),
|
|
495
|
+
rebaseMainContext: () => this.rebaseMainContext(agent.id),
|
|
496
|
+
rebaseBlockReason: () => this.rebaseBlockReason(agent.id),
|
|
497
|
+
rebaseDetachCount: () => this.rebaseDetachCount(agent.id),
|
|
498
|
+
interrupt: () => {
|
|
499
|
+
if (isRunningAgent(agent)) this.interruptRunning(agent);
|
|
500
|
+
},
|
|
501
|
+
herdrBlockReason: () => this.herdrBlockReason(agent.id),
|
|
502
|
+
openInHerdr: () => this.openInHerdr(agent.id),
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
|
|
342
506
|
/** Detaching ends the live session and leaves its session file on disk, untouched. */
|
|
343
507
|
private closeRunning(agent: RunningAgent): void {
|
|
508
|
+
this.retireRunning(agent);
|
|
509
|
+
this.announceDetached(agent.sessionId);
|
|
510
|
+
this.update();
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/** End a live agent's session here; its row and any breadcrumb are the caller's business. */
|
|
514
|
+
private retireRunning(agent: RunningAgent): void {
|
|
344
515
|
agent.aborted = true;
|
|
345
516
|
void agent.session?.abort();
|
|
346
517
|
agent.retire?.();
|
|
347
|
-
this.announceDetached(agent.sessionId);
|
|
348
|
-
this.update();
|
|
349
518
|
}
|
|
350
519
|
|
|
351
520
|
private interruptRunning(agent: RunningAgent): void {
|
|
@@ -511,24 +680,62 @@ export class UserAgentWidget {
|
|
|
511
680
|
} else {
|
|
512
681
|
const selected = entries[this.selectedIndex];
|
|
513
682
|
const confirmingDetach =
|
|
514
|
-
selected !== undefined &&
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
selected
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
683
|
+
selected !== undefined &&
|
|
684
|
+
this.confirmation.isArmedOn(`detach:${selected.agent.id}`);
|
|
685
|
+
const confirmingRebase =
|
|
686
|
+
selected !== undefined &&
|
|
687
|
+
this.confirmation.isArmedOn(`rebase:${selected.agent.id}`);
|
|
688
|
+
if (confirmingRebase && selected) {
|
|
689
|
+
const detachCount = this.rebaseDetachCount(selected.agent.id);
|
|
690
|
+
lines.push(
|
|
691
|
+
truncateToWidth(
|
|
692
|
+
` ${theme.fg(
|
|
693
|
+
"warning",
|
|
694
|
+
`⚠ Rebase will detach ${detachCount} other agent session${detachCount === 1 ? "" : "s"}. r again to confirm`,
|
|
695
|
+
)}`,
|
|
696
|
+
width,
|
|
697
|
+
),
|
|
698
|
+
);
|
|
699
|
+
} else if (selected && this.herdrPending === selected.agent.id) {
|
|
700
|
+
lines.push(truncateToWidth(` ${theme.fg("accent", "⧉ Opening a herdr pane…")}`, width));
|
|
701
|
+
} else if (selected && this.warning?.agentId === selected.agent.id) {
|
|
702
|
+
lines.push(truncateToWidth(` ${theme.fg("warning", `⚠ ${this.warning.text}`)}`, width));
|
|
703
|
+
} else {
|
|
704
|
+
const segments = [theme.fg("accent", "↑↓ select"), theme.fg("accent", "Enter view")];
|
|
705
|
+
if (!confirmingDetach)
|
|
706
|
+
segments.push(
|
|
707
|
+
this.copied?.agentId === selected?.agent.id &&
|
|
708
|
+
this.copied.target === "response"
|
|
709
|
+
? theme.fg("success", "✓ copied")
|
|
710
|
+
: theme.fg("accent", "c copy"),
|
|
711
|
+
this.copied?.agentId === selected?.agent.id &&
|
|
712
|
+
this.copied.target === "session-id"
|
|
713
|
+
? theme.fg("success", "✓ copied ID")
|
|
714
|
+
: theme.fg("accent", "i ID"),
|
|
715
|
+
);
|
|
716
|
+
if (!confirmingDetach && selected && this.canSquashMainContext(selected.agent.id))
|
|
717
|
+
segments.push(theme.fg("accent", "s squash"));
|
|
718
|
+
if (!confirmingDetach && selected && this.canRebaseMainContext(selected.agent.id))
|
|
719
|
+
segments.push(theme.fg("accent", "r rebase"));
|
|
720
|
+
if (!confirmingDetach && selected && this.herdrBlockReason(selected.agent.id) === undefined)
|
|
721
|
+
segments.push(theme.fg("accent", "h herdr"));
|
|
722
|
+
if (
|
|
723
|
+
!confirmingDetach &&
|
|
724
|
+
selected?.kind === "running" &&
|
|
725
|
+
isLiveAgent(selected.agent)
|
|
726
|
+
)
|
|
727
|
+
segments.push(theme.fg("accent", "Ctrl+x interrupt"));
|
|
728
|
+
segments.push(
|
|
729
|
+
theme.fg(
|
|
730
|
+
confirmingDetach ? "error" : "accent",
|
|
731
|
+
confirmingDetach ? "d again to confirm" : "d detach",
|
|
732
|
+
),
|
|
733
|
+
theme.fg("accent", "Esc back"),
|
|
734
|
+
);
|
|
735
|
+
lines.push(
|
|
736
|
+
truncateToWidth(` ${segments.join(theme.fg("accent", " · "))}`, width),
|
|
737
|
+
);
|
|
738
|
+
}
|
|
532
739
|
}
|
|
533
740
|
|
|
534
741
|
const maxEntries = Math.max(1, Math.floor((MAX_WIDGET_LINES - lines.length) / 2));
|
|
@@ -567,31 +774,47 @@ export class UserAgentWidget {
|
|
|
567
774
|
): string[] {
|
|
568
775
|
const running = isRunningAgent(agent);
|
|
569
776
|
const inFlight = isLiveAgent(agent);
|
|
777
|
+
const herdrPane = running ? undefined : agent.herdrPane;
|
|
570
778
|
const ok = running ? agent.error === undefined : agent.ok;
|
|
779
|
+
const status = inFlight
|
|
780
|
+
? "running"
|
|
781
|
+
: running
|
|
782
|
+
? "idle"
|
|
783
|
+
: herdrPane
|
|
784
|
+
? herdrPaneLabel(herdrPane)
|
|
785
|
+
: ok
|
|
786
|
+
? "completed"
|
|
787
|
+
: "failed";
|
|
571
788
|
const marker = selected ? theme.fg("accent", "⏺") : theme.fg("dim", "◯");
|
|
572
789
|
const icon = inFlight
|
|
573
790
|
? theme.fg("accent", SPINNER[this.frame % SPINNER.length])
|
|
574
|
-
:
|
|
575
|
-
? theme.fg("
|
|
576
|
-
:
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
791
|
+
: running
|
|
792
|
+
? theme.fg("muted", "◉")
|
|
793
|
+
: herdrPane
|
|
794
|
+
? theme.fg("muted", "⧉")
|
|
795
|
+
: ok
|
|
796
|
+
? theme.fg("success", "✓")
|
|
797
|
+
: theme.fg("error", "✗");
|
|
798
|
+
const parts = [agent.modelLabel, status];
|
|
799
|
+
if (inFlight) parts.push(contextLabel(agent.inheritedContext));
|
|
580
800
|
const mainContext = mainContextLabel(agent.mainContextState);
|
|
581
801
|
if (mainContext) parts.push(mainContext);
|
|
582
802
|
if (agent.turnCount > 0) parts.push(formatTurns(agent.turnCount));
|
|
583
803
|
if (agent.toolUses > 0) parts.push(formatToolUses(agent.toolUses));
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
804
|
+
// An agent born in a herdr pane never ran here; a duration would be noise.
|
|
805
|
+
if (!herdrPane || agent.turnCount > 0)
|
|
806
|
+
parts.push(
|
|
807
|
+
formatMs(
|
|
808
|
+
running ? (agent.completedAt ?? Date.now()) - agent.turnStartedAt : agent.durationMs,
|
|
809
|
+
),
|
|
810
|
+
);
|
|
589
811
|
|
|
590
812
|
const header = [
|
|
591
813
|
theme.fg("dim", connector),
|
|
592
814
|
marker,
|
|
593
815
|
icon,
|
|
594
816
|
theme.bold(`/${agent.command}`),
|
|
817
|
+
selected ? theme.fg("dim", `ID ${agent.sessionId.slice(0, 8)}`) : "",
|
|
595
818
|
renderAgentContextMeter(agent, theme),
|
|
596
819
|
theme.fg("muted", truncatePlain(agent.task, 52)),
|
|
597
820
|
theme.fg("dim", "·"),
|
|
@@ -611,19 +834,31 @@ export class UserAgentWidget {
|
|
|
611
834
|
const running = isRunningAgent(agent);
|
|
612
835
|
const source = isLiveAgent(agent) ? agent.latestFinalizedMessage : agent.responseText;
|
|
613
836
|
const status = running ? agent.status : agent.ok;
|
|
837
|
+
const herdrPane = running ? undefined : agent.herdrPane;
|
|
614
838
|
const cached = this.activityPreviews.get(agent);
|
|
615
839
|
if (
|
|
616
840
|
cached && cached.source === source && cached.status === status &&
|
|
841
|
+
cached.herdrPane === herdrPane &&
|
|
617
842
|
cached.error === agent.error && cached.width === width && cached.theme === theme
|
|
618
843
|
) return cached.text;
|
|
619
844
|
const description = running
|
|
620
845
|
? describeActivity(agent)
|
|
621
846
|
: {
|
|
622
|
-
text: agent.ok
|
|
847
|
+
text: !agent.ok
|
|
848
|
+
? `Error: ${agent.error ?? "unknown"}`
|
|
849
|
+
: agent.responseText || (herdrPane ? `runs in ${herdrPaneLabel(herdrPane)}` : ""),
|
|
623
850
|
truncation: "tail" as const,
|
|
624
851
|
};
|
|
625
852
|
const text = renderActivity(description, width, theme);
|
|
626
|
-
this.activityPreviews.set(agent, {
|
|
853
|
+
this.activityPreviews.set(agent, {
|
|
854
|
+
source,
|
|
855
|
+
status,
|
|
856
|
+
herdrPane,
|
|
857
|
+
error: agent.error,
|
|
858
|
+
width,
|
|
859
|
+
theme,
|
|
860
|
+
text,
|
|
861
|
+
});
|
|
627
862
|
return text;
|
|
628
863
|
}
|
|
629
864
|
}
|