@astralyn/pi 0.84.6 → 0.84.8

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 (86) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/core/compaction/compaction.d.ts +2 -0
  3. package/dist/core/compaction/compaction.d.ts.map +1 -1
  4. package/dist/core/compaction/compaction.js +7 -7
  5. package/dist/core/compaction/compaction.js.map +1 -1
  6. package/dist/extensions/background/index.d.ts.map +1 -1
  7. package/dist/extensions/background/index.js +48 -72
  8. package/dist/extensions/background/index.js.map +1 -1
  9. package/dist/extensions/background/manager.d.ts.map +1 -1
  10. package/dist/extensions/background/manager.js +5 -3
  11. package/dist/extensions/background/manager.js.map +1 -1
  12. package/dist/extensions/background/registry.d.ts +11 -13
  13. package/dist/extensions/background/registry.d.ts.map +1 -1
  14. package/dist/extensions/background/registry.js +71 -80
  15. package/dist/extensions/background/registry.js.map +1 -1
  16. package/dist/extensions/background/render.d.ts.map +1 -1
  17. package/dist/extensions/background/render.js +17 -4
  18. package/dist/extensions/background/render.js.map +1 -1
  19. package/dist/extensions/deepwiki/constants.d.ts +2 -2
  20. package/dist/extensions/deepwiki/constants.d.ts.map +1 -1
  21. package/dist/extensions/deepwiki/constants.js +5 -5
  22. package/dist/extensions/deepwiki/constants.js.map +1 -1
  23. package/dist/extensions/deepwiki/schema.js +1 -1
  24. package/dist/extensions/deepwiki/schema.js.map +1 -1
  25. package/dist/extensions/question/constants.d.ts +2 -2
  26. package/dist/extensions/question/constants.d.ts.map +1 -1
  27. package/dist/extensions/question/constants.js +5 -2
  28. package/dist/extensions/question/constants.js.map +1 -1
  29. package/dist/extensions/router/dialog-host.d.ts +60 -0
  30. package/dist/extensions/router/dialog-host.d.ts.map +1 -0
  31. package/dist/extensions/router/dialog-host.js +196 -0
  32. package/dist/extensions/router/dialog-host.js.map +1 -0
  33. package/dist/extensions/router/probe.d.ts.map +1 -1
  34. package/dist/extensions/router/probe.js +2 -0
  35. package/dist/extensions/router/probe.js.map +1 -1
  36. package/dist/extensions/router/ui.d.ts.map +1 -1
  37. package/dist/extensions/router/ui.js +117 -67
  38. package/dist/extensions/router/ui.js.map +1 -1
  39. package/dist/extensions/subagent/activity.d.ts +9 -0
  40. package/dist/extensions/subagent/activity.d.ts.map +1 -1
  41. package/dist/extensions/subagent/activity.js +21 -0
  42. package/dist/extensions/subagent/activity.js.map +1 -1
  43. package/dist/extensions/subagent/agents-command.d.ts.map +1 -1
  44. package/dist/extensions/subagent/agents-command.js +355 -71
  45. package/dist/extensions/subagent/agents-command.js.map +1 -1
  46. package/dist/extensions/subagent/agents.d.ts.map +1 -1
  47. package/dist/extensions/subagent/agents.js +7 -3
  48. package/dist/extensions/subagent/agents.js.map +1 -1
  49. package/dist/extensions/subagent/constants.d.ts +1 -0
  50. package/dist/extensions/subagent/constants.d.ts.map +1 -1
  51. package/dist/extensions/subagent/constants.js +4 -0
  52. package/dist/extensions/subagent/constants.js.map +1 -1
  53. package/dist/extensions/subagent/index.js +2 -2
  54. package/dist/extensions/subagent/index.js.map +1 -1
  55. package/dist/extensions/subagent/render.d.ts.map +1 -1
  56. package/dist/extensions/subagent/render.js +1 -7
  57. package/dist/extensions/subagent/render.js.map +1 -1
  58. package/dist/extensions/subagent/sdk-runner.d.ts.map +1 -1
  59. package/dist/extensions/subagent/sdk-runner.js +19 -0
  60. package/dist/extensions/subagent/sdk-runner.js.map +1 -1
  61. package/dist/extensions/subagent/settings.d.ts.map +1 -1
  62. package/dist/extensions/subagent/settings.js +11 -4
  63. package/dist/extensions/subagent/settings.js.map +1 -1
  64. package/dist/extensions/subagent/state.d.ts +9 -0
  65. package/dist/extensions/subagent/state.d.ts.map +1 -1
  66. package/dist/extensions/subagent/state.js +33 -2
  67. package/dist/extensions/subagent/state.js.map +1 -1
  68. package/dist/extensions/subagent/ui/model-list.d.ts +1 -0
  69. package/dist/extensions/subagent/ui/model-list.d.ts.map +1 -1
  70. package/dist/extensions/subagent/ui/model-list.js +5 -0
  71. package/dist/extensions/subagent/ui/model-list.js.map +1 -1
  72. package/dist/extensions/todo/constants.d.ts +1 -1
  73. package/dist/extensions/todo/constants.d.ts.map +1 -1
  74. package/dist/extensions/todo/constants.js +10 -27
  75. package/dist/extensions/todo/constants.js.map +1 -1
  76. package/dist/extensions/todo/state.d.ts.map +1 -1
  77. package/dist/extensions/todo/state.js +27 -16
  78. package/dist/extensions/todo/state.js.map +1 -1
  79. package/dist/extensions/todo/view.d.ts.map +1 -1
  80. package/dist/extensions/todo/view.js +16 -5
  81. package/dist/extensions/todo/view.js.map +1 -1
  82. package/docs/bundled/extensions/background.md +3 -3
  83. package/docs/bundled/extensions/todo.md +2 -2
  84. package/docs/compaction.md +2 -0
  85. package/npm-shrinkwrap.json +2 -2
  86. package/package.json +1 -1
