pi-subagents 0.23.1 → 0.24.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.
@@ -2,20 +2,14 @@
2
2
  * Chain Clarification TUI Component
3
3
  *
4
4
  * Shows templates and resolved behaviors for each step in a chain.
5
- * Supports editing templates, output paths, reads lists, and progress toggle.
5
+ * Supports runtime editing of templates, output paths, reads lists, and progress toggle.
6
6
  */
7
7
 
8
8
  import type { Theme } from "@mariozechner/pi-coding-agent";
9
9
  import type { Component, TUI } from "@mariozechner/pi-tui";
10
10
  import { matchesKey, visibleWidth, truncateToWidth } from "@mariozechner/pi-tui";
11
- import * as fs from "node:fs";
12
- import * as path from "node:path";
13
- import { getUserChainDir, type AgentConfig, type ChainConfig, type ChainStepConfig } from "../../agents/agents.ts";
11
+ import type { AgentConfig } from "../../agents/agents.ts";
14
12
  import type { ResolvedStepBehavior } from "../../shared/settings.ts";
15
- import type { TextEditorState } from "../../tui/text-editor.ts";
16
- import { createEditorState, ensureCursorVisible, getCursorDisplayPos, handleEditorInput, renderEditor, wrapText } from "../../tui/text-editor.ts";
17
- import { updateFrontmatterField } from "../../agents/agent-serializer.ts";
18
- import { serializeChain } from "../../agents/chain-serializer.ts";
19
13
  import { resolveModelCandidate, splitThinkingSuffix } from "../shared/model-fallback.ts";
20
14
  import { findModelInfo, getSupportedThinkingLevels, type ModelInfo, type ThinkingLevel } from "../../shared/model-info.ts";
21
15
 
