@oh-my-pi/pi-tui 18.4.2 → 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 (38) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/types/chat/thinking-display.d.ts +2 -0
  3. package/dist/types/components/editor.d.ts +5 -0
  4. package/dist/types/loop-watchdog.d.ts +9 -0
  5. package/dist/types/overlays/model-picker.d.ts +2 -1
  6. package/dist/types/setup/scenes/sign-in.d.ts +9 -9
  7. package/dist/types/setup/scenes/types.d.ts +0 -26
  8. package/dist/types/stdin-buffer.d.ts +10 -7
  9. package/dist/types/terminal.d.ts +9 -0
  10. package/dist/types/tools/web-search-types.d.ts +90 -0
  11. package/dist/types/tools/web-search.d.ts +1 -168
  12. package/package.json +9 -9
  13. package/src/chat/assistant-message.ts +192 -40
  14. package/src/chat/thinking-display.ts +73 -11
  15. package/src/chrome/transcript-container.ts +102 -17
  16. package/src/components/editor.ts +23 -9
  17. package/src/components/markdown.ts +110 -56
  18. package/src/loop-watchdog.ts +38 -10
  19. package/src/overlays/model-picker.ts +9 -6
  20. package/src/overlays/settings-selector.ts +2 -10
  21. package/src/setup/scenes/sign-in.ts +17 -12
  22. package/src/setup/scenes/types.ts +0 -27
  23. package/src/setup/wizard.ts +1 -1
  24. package/src/status-line/component.ts +16 -2
  25. package/src/stdin-buffer.ts +27 -21
  26. package/src/terminal.ts +23 -9
  27. package/src/theme/mermaid-cache.ts +10 -1
  28. package/src/theme/tui-adapters.ts +14 -1
  29. package/src/tools/bash.ts +53 -13
  30. package/src/tools/edit.ts +110 -29
  31. package/src/tools/streaming-output.ts +79 -16
  32. package/src/tools/web-search-types.ts +99 -0
  33. package/src/tools/web-search.ts +1 -172
  34. package/src/tui.ts +1 -1
  35. package/dist/types/setup/scenes/providers.d.ts +0 -3
  36. package/dist/types/setup/scenes/web-search.d.ts +0 -22
  37. package/src/setup/scenes/providers.ts +0 -110
  38. package/src/setup/scenes/web-search.ts +0 -165