@@ -8,6 +8,7 @@ import { BorderedLoader } from "../../modes/interactive/components/bordered-load
8
8
  import { getModelSelectorSearchText } from "../../modes/interactive/model-search.js";
9
9
  import { formatError, isValidRelayId, NO_UI_WARNING, ROUTER_THINKING_LEVELS, truncate } from "./constants.js";
10
10
  import { createModelChecklist, createSearchableSelector, createThinkingMapEditor, } from "./dialog.js";
11
+ import { createNativeRouterDialogs, RouterTuiSession, RouterTuiSessionClosedError, } from "./dialog-host.js";
11
12
  import { createDefaultModelConfig, displayModelLabel, resolveModelConfig, resolveRouterThinkingMap, summarizeThinkingMap, } from "./presets.js";
12
13
  import { probeRelayModels } from "./probe.js";
13
14
  import { applyRouterFile, registerOneRelay, unregisterOneRelay } from "./register.js";
@@ -52,6 +53,18 @@ class RelayAutoSaver {
52
53
  }
53
54
  }
54
55
  }
56
+ /** Abort the catalog request if its internal page or the whole Router session is disposed. */
57
+ class RouterProbeLoader extends BorderedLoader {
58
+ constructor(tui, theme, message) {
59
+ super(tui, theme, message, { cancellable: true });
60
+ this.disposeController = new AbortController();
61
+ this.probeSignal = AbortSignal.any([this.signal, this.disposeController.signal]);
62
+ }
63
+ dispose() {
64
+ this.disposeController.abort();
65
+ super.dispose();
66
+ }
67
+ }
55
68
  export function isCurrentRouterModel(current, relayId, modelId) {
56
69
  return current?.provider === relayId && current.id === modelId;
57
70
  }
@@ -61,14 +74,6 @@ export async function runRouterCommand(args, ctx, pi) {
61
74
  return;
62
75
  }
63
76
  const trimmed = args.trim().toLowerCase();