@@ -38,6 +32,166 @@ export interface ChainClarifyResult {
38
32
 
39
33
  type EditMode = "template" | "output" | "reads" | "model" | "thinking" | "skills";
40
34
 
35
+
36
+ interface TextEditorState {
37
+ buffer: string;
38
+ cursor: number;
39
+ viewportOffset: number;
40
+ }
41
+
42
+ function createEditorState(initial = ""): TextEditorState {
43
+ return { buffer: initial, cursor: 0, viewportOffset: 0 };
44
+ }
45
+
46
+ function wrapText(text: string, width: number): { lines: string[]; starts: number[] } {
47
+ if (width <= 0) return { lines: [text], starts: [0] };
48
+ if (text.length === 0) return { lines: [""], starts: [0] };
49
+
50
+ const lines: string[] = [];
51
+ const starts: number[] = [];
52
+ let offset = 0;
53
+ const segments = text.split("\n");
54
+ for (const [index, segment] of segments.entries()) {
55
+ if (segment.length === 0) {
56
+ starts.push(offset);
57
+ lines.push("");
58
+ } else {
59
+ let lineStart = 0;
60
+ let pos = 0;
61
+ let lineWidth = 0;
62
+ while (pos < segment.length) {
63
+ const char = String.fromCodePoint(segment.codePointAt(pos)!);
64
+ const charWidth = visibleWidth(char);
65
+ if (lineWidth > 0 && lineWidth + charWidth > width) {
66
+ starts.push(offset + lineStart);
67
+ lines.push(segment.slice(lineStart, pos));
68
+ lineStart = pos;
69
+ lineWidth = 0;
70
+ continue;
71
+ }
72
+ pos += char.length;
73
+ lineWidth += charWidth;
74
+ }
75
+ starts.push(offset + lineStart);
76
+ lines.push(segment.slice(lineStart));
77
+ }
78
+ offset += segment.length + (index < segments.length - 1 ? 1 : 0);
79
+ }
80
+ if (!text.endsWith("\n") && text.length > 0 && visibleWidth(lines[lines.length - 1] ?? "") === width) {
81
+ starts.push(text.length);
82
+ lines.push("");
83
+ }
84
+ return { lines, starts };
85
+ }
86
+
87
+ function getCursorDisplayPos(cursor: number, starts: number[]): { line: number; col: number } {
88
+ for (let i = starts.length - 1; i >= 0; i--) {
89
+ if (cursor >= starts[i]!) return { line: i, col: cursor - starts[i]! };
90
+ }
91
+ return { line: 0, col: 0 };
92
+ }
93
+
94
+ function ensureCursorVisible(cursorLine: number, viewportHeight: number, currentOffset: number): number {
95
+ if (cursorLine < currentOffset) return Math.max(0, cursorLine);
96
+ if (cursorLine >= currentOffset + viewportHeight) return Math.max(0, cursorLine - viewportHeight + 1);
97
+ return Math.max(0, currentOffset);
98
+ }
99
+
100
+ function isWordChar(ch: string): boolean {
101
+ const code = ch.charCodeAt(0);
102
+ return (code >= 48 && code <= 57) || (code >= 65 && code <= 90) || (code >= 97 && code <= 122) || code === 95;
103
+ }
104
+
105
+ function wordBackward(buffer: string, cursor: number): number {
106
+ let pos = cursor;
107
+ while (pos > 0 && !isWordChar(buffer[pos - 1]!)) pos--;
108
+ while (pos > 0 && isWordChar(buffer[pos - 1]!)) pos--;
109
+ return pos;
110
+ }
111
+
112
+ function wordForward(buffer: string, cursor: number): number {
113
+ let pos = cursor;
114
+ while (pos < buffer.length && isWordChar(buffer[pos]!)) pos++;
115
+ while (pos < buffer.length && !isWordChar(buffer[pos]!)) pos++;
116
+ return pos;
117
+ }
118
+
119
+ function normalizeInsertText(data: string): string | null {
120
+ let text = data.split("\x1b[200~").join("").split("\x1b[201~").join("");
121
+ text = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
122
+ const newline = text.indexOf("\n");
123
+ if (newline !== -1) text = text.slice(0, newline);
124
+ text = text.replace(/\t/g, " ");
125
+ if (text.length === 0) return null;
126
+ for (let i = 0; i < text.length; i++) {
127
+ if (text.charCodeAt(i) < 32) return null;
128
+ }
129
+ return text;
130
+ }
131
+
132
+ function handleEditorInput(state: TextEditorState, data: string, textWidth: number): TextEditorState | null {
133
+ if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c") || matchesKey(data, "return")) return null;
134
+
135
+ const { lines: wrapped, starts } = wrapText(state.buffer, textWidth);
136
+ const cursorPos = getCursorDisplayPos(state.cursor, starts);
137
+
138
+ if (matchesKey(data, "alt+left") || matchesKey(data, "ctrl+left")) return { ...state, cursor: wordBackward(state.buffer, state.cursor) };
139
+ if (matchesKey(data, "alt+right") || matchesKey(data, "ctrl+right")) return { ...state, cursor: wordForward(state.buffer, state.cursor) };
140
+ if (matchesKey(data, "left")) return state.cursor > 0 ? { ...state, cursor: state.cursor - 1 } : state;
141
+ if (matchesKey(data, "right")) return state.cursor < state.buffer.length ? { ...state, cursor: state.cursor + 1 } : state;
142
+ if (matchesKey(data, "up") && cursorPos.line > 0) {
143
+ const targetLine = cursorPos.line - 1;
144
+ return { ...state, cursor: starts[targetLine]! + Math.min(cursorPos.col, wrapped[targetLine]?.length ?? 0) };
145
+ }
146
+ if (matchesKey(data, "down") && cursorPos.line < wrapped.length - 1) {
147
+ const targetLine = cursorPos.line + 1;
148
+ return { ...state, cursor: starts[targetLine]! + Math.min(cursorPos.col, wrapped[targetLine]?.length ?? 0) };
149
+ }
150
+ if (matchesKey(data, "home")) return { ...state, cursor: starts[cursorPos.line]! };
151
+ if (matchesKey(data, "end")) return { ...state, cursor: starts[cursorPos.line]! + (wrapped[cursorPos.line]?.length ?? 0) };
152
+ if (matchesKey(data, "ctrl+home")) return { ...state, cursor: 0 };
153
+ if (matchesKey(data, "ctrl+end")) return { ...state, cursor: state.buffer.length };
154
+ if (matchesKey(data, "alt+backspace")) {
155
+ const target = wordBackward(state.buffer, state.cursor);
156
+ return target === state.cursor ? state : { ...state, buffer: state.buffer.slice(0, target) + state.buffer.slice(state.cursor), cursor: target };
157
+ }
158
+ if (matchesKey(data, "backspace")) {
159
+ return state.cursor > 0
160
+ ? { ...state, buffer: state.buffer.slice(0, state.cursor - 1) + state.buffer.slice(state.cursor), cursor: state.cursor - 1 }
161
+ : state;
162
+ }
163
+ if (matchesKey(data, "delete")) {
164
+ return state.cursor < state.buffer.length
165
+ ? { ...state, buffer: state.buffer.slice(0, state.cursor) + state.buffer.slice(state.cursor + 1) }
166
+ : state;
167
+ }
168
+
169
+ const insert = normalizeInsertText(data);
170
+ return insert
171
+ ? { ...state, buffer: state.buffer.slice(0, state.cursor) + insert + state.buffer.slice(state.cursor), cursor: state.cursor + insert.length }
172
+ : null;
173
+ }
174
+
175
+ function renderWithCursor(text: string, cursorPos: number): string {
176
+ const before = text.slice(0, cursorPos);
177
+ const cursorChar = text[cursorPos] ?? " ";
178
+ const after = text.slice(cursorPos + 1);
179
+ return `${before}\x1b[7m${cursorChar}\x1b[27m${after}`;
180
+ }
181
+
182
+ function renderEditor(state: TextEditorState, width: number, viewportHeight: number): string[] {
183
+ const { lines: wrapped, starts } = wrapText(state.buffer, width);
184
+ const cursorPos = getCursorDisplayPos(state.cursor, starts);
185
+ const lines: string[] = [];
186
+ for (let i = 0; i < viewportHeight; i++) {
187
+ const lineIdx = state.viewportOffset + i;
188
+ let content = lineIdx < wrapped.length ? wrapped[lineIdx] ?? "" : "";
189
+ if (lineIdx === cursorPos.line) content = renderWithCursor(content, cursorPos.col);
190
+ lines.push(content);
191
+ }
192
+ return lines;
193
+ }
194
+
41
195
  /**
42
196
  * TUI component for chain clarification.
43
197
  * Factory signature matches ctx.ui.custom: (tui, theme, kb, done) => Component
@@ -61,10 +215,8 @@ export class ChainClarifyComponent implements Component {
61
215
  private skillSelectedNames: Set<string> = new Set();
62
216
  private skillCursorIndex: number = 0;
63
217
  private filteredSkills: Array<{ name: string; source: string; description?: string }> = [];
64
- private saveMessage: { text: string; type: "info" | "error" } | null = null;
65
- private saveMessageTimer: ReturnType<typeof setTimeout> | null = null;
66
- private saveChainNameState: TextEditorState = createEditorState();
67
- private savingChain = false;
218
+ private noticeMessage: { text: string; type: "info" | "error" } | null = null;
219
+ private noticeMessageTimer: ReturnType<typeof setTimeout> | null = null;
68
220
  /** Run in background (async) mode */
69
221
  private runInBackground = false;
70
222
  private tui: TUI;
@@ -260,171 +412,18 @@ export class ChainClarifyComponent implements Component {
260
412
  this.behaviorOverrides.set(stepIndex, { ...existing, [field]: value });
261
413
  }
262
414
 
263
- private buildChainConfig(name: string): ChainConfig {
264
- const steps: ChainStepConfig[] = [];
265
- for (let i = 0; i < this.agentConfigs.length; i++) {
266
- const agent = this.agentConfigs[i]!;
267
- const behavior = this.getEffectiveBehavior(i);
268
- const override = this.behaviorOverrides.get(i);
269
- const template = this.templates[i] ?? "";
270
- const step: ChainStepConfig = { agent: agent.name, task: template };
271
- if (override?.output !== undefined) step.output = behavior.output;
272
- if (behavior.outputMode !== "inline") step.outputMode = behavior.outputMode;
273
- if (override?.reads !== undefined) step.reads = behavior.reads;
274
- if (override?.model !== undefined) step.model = behavior.model;
275
- if (override?.skills !== undefined) step.skills = behavior.skills;
276
- if (override?.progress !== undefined) step.progress = behavior.progress;
277
- steps.push(step);
278
- }
279
- return {
280
- name,
281
- description: `Chain: ${steps.map((s) => s.agent).join(" → ")}`,
282
- source: "user",
283
- filePath: "",
284
- steps,
285
- };
286
- }
287
-
288
- private enterSaveChainName(): void {
289
- this.savingChain = true;
290
- this.saveChainNameState = createEditorState();
291
- this.tui.requestRender();
292
- }
293
-
294
- private handleSaveChainNameInput(data: string): void {
295
- if (matchesKey(data, "tab")) return;
296
- const innerW = this.width - 2;
297
- const boxInnerWidth = Math.max(10, innerW - 4);
298
- const nextState = handleEditorInput(this.saveChainNameState, data, boxInnerWidth);
299
- if (nextState) {
300
- this.saveChainNameState = nextState;
301
- this.tui.requestRender();
302
- return;
303
- }
304
- if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
305
- this.savingChain = false;
306
- this.saveChainNameState = createEditorState();
307
- this.tui.requestRender();
308
- return;
309
- }
310
- if (matchesKey(data, "return")) {
311
- const name = this.saveChainNameState.buffer.trim();
312
- if (!name) {
313
- this.showSaveMessage("Name is required", "error");
314
- this.savingChain = false;
315
- this.saveChainNameState = createEditorState();
316
- return;
317
- }
318
- try {
319
- const dir = getUserChainDir();
320
- fs.mkdirSync(dir, { recursive: true });
321
- const filePath = path.join(dir, `${name}.chain.md`);
322
- const config = this.buildChainConfig(name);
323
- config.filePath = filePath;
324
- fs.writeFileSync(filePath, serializeChain(config), "utf-8");
325
- this.showSaveMessage(`Saved ${name}.chain.md`, "info");
326
- } catch (err) {
327
- this.showSaveMessage(err instanceof Error ? err.message : String(err), "error");
328
- }
329
- this.savingChain = false;
330
- this.saveChainNameState = createEditorState();
331
- }
332
- }
333
-
334
- private showSaveMessage(text: string, type: "info" | "error"): void {
335
- this.saveMessage = { text, type };
336
- if (this.saveMessageTimer) clearTimeout(this.saveMessageTimer);
337
- this.saveMessageTimer = setTimeout(() => {
338
- this.saveMessage = null;
339
- this.saveMessageTimer = null;
415
+ private showNotice(text: string, type: "info" | "error"): void {
416
+ this.noticeMessage = { text, type };
417
+ if (this.noticeMessageTimer) clearTimeout(this.noticeMessageTimer);
418
+ this.noticeMessageTimer = setTimeout(() => {
419
+ this.noticeMessage = null;
420
+ this.noticeMessageTimer = null;
340
421
  this.tui.requestRender();
341
422
  }, 2000);
342
423
  this.tui.requestRender();
343
424
  }
344
425
 
345
- private arraysEqual(a: string[] | false, b: string[] | false): boolean {
346
- if (a === b) return true;
347
- if (a === false || b === false) return false;
348
- if (a.length !== b.length) return false;
349
- for (let i = 0; i < a.length; i++) {
350
- if (a[i] !== b[i]) return false;
351
- }
352
- return true;
353
- }
354
-
355
- private saveOverridesToAgent(): void {
356
- const stepIndex = this.selectedStep;
357
- const agent = this.agentConfigs[stepIndex];
358
- if (!agent?.filePath) {
359
- this.showSaveMessage("Agent file not found", "error");
360
- return;
361
- }
362
-
363
- const override = this.behaviorOverrides.get(stepIndex);
364
- if (!override) {
365
- this.showSaveMessage("No changes to save", "info");
366
- return;
367
- }
368
-
369
- const base = this.resolvedBehaviors[stepIndex]!;
370
- const updates: Array<{ field: string; value: string | undefined }> = [];
371
-
372
- if (override.output !== undefined && override.output !== base.output) {
373
- updates.push({
374
- field: "output",
375
- value: override.output === false ? undefined : override.output,
376
- });
377
- }
378
-
379
- if (override.reads !== undefined && !this.arraysEqual(override.reads, base.reads)) {
380
- updates.push({
381
- field: "defaultReads",
382
- value: override.reads === false ? undefined : override.reads.join(", "),
383
- });
384
- }
385
-
386
- if (override.progress !== undefined && override.progress !== base.progress) {
387
- updates.push({
388
- field: "defaultProgress",
389
- value: override.progress ? "true" : undefined,
390
- });
391
- }
392
-
393
- if (override.skills !== undefined && !this.arraysEqual(override.skills, base.skills)) {
394
- updates.push({
395
- field: "skills",
396
- value: override.skills === false || override.skills.length === 0 ? undefined : override.skills.join(", "),
397
- });
398
- }
399
-
400
- if (override.model !== undefined) {
401
- const baseModel = agent.model ? this.resolveModelFullId(agent.model) : undefined;
402
- if (override.model !== baseModel) {
403
- updates.push({ field: "model", value: override.model });
404
- }
405
- }
406
-
407
- if (updates.length === 0) {
408
- this.showSaveMessage("No changes to save", "info");
409
- return;
410
- }
411
-
412
- try {
413
- for (const update of updates) {
414
- updateFrontmatterField(agent.filePath, update.field, update.value);
415
- }
416
- this.showSaveMessage("Saved agent settings", "info");
417
- } catch (err) {
418
- this.showSaveMessage(err instanceof Error ? err.message : String(err), "error");
419
- }
420
- }
421
-
422
426
  handleInput(data: string): void {
423
- if (this.savingChain) {
424
- this.handleSaveChainNameInput(data);
425
- return;
426
- }
427
-
428
427
  if (this.editingStep !== null) {
429
428
  if (this.editMode === "model") {
430
429
  this.handleModelSelectorInput(data);
@@ -521,15 +520,6 @@ export class ChainClarifyComponent implements Component {
521
520
  return;
522
521
  }
523
522
 
524
- if (data === "S") {
525
- this.saveOverridesToAgent();
526
- return;
527
- }
528
-
529
- if (data === "W" && this.mode === "chain") {
530
- this.enterSaveChainName();
531
- return;
532
- }
533
523
  }
534
524
 
535
525
  private enterEditMode(mode: EditMode): void {
@@ -646,7 +636,7 @@ export class ChainClarifyComponent implements Component {
646
636
  /** Enter thinking level selector mode */
647
637
  private enterThinkingSelector(): void {
648
638
  if (!this.getEffectiveBehavior(this.selectedStep).model) {
649
- this.showSaveMessage("Select a model first", "error");
639
+ this.showNotice("Select a model first", "error");
650
640
  return;
651
641
  }
652
642
  this.editingStep = this.selectedStep;
@@ -888,32 +878,7 @@ export class ChainClarifyComponent implements Component {
888
878
  }
889
879
  }
890
880
 
891
- private renderSaveChainName(): string[] {
892
- const lines: string[] = [];
893
- const innerW = this.width - 2;
894
- const boxInnerWidth = Math.max(10, innerW - 4);
895
- lines.push(this.renderHeader(" Save Chain "));
896
- lines.push(this.row(""));
897
- lines.push(this.row(` ${this.theme.fg("dim", "Name:")}`));
898
- const top = `┌${"─".repeat(boxInnerWidth)}┐`;
899
- const bottom = `└${"─".repeat(boxInnerWidth)}┘`;
900
- lines.push(this.row(` ${top}`));
901
- const editorState = { ...this.saveChainNameState };
902
- const wrapped = wrapText(editorState.buffer, boxInnerWidth);
903
- const cursorPos = getCursorDisplayPos(editorState.cursor, wrapped.starts);
904
- editorState.viewportOffset = ensureCursorVisible(cursorPos.line, 1, editorState.viewportOffset);
905
- const editorLine = renderEditor(editorState, boxInnerWidth, 1)[0] ?? "";
906
- lines.push(this.row(` │${this.pad(editorLine, boxInnerWidth)}│`));
907
- lines.push(this.row(` ${bottom}`));
908
- lines.push(this.row(""));
909
- lines.push(this.renderFooter(" [Enter] Save • [Esc] Cancel "));
910
- return lines;
911
- }
912
-
913
881
  render(_width: number): string[] {
914
- if (this.savingChain) {
915
- return this.renderSaveChainName();
916
- }
917
882
  if (this.editingStep !== null) {
918
883
  if (this.editMode === "model") {
919
884
  return this.renderModelSelector();
@@ -1141,18 +1106,18 @@ export class ChainClarifyComponent implements Component {
1141
1106
  const bgLabel = this.runInBackground ? '[b]g:ON' : '[b]g';
1142
1107
  switch (this.mode) {
1143
1108
  case 'single':
1144
- return ` [Enter] Run • [Esc] Cancel • e m t w s ${bgLabel} S `;
1109
+ return ` [Enter] Run • [Esc] Cancel • e m t w s ${bgLabel} `;
1145
1110
  case 'parallel':
1146
- return ` [Enter] Run • [Esc] Cancel • e m t s ${bgLabel} S • ↑↓ Nav `;
1111
+ return ` [Enter] Run • [Esc] Cancel • e m t s ${bgLabel} • ↑↓ Nav `;
1147
1112
  case 'chain':
1148
- return ` [Enter] Run • [Esc] Cancel • e m t w r p s ${bgLabel} S W • ↑↓ Nav `;
1113
+ return ` [Enter] Run • [Esc] Cancel • e m t w r p s ${bgLabel} • ↑↓ Nav `;
1149
1114
  }
1150
1115
  }
1151
1116
 
1152
- private appendSaveMessage(lines: string[]): void {
1153
- if (!this.saveMessage) return;
1154
- const color = this.saveMessage.type === "error" ? "error" : "success";
1155
- lines.push(this.row(` ${this.theme.fg(color, this.saveMessage.text)}`));
1117
+ private appendNotice(lines: string[]): void {
1118
+ if (!this.noticeMessage) return;
1119
+ const color = this.noticeMessage.type === "error" ? "error" : "success";
1120
+ lines.push(this.row(` ${this.theme.fg(color, this.noticeMessage.text)}`));
1156
1121
  }
1157
1122
 
1158
1123
  private renderSingleMode(): string[] {
@@ -1199,7 +1164,7 @@ export class ChainClarifyComponent implements Component {
1199
1164
 
1200
1165
  lines.push(this.row(""));
1201
1166
 
1202
- this.appendSaveMessage(lines);
1167
+ this.appendNotice(lines);
1203
1168
  lines.push(this.renderFooter(this.getFooterText()));
1204
1169
 
1205
1170
  return lines;
@@ -1251,7 +1216,7 @@ export class ChainClarifyComponent implements Component {
1251
1216
  lines.push(this.row(""));
1252
1217
  }
1253
1218
 
1254
- this.appendSaveMessage(lines);
1219
+ this.appendNotice(lines);
1255
1220
  lines.push(this.renderFooter(this.getFooterText()));
1256
1221
 
1257
1222
  return lines;
@@ -1354,7 +1319,7 @@ export class ChainClarifyComponent implements Component {
1354
1319
  lines.push(this.row(""));
1355
1320
  }
1356
1321
 
1357
- this.appendSaveMessage(lines);
1322
+ this.appendNotice(lines);
1358
1323
  lines.push(this.renderFooter(this.getFooterText()));
1359
1324
 
1360
1325
  return lines;
@@ -1362,7 +1327,7 @@ export class ChainClarifyComponent implements Component {
1362
1327
 
1363
1328
  invalidate(): void {}
1364
1329
  dispose(): void {
1365
- if (this.saveMessageTimer) clearTimeout(this.saveMessageTimer);
1366
- this.saveMessageTimer = null;
1330
+ if (this.noticeMessageTimer) clearTimeout(this.noticeMessageTimer);
1331
+ this.noticeMessageTimer = null;
1367
1332
  }
1368
1333
  }
@@ -0,0 +1,49 @@
1
+ import type { ActivityState, AsyncJobStep } from "./types.ts";
2
+
3
+ type StepStatusLike = Pick<AsyncJobStep, "status">;
4
+
5
+ function formatActivityAge(ms: number): string {
6
+ if (ms < 1000) return "now";
7
+ if (ms < 60000) return `${Math.floor(ms / 1000)}s`;
8
+ return `${Math.floor(ms / 60000)}m`;
9
+ }
10
+
11
+ export function formatActivityLabel(lastActivityAt: number | undefined, activityState?: ActivityState, now = Date.now()): string | undefined {
12
+ if (lastActivityAt === undefined) {
13
+ if (activityState === "needs_attention") return "needs attention";
14
+ if (activityState === "active_long_running") return "active but long-running";
15
+ return undefined;
16
+ }
17
+ const age = formatActivityAge(Math.max(0, now - lastActivityAt));
18
+ if (activityState === "needs_attention") return `no activity for ${age}`;
19
+ if (activityState === "active_long_running") return `active but long-running · last activity ${age} ago`;
20
+ return age === "now" ? "active now" : `active ${age} ago`;
21
+ }
22
+
23
+ function isCompletedStepStatus(status: AsyncJobStep["status"]): boolean {
24
+ return status === "complete" || status === "completed";
25
+ }
26
+
27
+ export function aggregateStepStatus(steps: StepStatusLike[]): AsyncJobStep["status"] {
28
+ if (steps.some((step) => step.status === "running")) return "running";
29
+ if (steps.some((step) => step.status === "failed")) return "failed";
30
+ if (steps.some((step) => step.status === "paused")) return "paused";
31
+ if (steps.length > 0 && steps.every((step) => isCompletedStepStatus(step.status))) return "complete";
32
+ return "pending";
33
+ }
34
+
35
+ export function formatAgentRunningLabel(count: number): string {
36
+ return count === 1 ? "1 agent running" : `${count} agents running`;
37
+ }
38
+
39
+ export function formatParallelOutcome(steps: StepStatusLike[], total: number, options: { showRunning?: boolean } = {}): string {
40
+ const running = steps.filter((step) => step.status === "running").length;
41
+ const done = steps.filter((step) => isCompletedStepStatus(step.status)).length;
42
+ const failed = steps.filter((step) => step.status === "failed").length;
43
+ const paused = steps.filter((step) => step.status === "paused").length;
44
+ const parts = [`${done}/${total} done`];
45
+ if (options.showRunning !== false && running > 0) parts.unshift(formatAgentRunningLabel(running));
46
+ if (failed > 0) parts.push(`${failed} failed`);
47
+ if (paused > 0) parts.push(`${paused} paused`);
48
+ return parts.join(" · ");
49
+ }
@@ -494,10 +494,6 @@ interface TopLevelParallelConfig {
494
494
  concurrency?: number;
495
495
  }
496
496
 
497
- interface AgentManagerConfig {
498
- newShortcut?: string;
499
- }
500
-
501
497
  export interface ExtensionConfig {
502
498
  asyncByDefault?: boolean;
503
499
  forceTopLevelAsync?: boolean;
@@ -505,7 +501,6 @@ export interface ExtensionConfig {
505
501
  maxSubagentDepth?: number;
506
502
  control?: ControlConfig;
507
503
  parallel?: TopLevelParallelConfig;
508
- agentManager?: AgentManagerConfig;
509
504
  worktreeSetupHook?: string;
510
505
  worktreeSetupHookTimeoutMs?: number;
511
506
  intercomBridge?: IntercomBridgeConfig;
@@ -4,14 +4,9 @@ import * as path from "node:path";
4
4
  import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
5
5
  import { Key, matchesKey } from "@mariozechner/pi-tui";
6
6
  import { discoverAgents, discoverAgentsAll, type ChainConfig } from "../agents/agents.ts";
7
- import { AgentManagerComponent, type ManagerResult } from "../manager-ui/agent-manager.ts";
8
- import { SubagentsStatusComponent } from "../tui/subagents-status.ts";
9
- import { discoverAvailableSkills } from "../agents/skills.ts";
10
7
  import type { SubagentParamsLike } from "../runs/foreground/subagent-executor.ts";
11
- import { resolveCurrentSessionId } from "../shared/session-identity.ts";
12
8
  import { isParallelStep, type ChainStep } from "../shared/settings.ts";
13
9
  import type { SlashSubagentResponse, SlashSubagentUpdate } from "./slash-bridge.ts";
14
- import { toModelInfo } from "../shared/model-info.ts";
15
10
  import {
16
11
  applySlashUpdate,
17
12
  buildSlashInitialResult,
@@ -25,7 +20,6 @@ import {
25
20
  SLASH_SUBAGENT_RESPONSE_EVENT,
26
21
  SLASH_SUBAGENT_STARTED_EVENT,
27
22
  SLASH_SUBAGENT_UPDATE_EVENT,
28
- type ExtensionConfig,
29
23
  type SingleResult,
30
24
  type SubagentState,
31
25
  } from "../shared/types.ts";
@@ -328,49 +322,6 @@ async function runSlashSubagent(
328
322
  }
329
323
  }
330
324
 
331
- async function openAgentManager(
332
- pi: ExtensionAPI,
333
- ctx: ExtensionContext,
334
- config: ExtensionConfig = {},
335
- ): Promise<void> {
336
- const agentData = { ...discoverAgentsAll(ctx.cwd), cwd: ctx.cwd };
337
- const models = ctx.modelRegistry.getAvailable().map(toModelInfo);
338
- const skills = discoverAvailableSkills(ctx.cwd);
339
-
340
- const result = await ctx.ui.custom<ManagerResult>(
341
- (tui, theme, _kb, done) => new AgentManagerComponent(tui, theme, agentData, models, skills, done, { newShortcut: config.agentManager?.newShortcut, preferredModelProvider: ctx.model?.provider }),
342
- { overlay: true, overlayOptions: { anchor: "center", width: 84, maxHeight: "80%" } },
343
- );
344
- if (!result) return;
345
-
346
- const launchOptions: SubagentParamsLike = {
347
- clarify: !result.skipClarify && !result.background,
348
- agentScope: "both",
349
- ...(result.fork ? { context: "fork" as const } : {}),
350
- ...(result.background ? { async: true } : {}),
351
- };
352
-
353
- if (result.action === "chain") {
354
- const chain = result.agents.map((name, i) => ({
355
- agent: name,
356
- ...(i === 0 ? { task: result.task } : {}),
357
- }));
358
- await runSlashSubagent(pi, ctx, { chain, task: result.task, ...launchOptions });
359
- return;
360
- }
361
-
362
- if (result.action === "launch") {
363
- await runSlashSubagent(pi, ctx, { agent: result.agent, task: result.task, ...launchOptions });
364
- } else if (result.action === "launch-chain") {
365
- await runSlashSubagent(pi, ctx, { chain: mapSavedChainSteps(result.chain, result.worktree), task: result.task, ...launchOptions });
366
- } else if (result.action === "parallel") {
367
- await runSlashSubagent(pi, ctx, {
368
- tasks: result.tasks,
369
- ...launchOptions,
370
- ...(result.worktree ? { worktree: true } : {}),
371
- });
372
- }
373
- }
374
325
 
375
326
  interface ParsedStep { name: string; config: InlineConfig; task?: string }
376
327
 
@@ -452,15 +403,7 @@ const parseAgentArgs = (
452
403
  export function registerSlashCommands(
453
404
  pi: ExtensionAPI,
454
405
  state: SubagentState,
455
- config: ExtensionConfig = {},
456
406
  ): void {
457
- pi.registerCommand("agents", {
458
- description: "Open the Agents Manager",
459
- handler: async (_args, ctx) => {
460
- await openAgentManager(pi, ctx, config);
461
- },
462
- });
463
-
464
407
  pi.registerCommand("run", {
465
408
  description: "Run a subagent directly: /run agent[output=file] [task] [--bg] [--fork]",
466
409
  getArgumentCompletions: makeAgentCompletions(state, false),
@@ -567,18 +510,6 @@ export function registerSlashCommands(
567
510
  },
568
511
  });
569
512
 
570
- pi.registerCommand("subagents-status", {
571
- description: "Show active and recent async subagent runs",
572
- handler: async (_args, ctx) => {
573
- const sessionId = resolveCurrentSessionId(ctx.sessionManager);
574
- state.baseCwd = ctx.cwd;
575
- state.currentSessionId = sessionId;
576
- await ctx.ui.custom<void>(
577
- (tui, theme, _kb, done) => new SubagentsStatusComponent(tui, theme, () => done(undefined), { sessionId }),
578
- { overlay: true, overlayOptions: { anchor: "center", width: 84, maxHeight: "80%" } },
579
- );
580
- },
581
- });
582
513
 
583
514
  pi.registerCommand("subagents-doctor", {
584
515
  description: "Show subagent diagnostics",
@@ -587,9 +518,4 @@ export function registerSlashCommands(
587
518
  },
588
519
  });
589
520
 
590
- pi.registerShortcut("ctrl+shift+a", {
591
- handler: async (ctx) => {
592
- await openAgentManager(pi, ctx, config);
593
- },
594
- });
595
521
  }