@giladbarnea/pi-user-agents 0.0.4 → 0.0.6
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 +36 -6
- package/PARSER_SPEC.md +3 -3
- package/README.md +83 -26
- package/agent-viewer.ts +66 -14
- package/attach.ts +339 -0
- package/command-line.ts +10 -10
- package/index.ts +29 -7
- package/package.json +1 -1
- package/rebase.ts +67 -0
- package/runner.ts +217 -69
- package/shared.ts +104 -11
- package/transcript.ts +51 -17
- package/widget.ts +122 -47
package/transcript.ts
CHANGED
|
@@ -6,13 +6,16 @@ import type {
|
|
|
6
6
|
AgentEntryData,
|
|
7
7
|
AgentMessage,
|
|
8
8
|
AgentResultMessage,
|
|
9
|
+
AttachedEntryData,
|
|
9
10
|
DetachedEntryData,
|
|
10
11
|
ExtensionAPI,
|
|
11
12
|
ExtensionCommandContext,
|
|
13
|
+
RebasedEntryData,
|
|
12
14
|
RunningAgent,
|
|
13
15
|
Theme,
|
|
14
16
|
} from "./shared.js";
|
|
15
17
|
import {
|
|
18
|
+
ATTACHED_ENTRY_TYPE,
|
|
16
19
|
contextLabel,
|
|
17
20
|
DETACHED_ENTRY_TYPE,
|
|
18
21
|
escapeAttribute,
|
|
@@ -22,6 +25,7 @@ import {
|
|
|
22
25
|
formatTurns,
|
|
23
26
|
mainContextLabel,
|
|
24
27
|
MESSAGE_TYPE,
|
|
28
|
+
REBASED_ENTRY_TYPE,
|
|
25
29
|
truncatePlain,
|
|
26
30
|
} from "./shared.js";
|
|
27
31
|
|
|
@@ -42,7 +46,6 @@ export function reportCommandError(
|
|
|
42
46
|
display: true,
|
|
43
47
|
details: {
|
|
44
48
|
command,
|
|
45
|
-
inheritedContext: command === "agent",
|
|
46
49
|
model: "",
|
|
47
50
|
modelLabel: "",
|
|
48
51
|
task: args.trim(),
|
|
@@ -53,14 +56,14 @@ export function reportCommandError(
|
|
|
53
56
|
}
|
|
54
57
|
|
|
55
58
|
/**
|
|
56
|
-
* Select the
|
|
59
|
+
* Select the squashed role messages from a child transcript.
|
|
57
60
|
*
|
|
58
|
-
* @example
|
|
61
|
+
* @example selectSquashedMessages([{ role: "user", content: "hello", timestamp: 0 }]).length // 1
|
|
59
62
|
*/
|
|
60
|
-
type
|
|
63
|
+
type SquashedMessage = Extract<AgentMessage, { role: "user" | "assistant" }>;
|
|
61
64
|
|
|
62
|
-
export function
|
|
63
|
-
const selected:
|
|
65
|
+
export function selectSquashedMessages(messages: readonly AgentMessage[]): SquashedMessage[] {
|
|
66
|
+
const selected: SquashedMessage[] = [];
|
|
64
67
|
let latestAssistant: Extract<AgentMessage, { role: "assistant" }> | undefined;
|
|
65
68
|
for (const message of messages) {
|
|
66
69
|
if (message.role === "assistant" && roleMessageText(message).trim()) {
|
|
@@ -110,13 +113,13 @@ export function buildAgentResultMessage(
|
|
|
110
113
|
};
|
|
111
114
|
}
|
|
112
115
|
|
|
113
|
-
const
|
|
116
|
+
const SQUASHED_CONVERSATION_PREFACE =
|
|
114
117
|
"The user has dispatched a background sub-agent with a task. The sub-agent is done. The following is the back and forth between them:";
|
|
115
118
|
|
|
116
119
|
export function formatResultMessage(agent: RunningAgent): string {
|
|
117
|
-
const messages =
|
|
120
|
+
const messages = selectSquashedMessages(agent.conversationMessages);
|
|
118
121
|
const lines = [
|
|
119
|
-
|
|
122
|
+
SQUASHED_CONVERSATION_PREFACE,
|
|
120
123
|
`<user_agent model="${escapeAttribute(agent.model)}" inherited_context="${agent.inheritedContext}">`,
|
|
121
124
|
];
|
|
122
125
|
let firstUserMessage = true;
|
|
@@ -135,7 +138,7 @@ export function formatResultMessage(agent: RunningAgent): string {
|
|
|
135
138
|
return lines.join("\n");
|
|
136
139
|
}
|
|
137
140
|
|
|
138
|
-
function roleMessageText(message:
|
|
141
|
+
function roleMessageText(message: SquashedMessage): string {
|
|
139
142
|
if (typeof message.content === "string") return message.content;
|
|
140
143
|
return message.content
|
|
141
144
|
.flatMap((part) => (part.type === "text" ? [part.text] : []))
|
|
@@ -214,11 +217,42 @@ export function registerUserAgentRenderer(pi: ExtensionAPI): void {
|
|
|
214
217
|
)
|
|
215
218
|
: undefined,
|
|
216
219
|
);
|
|
220
|
+
// Pi's chat container does not pad renderer components; its own chat lines self-pad with paddingX 1.
|
|
217
221
|
pi.registerEntryRenderer<DetachedEntryData>(DETACHED_ENTRY_TYPE, (entry, _state, theme) =>
|
|
218
222
|
entry.data
|
|
219
|
-
? new Text(theme.fg("dim", `Detached session ${entry.data.sessionId}`),
|
|
223
|
+
? new Text(theme.fg("dim", `Detached session ${entry.data.sessionId}`), 1, 0)
|
|
220
224
|
: undefined,
|
|
221
225
|
);
|
|
226
|
+
pi.registerEntryRenderer<AttachedEntryData>(ATTACHED_ENTRY_TYPE, (entry, _state, theme) =>
|
|
227
|
+
entry.data
|
|
228
|
+
? new Text(theme.fg("dim", `Attached session ${entry.data.sessionId}`), 1, 0)
|
|
229
|
+
: undefined,
|
|
230
|
+
);
|
|
231
|
+
pi.registerEntryRenderer<RebasedEntryData>(REBASED_ENTRY_TYPE, (entry, _state, theme) =>
|
|
232
|
+
entry.data ? new Text(theme.fg("dim", formatRebasedLine(entry.data)), 1, 0) : undefined,
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function formatRebasedLine(data: RebasedEntryData): string {
|
|
237
|
+
const base = `Rebased session ${data.sessionId} into this conversation`;
|
|
238
|
+
if (!data.stats) return base;
|
|
239
|
+
const parts = [
|
|
240
|
+
`added ${data.stats.messageCount} message${data.stats.messageCount === 1 ? "" : "s"}`,
|
|
241
|
+
`~${formatTokenCount(data.stats.tokenEstimate)} tokens`,
|
|
242
|
+
];
|
|
243
|
+
if (data.stats.compactionCount > 0)
|
|
244
|
+
parts.push(
|
|
245
|
+
`${data.stats.compactionCount} compaction event${data.stats.compactionCount === 1 ? "" : "s"}`,
|
|
246
|
+
);
|
|
247
|
+
return `${base} (${parts.join(", ")})`;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* @example formatTokenCount(135_264) // "135K"
|
|
252
|
+
* @example formatTokenCount(12) // "12"
|
|
253
|
+
*/
|
|
254
|
+
function formatTokenCount(tokens: number): string {
|
|
255
|
+
return tokens < 1000 ? `${tokens}` : `${Math.round(tokens / 1000)}K`;
|
|
222
256
|
}
|
|
223
257
|
|
|
224
258
|
function liveAgentCard(
|
|
@@ -251,7 +285,7 @@ function renderAgentCard(
|
|
|
251
285
|
if (parts.length > 0) text += ` ${theme.fg("dim", "·")} ${theme.fg("dim", parts.join(" · "))}`;
|
|
252
286
|
if (task) text += `\n ${theme.fg("dim", `task: ${truncatePlain(task, 88)}`)}`;
|
|
253
287
|
const card = new Container();
|
|
254
|
-
card.addChild(new Text(text,
|
|
288
|
+
card.addChild(new Text(text, 1, 0));
|
|
255
289
|
card.addChild(body);
|
|
256
290
|
return card;
|
|
257
291
|
}
|
|
@@ -270,12 +304,12 @@ function renderAgentCardBody(
|
|
|
270
304
|
"No output.";
|
|
271
305
|
if (expanded)
|
|
272
306
|
return ok
|
|
273
|
-
? new Markdown(output,
|
|
274
|
-
: new Text(theme.fg("error", output),
|
|
307
|
+
? new Markdown(output, 3, 0, getMarkdownTheme())
|
|
308
|
+
: new Text(theme.fg("error", output), 3, 0);
|
|
275
309
|
|
|
276
310
|
const previewText = `⎿ ${truncatePlain(details?.responsePreview ?? output, 110)}`;
|
|
277
|
-
if (!ok) return new Text(` ${theme.fg("error", previewText)}`,
|
|
278
|
-
return new Markdown(previewText,
|
|
311
|
+
if (!ok) return new Text(` ${theme.fg("error", previewText)}`, 1, 0);
|
|
312
|
+
return new Markdown(previewText, 3, 0, getMarkdownTheme(), {
|
|
279
313
|
color: (value) => theme.fg("dim", value),
|
|
280
314
|
});
|
|
281
315
|
}
|
|
@@ -296,7 +330,7 @@ function buildRendererParts(content: string, details: AgentCommandDetails | unde
|
|
|
296
330
|
const parts: string[] = [];
|
|
297
331
|
if (details?.modelLabel) parts.push(details.modelLabel);
|
|
298
332
|
else if (details?.model) parts.push(details.model);
|
|
299
|
-
if (details) parts.push(contextLabel(details.inheritedContext));
|
|
333
|
+
if (details?.inheritedContext !== undefined) parts.push(contextLabel(details.inheritedContext));
|
|
300
334
|
const mainContext = mainContextLabel(details?.mainContextState);
|
|
301
335
|
if (mainContext) parts.push(mainContext);
|
|
302
336
|
if (details?.turnCount) parts.push(formatTurns(details.turnCount));
|
package/widget.ts
CHANGED
|
@@ -15,9 +15,11 @@ import {
|
|
|
15
15
|
type ViewableAgent,
|
|
16
16
|
} from "./agent-viewer.js";
|
|
17
17
|
import { renderAgentContextMeter } from "./context-meter.js";
|
|
18
|
+
import { selectRebaseMessages } from "./rebase.js";
|
|
18
19
|
import type {
|
|
19
20
|
AgentResultMessage,
|
|
20
21
|
CompletedAgent,
|
|
22
|
+
RebaseDelivery,
|
|
21
23
|
RunningAgent,
|
|
22
24
|
Theme,
|
|
23
25
|
UIContext,
|
|
@@ -25,6 +27,7 @@ import type {
|
|
|
25
27
|
import {
|
|
26
28
|
contextLabel,
|
|
27
29
|
formatMs,
|
|
30
|
+
TimedConfirmation,
|
|
28
31
|
formatToolUses,
|
|
29
32
|
formatTurns,
|
|
30
33
|
logSteering,
|
|
@@ -49,13 +52,15 @@ export class UserAgentWidget {
|
|
|
49
52
|
private active = false;
|
|
50
53
|
private selectedIndex = 0;
|
|
51
54
|
private viewerOpen = false;
|
|
52
|
-
|
|
55
|
+
/** One confirmation slot for detaching the selected agent. */
|
|
56
|
+
private readonly detachConfirmation = new TimedConfirmation<ViewableAgent>(() => this.update());
|
|
53
57
|
private readonly completedAgents: CompletedAgent[] = [];
|
|
54
58
|
|
|
55
59
|
constructor(
|
|
56
60
|
private readonly runningAgents: Set<RunningAgent>,
|
|
57
|
-
private readonly
|
|
61
|
+
private readonly sendSquashedResult: (message: AgentResultMessage) => void,
|
|
58
62
|
private readonly announceDetached: (sessionId: string) => void,
|
|
63
|
+
private readonly rebaseDelivery: RebaseDelivery,
|
|
59
64
|
) {}
|
|
60
65
|
|
|
61
66
|
setUI(ui: UIContext): void {
|
|
@@ -64,14 +69,14 @@ export class UserAgentWidget {
|
|
|
64
69
|
this.ui = ui;
|
|
65
70
|
this.widgetRegistered = false;
|
|
66
71
|
this.tui = undefined;
|
|
67
|
-
this.
|
|
72
|
+
this.detachConfirmation.cancel();
|
|
68
73
|
this.inputUnsub = ui.onTerminalInput((data) => this.handleKey(data));
|
|
69
74
|
}
|
|
70
75
|
|
|
71
76
|
addCompleted(
|
|
72
77
|
agent: RunningAgent,
|
|
73
78
|
resultMessage: AgentResultMessage,
|
|
74
|
-
options: {
|
|
79
|
+
options: { squashable: boolean },
|
|
75
80
|
): void {
|
|
76
81
|
this.completedAgents.push({
|
|
77
82
|
id: agent.id,
|
|
@@ -79,8 +84,9 @@ export class UserAgentWidget {
|
|
|
79
84
|
command: agent.command,
|
|
80
85
|
modelLabel: agent.modelLabel,
|
|
81
86
|
task: agent.task,
|
|
87
|
+
dispatchBaseFingerprint: agent.dispatchBaseFingerprint,
|
|
82
88
|
mainContextState: agent.mainContextState,
|
|
83
|
-
|
|
89
|
+
pendingSquashMessage: options.squashable ? resultMessage : undefined,
|
|
84
90
|
ok: resultMessage.details.ok,
|
|
85
91
|
responseText: agent.responseText,
|
|
86
92
|
messages: structuredClone(transcriptMessages(agent)),
|
|
@@ -142,7 +148,7 @@ export class UserAgentWidget {
|
|
|
142
148
|
this.widgetRegistered = false;
|
|
143
149
|
this.tui = undefined;
|
|
144
150
|
this.active = false;
|
|
145
|
-
this.
|
|
151
|
+
this.detachConfirmation.cancel();
|
|
146
152
|
}
|
|
147
153
|
|
|
148
154
|
private clear(): void {
|
|
@@ -157,7 +163,7 @@ export class UserAgentWidget {
|
|
|
157
163
|
}
|
|
158
164
|
this.active = false;
|
|
159
165
|
this.selectedIndex = 0;
|
|
160
|
-
this.
|
|
166
|
+
this.detachConfirmation.cancel();
|
|
161
167
|
}
|
|
162
168
|
|
|
163
169
|
private runningAgentsForWidget(): RunningAgent[] {
|
|
@@ -201,13 +207,13 @@ export class UserAgentWidget {
|
|
|
201
207
|
}
|
|
202
208
|
|
|
203
209
|
if (matchesKey(data, "down")) {
|
|
204
|
-
this.
|
|
210
|
+
this.detachConfirmation.cancel();
|
|
205
211
|
this.selectedIndex = Math.min(entries.length - 1, this.selectedIndex + 1);
|
|
206
212
|
this.update();
|
|
207
213
|
return { consume: true };
|
|
208
214
|
}
|
|
209
215
|
if (matchesKey(data, "up")) {
|
|
210
|
-
this.
|
|
216
|
+
this.detachConfirmation.cancel();
|
|
211
217
|
if (this.selectedIndex === 0) {
|
|
212
218
|
this.deactivate();
|
|
213
219
|
return { consume: true };
|
|
@@ -227,12 +233,12 @@ export class UserAgentWidget {
|
|
|
227
233
|
selected?.kind === "running" &&
|
|
228
234
|
isLiveAgent(selected.agent)
|
|
229
235
|
) {
|
|
230
|
-
this.
|
|
236
|
+
this.detachConfirmation.cancel();
|
|
231
237
|
this.interruptRunning(selected.agent);
|
|
232
238
|
return { consume: true };
|
|
233
239
|
}
|
|
234
240
|
if (matchesKey(data, "enter")) {
|
|
235
|
-
this.
|
|
241
|
+
this.detachConfirmation.cancel();
|
|
236
242
|
if (selected) this.openSelected(selected.agent);
|
|
237
243
|
return { consume: true };
|
|
238
244
|
}
|
|
@@ -248,22 +254,33 @@ export class UserAgentWidget {
|
|
|
248
254
|
}
|
|
249
255
|
|
|
250
256
|
private confirmDetach(target: ViewableAgent): boolean {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
}
|
|
255
|
-
this.detachConfirmationTarget = target;
|
|
256
|
-
this.update();
|
|
257
|
-
return false;
|
|
257
|
+
const confirmed = this.detachConfirmation.press(target);
|
|
258
|
+
if (!confirmed) this.update();
|
|
259
|
+
return confirmed;
|
|
258
260
|
}
|
|
259
261
|
|
|
260
262
|
private deactivate(): void {
|
|
261
263
|
this.active = false;
|
|
262
264
|
this.selectedIndex = 0;
|
|
263
|
-
this.
|
|
265
|
+
this.detachConfirmation.cancel();
|
|
264
266
|
this.update();
|
|
265
267
|
}
|
|
266
268
|
|
|
269
|
+
/** Session ids with a live row or completed card, matched by exact id or prefix. */
|
|
270
|
+
matchAttachedSessionIds(query: string): string[] {
|
|
271
|
+
return this.entries()
|
|
272
|
+
.map((entry) => entry.agent.sessionId)
|
|
273
|
+
.filter((sessionId) => sessionId.startsWith(query));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Open the overlay of the row holding this session, as if the user selected it. */
|
|
277
|
+
openViewer(sessionId: string): boolean {
|
|
278
|
+
const entry = this.entries().find((candidate) => candidate.agent.sessionId === sessionId);
|
|
279
|
+
if (!entry) return false;
|
|
280
|
+
this.openSelected(entry.agent);
|
|
281
|
+
return true;
|
|
282
|
+
}
|
|
283
|
+
|
|
267
284
|
private clampSelection(): void {
|
|
268
285
|
const total = this.entries().length;
|
|
269
286
|
if (total === 0) {
|
|
@@ -286,8 +303,12 @@ export class UserAgentWidget {
|
|
|
286
303
|
agent,
|
|
287
304
|
theme,
|
|
288
305
|
done,
|
|
289
|
-
() => this.
|
|
290
|
-
() => this.
|
|
306
|
+
() => this.canSquashMainContext(agent.id),
|
|
307
|
+
() => this.squashMainContext(agent.id),
|
|
308
|
+
() => this.canRebaseMainContext(agent.id),
|
|
309
|
+
() => this.rebaseMainContext(agent.id),
|
|
310
|
+
() => this.rebaseBlockReason(agent.id),
|
|
311
|
+
() => this.rebaseDetachCount(agent.id),
|
|
291
312
|
() => {
|
|
292
313
|
if (isRunningAgent(agent)) this.interruptRunning(agent);
|
|
293
314
|
},
|
|
@@ -319,54 +340,107 @@ export class UserAgentWidget {
|
|
|
319
340
|
return [...this.runningAgents].find((agent) => agent.id === agentId && agent.status === "idle");
|
|
320
341
|
}
|
|
321
342
|
|
|
322
|
-
private
|
|
323
|
-
|
|
324
|
-
return this.completedAgents.some(
|
|
325
|
-
(agent) => agent.id === agentId && agent.pendingJoinMessage !== undefined,
|
|
326
|
-
);
|
|
343
|
+
private canSquashMainContext(agentId: string): boolean {
|
|
344
|
+
return this.deliverableAgent(agentId) !== undefined;
|
|
327
345
|
}
|
|
328
346
|
|
|
329
|
-
|
|
347
|
+
confirmMainContextSquash(agentId: string): boolean {
|
|
330
348
|
const agents = [
|
|
331
349
|
...[...this.runningAgents].filter((candidate) => candidate.id === agentId),
|
|
332
350
|
...this.completedAgents.filter((candidate) => candidate.id === agentId),
|
|
333
|
-
].filter((candidate) => candidate.mainContextState === "will-
|
|
351
|
+
].filter((candidate) => candidate.mainContextState === "will-squash");
|
|
334
352
|
if (agents.length === 0) return false;
|
|
335
|
-
for (const agent of agents) agent.mainContextState = "
|
|
353
|
+
for (const agent of agents) agent.mainContextState = "squashed";
|
|
336
354
|
this.update();
|
|
337
355
|
return true;
|
|
338
356
|
}
|
|
339
357
|
|
|
340
|
-
private
|
|
358
|
+
private squashMainContext(agentId: string): void {
|
|
341
359
|
const idleAgent = this.idleAgent(agentId);
|
|
342
360
|
if (idleAgent) {
|
|
343
|
-
this.
|
|
361
|
+
this.squashIdleAgent(idleAgent);
|
|
344
362
|
return;
|
|
345
363
|
}
|
|
346
364
|
const completedAgent = this.completedAgents.find((agent) => agent.id === agentId);
|
|
347
|
-
const message = completedAgent?.
|
|
365
|
+
const message = completedAgent?.pendingSquashMessage;
|
|
348
366
|
if (!completedAgent || !message) return;
|
|
349
367
|
|
|
350
|
-
completedAgent.
|
|
351
|
-
completedAgent.mainContextState = "will-
|
|
352
|
-
message.details.mainContextState = "will-
|
|
353
|
-
this.
|
|
354
|
-
logSteering(agentId, "
|
|
368
|
+
completedAgent.pendingSquashMessage = undefined;
|
|
369
|
+
completedAgent.mainContextState = "will-squash";
|
|
370
|
+
message.details.mainContextState = "will-squash";
|
|
371
|
+
this.sendSquashedResult(message);
|
|
372
|
+
logSteering(agentId, "squashed-main-context", { display: message.display });
|
|
373
|
+
this.update();
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** An agent whose latest turn result is still deliverable: idle live, or completed and squashable. */
|
|
377
|
+
private deliverableAgent(agentId: string): RunningAgent | CompletedAgent | undefined {
|
|
378
|
+
const idleAgent = this.idleAgent(agentId);
|
|
379
|
+
if (idleAgent?.pendingSquashMessage) return idleAgent;
|
|
380
|
+
return this.completedAgents.find(
|
|
381
|
+
(agent) => agent.id === agentId && agent.pendingSquashMessage !== undefined,
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
private canRebaseMainContext(agentId: string): boolean {
|
|
386
|
+
return this.deliverableAgent(agentId) !== undefined && this.rebaseBlockReason(agentId) === undefined;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/** How many other agent sessions the rebase cascade would detach. */
|
|
390
|
+
private rebaseDetachCount(agentId: string): number {
|
|
391
|
+
return (
|
|
392
|
+
[...this.runningAgents].filter((agent) => agent.id !== agentId).length +
|
|
393
|
+
this.completedAgents.filter((agent) => agent.id !== agentId).length
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/** Why a deliverable result cannot rebase right now; undefined when it can, or when nothing is deliverable. */
|
|
398
|
+
private rebaseBlockReason(agentId: string): string | undefined {
|
|
399
|
+
const agent = this.deliverableAgent(agentId);
|
|
400
|
+
if (!agent) return undefined;
|
|
401
|
+
// The session switch tears the extension runtime down, which would abort a mid-turn sibling.
|
|
402
|
+
if ([...this.runningAgents].some(isLiveAgent)) return "another agent is mid-turn";
|
|
403
|
+
if (!this.rebaseDelivery.canDeliver(agent))
|
|
404
|
+
return "the main session has drifted since dispatch";
|
|
405
|
+
return undefined;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/** Fast-forward the raw child conversation onto the main session, then retire the agent. */
|
|
409
|
+
private rebaseMainContext(agentId: string): void {
|
|
410
|
+
if (!this.canRebaseMainContext(agentId)) return;
|
|
411
|
+
const agent = this.deliverableAgent(agentId);
|
|
412
|
+
if (!agent) return;
|
|
413
|
+
// The reload will not preserve live rows; give every sibling its resumable-session breadcrumb.
|
|
414
|
+
for (const other of [...this.runningAgents]) if (other !== agent) this.closeRunning(other);
|
|
415
|
+
for (const other of [...this.completedAgents]) if (other !== agent) this.removeCompleted(other);
|
|
416
|
+
const message = agent.pendingSquashMessage;
|
|
417
|
+
this.rebaseDelivery.deliver(
|
|
418
|
+
agent,
|
|
419
|
+
selectRebaseMessages(agent.task, structuredClone(transcriptMessages(agent))),
|
|
420
|
+
);
|
|
421
|
+
agent.pendingSquashMessage = undefined;
|
|
422
|
+
agent.mainContextState = "rebased";
|
|
423
|
+
if (isRunningAgent(agent)) {
|
|
424
|
+
agent.status = "posted";
|
|
425
|
+
if (message) this.addCompleted(agent, message, { squashable: false });
|
|
426
|
+
agent.retire?.();
|
|
427
|
+
}
|
|
428
|
+
logSteering(agentId, "rebased-main-context");
|
|
355
429
|
this.update();
|
|
356
430
|
}
|
|
357
431
|
|
|
358
|
-
/**
|
|
359
|
-
private
|
|
360
|
-
const message = agent.
|
|
432
|
+
/** Squashing is one of the two terminal actions on a live idle agent: deliver the result, snapshot it, retire the session. */
|
|
433
|
+
private squashIdleAgent(agent: RunningAgent): void {
|
|
434
|
+
const message = agent.pendingSquashMessage;
|
|
361
435
|
if (!message) return;
|
|
362
|
-
agent.
|
|
363
|
-
agent.mainContextState = "will-
|
|
364
|
-
message.details.mainContextState = "will-
|
|
436
|
+
agent.pendingSquashMessage = undefined;
|
|
437
|
+
agent.mainContextState = "will-squash";
|
|
438
|
+
message.details.mainContextState = "will-squash";
|
|
365
439
|
agent.status = "posted";
|
|
366
|
-
this.addCompleted(agent, message, {
|
|
367
|
-
this.
|
|
440
|
+
this.addCompleted(agent, message, { squashable: false });
|
|
441
|
+
this.sendSquashedResult(message);
|
|
368
442
|
agent.retire?.();
|
|
369
|
-
logSteering(agent.id, "
|
|
443
|
+
logSteering(agent.id, "squashed-main-context", { display: message.display, live: true });
|
|
370
444
|
this.update();
|
|
371
445
|
}
|
|
372
446
|
|
|
@@ -418,7 +492,8 @@ export class UserAgentWidget {
|
|
|
418
492
|
lines.push(truncateToWidth(` ${theme.fg("dim", "←/↓ select agent")}`, width));
|
|
419
493
|
} else {
|
|
420
494
|
const selected = entries[this.selectedIndex];
|
|
421
|
-
const confirmingDetach =
|
|
495
|
+
const confirmingDetach =
|
|
496
|
+
selected !== undefined && this.detachConfirmation.isArmedOn(selected.agent);
|
|
422
497
|
const segments = [theme.fg("accent", "↑↓ select"), theme.fg("accent", "Enter view")];
|
|
423
498
|
if (
|
|
424
499
|
!confirmingDetach &&
|