pi-subagents-lite 1.2.0 → 1.4.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.
Files changed (53) hide show
  1. package/README.md +184 -225
  2. package/package.json +1 -1
  3. package/src/{agent-discovery.ts → agents/agent-discovery.ts} +8 -5
  4. package/src/{agent-manager.ts → agents/agent-manager.ts} +34 -74
  5. package/src/{agent-runner.ts → agents/agent-runner.ts} +115 -173
  6. package/src/agents/agent-status.ts +50 -0
  7. package/src/agents/agent-types.ts +339 -0
  8. package/src/{default-agents.ts → agents/default-agents.ts} +2 -5
  9. package/src/{output-file.ts → agents/output-file.ts} +68 -1
  10. package/src/{tool-execution.ts → agents/tool-execution.ts} +61 -223
  11. package/src/agents/types.ts +54 -0
  12. package/src/{usage.ts → agents/usage.ts} +7 -0
  13. package/src/{config-io.ts → config/config-io.ts} +20 -3
  14. package/src/config/config-store.ts +472 -0
  15. package/src/config/types.ts +26 -0
  16. package/src/events.ts +185 -0
  17. package/src/index.ts +8 -271
  18. package/src/{model-precedence.ts → models/model-precedence.ts} +33 -0
  19. package/src/{model-selector.ts → models/model-selector.ts} +1 -1
  20. package/src/{context.ts → prompt/context.ts} +1 -1
  21. package/src/prompt/prompts.ts +180 -0
  22. package/src/prompt/skill-loader.ts +195 -0
  23. package/src/registration.ts +101 -0
  24. package/src/shell.ts +101 -0
  25. package/src/spawn/spawn-coordinator.ts +232 -0
  26. package/src/status-note.ts +10 -0
  27. package/src/types.ts +47 -71
  28. package/src/ui/agent-widget.ts +61 -49
  29. package/src/{format.ts → ui/format.ts} +64 -26
  30. package/src/ui/menu/helpers.ts +93 -0
  31. package/src/ui/menu/menu-concurrency.ts +192 -0
  32. package/src/ui/menu/menu-debug.ts +125 -0
  33. package/src/ui/menu/menu-model-settings.ts +208 -0
  34. package/src/ui/menu/menu-running-agents.ts +224 -0
  35. package/src/ui/menu/menu-spawn-options.ts +87 -0
  36. package/src/ui/menu/menu-spawn-wizard.ts +418 -0
  37. package/src/ui/menu/menu-system-prompt.ts +109 -0
  38. package/src/ui/menu/menu-widget-settings.ts +130 -0
  39. package/src/ui/menu/menus.ts +101 -0
  40. package/src/ui/menu/submenus/confirm.ts +47 -0
  41. package/src/ui/menu/submenus/model-select.ts +70 -0
  42. package/src/ui/menu/submenus/numeric-input.ts +98 -0
  43. package/src/ui/menu/wrappers/settings-list.ts +205 -0
  44. package/src/{renderer.ts → ui/renderer.ts} +7 -6
  45. package/src/{result-viewer.ts → ui/result-viewer.ts} +7 -2
  46. package/src/ui/types.ts +11 -0
  47. package/src/agent-types.ts +0 -184
  48. package/src/config-mutator.ts +0 -183
  49. package/src/menus.ts +0 -1333
  50. package/src/prompts.ts +0 -94
  51. package/src/skill-loader.ts +0 -178
  52. package/src/state.ts +0 -83
  53. /package/src/{worktree-validator.ts → spawn/worktree-validator.ts} +0 -0
