@oh-my-pi/pi-tui 18.4.1 → 18.4.3

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 (67) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/types/chat/thinking-display.d.ts +2 -0
  3. package/dist/types/chrome/transcript-container.d.ts +15 -1
  4. package/dist/types/components/editor.d.ts +5 -0
  5. package/dist/types/loop-watchdog.d.ts +9 -0
  6. package/dist/types/overlays/model-browser.d.ts +15 -0
  7. package/dist/types/overlays/model-picker.d.ts +2 -1
  8. package/dist/types/overlays/rewind-selector.d.ts +2 -1
  9. package/dist/types/prompt/composer-attachments.d.ts +7 -0
  10. package/dist/types/prompt/model-mention-autocomplete.d.ts +4 -1
  11. package/dist/types/render/width-aware-text.d.ts +6 -0
  12. package/dist/types/setup/scenes/sign-in.d.ts +9 -9
  13. package/dist/types/setup/scenes/types.d.ts +0 -26
  14. package/dist/types/stdin-buffer.d.ts +10 -7
  15. package/dist/types/terminal.d.ts +16 -0
  16. package/dist/types/theme/color.d.ts +1 -0
  17. package/dist/types/theme/session-color.d.ts +3 -3
  18. package/dist/types/theme/shimmer.d.ts +1 -1
  19. package/dist/types/theme/theme-class.d.ts +1 -1
  20. package/dist/types/tools/web-search-types.d.ts +90 -0
  21. package/dist/types/tools/web-search.d.ts +1 -168
  22. package/dist/types/tui.d.ts +3 -2
  23. package/package.json +9 -9
  24. package/src/chat/assistant-message.ts +192 -40
  25. package/src/chat/thinking-display.ts +73 -11
  26. package/src/chat/tool-execution.ts +24 -4
  27. package/src/chrome/transcript-container.ts +178 -31
  28. package/src/components/editor.ts +51 -15
  29. package/src/components/image.ts +15 -7
  30. package/src/components/loader.ts +37 -17
  31. package/src/components/markdown.ts +110 -56
  32. package/src/loop-watchdog.ts +38 -10
  33. package/src/overlays/agent-hub.ts +16 -6
  34. package/src/overlays/copy-selector.ts +5 -0
  35. package/src/overlays/model-browser.ts +106 -25
  36. package/src/overlays/model-picker.ts +9 -6
  37. package/src/overlays/rewind-selector.ts +10 -2
  38. package/src/overlays/settings-selector.ts +2 -10
  39. package/src/prompt/composer-attachments.ts +54 -7
  40. package/src/prompt/composer.ts +5 -2
  41. package/src/prompt/custom-editor.ts +24 -6
  42. package/src/prompt/model-mention-autocomplete.ts +22 -10
  43. package/src/render/width-aware-text.ts +9 -0
  44. package/src/setup/scenes/sign-in.ts +17 -12
  45. package/src/setup/scenes/types.ts +0 -27
  46. package/src/setup/wizard.ts +1 -1
  47. package/src/status-line/component.ts +229 -134
  48. package/src/status-line/segments.ts +34 -29
  49. package/src/stdin-buffer.ts +27 -21
  50. package/src/terminal.ts +60 -20
  51. package/src/theme/color.ts +15 -0
  52. package/src/theme/mermaid-cache.ts +10 -1
  53. package/src/theme/session-color.ts +26 -4
  54. package/src/theme/shimmer.ts +55 -76
  55. package/src/theme/theme-class.ts +29 -12
  56. package/src/theme/tui-adapters.ts +14 -1
  57. package/src/tools/bash.ts +53 -13
  58. package/src/tools/edit.ts +110 -29
  59. package/src/tools/streaming-output.ts +79 -16
  60. package/src/tools/web-search-types.ts +99 -0
  61. package/src/tools/web-search.ts +1 -172
  62. package/src/tui.ts +143 -113
  63. package/src/utils.ts +15 -0
  64. package/dist/types/setup/scenes/providers.d.ts +0 -3
  65. package/dist/types/setup/scenes/web-search.d.ts +0 -22
  66. package/src/setup/scenes/providers.ts +0 -110
  67. package/src/setup/scenes/web-search.ts +0 -165
