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,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
+ }