@astralyn/pi 0.84.5 → 0.84.7

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 (119) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/core/agent-session.d.ts.map +1 -1
  3. package/dist/core/agent-session.js +4 -0
  4. package/dist/core/agent-session.js.map +1 -1
  5. package/dist/core/extensions/index.d.ts +1 -1
  6. package/dist/core/extensions/index.d.ts.map +1 -1
  7. package/dist/core/extensions/index.js.map +1 -1
  8. package/dist/core/extensions/runner.d.ts +1 -0
  9. package/dist/core/extensions/runner.d.ts.map +1 -1
  10. package/dist/core/extensions/runner.js +6 -0
  11. package/dist/core/extensions/runner.js.map +1 -1
  12. package/dist/core/extensions/types.d.ts +11 -0
  13. package/dist/core/extensions/types.d.ts.map +1 -1
  14. package/dist/core/extensions/types.js.map +1 -1
  15. package/dist/core/tools/bash.d.ts +5 -0
  16. package/dist/core/tools/bash.d.ts.map +1 -1
  17. package/dist/core/tools/bash.js +1 -1
  18. package/dist/core/tools/bash.js.map +1 -1
  19. package/dist/extensions/background/index.d.ts +95 -20
  20. package/dist/extensions/background/index.d.ts.map +1 -1
  21. package/dist/extensions/background/index.js +470 -136
  22. package/dist/extensions/background/index.js.map +1 -1
  23. package/dist/extensions/background/manager.d.ts +14 -0
  24. package/dist/extensions/background/manager.d.ts.map +1 -1
  25. package/dist/extensions/background/manager.js +125 -24
  26. package/dist/extensions/background/manager.js.map +1 -1
  27. package/dist/extensions/background/registry.d.ts +82 -0
  28. package/dist/extensions/background/registry.d.ts.map +1 -1
  29. package/dist/extensions/background/registry.js +237 -6
  30. package/dist/extensions/background/registry.js.map +1 -1
  31. package/dist/extensions/background/render.d.ts +35 -11
  32. package/dist/extensions/background/render.d.ts.map +1 -1
  33. package/dist/extensions/background/render.js +180 -30
  34. package/dist/extensions/background/render.js.map +1 -1
  35. package/dist/extensions/router/dialog-host.d.ts +60 -0
  36. package/dist/extensions/router/dialog-host.d.ts.map +1 -0
  37. package/dist/extensions/router/dialog-host.js +196 -0
  38. package/dist/extensions/router/dialog-host.js.map +1 -0
  39. package/dist/extensions/router/probe.d.ts.map +1 -1
  40. package/dist/extensions/router/probe.js +2 -0
  41. package/dist/extensions/router/probe.js.map +1 -1
  42. package/dist/extensions/router/ui.d.ts.map +1 -1
  43. package/dist/extensions/router/ui.js +117 -67
  44. package/dist/extensions/router/ui.js.map +1 -1
  45. package/dist/extensions/subagent/activity.d.ts +3 -5
  46. package/dist/extensions/subagent/activity.d.ts.map +1 -1
  47. package/dist/extensions/subagent/activity.js +8 -89
  48. package/dist/extensions/subagent/activity.js.map +1 -1
  49. package/dist/extensions/subagent/agents-command.d.ts.map +1 -1
  50. package/dist/extensions/subagent/agents-command.js +391 -161
  51. package/dist/extensions/subagent/agents-command.js.map +1 -1
  52. package/dist/extensions/subagent/agents.d.ts +2 -0
  53. package/dist/extensions/subagent/agents.d.ts.map +1 -1
  54. package/dist/extensions/subagent/agents.js +7 -1
  55. package/dist/extensions/subagent/agents.js.map +1 -1
  56. package/dist/extensions/subagent/budget.d.ts +15 -0
  57. package/dist/extensions/subagent/budget.d.ts.map +1 -0
  58. package/dist/extensions/subagent/budget.js +96 -0
  59. package/dist/extensions/subagent/budget.js.map +1 -0
  60. package/dist/extensions/subagent/cancellation.d.ts +29 -0
  61. package/dist/extensions/subagent/cancellation.d.ts.map +1 -0
  62. package/dist/extensions/subagent/cancellation.js +73 -0
  63. package/dist/extensions/subagent/cancellation.js.map +1 -0
  64. package/dist/extensions/subagent/index.d.ts.map +1 -1
  65. package/dist/extensions/subagent/index.js +5 -22
  66. package/dist/extensions/subagent/index.js.map +1 -1
  67. package/dist/extensions/subagent/model-selection.d.ts +16 -0
  68. package/dist/extensions/subagent/model-selection.d.ts.map +1 -0
  69. package/dist/extensions/subagent/model-selection.js +51 -0
  70. package/dist/extensions/subagent/model-selection.js.map +1 -0
  71. package/dist/extensions/subagent/render.d.ts +6 -1
  72. package/dist/extensions/subagent/render.d.ts.map +1 -1
  73. package/dist/extensions/subagent/render.js +23 -12
  74. package/dist/extensions/subagent/render.js.map +1 -1
  75. package/dist/extensions/subagent/resolve.d.ts.map +1 -1
  76. package/dist/extensions/subagent/resolve.js +2 -21
  77. package/dist/extensions/subagent/resolve.js.map +1 -1
  78. package/dist/extensions/subagent/runner.d.ts +4 -5
  79. package/dist/extensions/subagent/runner.d.ts.map +1 -1
  80. package/dist/extensions/subagent/runner.js +71 -264
  81. package/dist/extensions/subagent/runner.js.map +1 -1
  82. package/dist/extensions/subagent/sdk-runner.d.ts +13 -11
  83. package/dist/extensions/subagent/sdk-runner.d.ts.map +1 -1
  84. package/dist/extensions/subagent/sdk-runner.js +167 -148
  85. package/dist/extensions/subagent/sdk-runner.js.map +1 -1
  86. package/dist/extensions/subagent/settings.d.ts.map +1 -1
  87. package/dist/extensions/subagent/settings.js +11 -4
  88. package/dist/extensions/subagent/settings.js.map +1 -1
  89. package/dist/extensions/subagent/state.d.ts +89 -0
  90. package/dist/extensions/subagent/state.d.ts.map +1 -0
  91. package/dist/extensions/subagent/state.js +294 -0
  92. package/dist/extensions/subagent/state.js.map +1 -0
  93. package/dist/extensions/subagent/text.d.ts +4 -0
  94. package/dist/extensions/subagent/text.d.ts.map +1 -1
  95. package/dist/extensions/subagent/text.js +75 -0
  96. package/dist/extensions/subagent/text.js.map +1 -1
  97. package/dist/extensions/subagent/types.d.ts +3 -8
  98. package/dist/extensions/subagent/types.d.ts.map +1 -1
  99. package/dist/extensions/subagent/types.js.map +1 -1
  100. package/dist/extensions/subagent/ui/choices.d.ts +36 -0
  101. package/dist/extensions/subagent/ui/choices.d.ts.map +1 -0
  102. package/dist/extensions/subagent/ui/choices.js +54 -0
  103. package/dist/extensions/subagent/ui/choices.js.map +1 -0
  104. package/dist/extensions/subagent/ui/model-list.d.ts +1 -0
  105. package/dist/extensions/subagent/ui/model-list.d.ts.map +1 -1
  106. package/dist/extensions/subagent/ui/model-list.js +15 -18
  107. package/dist/extensions/subagent/ui/model-list.js.map +1 -1
  108. package/dist/index.d.ts +1 -1
  109. package/dist/index.d.ts.map +1 -1
  110. package/dist/index.js.map +1 -1
  111. package/docs/bundled/README.md +1 -1
  112. package/docs/bundled/extensions/background.md +55 -20
  113. package/docs/extensions.md +13 -0
  114. package/npm-shrinkwrap.json +2 -2
  115. package/package.json +1 -1
  116. package/dist/extensions/subagent/retry.d.ts +0 -17
  117. package/dist/extensions/subagent/retry.d.ts.map +0 -1
  118. package/dist/extensions/subagent/retry.js +0 -41
  119. package/dist/extensions/subagent/retry.js.map +0 -1