@@ -0,0 +1,101 @@
1
+ /**
2
+ * menus.ts — /agents command dispatcher.
3
+ *
4
+ * Uses SelectList from @earendil-works/pi-tui via ctx.ui.custom.
5
+ * Each iteration creates a fresh SelectList; submenu closes it before opening.
6
+ * No nested ctx.ui.custom calls.
7
+ *
8
+ * Module structure:
9
+ * - helpers.ts: shared helpers (buildSettingsListTheme, buildSelectListTheme, validateNumeric)
10
+ * - menu-model-settings.ts: showModelSettingsMenu
11
+ * - menu-concurrency.ts: showConcurrencySettingsMenu
12
+ * - menu-widget-settings.ts: showWidgetSettingsMenu
13
+ * - menu-running-agents.ts: showRunningAgentsMenu
14
+ * - menu-debug.ts: showDebugMenu
15
+ * - menu-spawn-options.ts: showSpawnOptionsMenu
16
+ * - menu-system-prompt.ts: showSystemPromptMenu
17
+ * - menus.ts (this file): dispatcher — main menu and settings menu
18
+ */
19
+
20
+ import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
21
+ import { SelectList, type SelectItem } from "@earendil-works/pi-tui";
22
+ import { buildSelectListTheme } from "./helpers.js";
23
+ import { SettingsListWrapper } from "./wrappers/settings-list.js";
24
+ import { showModelSettingsMenu } from "./menu-model-settings.js";
25
+ import { showConcurrencySettingsMenu } from "./menu-concurrency.js";
26
+ import { showWidgetSettingsMenu } from "./menu-widget-settings.js";
27
+ import { showRunningAgentsMenu } from "./menu-running-agents.js";
28
+ import { showDebugMenu } from "./menu-debug.js";
29
+ import { showSpawnOptionsMenu } from "./menu-spawn-options.js";
30
+ import { showSystemPromptMenu } from "./menu-system-prompt.js";
31
+
32
+ // Spawn wizard — co-located in this folder.
33
+ import { showSpawnAgentMenu } from "./menu-spawn-wizard.js";
34
+ export { showSpawnAgentMenu };
35
+
36
+
37
+ /**
38
+ * Render `items` as a titled SelectList and dispatch the chosen value.
39
+ * Re-loops after each dispatch until the user cancels (Esc or Back).
40
+ * Each iteration builds a fresh list so state never leaks between visits.
41
+ */
42
+ async function runSelectMenu(
43
+ ctx: ExtensionCommandContext,
44
+ title: string,
45
+ items: SelectItem[],
46
+ dispatch: (choice: string) => Promise<void>,
47
+ ): Promise<void> {
48
+ while (true) {
49
+ const choice = await ctx.ui.custom<string | undefined>((_tui, theme, _kb, done) => {
50
+ const list = new SelectList([...items], 10, buildSelectListTheme(theme));
51
+ list.onSelect = (item) => done(item.value);
52
+ return new SettingsListWrapper(list, { title, theme, onCancel: () => done(undefined) });
53
+ });
54
+ if (choice === undefined) return;
55
+ await dispatch(choice);
56
+ }
57
+ }
58
+
59
+ export async function showSettingsMenu(
60
+ ctx: ExtensionCommandContext,
61
+ modelOptions: string[],
62
+ ): Promise<void> {
63
+ const items: SelectItem[] = [
64
+ { value: "model", label: "Model settings", description: "Set global default and per-type model overrides" },
65
+ { value: "concurrency", label: "Concurrency settings", description: "Set per-model slot limits" },
66
+ { value: "spawnoptions", label: "Spawn options", description: "Default thinking, max turns, background, grace turns" },
67
+ { value: "systemprompt", label: "System prompt", description: "Prompt mode, custom prompt file, AGENTS.md" },
68
+ { value: "widget", label: "Widget settings", description: "Configure widget display options" },
69
+ ];
70
+
71
+ await runSelectMenu(ctx, "Settings", items, async (choice) => {
72
+ switch (choice) {
73
+ case "model": await showModelSettingsMenu(ctx, modelOptions); break;
74
+ case "concurrency": await showConcurrencySettingsMenu(ctx, modelOptions); break;
75
+ case "spawnoptions": await showSpawnOptionsMenu(ctx); break;
76
+ case "systemprompt": await showSystemPromptMenu(ctx); break;
77
+ case "widget": await showWidgetSettingsMenu(ctx); break;
78
+ }
79
+ });
80
+ }
81
+
82
+ export async function showAgentsMainMenu(
83
+ ctx: ExtensionCommandContext,
84
+ modelOptions: string[],
85
+ ): Promise<void> {
86
+ const items: SelectItem[] = [
87
+ { value: "running", label: "Running agents", description: "List running/queued agents" },
88
+ { value: "spawn", label: "Spawn agent", description: "Manually spawn a new agent" },
89
+ { value: "settings", label: "Settings", description: "Model, concurrency, and widget settings" },
90
+ { value: "debug", label: "Debug", description: "Agent types, briefing, diagnostics" },
91
+ ];
92
+
93
+ await runSelectMenu(ctx, "Agents", items, async (choice) => {
94
+ switch (choice) {
95
+ case "running": await showRunningAgentsMenu(ctx); break;
96
+ case "spawn": await showSpawnAgentMenu(ctx, modelOptions); break;
97
+ case "settings": await showSettingsMenu(ctx, modelOptions); break;
98
+ case "debug": await showDebugMenu(ctx); break;
99
+ }
100
+ });
101
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * confirm-submenu.ts — Yes/no confirm dialog for destructive actions.
3
+ *
4
+ * Creates a submenu factory for SettingsList items that need a confirmation
5
+ * dialog (clear overrides, reset concurrency, etc.).
6
+ */
7
+
8
+ import { SelectList, type Component } from "@earendil-works/pi-tui";
9
+ import { buildSelectListTheme } from "../helpers.js";
10
+
11
+ export interface ConfirmSubmenuOptions {
12
+ /** Message shown to the user */
13
+ message: string;
14
+ /** Theme from pi-coding-agent (fg, bold, italic) */
15
+ theme: any;
16
+ /** Called when user confirms (selects Yes) */
17
+ onConfirm: () => void;
18
+ }
19
+
20
+ /**
21
+ * Creates a submenu factory function compatible with SettingsList's submenu callback.
22
+ * Shows a Yes/No SelectList. Calls onConfirm on Yes, done() to close.
23
+ */
24
+ export function createConfirmSubmenu(
25
+ options: ConfirmSubmenuOptions,
26
+ ): (currentValue: string, done: (selectedValue?: string) => void) => Component {
27
+ return (_currentValue: string, done: (selectedValue?: string) => void) => {
28
+ const items = [
29
+ { value: "Yes", label: "Yes", description: options.message },
30
+ { value: "No", label: "No", description: options.message },
31
+ ];
32
+
33
+ const list = new SelectList(items, 5, buildSelectListTheme(options.theme));
34
+
35
+ list.onSelect = (item) => {
36
+ if (item.value === "Yes") {
37
+ options.onConfirm();
38
+ done("Yes");
39
+ } else {
40
+ done();
41
+ }
42
+ };
43
+ list.onCancel = () => done();
44
+
45
+ return list;
46
+ };
47
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * model-select-submenu.ts — 2-step model override submenu.
3
+ *
4
+ * Step 1: SelectList with override mode (session/permanent/clear)
5
+ * Step 2 (if session/permanent): ModelSelectorDialog for model selection
6
+ *
7
+ * The submenu factory must be created inside ctx.ui.custom to capture the theme.
8
+ */
9
+
10
+ import { SelectList, type Component } from "@earendil-works/pi-tui";
11
+ import { ModelSelectorDialog, type ModelOption } from "../../../models/model-selector.js";
12
+ import { buildModelOptions, buildSelectListTheme, createDelegatingComponent } from "../helpers.js";
13
+
14
+ export interface ModelSelectSubmenuOptions {
15
+ modelOptions: string[];
16
+ showClear: boolean;
17
+ theme: any; // Theme from pi-coding-agent (fg, bold, italic)
18
+ onSelect: (mode: "session" | "permanent" | "clear", model: string | null) => void;
19
+ }
20
+
21
+ /**
22
+ * Creates a submenu factory for SettingsList items that need the 2-step
23
+ * model override flow (mode selection → model selection).
24
+ */
25
+ export function createModelSelectSubmenu(
26
+ options: ModelSelectSubmenuOptions,
27
+ ): (currentValue: string, done: (selectedValue?: string) => void) => Component {
28
+ return (_currentValue: string, done: (selectedValue?: string) => void) => {
29
+ let selectedMode: "session" | "permanent" = "session";
30
+
31
+ const modeItems = [
32
+ { value: "session", label: "Set for this session (not saved)" },
33
+ { value: "permanent", label: "Set permanently (saved to config)" },
34
+ ];
35
+ if (options.showClear) {
36
+ modeItems.push({ value: "clear", label: "Clear" });
37
+ }
38
+
39
+ const modeList = new SelectList(modeItems, 5, buildSelectListTheme(options.theme));
40
+
41
+ const delegator = createDelegatingComponent(modeList);
42
+
43
+ modeList.onSelect = (item) => {
44
+ if (item.value === "clear") {
45
+ options.onSelect("clear", null);
46
+ done("clear");
47
+ return;
48
+ }
49
+ selectedMode = item.value as "session" | "permanent";
50
+ delegator.setActive(modelSelector);
51
+ };
52
+ modeList.onCancel = () => done();
53
+
54
+ const modelOpts = buildModelOptions(options.modelOptions);
55
+ const modelSelector = new ModelSelectorDialog(
56
+ modelOpts,
57
+ _currentValue === "(inherits parent)" ? null : _currentValue,
58
+ {
59
+ onSelect: (modelValue) => {
60
+ options.onSelect(selectedMode, modelValue);
61
+ done(modelValue);
62
+ },
63
+ onCancel: () => done(),
64
+ },
65
+ options.theme,
66
+ );
67
+
68
+ return delegator;
69
+ };
70
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * numeric-input-submenu.ts — Shared input submenu Components.
3
+ *
4
+ * - createNumericSubmenu: numeric input with validation
5
+ * - createInputSubmenu: plain text input
6
+ */
7
+
8
+ import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
9
+ import { Input, type Component } from "@earendil-works/pi-tui";
10
+
11
+ import { validateNumeric } from "../helpers.js";
12
+
13
+ /**
14
+ * Returns a `(initialValue, done) => submenu` function wired to
15
+ * `ctx.ui.notify` for errors.
16
+ *
17
+ * If `required` is true, empty input errors.
18
+ * If `required` is false (default), empty input calls `done()` to clear.
19
+ *
20
+ * Usage:
21
+ * createNumericSubmenu(ctx, onValid)
22
+ * createNumericSubmenu(ctx, { min, required? }, onValid, onEmpty?)
23
+ */
24
+ export function createNumericSubmenu(
25
+ ctx: ExtensionCommandContext,
26
+ optionsOrCallback?: { min?: number; required?: boolean; default?: number } | ((parsed: number) => void),
27
+ onValid?: (parsed: number) => void,
28
+ onEmpty?: () => void,
29
+ ): (initialValue: string, done: (selectedValue?: string) => void) => Component {
30
+ const opts = typeof optionsOrCallback === "function"
31
+ ? { onValid: optionsOrCallback }
32
+ : { onValid, ...optionsOrCallback };
33
+ const min = opts.min ?? 1;
34
+ const required = opts.required ?? false;
35
+ const fmtLabel = (n: number) => (n === 0 ? "\u2265 0" : `\u2265 ${n}`);
36
+ const onError = (msg: string) => ctx.ui.notify(msg, "error");
37
+
38
+ return (initialValue, done) => {
39
+ const input = new Input();
40
+ input.setValue(initialValue === "(not set)" ? "" : initialValue);
41
+ input.onSubmit = (value) => {
42
+ const trimmed = value.trim();
43
+ if (!trimmed || /^unlimited$/i.test(trimmed)) {
44
+ if (required) {
45
+ onError(`Invalid value \u2014 must be a number ${fmtLabel(min)}`);
46
+ return;
47
+ }
48
+ if (opts.default != null) {
49
+ opts.onValid?.(opts.default);
50
+ done(String(opts.default));
51
+ } else {
52
+ onEmpty?.();
53
+ done("(not set)");
54
+ }
55
+ return;
56
+ }
57
+ const parsed = validateNumeric(trimmed, min);
58
+ if (parsed === undefined) {
59
+ onError(`Invalid value \u2014 must be a number ${fmtLabel(min)}`);
60
+ return;
61
+ }
62
+ opts.onValid?.(parsed);
63
+ done(String(parsed));
64
+ };
65
+ input.onEscape = () => done();
66
+ return input;
67
+ };
68
+ }
69
+
70
+ /**
71
+ * Returns a `(initialValue, done) => Input` function for plain text submenus.
72
+ *
73
+ * If `required` is true, empty input shows an error and does not call `done`.
74
+ * If `required` is false (default), empty input calls `done()` to clear.
75
+ */
76
+ export function createInputSubmenu(
77
+ ctx: ExtensionCommandContext,
78
+ options?: { required?: boolean },
79
+ ): (initialValue: string, done: (value?: string) => void) => Input {
80
+ return (initialValue, done) => {
81
+ const input = new Input();
82
+ input.setValue(initialValue);
83
+ input.onSubmit = (value) => {
84
+ const trimmed = value.trim();
85
+ if (!trimmed) {
86
+ if (options?.required) {
87
+ ctx.ui.notify("Cannot be empty", "error");
88
+ return;
89
+ }
90
+ done();
91
+ return;
92
+ }
93
+ done(trimmed);
94
+ };
95
+ input.onEscape = () => done();
96
+ return input;
97
+ };
98
+ }
@@ -0,0 +1,205 @@
1
+ /**
2
+ * settings-list-wrapper.ts — Frames a list component with a title bar and separators.
3
+ *
4
+ * Wraps a SettingsList or SelectList with:
5
+ * - Top separator line
6
+ * - Header with title
7
+ * - List content
8
+ * - Bottom separator line
9
+ */
10
+
11
+ import { type Component, isFocusable } from "@earendil-works/pi-tui";
12
+
13
+ export interface SettingsListWrapperTheme {
14
+ bold: (text: string) => string;
15
+ fg: (color: any, text: string) => string;
16
+ }
17
+
18
+ export interface SettingsListWrapperOptions {
19
+ title: string;
20
+ theme: SettingsListWrapperTheme;
21
+ separatorChar?: string;
22
+ /** If true, skip j/k→arrow and arrow→enter/escape conversion. Input passes through unchanged. */
23
+ passthroughKeys?: boolean;
24
+ onCancel?: () => void;
25
+ /** Called with a rebuild(newItems) function so the caller can trigger in-place updates. */
26
+ onRebuild?: (rebuild: (items: any[]) => void) => void;
27
+ }
28
+
29
+ export class SettingsListWrapper implements Component {
30
+ private settingsList: Component;
31
+ private title: string;
32
+ private theme: SettingsListWrapperTheme;
33
+ private separatorChar: string;
34
+ private passthroughKeys: boolean;
35
+
36
+ constructor(settingsList: Component, options: SettingsListWrapperOptions) {
37
+ this.settingsList = settingsList;
38
+ this.title = options.title;
39
+ this.theme = options.theme;
40
+ this.separatorChar = options.separatorChar ?? "─";
41
+ this.passthroughKeys = options.passthroughKeys ?? false;
42
+
43
+ // Append Back item when onCancel provided
44
+ if (options.onCancel) {
45
+ const list = this.settingsList as any;
46
+ if (Array.isArray(list.items)) {
47
+ const closeMenu = options.onCancel;
48
+ // SelectList has onSelect; SettingsList has onChange. Push correct item shape.
49
+ const isSelectList = !!list.onSelect;
50
+ if (isSelectList) {
51
+ // SelectList expects SelectItem shape: { value, label }
52
+ list.items.push(
53
+ { value: "__sep__", label: " " },
54
+ { value: "__back__", label: "Back" },
55
+ );
56
+ // Intercept onSelect so the Back item closes the menu. SelectList
57
+ // reads its own onSelect property at dispatch time (this.onSelect on
58
+ // the target), so reassigning it here is what actually works — a Proxy
59
+ // cannot intercept the dispatch.
60
+ const prevOnSelect = list.onSelect;
61
+ list.onSelect = (item: any) => {
62
+ if (item.value === "__back__") {
63
+ closeMenu();
64
+ return;
65
+ }
66
+ if (item.value === "__sep__") return;
67
+ prevOnSelect?.(item);
68
+ };
69
+ list.onCancel = () => closeMenu();
70
+ } else {
71
+ // SettingsList expects SettingItem shape: { id, label, currentValue, submenu }
72
+ list.items.push(
73
+ { id: "__sep__", label: " ", currentValue: "" },
74
+ {
75
+ id: "__back__",
76
+ label: "Back",
77
+ currentValue: "",
78
+ submenu: (_v: string, subDone: (v?: string) => void) => {
79
+ subDone();
80
+ closeMenu();
81
+ return undefined as any;
82
+ },
83
+ },
84
+ );
85
+ }
86
+ }
87
+
88
+ // Auto-skip __sep__ items when navigating.
89
+ const _rawIndex = Symbol("rawIndex");
90
+ const isSep = (item: any) => item?.value === "__sep__" || item?.id === "__sep__";
91
+ Object.defineProperty(list, "selectedIndex", {
92
+ get() { return list[_rawIndex] ?? 0; },
93
+ set(idx) {
94
+ const curItems = list.items;
95
+ const cur = list[_rawIndex] ?? 0;
96
+ let i = Math.max(0, Math.min(idx, curItems.length - 1));
97
+ if (isSep(curItems[i])) {
98
+ const down = idx > cur;
99
+ if (down) {
100
+ let next = i + 1;
101
+ while (next < curItems.length && isSep(curItems[next])) next++;
102
+ if (next < curItems.length) i = next;
103
+ else {
104
+ next = i - 1;
105
+ while (next >= 0 && isSep(curItems[next])) next--;
106
+ if (next >= 0) i = next;
107
+ }
108
+ } else {
109
+ let next = i - 1;
110
+ while (next >= 0 && isSep(curItems[next])) next--;
111
+ if (next >= 0) i = next;
112
+ else {
113
+ next = i + 1;
114
+ while (next < curItems.length && isSep(curItems[next])) next++;
115
+ if (next < curItems.length) i = next;
116
+ }
117
+ }
118
+ }
119
+ list[_rawIndex] = i;
120
+ },
121
+ configurable: true,
122
+ });
123
+ list[_rawIndex] = list.selectedIndex ?? 0;
124
+
125
+ // Expose rebuild callback
126
+ if (options.onRebuild) {
127
+ const isSelectList = !!list.onSelect;
128
+ const rebuild = (newItems: any[]) => {
129
+ const wrapperItems = [
130
+ isSelectList
131
+ ? { value: "__sep__", label: " " }
132
+ : { id: "__sep__", label: " ", currentValue: "" },
133
+ isSelectList
134
+ ? { value: "__back__", label: "Back" }
135
+ : { id: "__back__", label: "Back", currentValue: "" },
136
+ ];
137
+ const fullItems = [...newItems, ...wrapperItems];
138
+ list.items = fullItems;
139
+ list.filteredItems = fullItems;
140
+ list.selectedIndex = 0;
141
+ list.submenuComponent = null;
142
+ };
143
+ options.onRebuild(rebuild);
144
+ }
145
+ }
146
+ }
147
+
148
+ invalidate(): void {
149
+ this.settingsList.invalidate?.();
150
+ }
151
+
152
+ private get hasSubmenu(): boolean {
153
+ const submenu = (this.settingsList as any)?.submenuComponent ?? null;
154
+ return isFocusable(submenu);
155
+ }
156
+
157
+ handleInput(data: string): void {
158
+ if (this.passthroughKeys) {
159
+ this.settingsList.handleInput?.(data);
160
+ return;
161
+ }
162
+ if (data === "k" || data === "j") {
163
+ if (this.hasSubmenu) {
164
+ // Submenu: pass through as normal letters
165
+ this.settingsList.handleInput?.(data);
166
+ } else {
167
+ // Main list: convert to arrow keys
168
+ this.settingsList.handleInput?.(data === "k" ? "\x1b[A" : "\x1b[B");
169
+ }
170
+ } else if (data === "\x1b[C" || data === "\x1bOC" || data === "\x1b[D" || data === "\x1bOD") {
171
+ if (this.hasSubmenu) {
172
+ // Submenu: pass arrow keys through (Input needs them for cursor)
173
+ this.settingsList.handleInput?.(data);
174
+ } else {
175
+ // Main list: → enters, ← escapes
176
+ this.settingsList.handleInput?.(data.includes("C") ? "\r" : "\x1b");
177
+ }
178
+ } else {
179
+ this.settingsList.handleInput?.(data);
180
+ }
181
+ }
182
+
183
+ render(width: number): string[] {
184
+ const lines: string[] = [];
185
+
186
+ // Top separator
187
+ lines.push(this.separatorChar.repeat(width));
188
+ lines.push("");
189
+
190
+ // Header (left-aligned with spacing, bold and colored)
191
+ const styledTitle = this.theme.bold(this.theme.fg("accent", this.title));
192
+ lines.push(" " + styledTitle);
193
+ lines.push("");
194
+
195
+ // SettingsList content
196
+ const settingsLines = this.settingsList.render(width);
197
+ lines.push(...settingsLines);
198
+
199
+ // Bottom separator
200
+ lines.push("");
201
+ lines.push(this.separatorChar.repeat(width));
202
+
203
+ return lines;
204
+ }
205
+ }
@@ -7,7 +7,6 @@
7
7
  import { Box, Container, Spacer, Text } from "@earendil-works/pi-tui";
8
8
  import type { Theme } from "./types.js";
9
9
  import { buildStatsParts, formatMs, getDisplayName } from "./format.js";
10
- import { __config } from "./state.js";
11
10
 
12
11
  // ============================================================================
13
12
  // Stats rendering helpers
@@ -21,13 +20,13 @@ export function agentNameLabel(d: Record<string, unknown>, theme: Theme): string
21
20
  }
22
21
 
23
22
  /** Build the stats line for an agent result card. */
24
- export function buildStatsLine(d: Record<string, unknown>, theme: Theme): string {
25
- const showCost = __config.agent.showCost === true;
23
+ export function buildStatsLine(d: Record<string, unknown>, theme: Theme, showCost: boolean): string {
26
24
  const parts = buildStatsParts({
27
25
  toolUses: (d.toolUses as number) ?? 0,
28
26
  turnCount: d.turnCount as number | undefined,
29
27
  maxTurns: d.maxTurns as number | undefined,
30
- tokens: (d.tokens as number) ?? 0,
28
+ input: (d.input as number) ?? 0,
29
+ output: (d.output as number) ?? 0,
31
30
  contextPercent: d.contextPercent as number | null,
32
31
  compactions: (d.compactions as number) ?? 0,
33
32
  cost: showCost ? (d.cost as number | undefined) : undefined,
@@ -62,6 +61,7 @@ export function renderAgentToolResult(
62
61
  result: { content: Array<{ type: string; text?: string }>; details?: Record<string, unknown>; isError?: boolean },
63
62
  options: { expanded?: boolean },
64
63
  theme: Theme,
64
+ showCost: boolean,
65
65
  ): Text {
66
66
  const { expanded } = options;
67
67
  const text = result.content[0]?.type === "text" ? result.content[0].text ?? "" : "";
@@ -71,7 +71,7 @@ export function renderAgentToolResult(
71
71
 
72
72
  if (d && d.turnCount != null) {
73
73
  const namePart = agentNameLabel(d, theme);
74
- const statsLine = buildStatsLine(d, theme);
74
+ const statsLine = buildStatsLine(d, theme, showCost);
75
75
  let lines = `${icon} ${namePart}·${statsLine}\n ${theme.fg("text", desc)}`;
76
76
  if (expanded && text) {
77
77
  lines += "\n" + text.split("\n").map(l => ` ${l}`).join("\n");
@@ -98,6 +98,7 @@ export function renderSubagentResult(
98
98
  message: { content?: string; details?: Record<string, unknown> },
99
99
  options: { expanded?: boolean },
100
100
  theme: Theme,
101
+ showCost: boolean,
101
102
  ): Container {
102
103
  const { expanded } = options;
103
104
  const d = message.details;
@@ -112,7 +113,7 @@ export function renderSubagentResult(
112
113
  const icon = isError ? theme.fg("error", "✗") : theme.fg("success", "✓");
113
114
 
114
115
  const namePart = agentNameLabel(d, theme);
115
- const statsLine = buildStatsLine(d, theme);
116
+ const statsLine = buildStatsLine(d, theme, showCost);
116
117
  let headerLine = `${icon} ${namePart}·${statsLine}\n ${theme.fg("text", (d.description as string) || "")}`;
117
118
  if (d.outputFile as string) {
118
119
  headerLine += `\n ${theme.fg("dim", `tail -f ${d.outputFile}`)}`;
@@ -16,8 +16,8 @@ import {
16
16
  type MarkdownTheme,
17
17
  } from "@earendil-works/pi-tui";
18
18
  import { DynamicBorder } from "@earendil-works/pi-coding-agent";
19
- import { type LifetimeUsage, formatTokens } from "./usage.js";
20
- import type { Theme } from "./ui/agent-widget.js";
19
+ import { type LifetimeUsage, formatTokens } from "../agents/usage.js";
20
+ import type { Theme } from "./agent-widget.js";
21
21
  import { formatMs } from "./format.js";
22
22
 
23
23
  /* ------------------------------------------------------------------ */
@@ -34,6 +34,7 @@ export interface ResultViewerStats {
34
34
  lifetimeUsage: LifetimeUsage;
35
35
  turnCount?: number;
36
36
  durationMs?: number;
37
+ modelName?: string;
37
38
  }
38
39
 
39
40
  /* ------------------------------------------------------------------ */
@@ -202,6 +203,10 @@ export class ResultViewer extends Container implements Component {
202
203
  private formatStatsLine(stats: ResultViewerStats): string {
203
204
  const parts: string[] = [];
204
205
 
206
+ if (stats.modelName) {
207
+ parts.push(stats.modelName);
208
+ }
209
+
205
210
  const { lifetimeUsage } = stats;
206
211
  parts.push(`↑${formatTokens(lifetimeUsage.input)}`);
207
212
  parts.push(`↓${formatTokens(lifetimeUsage.output)}`);
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Theme for terminal rendering — used by format.ts, renderer.ts, and UI widgets.
3
+ * Defined here (not in ui/agent-widget.ts) so non-UI modules can import it
4
+ * without depending on the UI layer.
5
+ */
6
+ export type Theme = {
7
+ fg(color: string, text: string): string;
8
+ bg(color: string, text: string): string;
9
+ bold(text: string): string;
10
+ italic?: (text: string) => string;
11
+ };