64
- if (!trimmed || trimmed === "list") {
65
- await openMainMenu(ctx, pi);
66
- return;
67
- }
68
- if (trimmed === "add") {
69
- await addRelayFlow(ctx, pi);
70
- return;
71
- }
72
77
  if (trimmed === "reload") {
73
78
  const file = await loadRouterFile();
74
79
  applyRouterFile(pi, file);
@@ -76,16 +81,63 @@ export async function runRouterCommand(args, ctx, pi) {
76
81
  ctx.ui.notify(`Reloaded ${file.relays.length} relay(s).`, "info");
77
82
  return;
78
83
  }
79
- // Treat as relay id open
84
+ if (ctx.mode === "tui") {
85
+ await runRouterTuiCommand(args, ctx, pi);
86
+ return;
87
+ }
88
+ await runRouterDialogCommand(args, ctx, pi, createNativeRouterDialogs(ctx));
89
+ }
90
+ async function runRouterTuiCommand(args, ctx, pi) {
91
+ let failure;
92
+ let closedDuringTransition = false;
93
+ await ctx.ui.custom((tui, theme, keybindings, done) => {
94
+ const session = new RouterTuiSession(tui, theme, keybindings, () => {
95
+ closedDuringTransition = true;
96
+ done();
97
+ });
98
+ void runRouterDialogCommand(args, ctx, pi, session).then(() => {
99
+ if (!closedDuringTransition)
100
+ done();
101
+ }, (error) => {
102
+ if (closedDuringTransition) {
103
+ if (!(error instanceof RouterTuiSessionClosedError)) {
104
+ try {
105
+ ctx.ui.notify(formatError(error), "error");
106
+ }
107
+ catch {
108
+ // The command context may have gone stale after the modal closed.
109
+ }
110
+ }
111
+ return;
112
+ }
113
+ failure = { error };
114
+ done();
115
+ });
116
+ return session;
117
+ });
118
+ if (failure)
119
+ throw failure.error;
120
+ }
121
+ async function runRouterDialogCommand(args, ctx, pi, dialogs) {
122
+ const trimmed = args.trim().toLowerCase();
123
+ if (!trimmed || trimmed === "list") {
124
+ await openMainMenu(ctx, pi, dialogs);
125
+ return;
126
+ }
127
+ if (trimmed === "add") {
128
+ await addRelayFlow(ctx, pi, dialogs);
129
+ return;
130
+ }
131
+ // Treat as relay id open.
80
132
  const file = await loadRouterFile();
81
133
  const exact = file.relays.find((relay) => relay.id.toLowerCase() === trimmed);
82
134
  if (exact) {
83
- await editRelayFlow(ctx, pi, exact);
135
+ await editRelayFlow(ctx, pi, dialogs, exact);
84
136
  return;
85
137
  }
86
- await openMainMenu(ctx, pi, args.trim());
138
+ await openMainMenu(ctx, pi, dialogs, args.trim());
87
139
  }