@@ -1,45 +1,14 @@
1
- import { clampThinkingLevel, getSupportedThinkingLevels } from "@earendil-works/pi-ai/compat";
2
- import { Container, Spacer, Text, TruncatedText, } from "@earendil-works/pi-tui";
1
+ import { Container, isFocusable, Spacer, Text, TruncatedText, } from "@earendil-works/pi-tui";
3
2
  import { getAgentDir } from "../../config.js";
4
3
  import { DynamicBorder } from "../../modes/interactive/components/dynamic-border.js";
5
4
  import { keyLabel } from "../../modes/interactive/components/keybinding-hints.js";
6
- import { AGENT_PROFILES } from "./agents.js";
7
- import { THINKING_LEVELS } from "./constants.js";
5
+ import { AGENT_PROFILE_LABELS, AGENT_PROFILES } from "./agents.js";
8
6
  import { loadSubagentConfig, updateProfileOverride } from "./settings.js";
7
+ import { truncate } from "./text.js";
8
+ import { buildModelChoices, buildSettingsRows, buildThinkingChoices, compareModels, } from "./ui/choices.js";
9
9
  import { ProfileModelListComponent } from "./ui/model-list.js";
10
10
  const MODEL_REFRESH_TIMEOUT_MS = 15_000;
11
11
  const MAX_REFRESH_ERROR_LENGTH = 240;
