@yandy0725/pi-subagents 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/README.md +155 -0
- package/README.zh.md +155 -0
- package/index.ts +1 -0
- package/package.json +49 -0
- package/src/config/agent-types.ts +127 -0
- package/src/config/custom-agents.ts +109 -0
- package/src/config/default-agents.ts +117 -0
- package/src/config/invocation-config.ts +30 -0
- package/src/debug.ts +14 -0
- package/src/handlers/index.ts +3 -0
- package/src/handlers/interrupt.ts +49 -0
- package/src/handlers/lifecycle.ts +63 -0
- package/src/handlers/tool-start.ts +32 -0
- package/src/index.ts +186 -0
- package/src/layered-settings.ts +105 -0
- package/src/lifecycle/child-lifecycle.ts +88 -0
- package/src/lifecycle/concurrency-limiter.ts +55 -0
- package/src/lifecycle/create-subagent-session.ts +240 -0
- package/src/lifecycle/parent-snapshot.ts +45 -0
- package/src/lifecycle/run-listeners.ts +37 -0
- package/src/lifecycle/subagent-manager.ts +353 -0
- package/src/lifecycle/subagent-session.ts +232 -0
- package/src/lifecycle/subagent-state.ts +216 -0
- package/src/lifecycle/subagent.ts +498 -0
- package/src/lifecycle/turn-limits.ts +13 -0
- package/src/lifecycle/usage.ts +65 -0
- package/src/lifecycle/workspace-bracket.ts +59 -0
- package/src/lifecycle/workspace.ts +47 -0
- package/src/observation/composite-subagent-observer.ts +49 -0
- package/src/observation/notification-state.ts +27 -0
- package/src/observation/notification.ts +186 -0
- package/src/observation/record-observer.ts +75 -0
- package/src/observation/renderer.ts +63 -0
- package/src/observation/subagent-events-observer.ts +94 -0
- package/src/runtime.ts +77 -0
- package/src/service/service-adapter.ts +131 -0
- package/src/service/service.ts +123 -0
- package/src/session/content-items.ts +51 -0
- package/src/session/context.ts +78 -0
- package/src/session/conversation.ts +44 -0
- package/src/session/env.ts +40 -0
- package/src/session/model-resolver.ts +121 -0
- package/src/session/prompts.ts +83 -0
- package/src/session/session-config.ts +172 -0
- package/src/session/session-dir.ts +38 -0
- package/src/settings.ts +227 -0
- package/src/tools/agent-tool.ts +220 -0
- package/src/tools/background-spawner.ts +66 -0
- package/src/tools/foreground-runner.ts +114 -0
- package/src/tools/get-result-tool.ts +120 -0
- package/src/tools/helpers.ts +105 -0
- package/src/tools/result-renderer.ts +109 -0
- package/src/tools/spawn-config.ts +150 -0
- package/src/tools/steer-tool.ts +90 -0
- package/src/types.ts +115 -0
- package/src/ui/agent-widget.ts +311 -0
- package/src/ui/display.ts +174 -0
- package/src/ui/session-navigation.ts +147 -0
- package/src/ui/session-navigator.ts +406 -0
- package/src/ui/subagents-settings.ts +77 -0
- package/src/ui/widget-renderer.ts +296 -0
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* session-navigator.ts — The `/subagents:sessions` command: pick a subagent and
|
|
3
|
+
* read its transcript through Pi's own per-entry session components.
|
|
4
|
+
*
|
|
5
|
+
* SDK/TUI consumer half of native session navigation. The unit-testable core
|
|
6
|
+
* (selection, sourcing) lives in `session-navigation.ts`; this module wires that
|
|
7
|
+
* core to the command picker and a read-only scrollable overlay, and owns the
|
|
8
|
+
* renderer — it mounts Pi's interactive components (`AssistantMessageComponent`,
|
|
9
|
+
* `ToolExecutionComponent`, …) into a `Container`, mirroring Pi's own
|
|
10
|
+
* `renderSessionContext` mapping. Rendering lives here, not in the pure module,
|
|
11
|
+
* because the components require a `TUI`, `cwd`, and markdown theme.
|
|
12
|
+
*
|
|
13
|
+
* The overlay is strictly read-only — steering stays in the `steer_subagent` tool
|
|
14
|
+
* and the widget. It consumes a `TranscriptSource`, so the evicted-agent-source
|
|
15
|
+
* follow-up swaps the source without touching the renderer or the overlay.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
AssistantMessageComponent,
|
|
20
|
+
BashExecutionComponent,
|
|
21
|
+
BranchSummaryMessageComponent,
|
|
22
|
+
CompactionSummaryMessageComponent,
|
|
23
|
+
getMarkdownTheme,
|
|
24
|
+
parseSkillBlock,
|
|
25
|
+
SkillInvocationMessageComponent,
|
|
26
|
+
type ToolDefinition,
|
|
27
|
+
ToolExecutionComponent,
|
|
28
|
+
UserMessageComponent,
|
|
29
|
+
} from "@earendil-works/pi-coding-agent";
|
|
30
|
+
import {
|
|
31
|
+
type Component,
|
|
32
|
+
Container,
|
|
33
|
+
type MarkdownTheme,
|
|
34
|
+
matchesKey,
|
|
35
|
+
Spacer,
|
|
36
|
+
type TUI,
|
|
37
|
+
truncateToWidth,
|
|
38
|
+
visibleWidth,
|
|
39
|
+
} from "@earendil-works/pi-tui";
|
|
40
|
+
import type { AgentConfigLookup } from "../config/agent-types";
|
|
41
|
+
import type { EvictedSubagent } from "../lifecycle/subagent-manager";
|
|
42
|
+
import type { SessionMessage } from "../types";
|
|
43
|
+
import { describeActivity, type Theme } from "../ui/display";
|
|
44
|
+
import {
|
|
45
|
+
fileSnapshotSource,
|
|
46
|
+
listNavigableAgents,
|
|
47
|
+
liveSource,
|
|
48
|
+
type NavigableSubagent,
|
|
49
|
+
type TranscriptSource,
|
|
50
|
+
} from "../ui/session-navigation";
|
|
51
|
+
|
|
52
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
/** Chrome lines: top border + header + header sep + footer sep + footer + bottom border. */
|
|
55
|
+
const CHROME_LINES = 6;
|
|
56
|
+
const MIN_VIEWPORT = 3;
|
|
57
|
+
const VIEWPORT_HEIGHT_PCT = 70;
|
|
58
|
+
|
|
59
|
+
/** Component factory shape Pi's `ui.custom` invokes to mount an overlay. */
|
|
60
|
+
export type OverlayComponentFactory<R> = (
|
|
61
|
+
tui: TUI,
|
|
62
|
+
theme: Theme,
|
|
63
|
+
keybindings: unknown,
|
|
64
|
+
done: (result: R) => void,
|
|
65
|
+
) => Component;
|
|
66
|
+
|
|
67
|
+
/** Narrow UI interface — only the `ctx.ui` methods the navigator calls. */
|
|
68
|
+
export interface SessionNavigatorUI {
|
|
69
|
+
select(title: string, options: string[]): Promise<string | undefined>;
|
|
70
|
+
notify(message: string, level: "info" | "warning" | "error"): void;
|
|
71
|
+
custom<R>(component: OverlayComponentFactory<R>, options?: unknown): Promise<R>;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Parameters for one `/subagents:sessions` invocation. */
|
|
75
|
+
export interface SessionNavigatorParams {
|
|
76
|
+
ui: SessionNavigatorUI;
|
|
77
|
+
agents: readonly NavigableSubagent[];
|
|
78
|
+
/** Descriptors of agents evicted by the cleanup sweep, sourced from disk when picked. */
|
|
79
|
+
evicted: readonly EvictedSubagent[];
|
|
80
|
+
registry: AgentConfigLookup;
|
|
81
|
+
/** Working directory for tool-call rendering (relative path display). */
|
|
82
|
+
cwd: string;
|
|
83
|
+
/** Reads a persisted session file for the file-snapshot source. */
|
|
84
|
+
readFile: (path: string) => string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Options for the read-only transcript overlay. */
|
|
88
|
+
export interface TranscriptOverlayOptions {
|
|
89
|
+
tui: TUI;
|
|
90
|
+
theme: Theme;
|
|
91
|
+
source: TranscriptSource;
|
|
92
|
+
done: (result: undefined) => void;
|
|
93
|
+
cwd: string;
|
|
94
|
+
markdownTheme: MarkdownTheme;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Handler for the `/subagents:sessions` slash command.
|
|
99
|
+
*
|
|
100
|
+
* Lists navigable subagents, lets the operator pick one, and opens its transcript
|
|
101
|
+
* read-only. Receives the agent snapshot (`manager.listAgents()`) rather than the
|
|
102
|
+
* manager, so it stays a reactive consumer with no inbound call into the core.
|
|
103
|
+
*/
|
|
104
|
+
export class SessionNavigatorHandler {
|
|
105
|
+
async handle({ ui, agents, evicted, registry, cwd, readFile }: SessionNavigatorParams): Promise<void> {
|
|
106
|
+
const entries = listNavigableAgents(agents, evicted, registry);
|
|
107
|
+
if (entries.length === 0) {
|
|
108
|
+
ui.notify("No subagent sessions to view.", "info");
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const choice = await ui.select(
|
|
113
|
+
"Subagent sessions",
|
|
114
|
+
entries.map((entry) => entry.label),
|
|
115
|
+
);
|
|
116
|
+
const entry = entries.find((candidate) => candidate.label === choice);
|
|
117
|
+
if (!entry) return;
|
|
118
|
+
|
|
119
|
+
let source: TranscriptSource;
|
|
120
|
+
try {
|
|
121
|
+
source = entry.kind === "live" ? liveSource(entry.record) : fileSnapshotSource(entry.outputFile, readFile);
|
|
122
|
+
} catch {
|
|
123
|
+
ui.notify("Could not read the session transcript file.", "error");
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const markdownTheme = getMarkdownTheme();
|
|
127
|
+
await ui.custom<undefined>(
|
|
128
|
+
(tui, theme, _keybindings, done) => new TranscriptOverlay({ tui, theme, source, done, cwd, markdownTheme }),
|
|
129
|
+
{
|
|
130
|
+
overlay: true,
|
|
131
|
+
overlayOptions: { anchor: "center", width: "90%", maxHeight: `${VIEWPORT_HEIGHT_PCT}%` },
|
|
132
|
+
},
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Read-only scrollable transcript overlay.
|
|
139
|
+
*
|
|
140
|
+
* Caches a `Container` of Pi's per-entry components and rebuilds it only when the
|
|
141
|
+
* source changes (live agents) — each paint reuses the cached tree, so markdown
|
|
142
|
+
* highlighting does not re-run per frame. This class owns scroll state, chrome,
|
|
143
|
+
* and the running-agent streaming indicator; the component mapping lives in
|
|
144
|
+
* `buildTranscriptComponents`.
|
|
145
|
+
*/
|
|
146
|
+
export class TranscriptOverlay implements Component {
|
|
147
|
+
private scrollOffset = 0;
|
|
148
|
+
private autoScroll = true;
|
|
149
|
+
private unsubscribe: (() => void) | undefined;
|
|
150
|
+
private closed = false;
|
|
151
|
+
|
|
152
|
+
private readonly tui: TUI;
|
|
153
|
+
private readonly theme: Theme;
|
|
154
|
+
private readonly source: TranscriptSource;
|
|
155
|
+
private readonly done: (result: undefined) => void;
|
|
156
|
+
private readonly cwd: string;
|
|
157
|
+
private readonly markdownTheme: MarkdownTheme;
|
|
158
|
+
private content: Container;
|
|
159
|
+
|
|
160
|
+
constructor({ tui, theme, source, done, cwd, markdownTheme }: TranscriptOverlayOptions) {
|
|
161
|
+
this.tui = tui;
|
|
162
|
+
this.theme = theme;
|
|
163
|
+
this.source = source;
|
|
164
|
+
this.done = done;
|
|
165
|
+
this.cwd = cwd;
|
|
166
|
+
this.markdownTheme = markdownTheme;
|
|
167
|
+
this.content = this.rebuild();
|
|
168
|
+
this.unsubscribe = source.subscribe(() => {
|
|
169
|
+
if (this.closed) return;
|
|
170
|
+
this.content = this.rebuild();
|
|
171
|
+
this.tui.requestRender();
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// fallow-ignore-next-line unused-class-member
|
|
176
|
+
handleInput(data: string): void {
|
|
177
|
+
if (matchesKey(data, "escape") || matchesKey(data, "q")) {
|
|
178
|
+
this.closed = true;
|
|
179
|
+
this.done(undefined);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const totalLines = this.buildContentLines(this.innerWidth()).length;
|
|
184
|
+
const viewportHeight = this.viewportHeight();
|
|
185
|
+
const maxScroll = Math.max(0, totalLines - viewportHeight);
|
|
186
|
+
|
|
187
|
+
if (matchesKey(data, "up") || matchesKey(data, "k")) {
|
|
188
|
+
this.scrollOffset = Math.max(0, this.scrollOffset - 1);
|
|
189
|
+
this.autoScroll = this.scrollOffset >= maxScroll;
|
|
190
|
+
} else if (matchesKey(data, "down") || matchesKey(data, "j")) {
|
|
191
|
+
this.scrollOffset = Math.min(maxScroll, this.scrollOffset + 1);
|
|
192
|
+
this.autoScroll = this.scrollOffset >= maxScroll;
|
|
193
|
+
} else if (matchesKey(data, "pageUp") || matchesKey(data, "shift+up")) {
|
|
194
|
+
this.scrollOffset = Math.max(0, this.scrollOffset - viewportHeight);
|
|
195
|
+
this.autoScroll = false;
|
|
196
|
+
} else if (matchesKey(data, "pageDown") || matchesKey(data, "shift+down")) {
|
|
197
|
+
this.scrollOffset = Math.min(maxScroll, this.scrollOffset + viewportHeight);
|
|
198
|
+
this.autoScroll = this.scrollOffset >= maxScroll;
|
|
199
|
+
} else if (matchesKey(data, "home")) {
|
|
200
|
+
this.scrollOffset = 0;
|
|
201
|
+
this.autoScroll = false;
|
|
202
|
+
} else if (matchesKey(data, "end")) {
|
|
203
|
+
this.scrollOffset = maxScroll;
|
|
204
|
+
this.autoScroll = true;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
render(width: number): string[] {
|
|
209
|
+
if (width < 6) return [];
|
|
210
|
+
const th = this.theme;
|
|
211
|
+
const innerW = width - 4;
|
|
212
|
+
const lines: string[] = [];
|
|
213
|
+
|
|
214
|
+
const pad = (s: string, len: number): string => s + " ".repeat(Math.max(0, len - visibleWidth(s)));
|
|
215
|
+
const row = (content: string): string =>
|
|
216
|
+
th.fg("border", "│") + " " + truncateToWidth(pad(content, innerW), innerW) + " " + th.fg("border", "│");
|
|
217
|
+
const hrTop = th.fg("border", `╭${"─".repeat(width - 2)}╮`);
|
|
218
|
+
const hrBot = th.fg("border", `╰${"─".repeat(width - 2)}╯`);
|
|
219
|
+
const hrMid = row(th.fg("dim", "─".repeat(innerW)));
|
|
220
|
+
|
|
221
|
+
lines.push(hrTop);
|
|
222
|
+
lines.push(row(th.bold("Subagent session")));
|
|
223
|
+
lines.push(hrMid);
|
|
224
|
+
|
|
225
|
+
const contentLines = this.buildContentLines(innerW);
|
|
226
|
+
const viewportHeight = this.viewportHeight();
|
|
227
|
+
const maxScroll = Math.max(0, contentLines.length - viewportHeight);
|
|
228
|
+
if (this.autoScroll) this.scrollOffset = maxScroll;
|
|
229
|
+
const visibleStart = Math.min(this.scrollOffset, maxScroll);
|
|
230
|
+
const visible = contentLines.slice(visibleStart, visibleStart + viewportHeight);
|
|
231
|
+
for (let i = 0; i < viewportHeight; i++) lines.push(row(visible[i] ?? ""));
|
|
232
|
+
|
|
233
|
+
lines.push(hrMid);
|
|
234
|
+
const scrollPct =
|
|
235
|
+
contentLines.length <= viewportHeight
|
|
236
|
+
? "100%"
|
|
237
|
+
: `${Math.round(((visibleStart + viewportHeight) / contentLines.length) * 100)}%`;
|
|
238
|
+
const footerLeft = th.fg("dim", `${contentLines.length} lines · ${scrollPct}`);
|
|
239
|
+
const footerRight = th.fg("dim", "↑↓ scroll · PgUp/PgDn · Esc close");
|
|
240
|
+
const footerGap = Math.max(1, innerW - visibleWidth(footerLeft) - visibleWidth(footerRight));
|
|
241
|
+
lines.push(row(footerLeft + " ".repeat(footerGap) + footerRight));
|
|
242
|
+
lines.push(hrBot);
|
|
243
|
+
|
|
244
|
+
return lines;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// fallow-ignore-next-line unused-class-member
|
|
248
|
+
invalidate(): void {
|
|
249
|
+
this.content.invalidate();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// fallow-ignore-next-line unused-class-member
|
|
253
|
+
dispose(): void {
|
|
254
|
+
this.closed = true;
|
|
255
|
+
if (this.unsubscribe) {
|
|
256
|
+
this.unsubscribe();
|
|
257
|
+
this.unsubscribe = undefined;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ---- Private ----
|
|
262
|
+
|
|
263
|
+
private innerWidth(): number {
|
|
264
|
+
return Math.max(0, this.tui.terminal.columns - 4);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
private viewportHeight(): number {
|
|
268
|
+
const maxRows = Math.floor((this.tui.terminal.rows * VIEWPORT_HEIGHT_PCT) / 100);
|
|
269
|
+
return Math.max(MIN_VIEWPORT, maxRows - CHROME_LINES);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
private buildContentLines(innerW: number): string[] {
|
|
273
|
+
if (innerW <= 0) return [];
|
|
274
|
+
const lines = this.content.render(innerW);
|
|
275
|
+
const streaming = this.source.streaming();
|
|
276
|
+
if (streaming) {
|
|
277
|
+
lines.push("", `◍ ${describeActivity(streaming.activeTools, streaming.responseText)}`);
|
|
278
|
+
}
|
|
279
|
+
return lines.map((l) => truncateToWidth(l, innerW));
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
private rebuild(): Container {
|
|
283
|
+
return buildTranscriptComponents(this.source.getMessages(), {
|
|
284
|
+
tui: this.tui,
|
|
285
|
+
cwd: this.cwd,
|
|
286
|
+
markdownTheme: this.markdownTheme,
|
|
287
|
+
getToolDefinition: (name) => this.source.getToolDefinition(name),
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** Dependencies the per-entry component tree needs from the SDK/TUI environment. */
|
|
293
|
+
interface TranscriptRenderOptions {
|
|
294
|
+
tui: TUI;
|
|
295
|
+
cwd: string;
|
|
296
|
+
markdownTheme: MarkdownTheme;
|
|
297
|
+
getToolDefinition: (name: string) => ToolDefinition | undefined;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Build a `Container` of Pi's per-entry components from a message snapshot,
|
|
302
|
+
* mirroring Pi's own interactive-mode `renderSessionContext` mapping. Tool
|
|
303
|
+
* results are matched to their tool-call components by id, exactly as Pi does.
|
|
304
|
+
* `custom`-role messages are skipped — rendering them needs the child session's
|
|
305
|
+
* message-renderer registry, which the navigator does not hold.
|
|
306
|
+
*/
|
|
307
|
+
function buildTranscriptComponents(messages: readonly SessionMessage[], opts: TranscriptRenderOptions): Container {
|
|
308
|
+
const container = new Container();
|
|
309
|
+
const pendingTools = new Map<string, ToolExecutionComponent>();
|
|
310
|
+
for (const message of messages) {
|
|
311
|
+
addMessageComponents(container, message, pendingTools, opts);
|
|
312
|
+
}
|
|
313
|
+
return container;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function addMessageComponents(
|
|
317
|
+
container: Container,
|
|
318
|
+
message: SessionMessage,
|
|
319
|
+
pendingTools: Map<string, ToolExecutionComponent>,
|
|
320
|
+
opts: TranscriptRenderOptions,
|
|
321
|
+
): void {
|
|
322
|
+
switch (message.role) {
|
|
323
|
+
case "assistant": {
|
|
324
|
+
container.addChild(new AssistantMessageComponent(message, false, opts.markdownTheme));
|
|
325
|
+
for (const content of message.content) {
|
|
326
|
+
if (content.type !== "toolCall") continue;
|
|
327
|
+
const tool = new ToolExecutionComponent(
|
|
328
|
+
content.name,
|
|
329
|
+
content.id,
|
|
330
|
+
content.arguments,
|
|
331
|
+
{ showImages: false },
|
|
332
|
+
opts.getToolDefinition(content.name),
|
|
333
|
+
opts.tui,
|
|
334
|
+
opts.cwd,
|
|
335
|
+
);
|
|
336
|
+
tool.setExpanded(true);
|
|
337
|
+
container.addChild(tool);
|
|
338
|
+
pendingTools.set(content.id, tool);
|
|
339
|
+
}
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
342
|
+
case "toolResult": {
|
|
343
|
+
pendingTools.get(message.toolCallId)?.updateResult(message);
|
|
344
|
+
pendingTools.delete(message.toolCallId);
|
|
345
|
+
break;
|
|
346
|
+
}
|
|
347
|
+
case "user": {
|
|
348
|
+
addUserComponents(container, message.content, opts.markdownTheme);
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
case "bashExecution": {
|
|
352
|
+
const bash = new BashExecutionComponent(message.command, opts.tui, message.excludeFromContext);
|
|
353
|
+
if (message.output) bash.appendOutput(message.output);
|
|
354
|
+
bash.setComplete(message.exitCode, message.cancelled, undefined, message.fullOutputPath);
|
|
355
|
+
container.addChild(bash);
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
358
|
+
case "compactionSummary": {
|
|
359
|
+
container.addChild(new Spacer(1));
|
|
360
|
+
const summary = new CompactionSummaryMessageComponent(message, opts.markdownTheme);
|
|
361
|
+
summary.setExpanded(true);
|
|
362
|
+
container.addChild(summary);
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
365
|
+
case "branchSummary": {
|
|
366
|
+
container.addChild(new Spacer(1));
|
|
367
|
+
const summary = new BranchSummaryMessageComponent(message, opts.markdownTheme);
|
|
368
|
+
summary.setExpanded(true);
|
|
369
|
+
container.addChild(summary);
|
|
370
|
+
break;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/** Render a user message (skill block + text) into the container, mirroring Pi. */
|
|
376
|
+
function addUserComponents(
|
|
377
|
+
container: Container,
|
|
378
|
+
content: string | readonly { type: string; text?: string }[],
|
|
379
|
+
markdownTheme: MarkdownTheme,
|
|
380
|
+
): void {
|
|
381
|
+
const text = userMessageText(content);
|
|
382
|
+
if (!text) return;
|
|
383
|
+
if (container.children.length > 0) container.addChild(new Spacer(1));
|
|
384
|
+
|
|
385
|
+
const skillBlock = parseSkillBlock(text);
|
|
386
|
+
if (!skillBlock) {
|
|
387
|
+
container.addChild(new UserMessageComponent(text, markdownTheme));
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
const skill = new SkillInvocationMessageComponent(skillBlock, markdownTheme);
|
|
391
|
+
skill.setExpanded(true);
|
|
392
|
+
container.addChild(skill);
|
|
393
|
+
if (skillBlock.userMessage) {
|
|
394
|
+
container.addChild(new Spacer(1));
|
|
395
|
+
container.addChild(new UserMessageComponent(skillBlock.userMessage, markdownTheme));
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/** Concatenate the text blocks of a user message's content (mirrors Pi). */
|
|
400
|
+
function userMessageText(content: string | readonly { type: string; text?: string }[]): string {
|
|
401
|
+
if (typeof content === "string") return content;
|
|
402
|
+
return content
|
|
403
|
+
.filter((block) => block.type === "text")
|
|
404
|
+
.map((block) => block.text ?? "")
|
|
405
|
+
.join("");
|
|
406
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// ---- Narrow interfaces ----
|
|
2
|
+
|
|
3
|
+
/** Narrow settings interface required by the subagents:settings command. */
|
|
4
|
+
export interface SubagentsSettingsManager {
|
|
5
|
+
readonly maxConcurrent: number;
|
|
6
|
+
readonly defaultMaxTurns: number | undefined;
|
|
7
|
+
readonly graceTurns: number;
|
|
8
|
+
applyMaxConcurrent(n: number): { message: string; level: "info" | "warning" };
|
|
9
|
+
applyDefaultMaxTurns(n: number): { message: string; level: "info" | "warning" };
|
|
10
|
+
applyGraceTurns(n: number): { message: string; level: "info" | "warning" };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Narrow UI interface — only the ctx.ui methods the settings handler calls. */
|
|
14
|
+
export interface SubagentsSettingsUI {
|
|
15
|
+
select(title: string, options: string[]): Promise<string | undefined>;
|
|
16
|
+
input(title: string, defaultValue?: string): Promise<string | undefined>;
|
|
17
|
+
notify(message: string, level: "info" | "warning" | "error"): void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// ---- Class ----
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Handler for the `/subagents:settings` slash command.
|
|
24
|
+
*
|
|
25
|
+
* Call `handle({ ui })` from the Pi command registration to open the interactive
|
|
26
|
+
* settings list. Lifted from `AgentsMenuHandler.showSettings`.
|
|
27
|
+
*/
|
|
28
|
+
export class SubagentsSettingsHandler {
|
|
29
|
+
constructor(private readonly settings: SubagentsSettingsManager) {}
|
|
30
|
+
|
|
31
|
+
async handle({ ui }: { ui: SubagentsSettingsUI }): Promise<void> {
|
|
32
|
+
const choice = await ui.select("Settings", [
|
|
33
|
+
`Max concurrency (current: ${this.settings.maxConcurrent})`,
|
|
34
|
+
`Default max turns (current: ${this.settings.defaultMaxTurns ?? "unlimited"})`,
|
|
35
|
+
`Grace turns (current: ${this.settings.graceTurns})`,
|
|
36
|
+
]);
|
|
37
|
+
if (!choice) return;
|
|
38
|
+
|
|
39
|
+
if (choice.startsWith("Max concurrency")) {
|
|
40
|
+
const val = await ui.input("Max concurrent background agents", String(this.settings.maxConcurrent));
|
|
41
|
+
if (val) {
|
|
42
|
+
const n = parseInt(val, 10);
|
|
43
|
+
if (n >= 1) {
|
|
44
|
+
const toast = this.settings.applyMaxConcurrent(n);
|
|
45
|
+
ui.notify(toast.message, toast.level);
|
|
46
|
+
} else {
|
|
47
|
+
ui.notify("Must be a positive integer.", "warning");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
} else if (choice.startsWith("Default max turns")) {
|
|
51
|
+
const val = await ui.input(
|
|
52
|
+
"Default max turns before wrap-up (0 = unlimited)",
|
|
53
|
+
String(this.settings.defaultMaxTurns ?? 0),
|
|
54
|
+
);
|
|
55
|
+
if (val) {
|
|
56
|
+
const n = parseInt(val, 10);
|
|
57
|
+
if (n >= 0) {
|
|
58
|
+
const toast = this.settings.applyDefaultMaxTurns(n);
|
|
59
|
+
ui.notify(toast.message, toast.level);
|
|
60
|
+
} else {
|
|
61
|
+
ui.notify("Must be 0 (unlimited) or a positive integer.", "warning");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
} else if (choice.startsWith("Grace turns")) {
|
|
65
|
+
const val = await ui.input("Grace turns after wrap-up steer", String(this.settings.graceTurns));
|
|
66
|
+
if (val) {
|
|
67
|
+
const n = parseInt(val, 10);
|
|
68
|
+
if (n >= 1) {
|
|
69
|
+
const toast = this.settings.applyGraceTurns(n);
|
|
70
|
+
ui.notify(toast.message, toast.level);
|
|
71
|
+
} else {
|
|
72
|
+
ui.notify("Must be a positive integer.", "warning");
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|