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,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* menu-concurrency.ts — Concurrency settings menu concern.
|
|
3
|
+
*
|
|
4
|
+
* Uses SettingsList from @earendil-works/pi-tui via ctx.ui.custom.
|
|
5
|
+
* Numeric input submenus for concurrency values.
|
|
6
|
+
* Confirm submenu for reset all.
|
|
7
|
+
*
|
|
8
|
+
* Exports:
|
|
9
|
+
* - showConcurrencySettingsMenu: per-provider and per-model slot limits
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
13
|
+
import { SettingsList, SelectList, type SettingItem } from "@earendil-works/pi-tui";
|
|
14
|
+
import { buildSettingsListTheme, buildSelectListTheme, createDelegatingComponent } from "./helpers.js";
|
|
15
|
+
import { createNumericSubmenu } from "./submenus/numeric-input.js";
|
|
16
|
+
import { createConfirmSubmenu } from "./submenus/confirm.js";
|
|
17
|
+
import { SettingsListWrapper } from "./wrappers/settings-list.js";
|
|
18
|
+
import { getStore } from "../../shell.js";
|
|
19
|
+
|
|
20
|
+
export async function showConcurrencySettingsMenu(
|
|
21
|
+
ctx: ExtensionCommandContext,
|
|
22
|
+
modelOptions: string[],
|
|
23
|
+
): Promise<void> {
|
|
24
|
+
// Build menu items from current store state.
|
|
25
|
+
const buildItems = (store: ReturnType<typeof getStore>, theme: any, modelOptions: string[], onRebuild?: () => void): SettingItem[] => {
|
|
26
|
+
const providers = [...new Set(modelOptions.map((m) => m.split("/")[0]))].sort();
|
|
27
|
+
const items: SettingItem[] = [];
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
// Submenu factory: pick Edit (→ value input) or Remove for an existing limit.
|
|
32
|
+
const editOrRemoveSubmenu = (
|
|
33
|
+
currentLimit: number,
|
|
34
|
+
onEdit: (parsed: number) => void,
|
|
35
|
+
onRemove: () => void,
|
|
36
|
+
): SettingItem["submenu"] => (_currentValue, subDone) => {
|
|
37
|
+
const list = new SelectList(
|
|
38
|
+
[{ value: "edit", label: "Edit limit" }, { value: "remove", label: "Remove limit" }],
|
|
39
|
+
5, buildSelectListTheme(theme),
|
|
40
|
+
);
|
|
41
|
+
const delegator = createDelegatingComponent(list);
|
|
42
|
+
list.onSelect = (item) => {
|
|
43
|
+
if (item.value === "edit") {
|
|
44
|
+
delegator.setActive(createNumericSubmenu(ctx, { min: 1 }, onEdit)(String(currentLimit), subDone));
|
|
45
|
+
} else {
|
|
46
|
+
onRemove();
|
|
47
|
+
subDone();
|
|
48
|
+
onRebuild?.();
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
list.onCancel = () => subDone();
|
|
52
|
+
return delegator;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// Submenu factory: pick a key from `options`, then enter a value.
|
|
56
|
+
const addLimitSubmenu = (
|
|
57
|
+
options: string[],
|
|
58
|
+
onPick: (key: string, parsed: number) => void,
|
|
59
|
+
): SettingItem["submenu"] => (_currentValue, subDone) => {
|
|
60
|
+
const list = new SelectList(
|
|
61
|
+
options.map((o) => ({ value: o, label: o })),
|
|
62
|
+
10, buildSelectListTheme(theme),
|
|
63
|
+
);
|
|
64
|
+
const delegator = createDelegatingComponent(list);
|
|
65
|
+
list.onSelect = (item) => {
|
|
66
|
+
delegator.setActive(createNumericSubmenu(ctx, { min: 1 }, (parsed) => onPick(item.value, parsed))("1", subDone));
|
|
67
|
+
};
|
|
68
|
+
list.onCancel = () => subDone();
|
|
69
|
+
return delegator;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// Global default
|
|
73
|
+
items.push({
|
|
74
|
+
id: "defaultConcurrency",
|
|
75
|
+
label: "Default concurrency limit",
|
|
76
|
+
currentValue: String(store.concurrency.default),
|
|
77
|
+
submenu: createNumericSubmenu(ctx, (parsed) => {
|
|
78
|
+
store.mutate.concurrency.setDefault(parsed);
|
|
79
|
+
ctx.ui.notify(`Default concurrency set to ${parsed}`, "info");
|
|
80
|
+
}),
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Per-provider limits
|
|
84
|
+
items.push({ id: "__sep__", label: " ", currentValue: "" });
|
|
85
|
+
items.push({ id: "__sep__", label: "── Per-provider limits ──", currentValue: "────────" });
|
|
86
|
+
const providerLimits = store.concurrency.providers;
|
|
87
|
+
for (const provider of Object.keys(providerLimits)) {
|
|
88
|
+
const limit = providerLimits[provider];
|
|
89
|
+
items.push({
|
|
90
|
+
id: `provider:${provider}`,
|
|
91
|
+
label: provider,
|
|
92
|
+
currentValue: `${limit} slots`,
|
|
93
|
+
submenu: editOrRemoveSubmenu(
|
|
94
|
+
limit,
|
|
95
|
+
(parsed) => {
|
|
96
|
+
store.mutate.concurrency.setProvider(provider, parsed);
|
|
97
|
+
ctx.ui.notify(`${provider} concurrency set to ${parsed}`, "info");
|
|
98
|
+
},
|
|
99
|
+
() => {
|
|
100
|
+
store.mutate.concurrency.removeProvider(provider);
|
|
101
|
+
ctx.ui.notify(`Removed per-provider limit for ${provider}`, "info");
|
|
102
|
+
},
|
|
103
|
+
),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
items.push({ id: "__sep__", label: "─────────────────────────", currentValue: "────────" });
|
|
108
|
+
// Add per-provider limit (submenu: provider selection → numeric input)
|
|
109
|
+
if (providers.length > 0) {
|
|
110
|
+
items.push({
|
|
111
|
+
id: "addProviderLimit",
|
|
112
|
+
label: "Add per-provider limit...",
|
|
113
|
+
currentValue: "",
|
|
114
|
+
submenu: addLimitSubmenu(providers, (provider, parsed) => {
|
|
115
|
+
store.mutate.concurrency.setProvider(provider, parsed);
|
|
116
|
+
ctx.ui.notify(`${provider} concurrency set to ${parsed}`, "info");
|
|
117
|
+
}),
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Per-model limits
|
|
122
|
+
items.push({ id: "__sep__", label: " ", currentValue: "" });
|
|
123
|
+
items.push({ id: "__sep__", label: "── Per-model limits ──", currentValue: "────────" });
|
|
124
|
+
const models = store.concurrency.models;
|
|
125
|
+
for (const modelKey of Object.keys(models)) {
|
|
126
|
+
const limit = models[modelKey];
|
|
127
|
+
items.push({
|
|
128
|
+
id: `model:${modelKey}`,
|
|
129
|
+
label: modelKey,
|
|
130
|
+
currentValue: `${limit} slots`,
|
|
131
|
+
submenu: editOrRemoveSubmenu(
|
|
132
|
+
limit,
|
|
133
|
+
(parsed) => {
|
|
134
|
+
store.mutate.concurrency.setModel(modelKey, parsed);
|
|
135
|
+
ctx.ui.notify(`${modelKey} concurrency set to ${parsed}`, "info");
|
|
136
|
+
},
|
|
137
|
+
() => {
|
|
138
|
+
store.mutate.concurrency.removeModel(modelKey);
|
|
139
|
+
ctx.ui.notify(`Removed per-model limit for ${modelKey}`, "info");
|
|
140
|
+
},
|
|
141
|
+
),
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Add per-model limit
|
|
146
|
+
items.push({ id: "__sep__", label: "─────────────────────────", currentValue: "────────" });
|
|
147
|
+
if (modelOptions.length > 0) {
|
|
148
|
+
items.push({
|
|
149
|
+
id: "addModelLimit",
|
|
150
|
+
label: "Add per-model limit...",
|
|
151
|
+
currentValue: "",
|
|
152
|
+
submenu: addLimitSubmenu(modelOptions, (modelKey, parsed) => {
|
|
153
|
+
store.mutate.concurrency.setModel(modelKey, parsed);
|
|
154
|
+
ctx.ui.notify(`${modelKey} concurrency set to ${parsed}`, "info");
|
|
155
|
+
}),
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Reset all to defaults
|
|
160
|
+
items.push({ id: "__sep__", label: " ", currentValue: "" });
|
|
161
|
+
items.push({
|
|
162
|
+
id: "resetAll",
|
|
163
|
+
label: "Reset all to defaults",
|
|
164
|
+
currentValue: "",
|
|
165
|
+
submenu: createConfirmSubmenu({
|
|
166
|
+
message: "Reset all concurrency limits to defaults?",
|
|
167
|
+
theme,
|
|
168
|
+
onConfirm: () => {
|
|
169
|
+
store.mutate.concurrency.reset();
|
|
170
|
+
ctx.ui.notify("Concurrency reset to defaults", "info");
|
|
171
|
+
},
|
|
172
|
+
}),
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
return items;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
let rebuild: ((items: any[]) => void) | undefined;
|
|
179
|
+
|
|
180
|
+
await ctx.ui.custom((_tui, theme, _kb, done) => {
|
|
181
|
+
const triggerRebuild = () => rebuild?.(buildItems(getStore(), theme, modelOptions, triggerRebuild));
|
|
182
|
+
const store = getStore();
|
|
183
|
+
const items = buildItems(store, theme, modelOptions, triggerRebuild);
|
|
184
|
+
const settingsList = new SettingsList(items, 15, buildSettingsListTheme(theme), (_id, _v) => triggerRebuild(), () => done(undefined));
|
|
185
|
+
return new SettingsListWrapper(settingsList, {
|
|
186
|
+
title: "Concurrency Settings",
|
|
187
|
+
theme,
|
|
188
|
+
onCancel: () => done(undefined),
|
|
189
|
+
onRebuild: (r) => { rebuild = r; },
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* menu-debug.ts — Debug menu concern.
|
|
3
|
+
*
|
|
4
|
+
* Uses SelectList from @earendil-works/pi-tui via ctx.ui.custom.
|
|
5
|
+
* Items: Agent types (notify), Agent briefing (send to LLM).
|
|
6
|
+
* Actions execute on select; Escape closes the menu.
|
|
7
|
+
*
|
|
8
|
+
* Exports:
|
|
9
|
+
* - showDebugMenu: agent types listing, agent briefing
|
|
10
|
+
*
|
|
11
|
+
* Private helpers (single-consumer, co-located):
|
|
12
|
+
* - showAgentTypes: list available agent types and their configs
|
|
13
|
+
* - handleAgentBriefing: send agent types/capabilities info to LLM
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
17
|
+
import { SelectList, type SelectItem } from "@earendil-works/pi-tui";
|
|
18
|
+
import { getAgentConfig, getAvailableTypes, getAllTypes } from "../../agents/agent-types.js";
|
|
19
|
+
import { buildSelectListTheme } from "./helpers.js";
|
|
20
|
+
import { SettingsListWrapper } from "./wrappers/settings-list.js";
|
|
21
|
+
import { getPiInstance } from "../../shell.js";
|
|
22
|
+
|
|
23
|
+
async function showAgentTypes(ctx: ExtensionCommandContext): Promise<void> {
|
|
24
|
+
const types = getAllTypes();
|
|
25
|
+
if (types.length === 0) {
|
|
26
|
+
ctx.ui.notify("No agent types available", "info");
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const lines: string[] = ["Available agent types:\n"];
|
|
31
|
+
for (const name of types) {
|
|
32
|
+
const cfg = getAgentConfig(name);
|
|
33
|
+
if (!cfg) continue;
|
|
34
|
+
const hidden = cfg.hidden === true ? " [HIDDEN]" : "";
|
|
35
|
+
const model = cfg.model ? ` Model: ${cfg.model}` : "";
|
|
36
|
+
const tools = cfg.registeredTools
|
|
37
|
+
? ` Tools: ${cfg.registeredTools.join(", ")}`
|
|
38
|
+
: " Tools: all built-in tools";
|
|
39
|
+
const source = cfg.source ? ` Source: ${cfg.source}` : "";
|
|
40
|
+
lines.push(` ${name}${hidden}`);
|
|
41
|
+
lines.push(` ${cfg.description}`);
|
|
42
|
+
if (model) lines.push(model);
|
|
43
|
+
lines.push(tools);
|
|
44
|
+
if (source) lines.push(source);
|
|
45
|
+
lines.push("");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
ctx.ui.notify(lines.join("\n"), "info");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function handleAgentBriefing(ctx: ExtensionCommandContext): Promise<void> {
|
|
52
|
+
const types = getAvailableTypes();
|
|
53
|
+
const agents = types.map((t) => ({ name: t, config: getAgentConfig(t) }));
|
|
54
|
+
|
|
55
|
+
const lines: string[] = [
|
|
56
|
+
"# Agent Types and Capabilities\n",
|
|
57
|
+
"The following agent types are available. Use the `agent` parameter to select one.\n",
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
for (const { name, config } of agents) {
|
|
61
|
+
if (!config) continue;
|
|
62
|
+
lines.push(`## ${config.displayName ?? name}`);
|
|
63
|
+
lines.push(config.description);
|
|
64
|
+
lines.push("");
|
|
65
|
+
|
|
66
|
+
if (config.registeredTools) {
|
|
67
|
+
lines.push(`**Tools:** ${config.registeredTools.join(", ")}`);
|
|
68
|
+
}
|
|
69
|
+
if (config.model) {
|
|
70
|
+
lines.push(`**Default model:** ${config.model}`);
|
|
71
|
+
}
|
|
72
|
+
if (config.maxTurns) {
|
|
73
|
+
lines.push(`**Max turns:** ${config.maxTurns}`);
|
|
74
|
+
}
|
|
75
|
+
lines.push("");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Parameter descriptions
|
|
79
|
+
lines.push("## Agent Tool Parameters\n");
|
|
80
|
+
lines.push("| Parameter | Description |");
|
|
81
|
+
lines.push("|-----------|-------------|");
|
|
82
|
+
lines.push("| `prompt` | The task for the agent (required) |");
|
|
83
|
+
lines.push("| `description` | One-line summary of what the agent should do (required) |");
|
|
84
|
+
lines.push("| `agent` | Which agent type to use (default: general-purpose) |");
|
|
85
|
+
lines.push("| `thinking` | Optional thinking mode override (e.g., `off`, `minimal`, `low`, `medium`, `high`, `xhigh`) |");
|
|
86
|
+
lines.push("| `run_in_background` | When `true`, result is auto-delivered — do NOT poll. Continue working while waiting. |");
|
|
87
|
+
lines.push("| `worktree_path` | Optional path to a git worktree of the parent's repo. See below for details. |");
|
|
88
|
+
lines.push("");
|
|
89
|
+
|
|
90
|
+
// Usage guidelines
|
|
91
|
+
lines.push("## Usage Guidelines\n");
|
|
92
|
+
lines.push("- Agents start fresh with their config — they do NOT inherit the parent conversation");
|
|
93
|
+
lines.push("- For parallel tasks, spawn multiple `run_in_background: true` agents in one turn");
|
|
94
|
+
lines.push(" → Results are auto-delivered — do NOT poll, the result will arrive when ready");
|
|
95
|
+
lines.push("");
|
|
96
|
+
lines.push("## `worktree_path` Parameter\n");
|
|
97
|
+
lines.push("Use `worktree_path` to run a subagent in a different git worktree of the parent's repository.");
|
|
98
|
+
lines.push("");
|
|
99
|
+
lines.push("- **Optional.** Omit to run the subagent in the parent's working directory (default behavior).");
|
|
100
|
+
lines.push("- **Must be a path** inside a git worktree of the parent's repo, including the main checkout. Not a different repo, not a non-git directory.");
|
|
101
|
+
lines.push("- **Relative paths** are resolved against the parent's working directory.");
|
|
102
|
+
lines.push("- **On failure** the validator returns a specific reason (e.g., 'not a worktree of the parent's repository', 'path does not exist') — use this to self-correct.");
|
|
103
|
+
lines.push("- **Agent type discovery:** The worktree's `.pi/agents/` directory is scanned for agent types when this param is set, so worktree-local types become available to that spawn.");
|
|
104
|
+
getPiInstance().sendUserMessage(lines.join("\n"));
|
|
105
|
+
ctx.ui.notify("Agent briefing sent to LLM", "info");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export async function showDebugMenu(ctx: ExtensionCommandContext): Promise<void> {
|
|
109
|
+
await ctx.ui.custom((_tui, theme, _kb, done) => {
|
|
110
|
+
const items: SelectItem[] = [
|
|
111
|
+
{ value: "agent-types", label: "Agent types", description: "List available agent types and their configs" },
|
|
112
|
+
{ value: "agent-briefing", label: "Agent briefing", description: "Send agent types/capabilities info to LLM (Optional, if having issues)" },
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
const selectList = new SelectList(items, 10, buildSelectListTheme(theme));
|
|
116
|
+
selectList.onSelect = async (item) => {
|
|
117
|
+
if (item.value === "agent-types") {
|
|
118
|
+
await showAgentTypes(ctx);
|
|
119
|
+
} else if (item.value === "agent-briefing") {
|
|
120
|
+
await handleAgentBriefing(ctx);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
return new SettingsListWrapper(selectList, { title: "Debug", theme, onCancel: () => done(undefined) });
|
|
124
|
+
});
|
|
125
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* menu-model-settings.ts — Model settings menu concern.
|
|
3
|
+
*
|
|
4
|
+
* Uses SettingsList from @earendil-works/pi-tui via ctx.ui.custom.
|
|
5
|
+
* Model overrides use 2-step submenu: override mode → model selection.
|
|
6
|
+
* Cost display toggle removed (already in widget settings → usage stats).
|
|
7
|
+
*
|
|
8
|
+
* Exports:
|
|
9
|
+
* - showModelSettingsMenu: model settings with global default, per-type overrides
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
13
|
+
import { SettingsList, SelectList, type SettingItem } from "@earendil-works/pi-tui";
|
|
14
|
+
import { getAgentConfig, getAllTypes } from "../../agents/agent-types.js";
|
|
15
|
+
import { CONFIG_AGENT_NON_MODEL_KEYS } from "../../config/types.js";
|
|
16
|
+
import { buildSettingsListTheme, buildSelectListTheme, createDelegatingComponent } from "./helpers.js";
|
|
17
|
+
import { createModelSelectSubmenu } from "./submenus/model-select.js";
|
|
18
|
+
import { createConfirmSubmenu } from "./submenus/confirm.js";
|
|
19
|
+
import { SettingsListWrapper } from "./wrappers/settings-list.js";
|
|
20
|
+
import { getStore } from "../../shell.js";
|
|
21
|
+
|
|
22
|
+
export async function showModelSettingsMenu(
|
|
23
|
+
ctx: ExtensionCommandContext,
|
|
24
|
+
modelOptions: string[],
|
|
25
|
+
): Promise<void> {
|
|
26
|
+
// Build menu items from current store state.
|
|
27
|
+
const buildItems = (store: ReturnType<typeof getStore>, theme: any): SettingItem[] => {
|
|
28
|
+
const items: SettingItem[] = [];
|
|
29
|
+
|
|
30
|
+
// Shared onSelect for model override submenus: applies session/permanent/clear
|
|
31
|
+
// mode to the given config key, with `label` used in notify messages.
|
|
32
|
+
const modelOverrideOnSelect = (
|
|
33
|
+
key: string,
|
|
34
|
+
label: string,
|
|
35
|
+
): (mode: "session" | "permanent" | "clear", model: string | null) => void =>
|
|
36
|
+
(mode, model) => {
|
|
37
|
+
if (mode === "clear") {
|
|
38
|
+
store.mutate.agent.clearModelOverride(key);
|
|
39
|
+
store.mutate.session.clearOverride(key);
|
|
40
|
+
ctx.ui.notify(`${label} overrides cleared`, "info");
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const effective = model === "(inherits parent)" ? null : model;
|
|
44
|
+
if (mode === "session") {
|
|
45
|
+
if (effective === null) {
|
|
46
|
+
store.mutate.session.clearOverride(key);
|
|
47
|
+
} else {
|
|
48
|
+
store.mutate.session.setOverride(key, effective);
|
|
49
|
+
}
|
|
50
|
+
} else {
|
|
51
|
+
store.mutate.agent.setModelOverride(key, effective);
|
|
52
|
+
}
|
|
53
|
+
ctx.ui.notify(
|
|
54
|
+
effective === null
|
|
55
|
+
? `${label} inherits parent model`
|
|
56
|
+
: `${label} model set to ${effective}`,
|
|
57
|
+
"info",
|
|
58
|
+
);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Global default model
|
|
62
|
+
const sessionDefault = store.sessionDefaultModel;
|
|
63
|
+
const hasSessionGlobal = sessionDefault != null;
|
|
64
|
+
const globalDisplayValue = hasSessionGlobal
|
|
65
|
+
? `${sessionDefault} [session]`
|
|
66
|
+
: store.agent.defaultModel
|
|
67
|
+
? store.agent.defaultModel
|
|
68
|
+
: "(inherits parent)";
|
|
69
|
+
|
|
70
|
+
items.push({
|
|
71
|
+
id: "defaultModel",
|
|
72
|
+
label: "Global default model",
|
|
73
|
+
currentValue: globalDisplayValue,
|
|
74
|
+
submenu: createModelSelectSubmenu({
|
|
75
|
+
modelOptions,
|
|
76
|
+
showClear: false,
|
|
77
|
+
theme,
|
|
78
|
+
onSelect: modelOverrideOnSelect("default", "Global default"),
|
|
79
|
+
}),
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Per-type overrides
|
|
83
|
+
items.push({ id: "__sep__", label: " ", currentValue: "" });
|
|
84
|
+
items.push({ id: "__sep__", label: "── Per-type overrides ──", currentValue: "────────" });
|
|
85
|
+
const types = getAllTypes();
|
|
86
|
+
const typeEntries = types.map((typeName) => {
|
|
87
|
+
const cfg = getAgentConfig(typeName);
|
|
88
|
+
const sessionOverride = store.sessionModelOverride(typeName);
|
|
89
|
+
const configOverride = store.agentConfigSnapshot()[typeName];
|
|
90
|
+
const hasSession = sessionOverride != null;
|
|
91
|
+
const hasConfigOverride = configOverride != null && typeof configOverride === "string";
|
|
92
|
+
const effectiveModel = store.modelFor(typeName, "(inherits parent)", cfg);
|
|
93
|
+
return { typeName, cfg, sessionOverride, configOverride, hasSession, hasConfigOverride, effectiveModel };
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const overridden = typeEntries.filter(e => e.hasSession || e.hasConfigOverride);
|
|
97
|
+
const nonOverridden = typeEntries.filter(e => !e.hasSession && !e.hasConfigOverride);
|
|
98
|
+
|
|
99
|
+
for (const { typeName, cfg, sessionOverride, configOverride, hasSession, effectiveModel } of overridden) {
|
|
100
|
+
const frontmatterHint = !hasSession && configOverride && cfg?.model ? `${cfg.model} → ` : "";
|
|
101
|
+
const displayModel = hasSession ? `${sessionOverride} [session]` : effectiveModel;
|
|
102
|
+
const hasPerm = !!configOverride;
|
|
103
|
+
|
|
104
|
+
items.push({
|
|
105
|
+
id: `type:${typeName}`,
|
|
106
|
+
label: typeName,
|
|
107
|
+
currentValue: `${frontmatterHint}${displayModel}`,
|
|
108
|
+
submenu: createModelSelectSubmenu({
|
|
109
|
+
modelOptions,
|
|
110
|
+
showClear: hasPerm,
|
|
111
|
+
theme,
|
|
112
|
+
onSelect: modelOverrideOnSelect(typeName, typeName),
|
|
113
|
+
}),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
items.push({ id: "__sep__", label: "─────────────────────────", currentValue: "────────" });
|
|
118
|
+
// Override another type...
|
|
119
|
+
if (nonOverridden.length > 0) {
|
|
120
|
+
items.push({
|
|
121
|
+
id: "overrideType",
|
|
122
|
+
label: "Override another type...",
|
|
123
|
+
currentValue: "",
|
|
124
|
+
submenu: (_currentValue, subDone) => {
|
|
125
|
+
const typeNames = nonOverridden.map(e => ({ value: e.typeName, label: e.typeName }));
|
|
126
|
+
const typeList = new SelectList(typeNames, 10, buildSelectListTheme(theme));
|
|
127
|
+
const delegator = createDelegatingComponent(typeList);
|
|
128
|
+
|
|
129
|
+
typeList.onSelect = (item) => {
|
|
130
|
+
const entry = nonOverridden.find(e => e.typeName === item.value)!;
|
|
131
|
+
// Delegate to createModelSelectSubmenu for the 2-step model flow
|
|
132
|
+
const modelSubmenu = createModelSelectSubmenu({
|
|
133
|
+
modelOptions,
|
|
134
|
+
showClear: false,
|
|
135
|
+
theme,
|
|
136
|
+
onSelect: modelOverrideOnSelect(entry.typeName, entry.typeName),
|
|
137
|
+
});
|
|
138
|
+
delegator.setActive(modelSubmenu(entry.effectiveModel, subDone));
|
|
139
|
+
};
|
|
140
|
+
typeList.onCancel = () => subDone();
|
|
141
|
+
|
|
142
|
+
return delegator;
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
items.push({ id: "__sep__", label: " ", currentValue: "" });
|
|
148
|
+
// Clear session overrides
|
|
149
|
+
const hasSessionOverrides = store.sessionDefaultModel != null ||
|
|
150
|
+
getAllTypes().some(type => store.sessionModelOverride(type) != null);
|
|
151
|
+
if (hasSessionOverrides) {
|
|
152
|
+
items.push({
|
|
153
|
+
id: "clearSession",
|
|
154
|
+
label: "Clear session overrides",
|
|
155
|
+
currentValue: "",
|
|
156
|
+
submenu: createConfirmSubmenu({
|
|
157
|
+
message: "Clear all session overrides?",
|
|
158
|
+
theme,
|
|
159
|
+
onConfirm: () => {
|
|
160
|
+
store.mutate.session.clearAll();
|
|
161
|
+
ctx.ui.notify("Session overrides cleared", "info");
|
|
162
|
+
},
|
|
163
|
+
}),
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Clear all overrides
|
|
168
|
+
items.push({
|
|
169
|
+
id: "clearAll",
|
|
170
|
+
label: "Clear all overrides",
|
|
171
|
+
currentValue: "",
|
|
172
|
+
submenu: createConfirmSubmenu({
|
|
173
|
+
message: "Clear all model overrides?",
|
|
174
|
+
theme,
|
|
175
|
+
onConfirm: () => {
|
|
176
|
+
const agentConfig = store.agentConfigSnapshot();
|
|
177
|
+
const hasOverrides = Object.entries(agentConfig).some(
|
|
178
|
+
([k, v]) => !CONFIG_AGENT_NON_MODEL_KEYS.includes(k) && v != null,
|
|
179
|
+
);
|
|
180
|
+
if (!hasOverrides && store.agent.defaultModel === null) {
|
|
181
|
+
ctx.ui.notify("No overrides to clear", "info");
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
store.mutate.agent.clearAllModelOverrides();
|
|
185
|
+
ctx.ui.notify("All model overrides cleared", "info");
|
|
186
|
+
},
|
|
187
|
+
}),
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
return items;
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
let rebuild: ((items: any[]) => void) | undefined;
|
|
194
|
+
|
|
195
|
+
await ctx.ui.custom((_tui, theme, _kb, done) => {
|
|
196
|
+
const store = getStore();
|
|
197
|
+
const items = buildItems(store, theme);
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
const settingsList = new SettingsList(items, 15, buildSettingsListTheme(theme), (_id, _v) => rebuild?.(buildItems(getStore(), theme)), () => done(undefined));
|
|
201
|
+
return new SettingsListWrapper(settingsList, {
|
|
202
|
+
title: "Model Settings",
|
|
203
|
+
theme,
|
|
204
|
+
onCancel: () => done(undefined),
|
|
205
|
+
onRebuild: (r) => { rebuild = r; },
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
}
|