@@ -28,7 +28,8 @@ export interface ResolvedRoleModel {
28
28
 
29
29
  /** Catalog refresh capability used by the session picker. */
30
30
  export interface ModelPickerRegistry extends ModelBrowserRegistry {
31
- refresh(strategy: "offline"): Promise<void>;
31
+ /** Bring the catalog up to date without rebuilding a current one; `true` when the picker must re-read it. */
32
+ refreshIfStale(): Promise<boolean>;
32
33
  }
33
34
 
34
35
  export interface ModelPickerCallbacks {
@@ -172,13 +173,15 @@ export class ModelPickerComponent implements Component {
172
173
  this.#browser.selectSelector(options.currentSelector);
173
174
  }
174
175
 
175
- // Reconcile with cached discovery state in the background. A --models
176
- // scope is registry-independent, so the offline reload would only repeat
177
- // the synchronous hydration above.
176
+ // Re-read only if the catalog moves (startup discovery landing, a
177
+ // models.yml edit): rebuilding a current catalog on every open blocks
178
+ // the first paint for seconds. A --models scope is registry-independent.
178
179
  if (this.#scopedModels.length === 0) {
179
180
  this.#registry
180
- .refresh("offline")
181
- .then(() => this.#syncFromRegistryState())
181
+ .refreshIfStale()
182
+ .then(changed => {
183
+ if (changed) this.#syncFromRegistryState();
184
+ })
182
185
  .catch(error => {
183
186
  this.#configError = error instanceof Error ? error.message : String(error);
184
187
  })
@@ -22,7 +22,8 @@
22
22
  * variants at a fork and jump between user turns elsewhere, `f` opens a
23
23
  * filter (typing narrows the current path to matching items; Esc leaves the
24
24
  * filter with the selection kept), Enter rewinds to the outlined item, A loads
25
- * earlier turns without changing selection, Esc cancels.
25
+ * earlier turns without changing selection (stepping above the oldest replayed
26
+ * turn loads them too), Esc cancels.
26
27
  */
27
28
  import type { AgentTool } from "@oh-my-pi/pi-agent-core";
28
29
  import {
@@ -471,7 +472,10 @@ export class RewindSelectorComponent implements Component {
471
472
  this.#move(delta, () => true);
472
473
  }
473
474
 
474
- /** Step the main selection by `delta` to the nearest visible target passing `accept`. */
475
+ /**
476
+ * Step the main selection by `delta` to the nearest visible target passing
477
+ * `accept`; stepping above the replayed tail loads the earlier history first.
478
+ */
475
479
  #move(delta: -1 | 1, accept: (target: OutlineTarget) => boolean): void {
476
480
  let index = this.#selected + delta;
477
481
  while (index >= 0 && index < this.#targets.length) {
@@ -485,6 +489,10 @@ export class RewindSelectorComponent implements Component {
485
489
  }
486
490
  index += delta;
487
491
  }
492
+ if (delta < 0 && this.#truncated) {
493
+ this.#loadFullHistory();
494
+ this.#move(delta, accept);
495
+ }
488
496
  }
489
497
 
490
498
  #isMainSelectable(index: number): boolean {
@@ -1139,15 +1139,7 @@ export class SettingsSelectorComponent implements Component {
1139
1139
  return entries.map(([provider, limit]) => `${provider}: ${limit}`).join(", ");
1140
1140
  }
1141
1141
 
1142
- #getMultiSelectOptions(def: SettingDef & { type: "multiselect" }) {
1143
- if (def.path !== "providers.webSearchOrder") return def.options;
1144
- const excluded: unknown = this.#context.settings.get("providers.webSearchExclude");
1145
- if (!Array.isArray(excluded)) return def.options;
1146
- return def.options.filter(option => !excluded.includes(option.value));
1147
- }
1148
-
1149
1142
  #createMultiSelect(def: SettingDef & { type: "multiselect" }, done: (value?: string) => void): Container {
1150
- const options = this.#getMultiSelectOptions(def);
1151
1143
  const current: unknown = this.#context.settings.get(def.path);
1152
1144
  const initial = Array.isArray(current)
1153
1145
  ? current.filter((entry): entry is string => typeof entry === "string")
@@ -1155,7 +1147,7 @@ export class SettingsSelectorComponent implements Component {
1155
1147
  return new MultiSelectSubmenu(
1156
1148
  def.label,
1157
1149
  def.description,
1158
- options,
1150
+ def.options,
1159
1151
  initial,
1160
1152
  def.ordered,
1161
1153
  value => {
@@ -1167,7 +1159,7 @@ export class SettingsSelectorComponent implements Component {
1167
1159
  }
1168
1160
 
1169
1161
  #formatMultiSelectValue(def: SettingDef & { type: "multiselect" }, value: unknown): string {
1170
- const options = this.#getMultiSelectOptions(def);
1162
+ const { options } = def;
1171
1163
  const labels = Array.isArray(value)
1172
1164
  ? value.flatMap(entry => {
1173
1165
  if (typeof entry !== "string") return [];
@@ -50,9 +50,12 @@ const SKILL_ICONS = [...new Set(Object.values(SYMBOL_PRESETS).map(m => m["icon.e
50
50
  /** Skill names as they appear in chips: word characters and dashes, dots only between segments. */
51
51
  const SKILL_NAME_SOURCE = "[\\w-]+(?:\\.[\\w-]+)*";
52
52
 
53
- const SKILL_CHIP_SOURCE = `(?:${SKILL_ICONS.map(icon =>
54
- /^[a-z]+$/i.test(icon) ? `(?<![A-Za-z])${RegExp.escape(icon)}` : RegExp.escape(icon),
55
- ).join("|")}) (${SKILL_NAME_SOURCE})(?![\\w-])`;
53
+ /** Regex source for a chip glyph; word glyphs (ASCII preset) must not continue a preceding word. */
54
+ function glyphSource(icon: string): string {
55
+ return /^[a-z]+$/i.test(icon) ? `(?<![A-Za-z])${RegExp.escape(icon)}` : RegExp.escape(icon);
56
+ }
57
+
58
+ const SKILL_CHIP_SOURCE = `(?:${SKILL_ICONS.map(glyphSource).join("|")}) (${SKILL_NAME_SOURCE})(?![\\w-])`;
56
59
 
57
60
  /**
58
61
  * Replaces `/skill:<name>` tokens for known skills with compact chip labels and
@@ -96,11 +99,16 @@ export function collapseModelMentions(
96
99
 
97
100
  const CHIP_ICON_KEY = { image: "chip.image", video: "chip.video", paste: "chip.paste" } as const;
98
101
 
102
+ /** Chip glyph the active theme renders for `kind`, including `symbols.overrides`. */
103
+ function activeChipIcon(kind: ChipKind): string {
104
+ return typeof theme === "undefined"
105
+ ? SYMBOL_PRESETS.unicode[CHIP_ICON_KEY[kind]]
106
+ : theme.symbol(CHIP_ICON_KEY[kind]);
107
+ }
108
+
99
109
  /** Compact atomic composer token for attachment `n` in the active symbol preset. */
100
110
  export function chipLabel(kind: ChipKind, n: number): string {
101
- const icon =
102
- typeof theme === "undefined" ? SYMBOL_PRESETS.unicode[CHIP_ICON_KEY[kind]] : theme.symbol(CHIP_ICON_KEY[kind]);
103
- return `${icon} #${n}`;
111
+ return `${activeChipIcon(kind)} #${n}`;
104
112
  }
105
113
 
106
114
  /** Every glyph a chip token may start with, across all symbol presets. */
@@ -111,7 +119,7 @@ const CHIP_ICONS: Record<ChipKind, readonly string[]> = {
111
119
  };
112
120
 
113
121
  const CHIP_TOKEN_SOURCE = `(?:${[...CHIP_ICONS.image, ...CHIP_ICONS.video, ...CHIP_ICONS.paste]
114
- .map(icon => (/^[a-z]+$/i.test(icon) ? `(?<![A-Za-z])${RegExp.escape(icon)}` : RegExp.escape(icon)))
122
+ .map(glyphSource)
115
123
  .join("|")}) #[1-9]\\d*`;
116
124
 
117
125
  /** Infers an attachment kind from a chip label emitted by any configured symbol preset. */
@@ -155,6 +163,45 @@ export const COMPOSER_TOKEN_REGEX = new RegExp(
155
163
  "gu",
156
164
  );
157
165
 
166
+ /**
167
+ * Attachment indices referenced by a composer buffer, per kind. Image/video count compact chips
168
+ * and expanded markers; paste counts compact chips only, since `[Paste #N]` markers number the
169
+ * base editor's separate paste buffer. Registered labels take precedence over theme glyphs so
170
+ * tokens created before a theme switch keep their kind; full numbers prevent `#1` matching `#10`.
171
+ */
172
+ export function referencedAttachments(
173
+ text: string,
174
+ recorded: ReadonlyMap<string, ChipKind>,
175
+ ): Record<ChipKind, Set<number>> {
176
+ const kinds: readonly ChipKind[] = ["image", "video", "paste"];
177
+ const kindByIcon = new Map<string, ChipKind>();
178
+ for (const kind of kinds) for (const icon of CHIP_ICONS[kind]) kindByIcon.set(icon, kind);
179
+ for (const kind of kinds) kindByIcon.set(activeChipIcon(kind), kind);
180
+ const icons = [...kindByIcon.keys()].sort((a, b) => b.length - a.length).map(glyphSource);
181
+ const labels = [...recorded.keys()].sort((a, b) => b.length - a.length).map(label => RegExp.escape(label));
182
+ const recordedSource = labels.length > 0 ? labels.join("|") : "(?!)";
183
+ const scanner = new RegExp(
184
+ `${PLACEHOLDER_REGEX.source}|(${recordedSource})(?!\\d)|(${icons.join("|")}) #([1-9]\\d*)`,
185
+ "gu",
186
+ );
187
+
188
+ const refs: Record<ChipKind, Set<number>> = { image: new Set(), video: new Set(), paste: new Set() };
189
+ for (const match of text.matchAll(scanner)) {
190
+ if (match[1] === "Image") refs.image.add(Number(match[2]));
191
+ else if (match[1] === "Video") refs.video.add(Number(match[2]));
192
+ const label = match[3];
193
+ if (label !== undefined) {
194
+ const kind = recorded.get(label);
195
+ if (kind !== undefined) refs[kind].add(Number(label.slice(label.lastIndexOf("#") + 1)));
196
+ } else {
197
+ const icon = match[4];
198
+ const kind = icon === undefined ? undefined : kindByIcon.get(icon);
199
+ if (kind !== undefined) refs[kind].add(Number(match[5]));
200
+ }
201
+ }
202
+ return refs;
203
+ }
204
+
158
205
  /** Add the registered model-chip labels to the composer placeholder matcher. */
159
206
  export function composerTokenRegex(mentionLabels: Iterable<string>): RegExp {
160
207
  const labels = [...new Set(mentionLabels)].sort((a, b) => b.length - a.length);
@@ -369,12 +369,15 @@ export class Composer implements TerminalFrameProvider {
369
369
  // editor drifts up above a band of blank rows (#11007).
370
370
  this.#transientChromeFloor = Math.min(this.#transientChromeFloor ?? transientRows, transientRows);
371
371
  const belowFloor = after.length - transientRows + this.#transientChromeFloor;
372
+ const now = performance.now();
373
+ const frame: AnimationFrame = { now, tick: Math.floor(now / 80) };
374
+ // Retirement measures the same live blocks the viewport lays out below;
375
+ // one open frame renders each of them once for both.
376
+ transcript.beginFrame(frame);
372
377
  const history = this.#offerHistory(transcript, width, rows, preRoots.length + belowFloor);
373
378
  const headerVisible = !this.#headerRetired && this.#offeredHistory?.source !== "header";
374
379
  const headerRows = headerVisible ? this.#header.render(width) : [];
375
380
  const before = [...headerRows, ...preRoots];
376
- const now = performance.now();
377
- const frame: AnimationFrame = { now, tick: Math.floor(now / 80) };
378
381
  // The live tail is laid out against the same baseline retirement is
379
382
  // billed against, so its compaction allocator (one row per block, no
380
383
  // inter-block blanks) engages only when a block genuinely cannot retire.
@@ -14,6 +14,7 @@ import { imageAttachmentSource } from "./image-source";
14
14
  import { isVideoPath } from "./video";
15
15
  import {
16
16
  attachmentSgr,
17
+ type ChipKind,
17
18
  COMPOSER_TOKEN_REGEX,
18
19
  chipLabel,
19
20
  collapseImageMarkers,
@@ -21,6 +22,8 @@ import {
21
22
  collapseSkillTokens,
22
23
  composerTokenRegex,
23
24
  modelChipStyle,
25
+ PLACEHOLDER_REGEX,
26
+ referencedAttachments,
24
27
  renderPlaceholders,
25
28
  skillChipLabel,
26
29
  skillChipStyle,
@@ -662,14 +665,29 @@ export class CustomEditor extends Editor {
662
665
  ) {
663
666
  return cached.chips;
664
667
  }
665
- const text = this.getText();
668
+ const recorded = new Map<string, ChipKind>();
669
+ if (this.pendingImages.length > 0) {
670
+ for (const [label, expansion] of this.atoms) {
671
+ const kind = expansion.startsWith("[Image #")
672
+ ? "image"
673
+ : expansion.startsWith("[Video #")
674
+ ? "video"
675
+ : undefined;
676
+ if (kind !== undefined && expansion.match(PLACEHOLDER_REGEX)?.[0] === expansion) {
677
+ recorded.set(label, kind);
678
+ }
679
+ }
680
+ }
681
+ for (const entry of this.pendingTexts) {
682
+ // A reused label belongs to the atom currently expanding it, not a deleted paste.
683
+ if (!recorded.has(entry.label)) recorded.set(entry.label, "paste");
684
+ }
685
+ const refs = referencedAttachments(this.getText(), recorded);
666
686
  const chips: ComposerChipDescriptor[] = [];
667
687
  for (let i = 0; i < this.pendingImages.length; i++) {
668
688
  const n = i + 1;
669
- const video =
670
- text.includes(chipLabel("video", n)) || text.includes(`[Video #${n}]`) || text.includes(`[Video #${n},`);
671
- const image =
672
- text.includes(chipLabel("image", n)) || text.includes(`[Image #${n}]`) || text.includes(`[Image #${n},`);
689
+ const video = refs.video.has(n);
690
+ const image = refs.image.has(n);
673
691
  if (!video && !image) continue;
674
692
  chips.push({
675
693
  kind: video ? "video" : "image",
@@ -679,7 +697,7 @@ export class CustomEditor extends Editor {
679
697
  });
680
698
  }
681
699
  for (const entry of this.pendingTexts) {
682
- if (!text.includes(entry.label)) continue;
700
+ if (!refs.paste.has(entry.n)) continue;
683
701
  chips.push({ kind: "paste", n: entry.n, text: entry });
684
702
  }
685
703
  this.#composerChipsCache = {
@@ -4,9 +4,11 @@ import type { ModelBrowserRegistry, ModelBrowserSource } from "../overlays/model
4
4
  import { modelMentionDisplayName } from "./model-mention-syntax";
5
5
  import type {
6
6
  buildSearchAffinity as BuildSearchAffinity,
7
- buildSessionModelScope as BuildSessionModelScope,
8
7
  ModelBrowserItem,
9
8
  rankModelItems as RankModelItems,
9
+ SearchAffinity,
10
+ SessionModelScope,
11
+ SessionModelScopeCache as SessionModelScopeCacheClass,
10
12
  } from "../overlays/model-browser";
11
13
  import { theme } from "../theme/theme";
12
14
 
@@ -17,7 +19,7 @@ const MAX_MODEL_MENTION_SUGGESTIONS = 20;
17
19
  export type ModelMentionCandidateSource = (query: string) => ReadonlyArray<ModelBrowserItem>;
18
20
 
19
21
  interface ModelBrowserModules {
20
- buildSessionModelScope: typeof BuildSessionModelScope;
22
+ SessionModelScopeCache: typeof SessionModelScopeCacheClass;
21
23
  buildSearchAffinity: typeof BuildSearchAffinity;
22
24
  rankModelItems: typeof RankModelItems;
23
25
  }
@@ -75,20 +77,30 @@ export function applyModelMentionCompletion(
75
77
  };
76
78
  }
77
79
 
78
- /** Create a fresh session-scoped model candidate lookup using picker ordering. */
80
+ /**
81
+ * Create a session-scoped model candidate lookup using picker ordering. The
82
+ * scope and search affinity are reused across queries until their inputs change.
83
+ */
79
84
  export function createModelMentionSource(host: {
80
85
  source: ModelBrowserSource;
81
86
  registry: ModelBrowserRegistry;
82
87
  scopedModels: () => ReadonlyArray<Model>;
83
88
  }): ModelMentionCandidateSource {
89
+ let scopeCache: SessionModelScopeCacheClass | undefined;
90
+ let affinityScope: SessionModelScope | undefined;
91
+ let affinityProviderOrder: readonly string[] | undefined;
92
+ let affinity: SearchAffinity | undefined;
84
93
  return query => {
85
- const { buildSearchAffinity, buildSessionModelScope, rankModelItems } = loadModelBrowser();
86
- const scope = buildSessionModelScope(host.source, host.registry, host.scopedModels());
94
+ const { SessionModelScopeCache, buildSearchAffinity, rankModelItems } = loadModelBrowser();
95
+ scopeCache ??= new SessionModelScopeCache(host.source, host.registry);
96
+ const scope = scopeCache.get(host.scopedModels());
87
97
  if (!query.trim()) return scope.items;
88
- return rankModelItems(query, scope.items, {
89
- roles: scope.roles,
90
- mruOrder: scope.mruOrder,
91
- affinity: buildSearchAffinity(host.source.modelProviderOrder, scope.roles, scope.mruOrder),
92
- });
98
+ const providerOrder = host.source.modelProviderOrder;
99
+ if (!affinity || affinityScope !== scope || affinityProviderOrder !== providerOrder) {
100
+ affinity = buildSearchAffinity(providerOrder, scope.roles, scope.mruOrder);
101
+ affinityScope = scope;
102
+ affinityProviderOrder = providerOrder;
103
+ }
104
+ return rankModelItems(query, scope.items, { roles: scope.roles, mruOrder: scope.mruOrder, affinity });
93
105
  };
94
106
  }
@@ -47,6 +47,15 @@ export class WidthAwareText implements Component {
47
47
  this.#inner.invalidate();
48
48
  }
49
49
 
50
+ /**
51
+ * Re-run the formatter on the next render because its inputs changed.
52
+ * Unlike {@link invalidate}, the inner `Text` keeps its wrap cache, so a
53
+ * reformat that yields the same string skips re-wrapping and re-tinting.
54
+ */
55
+ reformat(): void {
56
+ this.#cachedText = undefined;
57
+ }
58
+
50
59
  render(width: number): readonly string[] {
51
60
  const paddingX = this.#ignoreTight ? this.#paddingX : getPaddingX(this.#paddingX);
52
61
  const contentWidth = Math.max(1, width - paddingX * 2);
@@ -14,7 +14,7 @@ import { wrapTextWithAnsi } from "../../utils";
14
14
  import { getAgentDbPath } from "@oh-my-pi/pi-utils";
15
15
  import { OAuthSelectorComponent } from "../../overlays/oauth-selector";
16
16
  import { theme } from "../../theme/theme";
17
- import type { SetupSceneHost, SetupTab } from "./types";
17
+ import type { SetupScene, SetupSceneController, SetupSceneHost } from "./types";
18
18
 
19
19
  function loginUrlLink(url: string): string {
20
20
  return `\x1b]8;;${url}\x07Open login URL\x1b]8;;\x07`;
@@ -69,13 +69,15 @@ interface PromptState {
69
69
  }
70
70
 
71
71
  /**
72
- * "Sign in" panel: lets the user authenticate one or more model providers via
73
- * OAuth. Unlike a standalone scene it never auto-advances the wizard — the user
74
- * may sign in to several providers and then continue with Esc.
72
+ * "Sign in" scene: lets the user authenticate one or more model providers via
73
+ * OAuth. It never auto-advances the wizard — the user may sign in to several
74
+ * providers and then continue with Esc.
75
75
  */
76
- export class SignInTab implements SetupTab {
77
- readonly id = "sign-in";
78
- readonly label = "Sign in";
76
+ export class SignInScene implements SetupSceneController {
77
+ readonly title = "Sign in to your providers";
78
+ get subtitle(): string {
79
+ return `Sign in to one or more providers. Press ${editorKey("tui.select.cancel")} when you're done.`;
80
+ }
79
81
 
80
82
  #authStorage: AuthStorage;
81
83
  #selector: OAuthSelectorComponent;
@@ -99,11 +101,6 @@ export class SignInTab implements SetupTab {
99
101
  this.#selector = this.#createSelector();
100
102
  }
101
103
 
102
- /** Modal while an OAuth flow is running so the scene won't switch tabs or finish. */
103
- get modal(): boolean {
104
- return this.#loggingInProvider !== undefined;
105
- }
106
-
107
104
  dispose(): void {
108
105
  this.#disposed = true;
109
106
  this.#selector.stopValidation();
@@ -373,3 +370,11 @@ export class SignInTab implements SetupTab {
373
370
  this.#host.requestRender();
374
371
  }
375
372
  }
373
+
374
+ /** Onboarding scene for provider sign-in. */
375
+ export const providersSetupScene: SetupScene = {
376
+ id: "providers",
377
+ title: "Sign in to your providers",
378
+ minVersion: 1,
379
+ mount: host => new SignInScene(host),
380
+ };
@@ -5,7 +5,6 @@ import type { ComposerPreviewStatusSource } from "../../overlays/composer-shape-
5
5
  import type { ComposerShape } from "../../overlays/composer-shape-registry";
6
6
  import type { ModelBrowserSource } from "../../overlays/model-browser";
7
7
  import type { SymbolPreset } from "../../theme/theme";
8
- import type { SearchProviderId } from "../../tools/web-search";
9
8
  import type { Component, TUI } from "../../tui";
10
9
 
11
10
  /** Terminal capabilities used by setup overlays and the startup splash. */
@@ -21,7 +20,6 @@ export interface SetupHost extends SetupUiHost {
21
20
  readonly composerShape: ComposerShape;
22
21
  readonly symbolPreset: SymbolPreset;
23
22
  readonly colorBlindMode: boolean;
24
- readonly webSearchOrder: readonly string[];
25
23
  readonly disabledProviders: readonly string[];
26
24
  readonly authStorage: AuthStorage;
27
25
  readonly modelSource: ModelBrowserSource;
@@ -33,8 +31,6 @@ export interface SetupHost extends SetupUiHost {
33
31
  saveSymbolPreset(preset: SymbolPreset): void;
34
32
  saveColorBlindMode(enabled: boolean): void;
35
33
  saveTheme(mode: "dark" | "light", name: string): void;
36
- isSearchProviderAvailable(id: SearchProviderId): Promise<boolean>;
37
- saveSearchProvider(id: SearchProviderId | "auto"): void;
38
34
  captureBrowserSession(request: OAuthBrowserSessionRequest, signal?: AbortSignal): Promise<string>;
39
35
  copyToClipboard(text: string): Promise<void>;
40
36
  openInBrowser(url: string): void;
@@ -78,29 +74,6 @@ export interface SetupSceneController extends Component {
78
74
  routeMouse?(event: SgrMouseEvent, line: number, col: number): void;
79
75
  }
80
76
 
81
- /**
82
- * A single panel inside a tabbed setup scene. The host scene owns the tab bar
83
- * and forwards rendering/input to the active tab.
84
- */
85
- export interface SetupTab {
86
- readonly id: string;
87
- readonly label: string;
88
- /**
89
- * While `true` the tab owns all keyboard input (e.g. an in-progress OAuth
90
- * login). The parent scene MUST NOT switch tabs or finish while modal.
91
- */
92
- readonly modal: boolean;
93
- /** See {@link SetupSceneController.render}: `maxLines` is the tab-local row budget. */
94
- render(width: number, maxLines?: number): readonly string[];
95
- handleInput(data: string): void;
96
- invalidate(): void;
97
- /** Called when the tab becomes active (including initial mount). */
98
- onActivate?(): void;
99
- /** Mouse routing at tab-local coordinates; see {@link SetupSceneController.routeMouse}. */
100
- routeMouse?(event: SgrMouseEvent, line: number, col: number): void;
101
- dispose(): void;
102
- }
103
-
104
77
  /** Versioned onboarding scene definition. */
105
78
  export interface SetupScene {
106
79
  id: string;
@@ -3,7 +3,7 @@ import type { SetupHost } from "./scenes/types";
3
3
  import { composerSetupScene } from "./scenes/composer";
4
4
  import { glyphSetupScene } from "./scenes/glyph";
5
5
  import { modelSetupScene } from "./scenes/model";
6
- import { providersSetupScene } from "./scenes/providers";
6
+ import { providersSetupScene } from "./scenes/sign-in";
7
7
  import { themeSetupScene } from "./scenes/theme";
8
8
  import type { SetupScene } from "./scenes/types";
9
9
  import { SetupWizardComponent } from "./wizard-overlay";