pi-subagents-lite 1.3.0 → 1.4.1
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 +184 -235
- package/package.json +1 -1
- package/src/{agent-discovery.ts → agents/agent-discovery.ts} +10 -7
- package/src/{agent-manager.ts → agents/agent-manager.ts} +34 -74
- package/src/{agent-runner.ts → agents/agent-runner.ts} +130 -181
- package/src/{agent-status.ts → agents/agent-status.ts} +4 -4
- package/src/agents/agent-types.ts +339 -0
- package/src/{default-agents.ts → agents/default-agents.ts} +2 -5
- package/src/{output-file.ts → agents/output-file.ts} +68 -1
- package/src/{tool-execution.ts → agents/tool-execution.ts} +60 -222
- package/src/agents/types.ts +54 -0
- package/src/{usage.ts → agents/usage.ts} +7 -0
- package/src/{config-io.ts → config/config-io.ts} +20 -3
- package/src/config/config-store.ts +472 -0
- package/src/config/types.ts +26 -0
- package/src/events.ts +185 -0
- package/src/index.ts +8 -281
- package/src/{model-precedence.ts → models/model-precedence.ts} +33 -0
- package/src/{model-selector.ts → models/model-selector.ts} +1 -1
- package/src/{context.ts → prompt/context.ts} +1 -1
- package/src/prompt/prompts.ts +180 -0
- package/src/prompt/skill-loader.ts +195 -0
- package/src/registration.ts +101 -0
- package/src/shell.ts +101 -0
- package/src/spawn/spawn-coordinator.ts +232 -0
- package/src/status-note.ts +10 -0
- package/src/types.ts +47 -71
- package/src/ui/agent-widget.ts +61 -49
- package/src/{format.ts → ui/format.ts} +64 -26
- package/src/ui/menu/helpers.ts +93 -0
- package/src/ui/menu/menu-concurrency.ts +192 -0
- package/src/ui/menu/menu-debug.ts +125 -0
- package/src/ui/menu/menu-model-settings.ts +208 -0
- package/src/ui/menu/menu-running-agents.ts +224 -0
- package/src/ui/menu/menu-spawn-options.ts +87 -0
- package/src/ui/menu/menu-spawn-wizard.ts +418 -0
- package/src/ui/menu/menu-system-prompt.ts +109 -0
- package/src/ui/menu/menu-widget-settings.ts +130 -0
- package/src/ui/menu/menus.ts +101 -0
- package/src/ui/menu/submenus/confirm.ts +47 -0
- package/src/ui/menu/submenus/model-select.ts +70 -0
- package/src/ui/menu/submenus/numeric-input.ts +98 -0
- package/src/ui/menu/wrappers/settings-list.ts +205 -0
- package/src/{renderer.ts → ui/renderer.ts} +7 -6
- package/src/{result-viewer.ts → ui/result-viewer.ts} +7 -2
- package/src/ui/types.ts +11 -0
- package/src/agent-types.ts +0 -184
- package/src/config-mutator.ts +0 -183
- package/src/menus.ts +0 -1333
- package/src/prompts.ts +0 -94
- package/src/skill-loader.ts +0 -178
- package/src/state.ts +0 -83
- /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
|
-
|
|
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 "
|
|
20
|
-
import type { Theme } from "./
|
|
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)}`);
|
package/src/ui/types.ts
ADDED
|
@@ -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
|
+
};
|