@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.
- package/CHANGELOG.md +20 -0
- package/dist/types/chat/thinking-display.d.ts +2 -0
- package/dist/types/components/editor.d.ts +5 -0
- package/dist/types/loop-watchdog.d.ts +9 -0
- package/dist/types/overlays/model-picker.d.ts +2 -1
- package/dist/types/setup/scenes/sign-in.d.ts +9 -9
- package/dist/types/setup/scenes/types.d.ts +0 -26
- package/dist/types/stdin-buffer.d.ts +10 -7
- package/dist/types/terminal.d.ts +9 -0
- package/dist/types/tools/web-search-types.d.ts +90 -0
- package/dist/types/tools/web-search.d.ts +1 -168
- package/package.json +9 -9
- package/src/chat/assistant-message.ts +192 -40
- package/src/chat/thinking-display.ts +73 -11
- package/src/chrome/transcript-container.ts +102 -17
- package/src/components/editor.ts +23 -9
- package/src/components/markdown.ts +110 -56
- package/src/loop-watchdog.ts +38 -10
- package/src/overlays/model-picker.ts +9 -6
- package/src/overlays/settings-selector.ts +2 -10
- package/src/setup/scenes/sign-in.ts +17 -12
- package/src/setup/scenes/types.ts +0 -27
- package/src/setup/wizard.ts +1 -1
- package/src/status-line/component.ts +16 -2
- package/src/stdin-buffer.ts +27 -21
- package/src/terminal.ts +23 -9
- package/src/theme/mermaid-cache.ts +10 -1
- package/src/theme/tui-adapters.ts +14 -1
- package/src/tools/bash.ts +53 -13
- package/src/tools/edit.ts +110 -29
- package/src/tools/streaming-output.ts +79 -16
- package/src/tools/web-search-types.ts +99 -0
- package/src/tools/web-search.ts +1 -172
- package/src/tui.ts +1 -1
- package/dist/types/setup/scenes/providers.d.ts +0 -3
- package/dist/types/setup/scenes/web-search.d.ts +0 -22
- package/src/setup/scenes/providers.ts +0 -110
- 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.#
|
|
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
|
-
|
|
740
|
-
this.#
|
|
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.#
|
|
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.#
|
|
787
|
-
this.#pos = 0;
|
|
802
|
+
this.#setHead("", 0, true);
|
|
788
803
|
return;
|
|
789
804
|
}
|
|
790
805
|
if (this.#pos <= max) return;
|
|
791
806
|
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
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
|
+
}
|
package/src/tools/web-search.ts
CHANGED
|
@@ -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,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
|
-
};
|