12
- // Static display titles for the two built-in profiles; agents.ts owns the
13
- // model-facing copy.
14
- const PROFILE_LABELS = {
15
- explorer: "Explorer",
16
- general: "General",
17
- };
18
- function modelId(model) {
19
- return model ? `${model.provider}/${model.id}` : "none";
20
- }
21
- function findModel(models, value) {
22
- if (!value)
23
- return undefined;
24
- return models.find((model) => `${model.provider}/${model.id}` === value);
25
- }
26
- function boundText(value, limit) {
27
- const characters = [...value];
28
- return characters.length <= limit ? value : `${characters.slice(0, Math.max(0, limit - 1)).join("")}…`;
29
- }
30
- // Capability checks use the saved model when it is still in the catalog,
31
- // falling back to the parent session model otherwise.
32
- function effectiveModel(currentSessionModel, models, override) {
33
- if (override?.model) {
34
- const saved = findModel(models, override.model);
35
- if (saved)
36
- return saved;
37
- }
38
- return currentSessionModel;
39
- }
40
- function inheritedThinking(effective, currentThinking) {
41
- return effective ? clampThinkingLevel(effective, currentThinking) : currentThinking;
42
- }
43
12
  function refreshFailureMessage(providerIds) {
44
13
  if (providerIds.length === 1)
45
14
  return `Could not refresh ${providerIds[0]}; showing cached models.`;
@@ -58,6 +27,85 @@ function navigationHint(theme, keybindings) {
58
27
  .join("/");
59
28
  return keys ? `${theme.fg("dim", keys)}${theme.fg("muted", " navigate")}` : "";
60
29
  }
30
+ /** Small Pi-native list page used inside the single /agents custom UI lifecycle. */
31
+ class ChoiceMenuComponent extends Container {
32
+ get focused() {
33
+ return this._focused;
34
+ }
35
+ set focused(value) {
36
+ this._focused = value;
37
+ }
38
+ constructor(options) {
39
+ super();
40
+ this._focused = false;
41
+ this.theme = options.theme;
42
+ this.keybindings = options.keybindings;
43
+ this.choices = options.choices;
44
+ this.onDone = options.onDone;
45
+ this.title = options.title;
46
+ this.cancelDescription = options.cancelDescription;
47
+ this.selectedIndex = Math.min(Math.max(options.initialIndex ?? 0, 0), Math.max(0, options.choices.length - 1));
48
+ this.addChild(new DynamicBorder());
49
+ this.addChild(new Spacer(1));
50
+ this.titleText = new Text("", 1, 0);
51
+ this.addChild(this.titleText);
52
+ this.addChild(new Spacer(1));
53
+ this.listContainer = new Container();
54
+ this.addChild(this.listContainer);
55
+ this.addChild(new Spacer(1));
56
+ this.hintText = new Text("", 1, 0);
57
+ this.addChild(this.hintText);
58
+ this.addChild(new Spacer(1));
59
+ this.addChild(new DynamicBorder());
60
+ this.updateStaticText();
61
+ this.updateList();
62
+ }
63
+ updateStaticText() {
64
+ this.titleText.setText(this.theme.fg("accent", this.theme.bold(this.title)));
65
+ const hints = [
66
+ navigationHint(this.theme, this.keybindings),
67
+ hint(this.theme, this.keybindings, "tui.select.confirm", "select"),
68
+ hint(this.theme, this.keybindings, "tui.select.cancel", this.cancelDescription),
69
+ ].filter(Boolean);
70
+ this.hintText.setText(hints.join(this.theme.fg("muted", " • ")));
71
+ }
72
+ updateList() {
73
+ this.listContainer.clear();
74
+ for (const [index, choice] of this.choices.entries()) {
75
+ const selected = index === this.selectedIndex;
76
+ const prefix = selected ? this.theme.fg("accent", "→ ") : " ";
77
+ const label = this.theme.fg(selected ? "accent" : "text", choice.label);
78
+ this.listContainer.addChild(new TruncatedText(`${prefix}${label}`, 1, 0));
79
+ }
80
+ }
81
+ moveSelection(offset) {
82
+ if (this.choices.length === 0)
83
+ return;
84
+ this.selectedIndex = (this.selectedIndex + offset + this.choices.length) % this.choices.length;
85
+ this.updateList();
86
+ }
87
+ invalidate() {
88
+ this.updateStaticText();
89
+ this.updateList();
90
+ super.invalidate();
91
+ }
92
+ handleInput(data) {
93
+ if (this.keybindings.matches(data, "tui.select.up")) {
94
+ this.moveSelection(-1);
95
+ }
96
+ else if (this.keybindings.matches(data, "tui.select.down")) {
97
+ this.moveSelection(1);
98
+ }
99
+ else if (this.keybindings.matches(data, "tui.select.confirm")) {
100
+ const choice = this.choices[this.selectedIndex];
101
+ if (choice)
102
+ this.onDone(choice);
103
+ }
104
+ else if (this.keybindings.matches(data, "tui.select.cancel")) {
105
+ this.onDone(undefined);
106
+ }
107
+ }
108
+ }
61
109
  /** Compact Model/Thinking settings menu for one profile; every confirm persists immediately. */
62
110
  class ProfileSettingsMenuComponent extends Container {
63
111
  get focused() {
@@ -73,42 +121,48 @@ class ProfileSettingsMenuComponent extends Container {
73
121
  this.theme = options.theme;
74
122
  this.keybindings = options.keybindings;
75
123
  this.onDone = options.onDone;
76
- const effective = effectiveModel(options.currentSessionModel, options.models, options.override);
77
- this.rows = [
78
- {
79
- action: "model",
80
- label: "Model",
81
- value: options.override?.model
82
- ? `override — ${options.override.model}${findModel(options.models, options.override.model) ? "" : " [unavailable]"}`
83
- : `inherit — ${modelId(options.currentSessionModel)}`,
84
- override: options.override?.model !== undefined,
85
- },
86
- {
87
- action: "thinking",
88
- label: "Thinking",
89
- value: options.override?.thinking
90
- ? `override — ${options.override.thinking}`
91
- : `inherit — ${inheritedThinking(effective, options.currentThinking)}`,
92
- override: options.override?.thinking !== undefined,
93
- },
94
- ];
124
+ this.title = AGENT_PROFILE_LABELS[options.profile.name];
125
+ this.status = options.status;
126
+ this.rows = buildSettingsRows({
127
+ override: options.override,
128
+ models: options.models,
129
+ currentSessionModel: options.currentSessionModel,
130
+ currentThinking: options.currentThinking,
131
+ });
132
+ this.selectedIndex = Math.max(0, this.rows.findIndex((row) => row.action === options.selectedAction));
95
133
  this.addChild(new DynamicBorder());
96
134
  this.addChild(new Spacer(1));
97
- this.addChild(new Text(this.theme.fg("accent", this.theme.bold(PROFILE_LABELS[options.profile.name])), 1, 0));
135
+ this.titleText = new Text("", 1, 0);
136
+ this.addChild(this.titleText);
98
137
  this.addChild(new Spacer(1));
99
138
  this.listContainer = new Container();
100
139
  this.addChild(this.listContainer);
101
140
  this.addChild(new Spacer(1));
102
- const hints = [
103
- navigationHint(this.theme, options.keybindings),
104
- hint(this.theme, options.keybindings, "tui.select.confirm", "select"),
105
- hint(this.theme, options.keybindings, "tui.select.cancel", "back"),
106
- ].filter(Boolean);
107
- this.addChild(new Text(hints.join(this.theme.fg("muted", " • ")), 1, 0));
141
+ if (this.status) {
142
+ this.statusText = new Text("", 1, 0);
143
+ this.addChild(this.statusText);
144
+ this.addChild(new Spacer(1));
145
+ }
146
+ else {
147
+ this.statusText = undefined;
148
+ }
149
+ this.hintText = new Text("", 1, 0);
150
+ this.addChild(this.hintText);
108
151
  this.addChild(new Spacer(1));
109
152
  this.addChild(new DynamicBorder());
153
+ this.updateStaticText();
110
154
  this.updateList();
111
155
  }
156
+ updateStaticText() {
157
+ this.titleText.setText(this.theme.fg("accent", this.theme.bold(this.title)));
158
+ this.statusText?.setText(this.theme.fg("muted", this.status ?? ""));
159
+ const hints = [
160
+ navigationHint(this.theme, this.keybindings),
161
+ hint(this.theme, this.keybindings, "tui.select.confirm", "select"),
162
+ hint(this.theme, this.keybindings, "tui.select.cancel", "back"),
163
+ ].filter(Boolean);
164
+ this.hintText.setText(hints.join(this.theme.fg("muted", " • ")));
165
+ }
112
166
  updateList() {
113
167
  this.listContainer.clear();
114
168
  for (const [index, row] of this.rows.entries()) {
@@ -124,6 +178,11 @@ class ProfileSettingsMenuComponent extends Container {
124
178
  this.selectedIndex = (this.selectedIndex + offset + this.rows.length) % this.rows.length;
125
179
  this.updateList();
126
180
  }
181
+ invalidate() {
182
+ this.updateStaticText();
183
+ this.updateList();
184
+ super.invalidate();
185
+ }
127
186
  handleInput(data) {
128
187
  if (this.keybindings.matches(data, "tui.select.up")) {
129
188
  this.moveSelection(-1);
@@ -139,7 +198,7 @@ class ProfileSettingsMenuComponent extends Container {
139
198
  }
140
199
  }
141
200
  }
142
- /** Standalone searchable model picker for one profile; confirm saves immediately. */
201
+ /** Searchable model page for one profile; confirm saves immediately. */
143
202
  class ModelPickerComponent extends Container {
144
203
  get focused() {
145
204
  return this._focused;
@@ -156,9 +215,12 @@ class ModelPickerComponent extends Container {
156
215
  this.tui = options.tui;
157
216
  this.theme = options.theme;
158
217
  this.keybindings = options.keybindings;
218
+ this.title = `Model — ${AGENT_PROFILE_LABELS[options.profile.name]}`;
219
+ this.hasScopedModels = options.scopedModels.length > 0;
159
220
  this.addChild(new DynamicBorder());
160
221
  this.addChild(new Spacer(1));
161
- this.addChild(new Text(this.theme.fg("accent", this.theme.bold(`Model — ${PROFILE_LABELS[options.profile.name]}`)), 1, 0));
222
+ this.titleText = new Text("", 1, 0);
223
+ this.addChild(this.titleText);
162
224
  this.addChild(new Spacer(1));
163
225
  this.modelList = new ProfileModelListComponent({
164
226
  theme: options.theme,
@@ -171,15 +233,21 @@ class ModelPickerComponent extends Container {
171
233
  });
172
234
  this.addChild(this.modelList);
173
235
  this.addChild(new Spacer(1));
174
- const hints = [
175
- navigationHint(this.theme, options.keybindings),
176
- options.scopedModels.length > 0 ? hint(this.theme, options.keybindings, "tui.input.tab", "scope") : "",
177
- hint(this.theme, options.keybindings, "tui.select.confirm", "select"),
178
- hint(this.theme, options.keybindings, "tui.select.cancel", "back"),
179
- ].filter(Boolean);
180
- this.addChild(new Text(hints.join(this.theme.fg("muted", " • ")), 1, 0));
236
+ this.hintText = new Text("", 1, 0);
237
+ this.addChild(this.hintText);
181
238
  this.addChild(new Spacer(1));
182
239
  this.addChild(new DynamicBorder());
240
+ this.updateStaticText();
241
+ }
242
+ updateStaticText() {
243
+ this.titleText.setText(this.theme.fg("accent", this.theme.bold(this.title)));
244
+ const hints = [
245
+ navigationHint(this.theme, this.keybindings),
246
+ this.hasScopedModels ? hint(this.theme, this.keybindings, "tui.input.tab", "scope") : "",
247
+ hint(this.theme, this.keybindings, "tui.select.confirm", "select"),
248
+ hint(this.theme, this.keybindings, "tui.select.cancel", "back"),
249
+ ].filter(Boolean);
250
+ this.hintText.setText(hints.join(this.theme.fg("muted", " • ")));
183
251
  }
184
252
  get refreshSignal() {
185
253
  return this.refreshAbortController.signal;
@@ -206,20 +274,22 @@ class ModelPickerComponent extends Container {
206
274
  this.modelList.setStatus(message, tone);
207
275
  this.tui.requestRender();
208
276
  }
277
+ invalidate() {
278
+ this.updateStaticText();
279
+ super.invalidate();
280
+ }
209
281
  handleInput(data) {
210
282
  this.modelList.handleInput(data);
211
283
  }
212
284
  }
213
285
  /** Persist a single override atomically; the UI only reflects the value on success. */
214
- async function persistOverride(ctx, profile, patch, current) {
215
- const next = "model" in patch ? patch.model : patch.thinking;
216
- if (next === current)
217
- return;
286
+ async function persistOverride(ctx, profile, patch) {
218
287
  try {
219
- await updateProfileOverride(profile.name, patch, getAgentDir());
288
+ return await updateProfileOverride(profile.name, patch, getAgentDir());
220
289
  }
221
290
  catch (error) {
222
- ctx.ui.notify(`Could not save ${PROFILE_LABELS[profile.name]} settings: ${error instanceof Error ? error.message : String(error)}`, "error");
291
+ ctx.ui.notify(`Could not save ${AGENT_PROFILE_LABELS[profile.name]} settings: ${error instanceof Error ? error.message : String(error)}`, "error");
292
+ return undefined;
223
293
  }
224
294
  }
225
295
  async function refreshPickerModels(ctx, picker) {
@@ -245,7 +315,7 @@ async function refreshPickerModels(ctx, picker) {
245
315
  }
246
316
  const registryError = ctx.modelRegistry.getError();
247
317
  if (registryError) {
248
- picker.setRefreshStatus(boundText(registryError, MAX_REFRESH_ERROR_LENGTH), "error");
318
+ picker.setRefreshStatus(truncate(registryError, MAX_REFRESH_ERROR_LENGTH), "error");
249
319
  return;
250
320
  }
251
321
  picker.setRefreshStatus(undefined);
@@ -256,130 +326,290 @@ async function refreshPickerModels(ctx, picker) {
256
326
  const message = timedOut
257
327
  ? "Model refresh timed out; showing cached models."
258
328
  : `Could not refresh model catalogs: ${error instanceof Error ? error.message : String(error)}`;
259
- picker.setRefreshStatus(boundText(message, MAX_REFRESH_ERROR_LENGTH), "error");
329
+ picker.setRefreshStatus(truncate(message, MAX_REFRESH_ERROR_LENGTH), "error");
260
330
  }
261
331
  finally {
262
332
  clearTimeout(timeout);
263
333
  }
264
334
  }
335
+ /** One mounted /agents TUI whose child pages switch without restoring the editor between them. */
336
+ class AgentsMenuComponent extends Container {
337
+ get focused() {
338
+ return this._focused;
339
+ }
340
+ set focused(value) {
341
+ this._focused = value;
342
+ if (this.activeScreen && isFocusable(this.activeScreen))
343
+ this.activeScreen.focused = value;
344
+ }
345
+ constructor(options) {
346
+ super();
347
+ this.route = { kind: "profiles" };
348
+ this.selectedSettingsAction = "model";
349
+ this.saving = false;
350
+ this.closed = false;
351
+ this._focused = false;
352
+ this.tui = options.tui;
353
+ this.theme = options.theme;
354
+ this.keybindings = options.keybindings;
355
+ this.ctx = options.ctx;
356
+ this.currentThinking = options.currentThinking;
357
+ this.config = options.config;
358
+ this.onDone = options.onDone;
359
+ this.showProfiles();
360
+ }
361
+ disposeActiveScreen() {
362
+ const screen = this.activeScreen;
363
+ this.activeScreen = undefined;
364
+ if (!screen)
365
+ return;
366
+ const disposable = screen;
367
+ if (isFocusable(screen))
368
+ screen.focused = false;
369
+ disposable.dispose?.();
370
+ }
371
+ setActiveScreen(screen) {
372
+ this.disposeActiveScreen();
373
+ this.clear();
374
+ this.activeScreen = screen;
375
+ this.addChild(screen);
376
+ if (isFocusable(screen))
377
+ screen.focused = this._focused;
378
+ this.tui.requestRender();
379
+ }
380
+ showProfiles() {
381
+ this.route = { kind: "profiles" };
382
+ const choices = AGENT_PROFILES.map((profile) => ({
383
+ label: AGENT_PROFILE_LABELS[profile.name],
384
+ value: profile,
385
+ }));
386
+ const initialIndex = choices.findIndex((choice) => choice.value.name === this.selectedProfile?.name);
387
+ this.setActiveScreen(new ChoiceMenuComponent({
388
+ theme: this.theme,
389
+ keybindings: this.keybindings,
390
+ title: "Agents",
391
+ choices,
392
+ initialIndex,
393
+ cancelDescription: "close",
394
+ onDone: (choice) => {
395
+ if (!choice) {
396
+ this.onDone();
397
+ return;
398
+ }
399
+ this.selectedProfile = choice.value;
400
+ this.selectedSettingsAction = "model";
401
+ this.showSettings(choice.value);
402
+ },
403
+ }));
404
+ }
405
+ showSettings(profile, selectedAction = this.selectedSettingsAction) {
406
+ this.selectedProfile = profile;
407
+ this.selectedSettingsAction = selectedAction;
408
+ this.route = { kind: "settings", profile, selectedAction };
409
+ this.setActiveScreen(new ProfileSettingsMenuComponent({
410
+ theme: this.theme,
411
+ keybindings: this.keybindings,
412
+ profile,
413
+ override: this.config.profiles[profile.name],
414
+ models: this.ctx.modelRegistry.getAvailable(),
415
+ currentSessionModel: this.ctx.model,
416
+ currentThinking: this.currentThinking,
417
+ selectedAction,
418
+ status: this.saving ? "Saving…" : undefined,
419
+ onDone: (action) => {
420
+ if (!action) {
421
+ this.showProfiles();
422
+ return;
423
+ }
424
+ this.selectedSettingsAction = action;
425
+ if (action === "model")
426
+ this.showModelPicker(profile);
427
+ else
428
+ this.showThinkingPicker(profile);
429
+ },
430
+ }));
431
+ }
432
+ showModelPicker(profile) {
433
+ this.route = { kind: "model", profile };
434
+ const override = this.config.profiles[profile.name];
435
+ const picker = new ModelPickerComponent({
436
+ tui: this.tui,
437
+ theme: this.theme,
438
+ keybindings: this.keybindings,
439
+ profile,
440
+ savedModelId: override?.model,
441
+ models: this.ctx.modelRegistry.getAvailable(),
442
+ scopedModels: this.ctx.scopedModels,
443
+ currentSessionModel: this.ctx.model,
444
+ onDone: (choice) => {
445
+ if (!choice) {
446
+ this.showSettings(profile, "model");
447
+ return;
448
+ }
449
+ this.saveOverride(profile, { model: choice.modelId }, "model");
450
+ },
451
+ });
452
+ picker.setRefreshStatus("Refreshing model catalogs…");
453
+ this.setActiveScreen(picker);
454
+ void refreshPickerModels(this.ctx, picker);
455
+ }
456
+ showThinkingPicker(profile) {
457
+ this.route = { kind: "thinking", profile };
458
+ const override = this.config.profiles[profile.name];
459
+ const choices = [
460
+ ...buildThinkingChoices({
461
+ currentSessionModel: this.ctx.model,
462
+ models: this.ctx.modelRegistry.getAvailable(),
463
+ override,
464
+ currentThinking: this.currentThinking,
465
+ }).entries(),
466
+ ].map(([label, level]) => ({ label, value: level }));
467
+ const initialIndex = choices.findIndex((choice) => choice.label.endsWith(" ✓"));
468
+ this.setActiveScreen(new ChoiceMenuComponent({
469
+ theme: this.theme,
470
+ keybindings: this.keybindings,
471
+ title: `Thinking — ${AGENT_PROFILE_LABELS[profile.name]}`,
472
+ choices,
473
+ initialIndex,
474
+ cancelDescription: "back",
475
+ onDone: (choice) => {
476
+ if (!choice) {
477
+ this.showSettings(profile, "thinking");
478
+ return;
479
+ }
480
+ this.saveOverride(profile, { thinking: choice.value }, "thinking");
481
+ },
482
+ }));
483
+ }
484
+ saveOverride(profile, patch, returnAction) {
485
+ if (this.saving)
486
+ return;
487
+ this.saving = true;
488
+ this.showSettings(profile, returnAction);
489
+ void persistOverride(this.ctx, profile, patch)
490
+ .then((config) => this.completeSave(profile, config), () => this.completeSave(profile, undefined))
491
+ .catch(() => {
492
+ // A reload can stale the command context while the queued file write finishes.
493
+ });
494
+ }
495
+ completeSave(profile, config) {
496
+ if (this.closed)
497
+ return;
498
+ this.saving = false;
499
+ if (config)
500
+ this.config = config;
501
+ if (this.route.kind === "settings" && this.route.profile.name === profile.name) {
502
+ this.showSettings(profile, this.route.selectedAction);
503
+ }
504
+ }
505
+ handleInput(data) {
506
+ if (!this.saving) {
507
+ this.activeScreen?.handleInput?.(data);
508
+ return;
509
+ }
510
+ if (!this.keybindings.matches(data, "tui.select.cancel"))
511
+ return;
512
+ if (this.route.kind === "settings")
513
+ this.showProfiles();
514
+ else if (this.route.kind === "profiles")
515
+ this.onDone();
516
+ }
517
+ dispose() {
518
+ this.closed = true;
519
+ this.disposeActiveScreen();
520
+ this.clear();
521
+ }
522
+ }
265
523
  async function selectProfile(ctx) {
266
- const labels = AGENT_PROFILES.map((profile) => PROFILE_LABELS[profile.name]);
524
+ const labels = AGENT_PROFILES.map((profile) => AGENT_PROFILE_LABELS[profile.name]);
267
525
  const label = await ctx.ui.select("Agents", labels);
268
- return label === undefined ? undefined : AGENT_PROFILES.find((profile) => PROFILE_LABELS[profile.name] === label);
526
+ return label === undefined
527
+ ? undefined
528
+ : AGENT_PROFILES.find((profile) => AGENT_PROFILE_LABELS[profile.name] === label);
269
529
  }
270
- async function showTuiSettingsMenu(ctx, profile, override, currentThinking) {
271
- return ctx.ui.custom((_tui, theme, keybindings, done) => new ProfileSettingsMenuComponent({
272
- theme,
273
- keybindings,
274
- profile,
530
+ async function showDialogSettingsMenu(ctx, profile, override, currentThinking) {
531
+ const options = new Map();
532
+ for (const row of buildSettingsRows({
275
533
  override,
276
534
  models: ctx.modelRegistry.getAvailable(),
277
535
  currentSessionModel: ctx.model,
278
536
  currentThinking,
279
- onDone: done,
280
- }));
281
- }
282
- async function showDialogSettingsMenu(ctx, profile, override, currentThinking) {
283
- const models = ctx.modelRegistry.getAvailable();
284
- const effective = effectiveModel(ctx.model, models, override);
285
- const modelValue = override?.model
286
- ? `override — ${override.model}${findModel(models, override.model) ? "" : " [unavailable]"}`
287
- : `inherit — ${modelId(ctx.model)}`;
288
- const thinkingValue = override?.thinking
289
- ? `override — ${override.thinking}`
290
- : `inherit — ${inheritedThinking(effective, currentThinking)}`;
291
- const options = new Map();
292
- options.set(`Model — ${modelValue}`, "model");
293
- options.set(`Thinking — ${thinkingValue}`, "thinking");
294
- const label = await ctx.ui.select(PROFILE_LABELS[profile.name], [...options.keys()]);
537
+ })) {
538
+ options.set(`${row.label} — ${row.value}`, row.action);
539
+ }
540
+ const label = await ctx.ui.select(AGENT_PROFILE_LABELS[profile.name], [...options.keys()]);
295
541
  return label === undefined ? undefined : options.get(label);
296
542
  }
297
- async function showTuiModelPicker(ctx, profile, override) {
298
- const chosen = await ctx.ui.custom((tui, theme, keybindings, done) => {
299
- const picker = new ModelPickerComponent({
300
- tui,
301
- theme,
302
- keybindings,
303
- profile,
304
- savedModelId: override?.model,
305
- models: ctx.modelRegistry.getAvailable(),
306
- scopedModels: ctx.scopedModels,
307
- currentSessionModel: ctx.model,
308
- onDone: done,
309
- });
310
- picker.setRefreshStatus("Refreshing model catalogs…");
311
- void refreshPickerModels(ctx, picker);
312
- return picker;
313
- });
314
- return chosen === undefined ? undefined : { modelId: chosen.modelId };
315
- }
316
543
  async function showDialogModelPicker(ctx, profile, override) {
317
- const models = ctx.modelRegistry.getAvailable().sort((left, right) => {
318
- const provider = left.provider.localeCompare(right.provider);
319
- return provider !== 0 ? provider : left.id.localeCompare(right.id);
544
+ const models = [...ctx.modelRegistry.getAvailable()].sort(compareModels);
545
+ const choices = buildModelChoices({
546
+ models,
547
+ currentSessionModel: ctx.model,
548
+ savedModelId: override?.model,
320
549
  });
321
- const choices = new Map();
322
- choices.set(`inherit (${modelId(ctx.model)})${override?.model ? "" : " ✓"}`, undefined);
323
- if (override?.model && !findModel(models, override.model)) {
324
- choices.set(`${override.model} [unavailable] ✓`, override.model);
325
- }
326
- for (const model of models) {
327
- const id = modelId(model);
328
- choices.set(`${id}${override?.model === id ? " ✓" : ""}`, id);
329
- }
330
- const label = await ctx.ui.select(`Model — ${PROFILE_LABELS[profile.name]}`, [...choices.keys()]);
550
+ const label = await ctx.ui.select(`Model — ${AGENT_PROFILE_LABELS[profile.name]}`, [...choices.keys()]);
331
551
  return label === undefined ? undefined : { modelId: choices.get(label) };
332
552
  }
333
- async function showThinkingPicker(ctx, profile, override, currentThinking) {
334
- const models = ctx.modelRegistry.getAvailable();
335
- const effective = effectiveModel(ctx.model, models, override);
336
- const levels = effective ? getSupportedThinkingLevels(effective) : [...THINKING_LEVELS];
337
- const inherited = inheritedThinking(effective, currentThinking);
338
- const choices = new Map();
339
- choices.set(`inherit (${inherited})${override?.thinking ? "" : " ✓"}`, undefined);
340
- for (const level of levels) {
341
- choices.set(`${level}${override?.thinking === level ? " ✓" : ""}`, level);
342
- }
343
- const label = await ctx.ui.select(`Thinking — ${PROFILE_LABELS[profile.name]}`, [...choices.keys()]);
553
+ async function showDialogThinkingPicker(ctx, profile, override, currentThinking) {
554
+ const choices = buildThinkingChoices({
555
+ currentSessionModel: ctx.model,
556
+ models: ctx.modelRegistry.getAvailable(),
557
+ override,
558
+ currentThinking,
559
+ });
560
+ const label = await ctx.ui.select(`Thinking — ${AGENT_PROFILE_LABELS[profile.name]}`, [...choices.keys()]);
344
561
  return label === undefined ? undefined : { level: choices.get(label) };
345
562
  }
346
- async function runProfileSettings(ctx, profile, currentThinking) {
563
+ async function runDialogProfileSettings(ctx, profile, currentThinking) {
347
564
  const agentDir = getAgentDir();
348
- const isTui = ctx.mode === "tui";
349
565
  while (true) {
350
566
  const config = await loadSubagentConfig(agentDir);
351
567
  const override = config.profiles[profile.name];
352
- const action = isTui
353
- ? await showTuiSettingsMenu(ctx, profile, override, currentThinking)
354
- : await showDialogSettingsMenu(ctx, profile, override, currentThinking);
568
+ const action = await showDialogSettingsMenu(ctx, profile, override, currentThinking);
355
569
  if (!action)
356
570
  return;
357
571
  if (action === "model") {
358
- const chosen = isTui
359
- ? await showTuiModelPicker(ctx, profile, override)
360
- : await showDialogModelPicker(ctx, profile, override);
572
+ const chosen = await showDialogModelPicker(ctx, profile, override);
361
573
  if (!chosen)
362
574
  continue;
363
- await persistOverride(ctx, profile, { model: chosen.modelId }, override?.model);
575
+ await persistOverride(ctx, profile, { model: chosen.modelId });
364
576
  }
365
577
  else {
366
- const chosen = await showThinkingPicker(ctx, profile, override, currentThinking);
578
+ const chosen = await showDialogThinkingPicker(ctx, profile, override, currentThinking);
367
579
  if (!chosen)
368
580
  continue;
369
- await persistOverride(ctx, profile, { thinking: chosen.level }, override?.thinking);
581
+ await persistOverride(ctx, profile, { thinking: chosen.level });
370
582
  }
371
583
  }
372
584
  }
373
- export async function showAgentsCommand(ctx, currentThinking) {
374
- if (!ctx.hasUI) {
375
- ctx.ui.notify("/agents requires an interactive UI.", "warning");
376
- return;
377
- }
585
+ async function showTuiAgentsCommand(ctx, currentThinking) {
586
+ const config = await loadSubagentConfig(getAgentDir());
587
+ await ctx.ui.custom((tui, theme, keybindings, done) => new AgentsMenuComponent({
588
+ tui,
589
+ theme,
590
+ keybindings,
591
+ ctx,
592
+ currentThinking,
593
+ config,
594
+ onDone: () => done(),
595
+ }));
596
+ }
597
+ async function showDialogAgentsCommand(ctx, currentThinking) {
378
598
  while (true) {
379
599
  const profile = await selectProfile(ctx);
380
600
  if (!profile)
381
601
  return;
382
- await runProfileSettings(ctx, profile, currentThinking);
602
+ await runDialogProfileSettings(ctx, profile, currentThinking);
603
+ }
604
+ }
605
+ export async function showAgentsCommand(ctx, currentThinking) {
606
+ if (!ctx.hasUI) {
607
+ ctx.ui.notify("/agents requires an interactive UI.", "warning");
608
+ return;
383
609
  }
610
+ if (ctx.mode === "tui")
611
+ await showTuiAgentsCommand(ctx, currentThinking);
612
+ else
613
+ await showDialogAgentsCommand(ctx, currentThinking);
384
614
  }
385
615
  //# sourceMappingURL=agents-command.js.map