@oh-my-pi/pi-coding-agent 15.9.67 → 15.10.0
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 +63 -1
- package/dist/types/cli/args.d.ts +1 -1
- package/dist/types/cli/gallery-cli.d.ts +43 -0
- package/dist/types/cli/gallery-fixtures/agentic.d.ts +2 -0
- package/dist/types/cli/gallery-fixtures/codeintel.d.ts +3 -0
- package/dist/types/cli/gallery-fixtures/edit.d.ts +3 -0
- package/dist/types/cli/gallery-fixtures/fs.d.ts +2 -0
- package/dist/types/cli/gallery-fixtures/index.d.ts +4 -0
- package/dist/types/cli/gallery-fixtures/interaction.d.ts +3 -0
- package/dist/types/cli/gallery-fixtures/memory.d.ts +2 -0
- package/dist/types/cli/gallery-fixtures/misc.d.ts +3 -0
- package/dist/types/cli/gallery-fixtures/search.d.ts +3 -0
- package/dist/types/cli/gallery-fixtures/shell.d.ts +3 -0
- package/dist/types/cli/gallery-fixtures/types.d.ts +44 -0
- package/dist/types/cli/gallery-fixtures/web.d.ts +2 -0
- package/dist/types/cli/gallery-screenshot.d.ts +35 -0
- package/dist/types/commands/gallery.d.ts +47 -0
- package/dist/types/config/keybindings.d.ts +6 -1
- package/dist/types/config/model-id-affixes.d.ts +2 -0
- package/dist/types/config/model-registry.d.ts +8 -1
- package/dist/types/config/settings-schema.d.ts +32 -6
- package/dist/types/extensibility/plugins/marketplace-auto-update.d.ts +8 -0
- package/dist/types/lsp/types.d.ts +10 -0
- package/dist/types/main.d.ts +3 -2
- package/dist/types/memory-backend/index.d.ts +2 -1
- package/dist/types/memory-backend/resolve.d.ts +1 -1
- package/dist/types/memory-backend/types.d.ts +1 -1
- package/dist/types/modes/components/custom-editor.d.ts +2 -1
- package/dist/types/modes/components/tool-execution.d.ts +18 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
- package/dist/types/modes/index.d.ts +5 -4
- package/dist/types/modes/interactive-mode.d.ts +1 -1
- package/dist/types/modes/setup-version.d.ts +11 -0
- package/dist/types/modes/setup-wizard/index.d.ts +2 -1
- package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +2 -1
- package/dist/types/modes/types.d.ts +1 -1
- package/dist/types/sdk.d.ts +1 -1
- package/dist/types/task/executor.d.ts +7 -0
- package/dist/types/telemetry-export.d.ts +1 -1
- package/dist/types/tools/eval-render.d.ts +1 -8
- package/dist/types/tools/fetch.d.ts +15 -7
- package/dist/types/tools/render-utils.d.ts +8 -0
- package/dist/types/tools/renderers.d.ts +16 -2
- package/dist/types/tools/search.d.ts +1 -1
- package/dist/types/tools/write.d.ts +2 -0
- package/dist/types/web/scrapers/github.d.ts +22 -0
- package/dist/types/web/search/providers/perplexity.d.ts +8 -1
- package/dist/types/web/search/types.d.ts +1 -1
- package/package.json +9 -9
- package/scripts/dev-launch +42 -0
- package/scripts/dev-launch-preload.ts +19 -0
- package/src/cli/args.ts +2 -2
- package/src/cli/gallery-cli.ts +223 -0
- package/src/cli/gallery-fixtures/agentic.ts +292 -0
- package/src/cli/gallery-fixtures/codeintel.ts +188 -0
- package/src/cli/gallery-fixtures/edit.ts +194 -0
- package/src/cli/gallery-fixtures/fs.ts +153 -0
- package/src/cli/gallery-fixtures/index.ts +40 -0
- package/src/cli/gallery-fixtures/interaction.ts +49 -0
- package/src/cli/gallery-fixtures/memory.ts +81 -0
- package/src/cli/gallery-fixtures/misc.ts +221 -0
- package/src/cli/gallery-fixtures/search.ts +213 -0
- package/src/cli/gallery-fixtures/shell.ts +167 -0
- package/src/cli/gallery-fixtures/types.ts +41 -0
- package/src/cli/gallery-fixtures/web.ts +158 -0
- package/src/cli/gallery-screenshot.ts +279 -0
- package/src/cli-commands.ts +1 -0
- package/src/commands/gallery.ts +52 -0
- package/src/commands/launch.ts +1 -1
- package/src/config/keybindings.ts +15 -6
- package/src/config/model-equivalence.ts +35 -12
- package/src/config/model-id-affixes.ts +39 -22
- package/src/config/model-registry.ts +16 -16
- package/src/config/settings-schema.ts +18 -5
- package/src/config/settings.ts +11 -0
- package/src/dap/client.ts +14 -16
- package/src/edit/renderer.ts +36 -48
- package/src/eval/__tests__/agent-bridge.test.ts +75 -32
- package/src/eval/agent-bridge.ts +34 -7
- package/src/extensibility/extensions/runner.ts +1 -0
- package/src/extensibility/plugins/doctor.ts +0 -1
- package/src/extensibility/plugins/marketplace-auto-update.ts +49 -0
- package/src/goals/tools/goal-tool.ts +2 -2
- package/src/internal-urls/docs-index.generated.ts +5 -5
- package/src/lsp/client.ts +104 -55
- package/src/lsp/types.ts +10 -0
- package/src/main.ts +44 -49
- package/src/memory-backend/index.ts +13 -1
- package/src/memory-backend/resolve.ts +3 -5
- package/src/memory-backend/types.ts +1 -1
- package/src/modes/components/custom-editor.ts +10 -1
- package/src/modes/components/status-line.ts +3 -5
- package/src/modes/components/tool-execution.ts +61 -16
- package/src/modes/controllers/command-controller.ts +13 -2
- package/src/modes/controllers/input-controller.ts +11 -3
- package/src/modes/controllers/selector-controller.ts +2 -2
- package/src/modes/index.ts +5 -4
- package/src/modes/interactive-mode.ts +17 -3
- package/src/modes/setup-version.ts +11 -0
- package/src/modes/setup-wizard/index.ts +3 -2
- package/src/modes/setup-wizard/scenes/web-search.ts +3 -2
- package/src/modes/types.ts +1 -1
- package/src/modes/utils/context-usage.ts +10 -6
- package/src/modes/utils/hotkeys-markdown.ts +1 -0
- package/src/sdk.ts +21 -23
- package/src/session/agent-session.ts +7 -7
- package/src/slash-commands/builtin-registry.ts +1 -1
- package/src/slash-commands/helpers/usage-report.ts +2 -0
- package/src/task/executor.ts +20 -2
- package/src/task/render.ts +1 -2
- package/src/telemetry-export.ts +25 -7
- package/src/tools/eval-backends.ts +6 -17
- package/src/tools/eval-render.ts +21 -18
- package/src/tools/eval.ts +5 -4
- package/src/tools/fetch.ts +94 -84
- package/src/tools/render-utils.ts +17 -3
- package/src/tools/renderers.ts +16 -1
- package/src/tools/report-tool-issue.ts +1 -1
- package/src/tools/search.ts +173 -81
- package/src/tools/todo.ts +20 -7
- package/src/tools/write.ts +22 -1
- package/src/web/scrapers/github.ts +255 -3
- package/src/web/scrapers/youtube.ts +3 -2
- package/src/web/search/providers/perplexity.ts +199 -51
- package/src/web/search/render.ts +39 -54
- package/src/web/search/types.ts +5 -1
- package/dist/types/eval/__tests__/shared-executors.test.d.ts +0 -1
- package/src/eval/__tests__/shared-executors.test.ts +0 -609
|
@@ -113,8 +113,9 @@ export const handleYouTube: SpecialHandler = async (
|
|
|
113
113
|
const notes: string[] = [];
|
|
114
114
|
const videoUrl = `https://www.youtube.com/watch?v=${yt.videoId}`;
|
|
115
115
|
|
|
116
|
-
// Prefer Parallel extract when
|
|
117
|
-
|
|
116
|
+
// Prefer Parallel extract when it sits in the reader chain and creds exist
|
|
117
|
+
const fetchPreference = settings.get("providers.fetch");
|
|
118
|
+
if ((fetchPreference === "auto" || fetchPreference === "parallel") && findParallelApiKey(storage)) {
|
|
118
119
|
try {
|
|
119
120
|
const parallelResult = await extractWithParallel(
|
|
120
121
|
[videoUrl],
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Perplexity Web Search Provider
|
|
3
3
|
*
|
|
4
|
-
* Supports
|
|
4
|
+
* Supports four auth modes:
|
|
5
5
|
* - Cookies (`PERPLEXITY_COOKIES`) via `www.perplexity.ai/rest/sse/perplexity_ask`
|
|
6
6
|
* - OAuth/session bearer via `AuthStorage` and `www.perplexity.ai/rest/sse/perplexity_ask`
|
|
7
7
|
* - API key (`PERPLEXITY_API_KEY`) via `api.perplexity.ai/chat/completions`
|
|
8
|
+
* - Anonymous via `www.perplexity.ai/rest/sse/perplexity_ask`
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
11
|
import { type AuthStorage, getEnvApiKey } from "@oh-my-pi/pi-ai";
|
|
@@ -32,6 +33,8 @@ const DEFAULT_NUM_SEARCH_RESULTS = 20;
|
|
|
32
33
|
const OAUTH_EXPIRY_BUFFER_MS = 5 * 60 * 1000;
|
|
33
34
|
const OAUTH_API_VERSION = "2.18";
|
|
34
35
|
const OAUTH_USER_AGENT = "Perplexity/641 CFNetwork/1568 Darwin/25.2.0";
|
|
36
|
+
const ANONYMOUS_USER_AGENT =
|
|
37
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36";
|
|
35
38
|
|
|
36
39
|
type PerplexityAuth =
|
|
37
40
|
| {
|
|
@@ -45,6 +48,9 @@ type PerplexityAuth =
|
|
|
45
48
|
| {
|
|
46
49
|
type: "cookies";
|
|
47
50
|
cookies: string;
|
|
51
|
+
}
|
|
52
|
+
| {
|
|
53
|
+
type: "anonymous";
|
|
48
54
|
};
|
|
49
55
|
|
|
50
56
|
interface PerplexityOAuthStreamMarkdownBlock {
|
|
@@ -149,6 +155,112 @@ function mergeOAuthEventSnapshot(
|
|
|
149
155
|
|
|
150
156
|
return merged;
|
|
151
157
|
}
|
|
158
|
+
|
|
159
|
+
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
160
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
161
|
+
return value as Record<string, unknown>;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function parseJson(text: string): unknown | null {
|
|
165
|
+
try {
|
|
166
|
+
return JSON.parse(text);
|
|
167
|
+
} catch {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function textFromChunks(value: unknown): string | null {
|
|
173
|
+
if (!Array.isArray(value) || value.length === 0) return null;
|
|
174
|
+
let text = "";
|
|
175
|
+
for (const chunk of value) {
|
|
176
|
+
if (typeof chunk !== "string") return null;
|
|
177
|
+
text += chunk;
|
|
178
|
+
}
|
|
179
|
+
return text.length > 0 ? text : null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function textFromStructuredAnswer(value: unknown): string | null {
|
|
183
|
+
if (!Array.isArray(value)) return null;
|
|
184
|
+
for (const item of value) {
|
|
185
|
+
const record = asRecord(item);
|
|
186
|
+
if (!record) continue;
|
|
187
|
+
const text = record.text;
|
|
188
|
+
if (typeof text === "string" && text.length > 0) return text;
|
|
189
|
+
const chunks = textFromChunks(record.chunks);
|
|
190
|
+
if (chunks) return chunks;
|
|
191
|
+
}
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function answerFromTextPayload(payload: Record<string, unknown>): string | null {
|
|
196
|
+
const structured = textFromStructuredAnswer(payload.structured_answer);
|
|
197
|
+
if (structured) return structured;
|
|
198
|
+
const chunks = textFromChunks(payload.chunks);
|
|
199
|
+
if (chunks) return chunks;
|
|
200
|
+
const answer = payload.answer;
|
|
201
|
+
return typeof answer === "string" && answer.length > 0 ? answer : null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function parseOAuthTextPayload(text: string): Record<string, unknown> | null {
|
|
205
|
+
const parsed = parseJson(text);
|
|
206
|
+
const direct = asRecord(parsed);
|
|
207
|
+
if (direct) return direct;
|
|
208
|
+
if (!Array.isArray(parsed)) return null;
|
|
209
|
+
|
|
210
|
+
for (const item of parsed) {
|
|
211
|
+
const step = asRecord(item);
|
|
212
|
+
const content = asRecord(step?.content);
|
|
213
|
+
const answer = content?.answer;
|
|
214
|
+
if (typeof answer !== "string" || answer.length === 0) continue;
|
|
215
|
+
const payload = asRecord(parseJson(answer));
|
|
216
|
+
if (payload) return payload;
|
|
217
|
+
}
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function parseOAuthTextAnswer(text: string): string {
|
|
222
|
+
const payload = parseOAuthTextPayload(text);
|
|
223
|
+
if (payload) {
|
|
224
|
+
const answer = answerFromTextPayload(payload);
|
|
225
|
+
if (answer) return answer;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const parsed = parseJson(text);
|
|
229
|
+
if (!Array.isArray(parsed)) return text;
|
|
230
|
+
for (const item of parsed) {
|
|
231
|
+
const step = asRecord(item);
|
|
232
|
+
const content = asRecord(step?.content);
|
|
233
|
+
const answer = content?.answer;
|
|
234
|
+
if (typeof answer === "string" && answer.length > 0) return answer;
|
|
235
|
+
}
|
|
236
|
+
return text;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function sourcesFromTextPayload(text: string | undefined): SearchSource[] {
|
|
240
|
+
if (!text) return [];
|
|
241
|
+
const payload = parseOAuthTextPayload(text);
|
|
242
|
+
const webResults = payload?.web_results;
|
|
243
|
+
if (!Array.isArray(webResults) || webResults.length === 0) return [];
|
|
244
|
+
|
|
245
|
+
const sources: SearchSource[] = [];
|
|
246
|
+
for (const value of webResults) {
|
|
247
|
+
const result = asRecord(value);
|
|
248
|
+
if (!result) continue;
|
|
249
|
+
const url = result.url;
|
|
250
|
+
if (typeof url !== "string" || url.length === 0) continue;
|
|
251
|
+
const name = result.name ?? result.title;
|
|
252
|
+
const snippet = result.snippet;
|
|
253
|
+
const timestamp = result.timestamp;
|
|
254
|
+
sources.push({
|
|
255
|
+
title: typeof name === "string" && name.length > 0 ? name : url,
|
|
256
|
+
url,
|
|
257
|
+
snippet: typeof snippet === "string" ? snippet : undefined,
|
|
258
|
+
publishedDate: typeof timestamp === "string" ? timestamp : undefined,
|
|
259
|
+
ageSeconds: dateToAgeSeconds(typeof timestamp === "string" ? timestamp : undefined),
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
return sources;
|
|
263
|
+
}
|
|
152
264
|
export interface PerplexitySearchParams {
|
|
153
265
|
signal?: AbortSignal;
|
|
154
266
|
query: string;
|
|
@@ -216,7 +328,7 @@ async function findPerplexityAuth(
|
|
|
216
328
|
authStorage: AuthStorage,
|
|
217
329
|
sessionId: string | undefined,
|
|
218
330
|
signal: AbortSignal | undefined,
|
|
219
|
-
): Promise<PerplexityAuth
|
|
331
|
+
): Promise<PerplexityAuth> {
|
|
220
332
|
// 1. PERPLEXITY_COOKIES env var
|
|
221
333
|
const cookies = $env.PERPLEXITY_COOKIES?.trim();
|
|
222
334
|
if (cookies) {
|
|
@@ -235,7 +347,9 @@ async function findPerplexityAuth(
|
|
|
235
347
|
if (apiKey) {
|
|
236
348
|
return { type: "api_key", token: apiKey };
|
|
237
349
|
}
|
|
238
|
-
|
|
350
|
+
|
|
351
|
+
// 4. The consumer ask endpoint currently accepts unauthenticated browser-style requests.
|
|
352
|
+
return { type: "anonymous" };
|
|
239
353
|
}
|
|
240
354
|
|
|
241
355
|
/** Call Perplexity API-key endpoint. */
|
|
@@ -284,7 +398,7 @@ function buildOAuthSources(event: PerplexityOAuthStreamEvent): SearchSource[] {
|
|
|
284
398
|
}));
|
|
285
399
|
}
|
|
286
400
|
|
|
287
|
-
|
|
401
|
+
const sources = (event.sources_list ?? [])
|
|
288
402
|
.filter(source => typeof source.url === "string" && source.url.length > 0)
|
|
289
403
|
.map(source => ({
|
|
290
404
|
title: source.title ?? source.url ?? "",
|
|
@@ -293,11 +407,13 @@ function buildOAuthSources(event: PerplexityOAuthStreamEvent): SearchSource[] {
|
|
|
293
407
|
publishedDate: source.date,
|
|
294
408
|
ageSeconds: dateToAgeSeconds(source.date),
|
|
295
409
|
}));
|
|
410
|
+
if (sources.length > 0) return sources;
|
|
411
|
+
return sourcesFromTextPayload(event.text);
|
|
296
412
|
}
|
|
297
413
|
|
|
298
414
|
function buildOAuthAnswer(event: PerplexityOAuthStreamEvent): string {
|
|
299
415
|
if (!event.blocks?.length) {
|
|
300
|
-
return typeof event.text === "string" ? event.text : "";
|
|
416
|
+
return typeof event.text === "string" ? parseOAuthTextAnswer(event.text) : "";
|
|
301
417
|
}
|
|
302
418
|
|
|
303
419
|
const markdownBlock = event.blocks.find(
|
|
@@ -324,51 +440,77 @@ function buildOAuthAnswer(event: PerplexityOAuthStreamEvent): string {
|
|
|
324
440
|
}
|
|
325
441
|
}
|
|
326
442
|
if (typeof event.text === "string" && event.text.length > 0) {
|
|
327
|
-
return event.text;
|
|
443
|
+
return parseOAuthTextAnswer(event.text);
|
|
328
444
|
}
|
|
329
445
|
return "";
|
|
330
446
|
}
|
|
331
447
|
|
|
332
|
-
async function
|
|
333
|
-
auth: { type: "oauth"; token: string } | { type: "cookies"; cookies: string },
|
|
448
|
+
async function callPerplexityAsk(
|
|
449
|
+
auth: { type: "oauth"; token: string } | { type: "cookies"; cookies: string } | { type: "anonymous" },
|
|
334
450
|
params: PerplexitySearchParams,
|
|
335
451
|
): Promise<{ answer: string; sources: SearchSource[]; model?: string; requestId?: string }> {
|
|
336
452
|
const requestId = crypto.randomUUID();
|
|
337
|
-
|
|
453
|
+
// The consumer `perplexity_ask` endpoint is itself a research assistant and
|
|
454
|
+
// has no system-message slot. Prepending the API-style system prompt to the
|
|
455
|
+
// query makes the model read it as a meta-instruction and refuse with
|
|
456
|
+
// "I don't have access to web-search tools in this turn", so ask-endpoint
|
|
457
|
+
// searches send the bare query. (The API-key path still uses system_prompt
|
|
458
|
+
// as a proper `system` message.)
|
|
459
|
+
const effectiveQuery = params.query;
|
|
460
|
+
|
|
461
|
+
const headers: Record<string, string> = {
|
|
462
|
+
"Content-Type": "application/json",
|
|
463
|
+
Accept: "text/event-stream",
|
|
464
|
+
Origin: "https://www.perplexity.ai",
|
|
465
|
+
Referer: "https://www.perplexity.ai/",
|
|
466
|
+
"User-Agent": auth.type === "anonymous" ? ANONYMOUS_USER_AGENT : OAUTH_USER_AGENT,
|
|
467
|
+
"X-Request-ID": requestId,
|
|
468
|
+
};
|
|
469
|
+
if (auth.type === "oauth") {
|
|
470
|
+
// The ask endpoint authenticates via the next-auth session cookie, NOT a
|
|
471
|
+
// bearer header — a bearer (even a garbage one) is ignored and the request
|
|
472
|
+
// silently falls back to the anonymous free `turbo` model regardless of
|
|
473
|
+
// `model_preference`. The stored OAuth token IS the Perplexity session JWT
|
|
474
|
+
// (the native app injects the same value as this cookie), so sending it as
|
|
475
|
+
// the cookie is what unlocks the account's Pro model selection.
|
|
476
|
+
headers.Cookie = `__Secure-next-auth.session-token=${auth.token}`;
|
|
477
|
+
} else if (auth.type === "cookies") {
|
|
478
|
+
headers.Cookie = auth.cookies;
|
|
479
|
+
}
|
|
480
|
+
if (auth.type !== "anonymous") {
|
|
481
|
+
headers["X-App-ApiClient"] = "default";
|
|
482
|
+
headers["X-App-ApiVersion"] = OAUTH_API_VERSION;
|
|
483
|
+
headers["X-Perplexity-Request-Reason"] = "submit";
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const requestParams: Record<string, unknown> = {
|
|
487
|
+
query_str: effectiveQuery,
|
|
488
|
+
search_focus: "internet",
|
|
489
|
+
mode: "copilot",
|
|
490
|
+
model_preference: "experimental",
|
|
491
|
+
sources: ["web"],
|
|
492
|
+
attachments: [],
|
|
493
|
+
frontend_uuid: crypto.randomUUID(),
|
|
494
|
+
frontend_context_uuid: crypto.randomUUID(),
|
|
495
|
+
version: OAUTH_API_VERSION,
|
|
496
|
+
language: "en-US",
|
|
497
|
+
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
498
|
+
search_recency_filter: params.search_recency_filter ?? null,
|
|
499
|
+
is_incognito: true,
|
|
500
|
+
use_schematized_api: true,
|
|
501
|
+
skip_search_enabled: true,
|
|
502
|
+
};
|
|
503
|
+
if (auth.type === "anonymous") {
|
|
504
|
+
requestParams.send_back_text_in_streaming_api = true;
|
|
505
|
+
requestParams.source = "default";
|
|
506
|
+
}
|
|
338
507
|
|
|
339
508
|
const response = await fetch(PERPLEXITY_OAUTH_ASK_URL, {
|
|
340
509
|
method: "POST",
|
|
341
|
-
headers
|
|
342
|
-
...(auth.type === "cookies" ? { Cookie: auth.cookies } : { Authorization: `Bearer ${auth.token}` }),
|
|
343
|
-
"Content-Type": "application/json",
|
|
344
|
-
Accept: "text/event-stream",
|
|
345
|
-
Origin: "https://www.perplexity.ai",
|
|
346
|
-
Referer: "https://www.perplexity.ai/",
|
|
347
|
-
"User-Agent": OAUTH_USER_AGENT,
|
|
348
|
-
"X-App-ApiClient": "default",
|
|
349
|
-
"X-App-ApiVersion": OAUTH_API_VERSION,
|
|
350
|
-
"X-Perplexity-Request-Reason": "submit",
|
|
351
|
-
"X-Request-ID": requestId,
|
|
352
|
-
},
|
|
510
|
+
headers,
|
|
353
511
|
body: JSON.stringify({
|
|
354
512
|
query_str: effectiveQuery,
|
|
355
|
-
params:
|
|
356
|
-
query_str: effectiveQuery,
|
|
357
|
-
search_focus: "internet",
|
|
358
|
-
mode: "copilot",
|
|
359
|
-
model_preference: "pplx_pro_upgraded",
|
|
360
|
-
sources: ["web"],
|
|
361
|
-
attachments: [],
|
|
362
|
-
frontend_uuid: crypto.randomUUID(),
|
|
363
|
-
frontend_context_uuid: crypto.randomUUID(),
|
|
364
|
-
version: OAUTH_API_VERSION,
|
|
365
|
-
language: "en-US",
|
|
366
|
-
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
367
|
-
search_recency_filter: params.search_recency_filter ?? null,
|
|
368
|
-
is_incognito: true,
|
|
369
|
-
use_schematized_api: true,
|
|
370
|
-
skip_search_enabled: true,
|
|
371
|
-
},
|
|
513
|
+
params: requestParams,
|
|
372
514
|
}),
|
|
373
515
|
signal: withHardTimeout(params.signal),
|
|
374
516
|
});
|
|
@@ -379,13 +521,13 @@ async function callPerplexityOAuth(
|
|
|
379
521
|
if (classified) throw classified;
|
|
380
522
|
throw new SearchProviderError(
|
|
381
523
|
"perplexity",
|
|
382
|
-
`Perplexity
|
|
524
|
+
`Perplexity ask API error (${response.status}): ${errorText}`,
|
|
383
525
|
response.status,
|
|
384
526
|
);
|
|
385
527
|
}
|
|
386
528
|
|
|
387
529
|
if (!response.body) {
|
|
388
|
-
throw new SearchProviderError("perplexity", "Perplexity
|
|
530
|
+
throw new SearchProviderError("perplexity", "Perplexity ask API returned no response body", 500);
|
|
389
531
|
}
|
|
390
532
|
|
|
391
533
|
let answer = "";
|
|
@@ -397,7 +539,7 @@ async function callPerplexityOAuth(
|
|
|
397
539
|
for await (const event of readSseJson<PerplexityOAuthStreamEvent>(response.body, params.signal)) {
|
|
398
540
|
if (event.error_code) {
|
|
399
541
|
const message = event.error_message ?? event.error_code;
|
|
400
|
-
throw new SearchProviderError("perplexity", `Perplexity
|
|
542
|
+
throw new SearchProviderError("perplexity", `Perplexity ask stream error: ${message}`, 400);
|
|
401
543
|
}
|
|
402
544
|
|
|
403
545
|
mergedEvent = mergeOAuthEventSnapshot(mergedEvent, event);
|
|
@@ -500,20 +642,17 @@ function applySourceLimit(result: SearchResponse, limit?: number): SearchRespons
|
|
|
500
642
|
/** Execute Perplexity web search */
|
|
501
643
|
export async function searchPerplexity(params: PerplexitySearchParams): Promise<SearchResponse> {
|
|
502
644
|
const auth = await findPerplexityAuth(params.authStorage, params.sessionId, params.signal);
|
|
503
|
-
if (!auth) {
|
|
504
|
-
throw new Error("Perplexity auth not found. Set PERPLEXITY_COOKIES, PERPLEXITY_API_KEY, or login via OAuth.");
|
|
505
|
-
}
|
|
506
645
|
|
|
507
|
-
if (auth.type
|
|
508
|
-
const
|
|
646
|
+
if (auth.type !== "api_key") {
|
|
647
|
+
const askResult = await callPerplexityAsk(auth, params);
|
|
509
648
|
return applySourceLimit(
|
|
510
649
|
{
|
|
511
650
|
provider: "perplexity",
|
|
512
|
-
answer:
|
|
513
|
-
sources:
|
|
514
|
-
model:
|
|
515
|
-
requestId:
|
|
516
|
-
authMode: "oauth",
|
|
651
|
+
answer: askResult.answer || undefined,
|
|
652
|
+
sources: askResult.sources,
|
|
653
|
+
model: askResult.model,
|
|
654
|
+
requestId: askResult.requestId,
|
|
655
|
+
authMode: auth.type === "anonymous" ? "anonymous" : "oauth",
|
|
517
656
|
},
|
|
518
657
|
params.num_results,
|
|
519
658
|
);
|
|
@@ -562,6 +701,15 @@ export class PerplexityProvider extends SearchProvider {
|
|
|
562
701
|
return !!$env.PERPLEXITY_COOKIES?.trim() || authStorage.hasAuth("perplexity") || !!findApiKey();
|
|
563
702
|
}
|
|
564
703
|
|
|
704
|
+
/**
|
|
705
|
+
* Perplexity accepts anonymous browser-style ask requests, but keep auto
|
|
706
|
+
* provider selection credential-gated so a configured provider keeps priority
|
|
707
|
+
* over the anonymous fallback.
|
|
708
|
+
*/
|
|
709
|
+
isExplicitlyAvailable(_authStorage: AuthStorage): boolean {
|
|
710
|
+
return true;
|
|
711
|
+
}
|
|
712
|
+
|
|
565
713
|
search(params: SearchParams): Promise<SearchResponse> {
|
|
566
714
|
return searchPerplexity({
|
|
567
715
|
signal: params.signal,
|
package/src/web/search/render.ts
CHANGED
|
@@ -15,23 +15,16 @@ import {
|
|
|
15
15
|
formatMoreItems,
|
|
16
16
|
formatStatusIcon,
|
|
17
17
|
getDomain,
|
|
18
|
-
getPreviewLines,
|
|
19
18
|
PREVIEW_LIMITS,
|
|
20
|
-
|
|
19
|
+
replaceTabs,
|
|
21
20
|
truncateToWidth,
|
|
22
21
|
} from "../../tools/render-utils";
|
|
23
|
-
import { renderStatusLine, renderTreeList } from "../../tui";
|
|
22
|
+
import { renderStatusLine, renderTreeList, urlHyperlink } from "../../tui";
|
|
24
23
|
import { CachedOutputBlock, markFramedBlockComponent } from "../../tui/output-block";
|
|
25
24
|
import { getSearchProviderLabel } from "./provider";
|
|
26
25
|
import type { SearchResponse } from "./types";
|
|
27
26
|
|
|
28
|
-
const MAX_COLLAPSED_ANSWER_LINES = PREVIEW_LIMITS.COLLAPSED_LINES;
|
|
29
|
-
const MAX_SNIPPET_LINES = 2;
|
|
30
|
-
const MAX_SNIPPET_LINE_LEN = TRUNCATE_LENGTHS.LINE;
|
|
31
27
|
const MAX_COLLAPSED_ITEMS = PREVIEW_LIMITS.COLLAPSED_ITEMS;
|
|
32
|
-
const MAX_QUERY_PREVIEW = 2;
|
|
33
|
-
const MAX_QUERY_LEN = 90;
|
|
34
|
-
const MAX_REQUEST_ID_LEN = 36;
|
|
35
28
|
|
|
36
29
|
function renderFallbackText(contentText: string, expanded: boolean, theme: Theme): Component {
|
|
37
30
|
const lines = contentText.split("\n").filter(line => line.trim());
|
|
@@ -66,6 +59,21 @@ export interface SearchRenderDetails {
|
|
|
66
59
|
error?: string;
|
|
67
60
|
}
|
|
68
61
|
|
|
62
|
+
/** Render a web search failure as a framed error panel, matching the success layout. */
|
|
63
|
+
function renderSearchErrorPanel(message: string, providerLabel: string | undefined, theme: Theme): Component {
|
|
64
|
+
const header = renderStatusLine({ icon: "error", title: "Web Search", description: providerLabel }, theme);
|
|
65
|
+
const body = theme.fg("error", `Error: ${replaceTabs(message)}`);
|
|
66
|
+
const outputBlock = new CachedOutputBlock();
|
|
67
|
+
return markFramedBlockComponent({
|
|
68
|
+
render(width: number): string[] {
|
|
69
|
+
return outputBlock.render({ header, state: "error", sections: [{ lines: [body] }], width }, theme);
|
|
70
|
+
},
|
|
71
|
+
invalidate() {
|
|
72
|
+
outputBlock.invalidate();
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
69
77
|
/** Render web search result with tree-based layout */
|
|
70
78
|
export function renderSearchResult(
|
|
71
79
|
result: { content: Array<{ type: string; text?: string }>; details?: SearchRenderDetails },
|
|
@@ -78,9 +86,12 @@ export function renderSearchResult(
|
|
|
78
86
|
): Component {
|
|
79
87
|
const details = result.details;
|
|
80
88
|
|
|
81
|
-
// Handle error case
|
|
89
|
+
// Handle error case as a framed panel, matching the success layout.
|
|
82
90
|
if (details?.error) {
|
|
83
|
-
|
|
91
|
+
const errorProvider = details.response?.provider;
|
|
92
|
+
const errorProviderLabel =
|
|
93
|
+
errorProvider && errorProvider !== "none" ? getSearchProviderLabel(errorProvider) : undefined;
|
|
94
|
+
return renderSearchErrorPanel(details.error, errorProviderLabel, theme);
|
|
84
95
|
}
|
|
85
96
|
|
|
86
97
|
const rawText = result.content?.find(block => block.type === "text")?.text?.trim() ?? "";
|
|
@@ -91,8 +102,6 @@ export function renderSearchResult(
|
|
|
91
102
|
|
|
92
103
|
const sources = Array.isArray(response.sources) ? response.sources : [];
|
|
93
104
|
const sourceCount = sources.length;
|
|
94
|
-
const citations = Array.isArray(response.citations) ? response.citations : [];
|
|
95
|
-
const citationCount = citations.length;
|
|
96
105
|
const searchQueries = Array.isArray(response.searchQueries)
|
|
97
106
|
? response.searchQueries.filter(item => typeof item === "string")
|
|
98
107
|
: [];
|
|
@@ -118,16 +127,11 @@ export function renderSearchResult(
|
|
|
118
127
|
theme,
|
|
119
128
|
);
|
|
120
129
|
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
);
|
|
127
|
-
if (response.model) metaLines.push(`${theme.fg("muted", "Model:")} ${theme.fg("text", response.model)}`);
|
|
128
|
-
metaLines.push(`${theme.fg("muted", "Sources:")} ${theme.fg("text", String(sourceCount))}`);
|
|
129
|
-
if (citationCount > 0)
|
|
130
|
-
metaLines.push(`${theme.fg("muted", "Citations:")} ${theme.fg("text", String(citationCount))}`);
|
|
130
|
+
const authShort =
|
|
131
|
+
response.authMode === "oauth" ? "OAuth" : response.authMode === "api_key" ? "API" : response.authMode;
|
|
132
|
+
let providerInfo = response.model ? `${response.model} @ ${providerLabel}` : providerLabel;
|
|
133
|
+
if (authShort) providerInfo += ` (${authShort})`;
|
|
134
|
+
const metaLines: string[] = [`${theme.fg("muted", "Provider:")} ${theme.fg("text", providerInfo)}`];
|
|
131
135
|
if (response.usage) {
|
|
132
136
|
const usageParts: string[] = [];
|
|
133
137
|
if (response.usage.inputTokens !== undefined) usageParts.push(`in ${response.usage.inputTokens}`);
|
|
@@ -137,17 +141,6 @@ export function renderSearchResult(
|
|
|
137
141
|
if (usageParts.length > 0)
|
|
138
142
|
metaLines.push(`${theme.fg("muted", "Usage:")} ${theme.fg("text", usageParts.join(theme.sep.dot))}`);
|
|
139
143
|
}
|
|
140
|
-
if (response.requestId) {
|
|
141
|
-
metaLines.push(
|
|
142
|
-
`${theme.fg("muted", "Request:")} ${theme.fg("text", truncateToWidth(response.requestId, MAX_REQUEST_ID_LEN))}`,
|
|
143
|
-
);
|
|
144
|
-
}
|
|
145
|
-
if (searchQueries.length > 0) {
|
|
146
|
-
const queriesPreview = searchQueries.slice(0, MAX_QUERY_PREVIEW);
|
|
147
|
-
const queryList = queriesPreview.map(q => truncateToWidth(q, MAX_QUERY_LEN));
|
|
148
|
-
const suffix = searchQueries.length > queriesPreview.length ? "…" : "";
|
|
149
|
-
metaLines.push(`${theme.fg("muted", "Queries:")} ${theme.fg("text", queryList.join("; "))}${suffix}`);
|
|
150
|
-
}
|
|
151
144
|
|
|
152
145
|
const answerMarkdown = contentText ? new Markdown(contentText, 0, 0, getMarkdownTheme()) : undefined;
|
|
153
146
|
const outputBlock = new CachedOutputBlock();
|
|
@@ -163,15 +156,15 @@ export function renderSearchResult(
|
|
|
163
156
|
let answerLines: string[];
|
|
164
157
|
if (renderedAnswer.length === 0) {
|
|
165
158
|
answerLines = [theme.fg("muted", "No answer text returned")];
|
|
166
|
-
} else if (expanded) {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const collapsedCap = args?.maxAnswerLines ?? MAX_COLLAPSED_ANSWER_LINES;
|
|
170
|
-
answerLines = renderedAnswer.slice(0, collapsedCap);
|
|
159
|
+
} else if (args?.maxAnswerLines !== undefined && !expanded) {
|
|
160
|
+
// CLI compact mode (`omp q`) caps the answer; the TUI passes no cap and shows it in full.
|
|
161
|
+
answerLines = renderedAnswer.slice(0, args.maxAnswerLines);
|
|
171
162
|
const remaining = renderedAnswer.length - answerLines.length;
|
|
172
163
|
if (remaining > 0) {
|
|
173
164
|
answerLines.push(theme.fg("muted", formatMoreItems(remaining, "line")));
|
|
174
165
|
}
|
|
166
|
+
} else {
|
|
167
|
+
answerLines = renderedAnswer;
|
|
175
168
|
}
|
|
176
169
|
|
|
177
170
|
const sourceTree = renderTreeList(
|
|
@@ -187,30 +180,22 @@ export function renderSearchResult(
|
|
|
187
180
|
: typeof src.url === "string" && src.url.trim()
|
|
188
181
|
? src.url
|
|
189
182
|
: "Untitled";
|
|
190
|
-
const title = truncateToWidth(titleText, MAX_SNIPPET_LINE_LEN);
|
|
191
183
|
const url = typeof src.url === "string" ? src.url : "";
|
|
192
184
|
const domain = url ? getDomain(url) : "";
|
|
193
185
|
const age =
|
|
194
186
|
formatAge(src.ageSeconds) || (typeof src.publishedDate === "string" ? src.publishedDate : "");
|
|
195
187
|
const metaParts: string[] = [];
|
|
196
188
|
if (domain) metaParts.push(theme.fg("dim", `(${domain})`));
|
|
197
|
-
if (typeof src.author === "string" && src.author.trim())
|
|
198
|
-
metaParts.push(theme.fg("muted", truncateToWidth(src.author.trim(), 40)));
|
|
199
189
|
if (age) metaParts.push(theme.fg("muted", age));
|
|
200
190
|
const metaSep = theme.fg("dim", theme.sep.dot);
|
|
201
191
|
const metaSuffix = metaParts.length > 0 ? ` ${metaParts.join(metaSep)}` : "";
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
srcLines.push(theme.fg("muted", `${theme.format.dash} ${snippetLine}`));
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
if (url) srcLines.push(theme.fg("mdLinkUrl", truncateToWidth(url, MAX_SNIPPET_LINE_LEN)));
|
|
213
|
-
return srcLines;
|
|
192
|
+
// One line per source: the title links to its URL, followed by domain · age.
|
|
193
|
+
// Reserve room for the box borders, the tree branch, and the meta suffix.
|
|
194
|
+
const lineBudget = Math.max(24, width - 6);
|
|
195
|
+
const titleBudget = Math.max(12, lineBudget - Bun.stringWidth(metaSuffix));
|
|
196
|
+
const title = theme.fg("accent", truncateToWidth(titleText, titleBudget));
|
|
197
|
+
const linkedTitle = url ? urlHyperlink(url, title) : title;
|
|
198
|
+
return [`${linkedTitle}${metaSuffix}`];
|
|
214
199
|
},
|
|
215
200
|
},
|
|
216
201
|
theme,
|
package/src/web/search/types.ts
CHANGED
|
@@ -33,7 +33,11 @@ export const SEARCH_PROVIDER_OPTIONS = [
|
|
|
33
33
|
description: "Automatically uses the first configured web-search provider",
|
|
34
34
|
},
|
|
35
35
|
{ value: "tavily", label: "Tavily", description: "Requires TAVILY_API_KEY" },
|
|
36
|
-
{
|
|
36
|
+
{
|
|
37
|
+
value: "perplexity",
|
|
38
|
+
label: "Perplexity",
|
|
39
|
+
description: "Uses auth when configured; explicit selection falls back to anonymous search",
|
|
40
|
+
},
|
|
37
41
|
{ value: "brave", label: "Brave", description: "Requires BRAVE_API_KEY" },
|
|
38
42
|
{ value: "jina", label: "Jina", description: "Requires JINA_API_KEY" },
|
|
39
43
|
{ value: "kimi", label: "Kimi", description: "Requires MOONSHOT_SEARCH_API_KEY or MOONSHOT_API_KEY" },
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|