88
- async function openMainMenu(ctx, pi, initialQuery) {
140
+ async function openMainMenu(ctx, pi, dialogs, initialQuery) {
89
141
  let cursor;
90
142
  let query = initialQuery;
91
143
  while (true) {
@@ -111,8 +163,8 @@ async function openMainMenu(ctx, pi, initialQuery) {
111
163
  searchText: "reload refresh",
112
164
  },
113
165
  ];
114
- const selected = ctx.mode === "tui"
115
- ? await ctx.ui.custom(createSearchableSelector({
166
+ const selected = dialogs.kind === "tui"
167
+ ? await dialogs.show(createSearchableSelector({
116
168
  title: "API relays",
117
169
  subtitle: file.relays.length === 0
118
170
  ? "No relays yet — add a base URL + API key."
@@ -122,13 +174,13 @@ async function openMainMenu(ctx, pi, initialQuery) {
122
174
  initialQuery: query,
123
175
  maxVisible: 10,
124
176
  }))
125
- : await selectNative(ctx, "API relays", items.map((item) => ({ value: item.value, label: item.label, description: item.description })));
177
+ : await selectRouterItem(dialogs, "API relays", items.map((item) => ({ value: item.value, label: item.label, description: item.description })));
126
178
  query = undefined;
127
179
  if (selected === undefined)
128
180
  return;
129
181
  cursor = selected;
130
182
  if (selected === "action:add") {
131
- const id = await addRelayFlow(ctx, pi);
183
+ const id = await addRelayFlow(ctx, pi, dialogs);
132
184
  if (id)
133
185
  cursor = `relay:${id}`;
134
186
  continue;
@@ -148,14 +200,14 @@ async function openMainMenu(ctx, pi, initialQuery) {
148
200
  ctx.ui.notify(`Relay "${id}" not found.`, "warning");
149
201
  continue;
150
202
  }
151
- await editRelayFlow(ctx, pi, relay);
203
+ await editRelayFlow(ctx, pi, dialogs, relay);
152
204
  }
153
205
  }
154
206
  }
155
- async function addRelayFlow(ctx, pi) {
207
+ async function addRelayFlow(ctx, pi, dialogs) {
156
208
  const file = await loadRouterFile();
157
209
  const existing = new Set(file.relays.map((relay) => relay.id));
158
- const id = await promptText(ctx, "New relay · name (provider id)", "my-relay", (value) => {
210
+ const id = await promptText(ctx, dialogs, "New relay · name (provider id)", "my-relay", (value) => {
159
211
  const trimmed = value.trim();
160
212
  if (!trimmed)
161
213
  return "Name is required.";
@@ -167,7 +219,7 @@ async function addRelayFlow(ctx, pi) {
167
219
  });
168
220
  if (id === undefined)
169
221
  return undefined;
170
- const baseUrl = await promptText(ctx, `New relay · ${id.trim()} · base URL`, "https://relay.example/v1", (value) => {
222
+ const baseUrl = await promptText(ctx, dialogs, `New relay · ${id.trim()} · base URL`, "https://relay.example/v1", (value) => {
171
223
  const trimmed = value.trim();
172
224
  if (!trimmed)
173
225
  return "Base URL is required.";
@@ -183,7 +235,7 @@ async function addRelayFlow(ctx, pi) {
183
235
  });
184
236
  if (baseUrl === undefined)
185
237
  return undefined;
186
- const apiKey = await promptText(ctx, `New relay · ${id.trim()} · API key`, "sk-… or $RELAY_KEY", (value) => {
238
+ const apiKey = await promptText(ctx, dialogs, `New relay · ${id.trim()} · API key`, "sk-… or $RELAY_KEY", (value) => {
187
239
  if (!value.trim())
188
240
  return "API key is required.";
189
241
  return undefined;
@@ -199,7 +251,7 @@ async function addRelayFlow(ctx, pi) {
199
251
  // Connection fields are committed before the network step so cancelling or
200
252
  // failing catalog discovery never loses the relay the user just entered.
201
253
  await persistRelay(ctx, pi, relay);
202
- await fetchAndSelectModels(ctx, pi, relay);
254
+ await fetchAndSelectModels(ctx, pi, dialogs, relay);
203
255
  if (relay.models.length === 0) {
204
256
  ctx.ui.notify(`Relay "${relay.id}" has no models yet.`, "warning");
205
257
  }
@@ -209,7 +261,7 @@ async function addRelayFlow(ctx, pi) {
209
261
  * Relay editor — flat menu. Every mutation writes router.json immediately.
210
262
  * Hierarchy: list → relay → (models list | connection field | fetch | remove).
211
263
  */
212
- async function editRelayFlow(ctx, pi, initial) {
264
+ async function editRelayFlow(ctx, pi, dialogs, initial) {
213
265
  const relay = structuredClone(initial);
214
266
  while (true) {
215
267
  const modelSummary = relay.models.length === 0
@@ -220,7 +272,7 @@ async function editRelayFlow(ctx, pi, initial) {
220
272
  .slice(0, 2)
221
273
  .map((m) => displayModelLabel(m))
222
274
  .join(", ")}…`;
223
- const choice = await selectNative(ctx, `Relay · ${relay.id}`, [
275
+ const choice = await selectRouterItem(dialogs, `Relay · ${relay.id}`, [
224
276
  {
225
277
  value: "models",
226
278
  label: "Models",
@@ -234,7 +286,7 @@ async function editRelayFlow(ctx, pi, initial) {
234
286
  if (choice === undefined || choice === "back")
235
287
  return;
236
288
  if (choice === "baseUrl") {
237
- const next = await promptText(ctx, `Relay · ${relay.id} · base URL`, relay.baseUrl, (value) => {
289
+ const next = await promptText(ctx, dialogs, `Relay · ${relay.id} · base URL`, relay.baseUrl, (value) => {
238
290
  try {
239
291
  const url = new URL(value.trim());
240
292
  if (url.protocol !== "http:" && url.protocol !== "https:")
@@ -255,7 +307,7 @@ async function editRelayFlow(ctx, pi, initial) {
255
307
  continue;
256
308
  }
257
309
  if (choice === "apiKey") {
258
- const next = await promptText(ctx, `Relay · ${relay.id} · API key`, relay.apiKey, (value) => value.trim() ? undefined : "API key is required.");
310
+ const next = await promptText(ctx, dialogs, `Relay · ${relay.id} · API key`, relay.apiKey, (value) => value.trim() ? undefined : "API key is required.");
259
311
  if (next === undefined)
260
312
  continue;
261
313
  const trimmed = next.trim();
@@ -268,7 +320,7 @@ async function editRelayFlow(ctx, pi, initial) {
268
320
  if (choice === "models") {
269
321
  // Always open the models screen (fetch lives only here). Empty list still
270
322
  // shows Fetch so the path is Relay → Models → Fetch, not a top-level shortcut.
271
- await manageModelsFlow(ctx, pi, relay);
323
+ await manageModelsFlow(ctx, pi, dialogs, relay);
272
324
  continue;
273
325
  }
274
326
  if (choice === "remove") {
@@ -276,7 +328,7 @@ async function editRelayFlow(ctx, pi, initial) {
276
328
  ctx.ui.notify(`Switch away from relay "${relay.id}" before removing it.`, "warning");
277
329
  continue;
278
330
  }
279
- const ok = await ctx.ui.confirm(`Remove relay "${relay.id}"?`, "Models will disappear from /model.");
331
+ const ok = await dialogs.confirm(`Remove relay "${relay.id}"?`, "Models will disappear from /model.");
280
332
  if (!ok)
281
333
  continue;
282
334
  await removeRelay(relay.id);
@@ -291,7 +343,7 @@ async function editRelayFlow(ctx, pi, initial) {
291
343
  * Models screen: searchable configured models plus fetch/manual-add actions.
292
344
  * Selecting a model opens its editor (name / thinking / remove).
293
345
  */
294
- async function manageModelsFlow(ctx, pi, relay) {
346
+ async function manageModelsFlow(ctx, pi, dialogs, relay) {
295
347
  let cursor;
296
348
  while (true) {
297
349
  const items = [
@@ -325,16 +377,16 @@ async function manageModelsFlow(ctx, pi, relay) {
325
377
  ];
326
378
  const currentValue = `model:${ctx.model?.id}`;
327
379
  const initialValue = cursor ?? (ctx.model?.provider === relay.id ? currentValue : undefined);
328
- const choice = await selectNative(ctx, `Relay · ${relay.id} · models`, items, { initialValue });
380
+ const choice = await selectRouterItem(dialogs, `Relay · ${relay.id} · models`, items, { initialValue });
329
381
  if (choice === undefined || choice === "action:back")
330
382
  return;
331
383
  cursor = choice;
332
384
  if (choice === "action:fetch") {
333
- await fetchAndSelectModels(ctx, pi, relay);
385
+ await fetchAndSelectModels(ctx, pi, dialogs, relay);
334
386
  continue;
335
387
  }
336
388
  if (choice === "action:manual") {
337
- const additions = await manualModelEntry(ctx);
389
+ const additions = await manualModelEntry(ctx, dialogs);
338
390
  if (!additions)
339
391
  continue;
340
392
  relay.models = mergeAddedModels(relay.models, additions);
@@ -346,16 +398,16 @@ async function manageModelsFlow(ctx, pi, relay) {
346
398
  const model = relay.models.find((entry) => entry.id === modelId);
347
399
  if (!model)
348
400
  continue;
349
- await editModelFlow(ctx, pi, relay, model);
401
+ await editModelFlow(ctx, pi, dialogs, relay, model);
350
402
  }
351
403
  }
352
404
  }
353
405
  /** Single-model editor: display name + thinking. Every confirmed change is live. */
354
- async function editModelFlow(ctx, pi, relay, model) {
406
+ async function editModelFlow(ctx, pi, dialogs, relay, model) {
355
407
  while (true) {
356
408
  const resolved = resolveModelConfig(model);
357
409
  const nameDesc = resolved.name ? resolved.name : "(empty · /model shows id)";
358
- const action = await selectNative(ctx, `Relay · ${relay.id} · ${model.id}`, [
410
+ const action = await selectRouterItem(dialogs, `Relay · ${relay.id} · ${model.id}`, [
359
411
  { value: "name", label: "Display name", description: nameDesc },
360
412
  {
361
413
  value: "thinking",
@@ -368,7 +420,7 @@ async function editModelFlow(ctx, pi, relay, model) {
368
420
  if (action === undefined || action === "back")
369
421
  return;
370
422
  if (action === "name") {
371
- const next = await promptText(ctx, `Display name · ${model.id} (empty = show id)`, resolved.name ?? "");
423
+ const next = await promptText(ctx, dialogs, `Display name · ${model.id} (empty = show id)`, resolved.name ?? "");
372
424
  if (next === undefined)
373
425
  continue;
374
426
  const trimmed = next.trim();
@@ -385,8 +437,8 @@ async function editModelFlow(ctx, pi, relay, model) {
385
437
  }
386
438
  if (action === "thinking") {
387
439
  const saver = new RelayAutoSaver(ctx, pi, relay);
388
- if (ctx.mode === "tui") {
389
- await ctx.ui.custom(createThinkingMapEditor({
440
+ if (dialogs.kind === "tui") {
441
+ await dialogs.show(createThinkingMapEditor({
390
442
  title: `Thinking · ${relay.id} / ${model.id}`,
391
443
  map: resolved.thinkingLevelMap,
392
444
  levels: ROUTER_THINKING_LEVELS,
@@ -398,7 +450,7 @@ async function editModelFlow(ctx, pi, relay, model) {
398
450
  }));
399
451
  }
400
452
  else {
401
- await editThinkingMapNative(ctx, resolved.thinkingLevelMap, (nextMap) => {
453
+ await editThinkingMapNative(dialogs, resolved.thinkingLevelMap, (nextMap) => {
402
454
  model.thinkingLevelMap = nextMap;
403
455
  model.reasoning = true;
404
456
  saver.save();
@@ -412,7 +464,7 @@ async function editModelFlow(ctx, pi, relay, model) {
412
464
  ctx.ui.notify(`Switch away from model "${model.id}" before removing it.`, "warning");
413
465
  continue;
414
466
  }
415
- const ok = await ctx.ui.confirm(`Remove model "${model.id}"?`, "Its display name and thinking settings will be removed.");
467
+ const ok = await dialogs.confirm(`Remove model "${model.id}"?`, "Its display name and thinking settings will be removed.");
416
468
  if (!ok)
417
469
  continue;
418
470
  relay.models = relay.models.filter((entry) => entry.id !== model.id);
@@ -422,10 +474,10 @@ async function editModelFlow(ctx, pi, relay, model) {
422
474
  }
423
475
  }
424
476
  }
425
- async function editThinkingMapNative(ctx, map, onChange) {
477
+ async function editThinkingMapNative(dialogs, map, onChange) {
426
478
  let working = resolveRouterThinkingMap(map);
427
479
  while (true) {
428
- const choice = await selectNative(ctx, "Toggle thinking level", [
480
+ const choice = await selectRouterItem(dialogs, "Toggle thinking level", [
429
481
  ...ROUTER_THINKING_LEVELS.map((level) => ({
430
482
  value: level,
431
483
  label: level,
@@ -447,21 +499,19 @@ async function editThinkingMapNative(ctx, map, onChange) {
447
499
  onChange({ ...working });
448
500
  }
449
501
  }
450
- async function fetchAndSelectModels(ctx, pi, relay) {
502
+ async function fetchAndSelectModels(ctx, pi, dialogs, relay) {
451
503
  const key = resolveProbeApiKey(relay.apiKey);
452
504
  if (key.error) {
453
505
  ctx.ui.notify(key.error, "warning");
454
- await recoverWithManualModels(ctx, pi, relay);
506
+ await recoverWithManualModels(ctx, pi, dialogs, relay);
455
507
  return;
456
508
  }
457
509
  let result;
458
510
  while (true) {
459
511
  const probeOptions = { baseUrl: relay.baseUrl, apiKey: key.value };
460
- if (ctx.mode === "tui") {
461
- result = await ctx.ui.custom((tui, theme, _kb, done) => {
462
- const loader = new BorderedLoader(tui, theme, `Fetching models · ${relay.id}`, {
463
- cancellable: true,
464
- });
512
+ if (dialogs.kind === "tui") {
513
+ result = await dialogs.show((tui, theme, _kb, done) => {
514
+ const loader = new RouterProbeLoader(tui, theme, `Fetching models · ${relay.id}`);
465
515
  let settled = false;
466
516
  const finish = (value) => {
467
517
  if (settled)
@@ -471,7 +521,7 @@ async function fetchAndSelectModels(ctx, pi, relay) {
471
521
  done(value);
472
522
  };
473
523
  loader.onAbort = () => finish(undefined);
474
- void probeRelayModels({ ...probeOptions, signal: loader.signal })
524
+ void probeRelayModels({ ...probeOptions, signal: loader.probeSignal })
475
525
  .then(finish)
476
526
  .catch((error) => finish({ ok: false, error: formatError(error) }));
477
527
  return loader;
@@ -485,7 +535,7 @@ async function fetchAndSelectModels(ctx, pi, relay) {
485
535
  return;
486
536
  if (result.ok && result.models.length === 0) {
487
537
  ctx.ui.notify("Server returned an empty model list.", "warning");
488
- const recovery = await selectNative(ctx, `Catalog empty · ${relay.id}`, [
538
+ const recovery = await selectRouterItem(dialogs, `Catalog empty · ${relay.id}`, [
489
539
  { value: "retry", label: "Retry", description: "Fetch /models again" },
490
540
  { value: "manual", label: "Add models manually", description: "Enter model ids without a catalog" },
491
541
  { value: "back", label: "Back", description: "Return to relay settings" },
@@ -493,13 +543,13 @@ async function fetchAndSelectModels(ctx, pi, relay) {
493
543
  if (recovery === "retry")
494
544
  continue;
495
545
  if (recovery === "manual")
496
- await recoverWithManualModels(ctx, pi, relay);
546
+ await recoverWithManualModels(ctx, pi, dialogs, relay);
497
547
  return;
498
548
  }
499
549
  if (result.ok)
500
550
  break;
501
551
  ctx.ui.notify(`Fetch failed: ${result.error}`, "error");
502
- const recovery = await selectNative(ctx, `Catalog unavailable · ${relay.id}`, [
552
+ const recovery = await selectRouterItem(dialogs, `Catalog unavailable · ${relay.id}`, [
503
553
  { value: "retry", label: "Retry", description: "Fetch /models again" },
504
554
  { value: "manual", label: "Add models manually", description: "Enter model ids without a catalog" },
505
555
  { value: "back", label: "Back", description: "Return to relay settings" },
@@ -507,7 +557,7 @@ async function fetchAndSelectModels(ctx, pi, relay) {
507
557
  if (recovery === "retry")
508
558
  continue;
509
559
  if (recovery === "manual")
510
- await recoverWithManualModels(ctx, pi, relay);
560
+ await recoverWithManualModels(ctx, pi, dialogs, relay);
511
561
  return;
512
562
  }
513
563
  if (!result || !result.ok)
@@ -536,8 +586,8 @@ async function fetchAndSelectModels(ctx, pi, relay) {
536
586
  });
537
587
  saver.save();
538
588
  };
539
- if (ctx.mode === "tui") {
540
- const choice = await ctx.ui.custom(createModelChecklist({
589
+ if (dialogs.kind === "tui") {
590
+ const choice = await dialogs.show(createModelChecklist({
541
591
  title: `Select models · ${relay.id}`,
542
592
  subtitle: "Space toggles immediately · Enter/Esc returns",
543
593
  models: catalog,
@@ -550,7 +600,7 @@ async function fetchAndSelectModels(ctx, pi, relay) {
550
600
  applySelectionIfChanged(relay, choice.selectedIds, applySelection);
551
601
  }
552
602
  else {
553
- const ok = await ctx.ui.confirm(`Import ${result.models.length} models?`, "Non-TUI mode imports the full catalog.");
603
+ const ok = await dialogs.confirm(`Import ${result.models.length} models?`, "Non-TUI mode imports the full catalog.");
554
604
  if (ok)
555
605
  applySelection([...new Set([...initiallySelected, ...result.models.map((model) => model.id)])]);
556
606
  }
@@ -558,8 +608,8 @@ async function fetchAndSelectModels(ctx, pi, relay) {
558
608
  if (relay.models.length === 0)
559
609
  ctx.ui.notify(`No models enabled for "${relay.id}".`, "warning");
560
610
  }
561
- async function manualModelEntry(ctx) {
562
- const value = await ctx.ui.input("Model IDs (comma-separated)", "gpt-5.6-sol, gpt-5.6-luna");
611
+ async function manualModelEntry(ctx, dialogs) {
612
+ const value = await dialogs.input("Model IDs (comma-separated)", "gpt-5.6-sol, gpt-5.6-luna");
563
613
  if (value === undefined)
564
614
  return undefined;
565
615
  const ids = [
@@ -574,8 +624,8 @@ async function manualModelEntry(ctx) {
574
624
  }
575
625
  return ids.map((id) => createDefaultModelConfig(id));
576
626
  }
577
- async function recoverWithManualModels(ctx, pi, relay) {
578
- const additions = await manualModelEntry(ctx);
627
+ async function recoverWithManualModels(ctx, pi, dialogs, relay) {
628
+ const additions = await manualModelEntry(ctx, dialogs);
579
629
  if (!additions)
580
630
  return;
581
631
  relay.models = mergeAddedModels(relay.models, additions);
@@ -624,9 +674,9 @@ async function persistRelay(ctx, pi, relay) {
624
674
  registerOneRelay(pi, relay);
625
675
  await ctx.modelRegistry.refresh();
626
676
  }
627
- async function promptText(ctx, title, placeholder, validate) {
677
+ async function promptText(ctx, dialogs, title, placeholder, validate) {
628
678
  while (true) {
629
- const value = await ctx.ui.input(title, placeholder);
679
+ const value = await dialogs.input(title, placeholder);
630
680
  if (value === undefined)
631
681
  return undefined;
632
682
  const error = validate?.(value);
@@ -637,9 +687,9 @@ async function promptText(ctx, title, placeholder, validate) {
637
687
  return value;
638
688
  }
639
689
  }
640
- async function selectNative(ctx, title, items, opts) {
641
- if (ctx.mode === "tui") {
642
- return ctx.ui.custom(createSearchableSelector({
690
+ async function selectRouterItem(dialogs, title, items, opts) {
691
+ if (dialogs.kind === "tui") {
692
+ return dialogs.show(createSearchableSelector({
643
693
  title,
644
694
  items,
645
695
  initialValue: opts?.initialValue,
@@ -648,7 +698,7 @@ async function selectNative(ctx, title, items, opts) {
648
698
  }));
649
699
  }
650
700
  const labels = items.map((item) => (item.description ? `${item.label} — ${item.description}` : item.label));
651
- const selected = await ctx.ui.select(title, labels);
701
+ const selected = await dialogs.select(title, labels);
652
702
  if (selected === undefined)
653
703
  return undefined;
654
704
  return items[labels.indexOf(selected)]?.value;