@@ -718,6 +718,12 @@ const MAX_PENDING = 10;
718
718
  export class TailBuffer {
719
719
  #pending: string[] = [];
720
720
  #pos = 0; // byte count of the currently-held tail (after trims)
721
+ // `#pending[0]` and its byte count. Clean = well-formed and `#headBytes` is its exact
722
+ // UTF-8 length (a head compacted from chunks that split a surrogate pair is neither).
723
+ #headBytes = 0;
724
+ #headClean = true;
725
+ // Every chunk after `#pending[0]` is well-formed (no lone surrogates).
726
+ #restWellFormed = true;
721
727
 
722
728
  constructor(readonly maxBytes: number) {}
723
729
 
@@ -726,8 +732,7 @@ export class TailBuffer {
726
732
 
727
733
  const max = this.maxBytes;
728
734
  if (max === 0) {
729
- this.#pending.length = 0;
730
- this.#pos = 0;
735
+ this.#setHead("", 0, true);
731
736
  return;
732
737
  }
733
738
 
@@ -736,19 +741,17 @@ export class TailBuffer {
736
741
  // If the incoming chunk alone is >= budget, it fully dominates the tail.
737
742
  if (n >= max) {
738
743
  const { text: t, bytes } = truncateTailBytes(text, max);
739
- this.#pending[0] = t;
740
- this.#pending.length = 1;
741
- this.#pos = bytes;
744
+ // A chunk of exactly `max` bytes comes back verbatim, lone surrogates included.
745
+ this.#setHead(t, bytes, t.isWellFormed());
742
746
  return;
743
747
  }
744
748
 
745
- this.#pos += n;
746
-
747
749
  if (this.#pending.length === 0) {
748
- this.#pending[0] = text;
749
- this.#pending.length = 1;
750
+ this.#setHead(text, n, text.isWellFormed());
750
751
  } else {
752
+ this.#pos += n;
751
753
  this.#pending.push(text);
754
+ if (this.#restWellFormed && !text.isWellFormed()) this.#restWellFormed = false;
752
755
  if (this.#pending.length > MAX_PENDING) this.#compact();
753
756
  }
754
757
 
@@ -770,9 +773,22 @@ export class TailBuffer {
770
773
 
771
774
  // -- private ---------------------------------------------------------------
772
775
 
776
+ /** Replace the whole buffer with a single chunk. */
777
+ #setHead(text: string, bytes: number, clean: boolean): void {
778
+ this.#pending[0] = text;
779
+ this.#pending.length = 1;
780
+ this.#pos = bytes;
781
+ this.#headBytes = bytes;
782
+ this.#headClean = clean;
783
+ this.#restWellFormed = true;
784
+ }
785
+
773
786
  #compact(): void {
774
787
  this.#pending[0] = this.#pending.join("");
775
788
  this.#pending.length = 1;
789
+ this.#headBytes = this.#pos;
790
+ this.#headClean = this.#headClean && this.#restWellFormed;
791
+ this.#restWellFormed = true;
776
792
  }
777
793
 
778
794
  #flush(): string {
@@ -783,17 +799,64 @@ export class TailBuffer {
783
799
 
784
800
  #trimTo(max: number): void {
785
801
  if (max === 0) {
786
- this.#pending.length = 0;
787
- this.#pos = 0;
802
+ this.#setHead("", 0, true);
788
803
  return;
789
804
  }
790
805
  if (this.#pos <= max) return;
791
806
 
792
- const joined = this.#flush();
793
- const { text, bytes } = truncateTailBytes(joined, max);
794
- this.#pos = bytes;
795
- this.#pending[0] = text;
796
- this.#pending.length = 1;
807
+ // Steady state (one `text()` per appended chunk): the overflow lies inside the
808
+ // already-trimmed head, so only the head's front is cut and the chunks after it
809
+ // are kept verbatim instead of re-encoding the whole window. Those chunks must be
810
+ // well-formed: the full path round-trips them through UTF-8, which rewrites lone
811
+ // surrogates (and heals pairs split across chunk boundaries).
812
+ const restBytes = this.#pos - this.#headBytes;
813
+ if (restBytes < max && this.#restWellFormed) {
814
+ const head = this.#pending[0];
815
+ const keep = max - restBytes;
816
+ const drop = this.#headBytes - keep;
817
+ if (head.length === this.#headBytes) {
818
+ // All-ASCII head: byte offsets are char offsets and every byte is a boundary.
819
+ this.#pending[0] = head.slice(drop);
820
+ this.#headBytes = keep;
821
+ } else if (this.#headClean && drop <= keep) {
822
+ // Walk code points to the first boundary at or after `drop` bytes.
823
+ let dropped = 0;
824
+ let index = 0;
825
+ while (dropped < drop) {
826
+ const code = head.charCodeAt(index);
827
+ if (code < 0x80) {
828
+ dropped += 1;
829
+ index += 1;
830
+ } else if (code < 0x800) {
831
+ dropped += 2;
832
+ index += 1;
833
+ } else if (code >= 0xd800 && code <= 0xdbff) {
834
+ // Well-formed, so a high surrogate always starts a pair.
835
+ dropped += 4;
836
+ index += 2;
837
+ } else {
838
+ dropped += 3;
839
+ index += 1;
840
+ }
841
+ }
842
+ this.#pending[0] = head.slice(index);
843
+ this.#headBytes -= dropped;
844
+ } else {
845
+ // A head that already fits comes back verbatim, lone surrogates included.
846
+ const { text, bytes } = truncateTailBytes(head, keep);
847
+ this.#pending[0] = text;
848
+ this.#headBytes = bytes;
849
+ this.#headClean = text.isWellFormed();
850
+ this.#pos = this.#headBytes + restBytes;
851
+ return;
852
+ }
853
+ this.#headClean = true;
854
+ this.#pos = this.#headBytes + restBytes;
855
+ return;
856
+ }
857
+
858
+ const { text, bytes } = truncateTailBytes(this.#flush(), max);
859
+ this.#setHead(text, bytes, text.isWellFormed());
797
860
  }
798
861
  }
799
862
 
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Web search response types shared by the coding agent's search providers and
3
+ * the transcript renderer in {@link ./web-search}. Render-free, so provider code
4
+ * can import it without loading UI components.
5
+ */
6
+
7
+ /**
8
+ * Display label for every search implementation: `web/*` engines and the
9
+ * chat-model groundings. Keys are the ids providers report in
10
+ * {@link SearchResponse.provider} and search errors.
11
+ */
12
+ export const SEARCH_PROVIDER_LABELS = {
13
+ parallel: "Parallel",
14
+ perplexity: "Perplexity",
15
+ gemini: "Gemini",
16
+ anthropic: "Anthropic",
17
+ codex: "OpenAI Codex",
18
+ openai: "OpenAI API",
19
+ xai: "xAI",
20
+ openrouter: "OpenRouter",
21
+ zai: "Z.AI",
22
+ exa: "Exa",
23
+ tinyfish: "TinyFish",
24
+ jina: "Jina",
25
+ kagi: "Kagi",
26
+ tavily: "Tavily",
27
+ firecrawl: "Firecrawl",
28
+ brave: "Brave",
29
+ kimi: "Kimi",
30
+ synthetic: "Synthetic",
31
+ ollama: "Ollama",
32
+ searxng: "SearXNG",
33
+ startpage: "Startpage",
34
+ duckduckgo: "DuckDuckGo",
35
+ ecosia: "Ecosia",
36
+ google: "Google",
37
+ mojeek: "Mojeek",
38
+ public: "Public Web",
39
+ } as const;
40
+
41
+ /** Id of a search implementation; see {@link SEARCH_PROVIDER_LABELS}. */
42
+ export type SearchProviderId = keyof typeof SEARCH_PROVIDER_LABELS;
43
+
44
+ /** Label for a provider id; ids from older transcripts fall back to the raw id. */
45
+ export function getSearchProviderLabel(id: SearchProviderId): string {
46
+ return SEARCH_PROVIDER_LABELS[id] ?? id;
47
+ }
48
+
49
+ /** Source returned by search (all providers) */
50
+ export interface SearchSource {
51
+ title: string;
52
+ url: string;
53
+ snippet?: string;
54
+ /** ISO date string or relative ("2d ago") */
55
+ publishedDate?: string;
56
+ /** Age in seconds for consistent formatting */
57
+ ageSeconds?: number;
58
+ author?: string;
59
+ }
60
+
61
+ /** Citation with text reference (LLM-mediated providers) */
62
+ export interface SearchCitation {
63
+ url: string;
64
+ title: string;
65
+ citedText?: string;
66
+ }
67
+
68
+ /** Usage metrics */
69
+ export interface SearchUsage {
70
+ inputTokens?: number;
71
+ outputTokens?: number;
72
+ /** Anthropic: number of web search requests made */
73
+ searchRequests?: number;
74
+ /** Perplexity: combined token count */
75
+ totalTokens?: number;
76
+ }
77
+
78
+ /** Unified response across providers */
79
+ export interface SearchResponse {
80
+ provider: SearchProviderId | "none";
81
+ /** Synthesized answer text (LLM-mediated providers) */
82
+ answer?: string;
83
+ /** Search result sources */
84
+ sources: SearchSource[];
85
+ /** Text citations with context */
86
+ citations?: SearchCitation[];
87
+ /** Intermediate search queries (anthropic) */
88
+ searchQueries?: string[];
89
+ /** Follow-up question suggestions (provider-dependent) */
90
+ relatedQuestions?: string[];
91
+ /** Token usage metrics */
92
+ usage?: SearchUsage;
93
+ /** Model used */
94
+ model?: string;
95
+ /** Request ID for debugging */
96
+ requestId?: string;
97
+ /** Authentication mode used by the provider (e.g. oauth, api-key) */
98
+ authMode?: string;
99
+ }
@@ -22,6 +22,7 @@ import {
22
22
  } from "../render/render-utils";
23
23
  import { renderStatusLine, renderTreeList, urlHyperlink } from "../render";
24
24
  import { framedToolCard } from "../render/tool-card";
25
+ import { getSearchProviderLabel, type SearchResponse } from "./web-search-types";
25
26
 
26
27
  const MAX_COLLAPSED_ITEMS = PREVIEW_LIMITS.COLLAPSED_ITEMS;
27
28
 
@@ -246,175 +247,3 @@ export const webSearchToolRenderer = {
246
247
  renderResult: renderSearchResult,
247
248
  mergeCallAndResult: true,
248
249
  } satisfies ToolRenderer<{ query?: string; [key: string]: unknown }, SearchRenderDetails>;
249
-
250
- /**
251
- * Web Search Types
252
- *
253
- * Unified types for web search responses across supported providers.
254
- */
255
-
256
- export const SEARCH_PROVIDER_OPTIONS = [
257
- {
258
- value: "auto",
259
- label: "Auto",
260
- description: "Automatically uses the first configured web-search provider",
261
- },
262
- {
263
- value: "parallel",
264
- label: "Parallel",
265
- description: "Uses API auth when configured; otherwise searches through the keyless public MCP",
266
- },
267
- {
268
- value: "perplexity",
269
- label: "Perplexity",
270
- description: "Uses auth when configured; explicit selection falls back to anonymous search",
271
- },
272
- {
273
- value: "gemini",
274
- label: "Gemini",
275
- description: "Google Search grounding via Gemini (uses google-gemini-cli or google-antigravity OAuth)",
276
- },
277
- {
278
- value: "anthropic",
279
- label: "Anthropic",
280
- description: "Claude's native web_search tool (uses Anthropic OAuth or ANTHROPIC_API_KEY)",
281
- },
282
- {
283
- value: "codex",
284
- label: "OpenAI Codex",
285
- description: "OpenAI's native web_search (uses ChatGPT OAuth via /login openai-codex)",
286
- },
287
- {
288
- value: "openai",
289
- label: "OpenAI API",
290
- description:
291
- "OpenAI hosted web_search via OPENAI_API_KEY or OpenAI API-key registry credentials; billed by API usage",
292
- },
293
- {
294
- value: "xai",
295
- label: "xAI",
296
- description:
297
- "Grok web search via xAI Responses API (uses SuperGrok/X Premium+ OAuth via /login xai-oauth, or XAI_API_KEY)",
298
- },
299
- {
300
- value: "openrouter",
301
- label: "OpenRouter",
302
- description: "OpenRouter plugins-based web search with model-selected grounding",
303
- },
304
- { value: "zai", label: "Z.AI", description: "Calls Z.AI webSearchPrime MCP" },
305
- { value: "exa", label: "Exa", description: "API via /login exa or EXA_API_KEY; explicit keyless fallback via MCP" },
306
- { value: "tinyfish", label: "TinyFish", description: "Requires TINYFISH_API_KEY" },
307
- { value: "jina", label: "Jina", description: "Requires JINA_API_KEY" },
308
- { value: "kagi", label: "Kagi", description: "Requires KAGI_API_KEY and Kagi Search API beta access" },
309
- { value: "tavily", label: "Tavily", description: "Requires TAVILY_API_KEY" },
310
- {
311
- value: "firecrawl",
312
- label: "Firecrawl",
313
- description: "Uses Firecrawl API when FIRECRAWL_API_KEY is set; falls back to keyless mode",
314
- },
315
- { value: "brave", label: "Brave", description: "Requires BRAVE_API_KEY" },
316
- {
317
- value: "kimi",
318
- label: "Kimi",
319
- description:
320
- "Kimi Code search (requires a Kimi Code Console key via KIMI_SEARCH_API_KEY/MOONSHOT_SEARCH_API_KEY or /login kimi-code; not MOONSHOT_API_KEY)",
321
- },
322
- { value: "synthetic", label: "Synthetic", description: "Requires SYNTHETIC_API_KEY" },
323
- { value: "ollama", label: "Ollama", description: "Requires OLLAMA_CLOUD_API_KEY" },
324
- { value: "searxng", label: "SearXNG", description: "Requires SEARXNG_ENDPOINT or searxng.endpoint" },
325
- {
326
- value: "startpage",
327
- label: "Startpage",
328
- description: "Credential-free scrape of Startpage (Google-backed) results; may be bot-challenged",
329
- },
330
- {
331
- value: "duckduckgo",
332
- label: "DuckDuckGo",
333
- description: "Credential-free best-effort fallback; may be bot-challenged on datacenter/shared-egress IPs",
334
- },
335
- {
336
- value: "ecosia",
337
- label: "Ecosia",
338
- description: "Credential-free browser-backed scrape of Ecosia (Google-backed) results",
339
- },
340
- {
341
- value: "google",
342
- label: "Google",
343
- description: "Credential-free browser-backed fallback; slower and may be bot-challenged",
344
- },
345
- {
346
- value: "mojeek",
347
- label: "Mojeek",
348
- description: "Credential-free browser-backed scrape of Mojeek's independent index",
349
- },
350
- {
351
- value: "public",
352
- label: "Public Web",
353
- description: "Queries every credential-free engine in parallel and consolidates deduplicated results",
354
- },
355
- ] as const;
356
-
357
- /** Supported web search providers (every option except `auto`). */
358
- export type SearchProviderId = Exclude<(typeof SEARCH_PROVIDER_OPTIONS)[number]["value"], "auto">;
359
-
360
- /** Display labels, derived from {@link SEARCH_PROVIDER_OPTIONS}. */
361
- export const SEARCH_PROVIDER_LABELS = Object.fromEntries(
362
- SEARCH_PROVIDER_OPTIONS.flatMap(option => (option.value === "auto" ? [] : [[option.value, option.label] as const])),
363
- ) as Record<SearchProviderId, string>;
364
-
365
- /** Source returned by search (all providers) */
366
- export interface SearchSource {
367
- title: string;
368
- url: string;
369
- snippet?: string;
370
- /** ISO date string or relative ("2d ago") */
371
- publishedDate?: string;
372
- /** Age in seconds for consistent formatting */
373
- ageSeconds?: number;
374
- author?: string;
375
- }
376
-
377
- /** Citation with text reference (LLM-mediated providers) */
378
- export interface SearchCitation {
379
- url: string;
380
- title: string;
381
- citedText?: string;
382
- }
383
-
384
- /** Usage metrics */
385
- export interface SearchUsage {
386
- inputTokens?: number;
387
- outputTokens?: number;
388
- /** Anthropic: number of web search requests made */
389
- searchRequests?: number;
390
- /** Perplexity: combined token count */
391
- totalTokens?: number;
392
- }
393
-
394
- /** Unified response across providers */
395
- export interface SearchResponse {
396
- provider: SearchProviderId | "none";
397
- /** Synthesized answer text (LLM-mediated providers) */
398
- answer?: string;
399
- /** Search result sources */
400
- sources: SearchSource[];
401
- /** Text citations with context */
402
- citations?: SearchCitation[];
403
- /** Intermediate search queries (anthropic) */
404
- searchQueries?: string[];
405
- /** Follow-up question suggestions (provider-dependent) */
406
- relatedQuestions?: string[];
407
- /** Token usage metrics */
408
- usage?: SearchUsage;
409
- /** Model used */
410
- model?: string;
411
- /** Request ID for debugging */
412
- requestId?: string;
413
- /** Authentication mode used by the provider (e.g. oauth, api-key) */
414
- authMode?: string;
415
- }
416
-
417
- /** Cheap, sync metadata accessor — never triggers a provider load. */
418
- export function getSearchProviderLabel(id: SearchProviderId): string {
419
- return SEARCH_PROVIDER_LABELS[id] ?? id;
420
- }
package/src/tui.ts CHANGED
@@ -1328,7 +1328,7 @@ export class TUI extends Container {
1328
1328
  this.#beginResizeAltPaint();
1329
1329
  },
1330
1330
  () => this.stop(),
1331
- { deferInput: this.#inputDeferred },
1331
+ { deferInput: this.#inputDeferred, isLoopStalled: () => this.#watchdog.isStalled() },
1332
1332
  );
1333
1333
  if (this.#stopped) return;
1334
1334
  this.#cancelPostmortemRestore?.();
@@ -1,3 +0,0 @@
1
- import type { SetupScene } from "./types.js";
2
- /** Configure provider sign-in and the preferred web search backend. */
3
- export declare const providersSetupScene: SetupScene;
@@ -1,22 +0,0 @@
1
- import { type SgrMouseEvent } from "../../mouse.js";
2
- import type { SetupSceneHost, SetupTab } from "./types.js";
3
- /**
4
- * "Web search" panel: picks the provider the web_search tool should prefer and
5
- * reports whether the highlighted provider is ready to use given current
6
- * credentials (env keys or OAuth sign-ins from the Sign in tab) or an
7
- * unauthenticated fallback.
8
- */
9
- export declare class WebSearchTab implements SetupTab {
10
- #private;
11
- readonly id = "web-search";
12
- readonly label = "Web search";
13
- readonly modal = false;
14
- constructor(host: SetupSceneHost);
15
- onActivate(): void;
16
- handleInput(data: string): void;
17
- /** Wheel moves the highlight; hover lights the row under the pointer; click confirms it. */
18
- routeMouse(event: SgrMouseEvent, line: number, col: number): void;
19
- invalidate(): void;
20
- dispose(): void;
21
- render(width: number, maxLines?: number): readonly string[];
22
- }
@@ -1,110 +0,0 @@
1
- import { type SgrMouseEvent } from "../../mouse";
2
- import { TabBar } from "../../components/tab-bar";
3
- import { editorKey } from "../../chrome/keybinding-hints";
4
- import { getTabBarTheme } from "../../chrome/shared";
5
- import { SignInTab } from "./sign-in";
6
- import type { SetupScene, SetupSceneController, SetupSceneHost, SetupTab } from "./types";
7
- import { WebSearchTab } from "./web-search";
8
-
9
- /**
10
- * Tabbed "Set up your providers" scene. Composes independent panels (model
11
- * sign-in, web search) behind a {@link TabBar}; the active panel owns
12
- * rendering and input, while modal panels (e.g. an in-flight OAuth login)
13
- * temporarily suppress tab switching.
14
- */
15
- class ProvidersSceneController implements SetupSceneController {
16
- title = "Set up your providers";
17
- get subtitle(): string {
18
- return `Sign in and pick a web search provider. Press ${editorKey("tui.select.cancel")} when you're done.`;
19
- }
20
-
21
- #tabs: SetupTab[];
22
- #tabBar: TabBar;
23
- /** Lines the tab bar occupied in the last render (body starts one blank line below). */
24
- #tabRowCount = 1;
25
-
26
- constructor(host: SetupSceneHost) {
27
- this.#tabs = [new SignInTab(host), new WebSearchTab(host)];
28
- this.#tabBar = new TabBar(
29
- "Providers",
30
- this.#tabs.map(tab => ({ id: tab.id, label: tab.label })),
31
- getTabBarTheme(),
32
- );
33
- this.#tabBar.onTabChange = () => {
34
- this.#activeTab().onActivate?.();
35
- host.requestRender();
36
- };
37
- }
38
-
39
- #activeTab(): SetupTab {
40
- return this.#tabs[this.#tabBar.getActiveIndex()] ?? this.#tabs[0];
41
- }
42
-
43
- onMount(): void {
44
- this.#activeTab().onActivate?.();
45
- }
46
-
47
- invalidate(): void {
48
- for (const tab of this.#tabs) tab.invalidate();
49
- }
50
-
51
- handleInput(data: string): void {
52
- const tab = this.#activeTab();
53
- if (tab.modal) {
54
- tab.handleInput(data);
55
- return;
56
- }
57
- if (this.#tabBar.handleInput(data)) return;
58
- tab.handleInput(data);
59
- }
60
-
61
- /**
62
- * Hit-test mouse reports against the last render: rows inside the tab bar
63
- * hover/switch tabs (suppressed while the active panel is modal, matching
64
- * keyboard tab cycling); everything else forwards to the active panel at
65
- * panel-local coordinates. Wheel always goes to the panel so scrolling
66
- * works regardless of pointer position.
67
- */
68
- routeMouse(event: SgrMouseEvent, line: number, col: number): void {
69
- const tab = this.#activeTab();
70
- if (event.wheel === null && line >= 0 && line < this.#tabRowCount) {
71
- if (tab.modal) return;
72
- const hit = this.#tabBar.tabAt(line, col);
73
- if (event.motion) {
74
- this.#tabBar.setHoverTab(hit && !hit.muted ? hit.id : null);
75
- } else if (event.leftClick && hit) {
76
- this.#tabBar.selectTab(hit.id);
77
- }
78
- return;
79
- }
80
- if (event.motion) this.#tabBar.setHoverTab(null);
81
- const spacerRowsAfterTabs = 1;
82
- const bodyLine = line - this.#tabRowCount - spacerRowsAfterTabs;
83
- if (tab.routeMouse) {
84
- tab.routeMouse(event, bodyLine, col);
85
- return;
86
- }
87
- if (event.wheel !== null && !tab.modal) {
88
- tab.handleInput(event.wheel === -1 ? "\x1b[A" : "\x1b[B");
89
- }
90
- }
91
-
92
- render(width: number, maxLines?: number): readonly string[] {
93
- const tabLines = this.#tabBar.render(width);
94
- this.#tabRowCount = tabLines.length;
95
- const tabBudget = maxLines === undefined ? undefined : Math.max(1, maxLines - tabLines.length - 1);
96
- return [...tabLines, "", ...this.#activeTab().render(width, tabBudget)];
97
- }
98
-
99
- dispose(): void {
100
- for (const tab of this.#tabs) tab.dispose();
101
- }
102
- }
103
-
104
- /** Configure provider sign-in and the preferred web search backend. */
105
- export const providersSetupScene: SetupScene = {
106
- id: "providers",
107
- title: "Set up your providers",
108
- minVersion: 1,
109
- mount: host => new ProvidersSceneController(host),
110
- };