@bastani/atomic 0.8.25 → 0.8.26-alpha.1
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 +11 -0
- package/dist/builtin/intercom/CHANGELOG.md +6 -0
- package/dist/builtin/intercom/index-heavy.ts +1754 -0
- package/dist/builtin/intercom/index.ts +374 -1746
- package/dist/builtin/intercom/package.json +1 -1
- package/dist/builtin/intercom/result-renderers.ts +77 -0
- package/dist/builtin/mcp/CHANGELOG.md +10 -0
- package/dist/builtin/mcp/index.ts +151 -57
- package/dist/builtin/mcp/package.json +1 -1
- package/dist/builtin/subagents/CHANGELOG.md +6 -0
- package/dist/builtin/subagents/package.json +1 -1
- package/dist/builtin/web-access/CHANGELOG.md +6 -0
- package/dist/builtin/web-access/index-heavy.ts +2060 -0
- package/dist/builtin/web-access/index.ts +182 -2274
- package/dist/builtin/web-access/package.json +1 -1
- package/dist/builtin/web-access/result-renderers.ts +364 -0
- package/dist/builtin/workflows/CHANGELOG.md +9 -0
- package/dist/builtin/workflows/package.json +1 -1
- package/dist/builtin/workflows/src/extension/index.ts +13 -3
- package/dist/builtin/workflows/src/runs/foreground/stage-runner.ts +53 -2
- package/dist/builtin/workflows/src/tui/inline-form-overlay.ts +12 -3
- package/dist/builtin/workflows/src/tui/inline-form-store.ts +17 -6
- package/dist/core/agent-session-services.d.ts.map +1 -1
- package/dist/core/agent-session-services.js +13 -0
- package/dist/core/agent-session-services.js.map +1 -1
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +7 -0
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/types.d.ts +13 -1
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +17 -0
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/timings.d.ts +9 -0
- package/dist/core/timings.d.ts.map +1 -1
- package/dist/core/timings.js +28 -1
- package/dist/core/timings.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +4 -2
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/custom-message.d.ts +1 -0
- package/dist/modes/interactive/components/custom-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/custom-message.js +36 -4
- package/dist/modes/interactive/components/custom-message.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +19 -7
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,1534 +1,199 @@
|
|
|
1
|
-
import type { ExtensionAPI, ExtensionContext } from "@bastani/atomic";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { StringEnum, complete, getModel, type Model } from "@mariozechner/pi-ai";
|
|
5
|
-
import { fetchAllContent, type ExtractedContent } from "./extract.js";
|
|
6
|
-
import { clearCloneCache } from "./github-extract.js";
|
|
7
|
-
import { search, type SearchProvider, type ResolvedSearchProvider } from "./gemini-search.js";
|
|
8
|
-
import { executeCodeSearch } from "./code-search.js";
|
|
9
|
-
import type { SearchResult } from "./perplexity.js";
|
|
10
|
-
import { formatSeconds } from "./utils.js";
|
|
11
|
-
import {
|
|
12
|
-
clearResults,
|
|
13
|
-
deleteResult,
|
|
14
|
-
generateId,
|
|
15
|
-
getAllResults,
|
|
16
|
-
getResult,
|
|
17
|
-
restoreFromSession,
|
|
18
|
-
storeResult,
|
|
19
|
-
type QueryResultData,
|
|
20
|
-
type StoredSearchData,
|
|
21
|
-
} from "./storage.js";
|
|
22
|
-
import { activityMonitor, type ActivityEntry } from "./activity.js";
|
|
23
|
-
import { startCuratorServer, type CuratorServerHandle } from "./curator-server.js";
|
|
24
|
-
import {
|
|
25
|
-
buildDeterministicSummary,
|
|
26
|
-
generateSummaryDraft,
|
|
27
|
-
type SummaryGenerationContext,
|
|
28
|
-
type SummaryMeta,
|
|
29
|
-
} from "./summary-review.js";
|
|
30
|
-
import { randomUUID } from "node:crypto";
|
|
31
|
-
import { execFileSync } from "node:child_process";
|
|
32
|
-
import { createRequire } from "node:module";
|
|
33
|
-
import { platform, homedir } from "node:os";
|
|
34
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext, HandlerFn, MessageRenderer, RegisteredCommand, ToolDefinition } from "@bastani/atomic";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
35
4
|
import { join } from "node:path";
|
|
36
|
-
import {
|
|
37
|
-
import {
|
|
38
|
-
import {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
Object.assign(config, updates);
|
|
98
|
-
const dir = join(homedir(), CONFIG_DIR_NAME);
|
|
99
|
-
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
100
|
-
writeFileSync(WEB_SEARCH_CONFIG_PATH, JSON.stringify(config, null, 2) + "\n");
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const DEFAULT_SHORTCUTS = { curate: "ctrl+shift+s", activity: "ctrl+shift+w" };
|
|
104
|
-
const DEFAULT_CURATOR_TIMEOUT_SECONDS = 20;
|
|
105
|
-
const MAX_CURATOR_TIMEOUT_SECONDS = 600;
|
|
106
|
-
|
|
107
|
-
function loadConfigForExtensionInit(): WebSearchConfig {
|
|
108
|
-
try {
|
|
109
|
-
return loadConfig();
|
|
110
|
-
} catch (err) {
|
|
111
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
112
|
-
console.error(`[pi-web-access] ${message}`);
|
|
113
|
-
return {};
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function normalizeProviderInput(value: unknown): SearchProvider | undefined {
|
|
118
|
-
if (value === undefined) return undefined;
|
|
119
|
-
if (typeof value !== "string") return "auto";
|
|
120
|
-
const normalized = value.trim().toLowerCase();
|
|
121
|
-
if (normalized === "auto" || normalized === "exa" || normalized === "perplexity" || normalized === "gemini") {
|
|
122
|
-
return normalized;
|
|
123
|
-
}
|
|
124
|
-
return "auto";
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function normalizeCuratorTimeoutSeconds(value: unknown): number | undefined {
|
|
128
|
-
if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
|
|
129
|
-
const normalized = Math.floor(value);
|
|
130
|
-
if (normalized < 1) return undefined;
|
|
131
|
-
return Math.min(normalized, MAX_CURATOR_TIMEOUT_SECONDS);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function resolveWorkflow(input: unknown, hasUI: boolean): WebSearchWorkflow {
|
|
135
|
-
if (!hasUI) return "none";
|
|
136
|
-
if (typeof input === "string" && input.trim().toLowerCase() === "none") return "none";
|
|
137
|
-
return "summary-review";
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function normalizeQueryList(queryList: unknown[]): string[] {
|
|
141
|
-
const normalized: string[] = [];
|
|
142
|
-
for (const query of queryList) {
|
|
143
|
-
if (typeof query !== "string") continue;
|
|
144
|
-
const trimmed = query.trim();
|
|
145
|
-
if (trimmed.length > 0) normalized.push(trimmed);
|
|
146
|
-
}
|
|
147
|
-
return normalized;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function getCuratorTimeoutSeconds(): number {
|
|
151
|
-
const source = loadConfig();
|
|
152
|
-
return normalizeCuratorTimeoutSeconds(source.curatorTimeoutSeconds) ?? DEFAULT_CURATOR_TIMEOUT_SECONDS;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
async function getProviderAvailability(): Promise<ProviderAvailability> {
|
|
156
|
-
const geminiWebAvail = await isGeminiWebAvailable();
|
|
157
|
-
return {
|
|
158
|
-
perplexity: isPerplexityAvailable(),
|
|
159
|
-
exa: isExaAvailable(),
|
|
160
|
-
gemini: isGeminiApiAvailable() || !!geminiWebAvail,
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
async function loadCuratorBootstrap(requestedProvider: unknown): Promise<CuratorBootstrap> {
|
|
165
|
-
const availableProviders = await getProviderAvailability();
|
|
166
|
-
return {
|
|
167
|
-
availableProviders,
|
|
168
|
-
defaultProvider: resolveProvider(requestedProvider, availableProviders),
|
|
169
|
-
timeoutSeconds: getCuratorTimeoutSeconds(),
|
|
170
|
-
};
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function resolveProvider(
|
|
174
|
-
requested: unknown,
|
|
175
|
-
available: ProviderAvailability,
|
|
176
|
-
): ResolvedSearchProvider {
|
|
177
|
-
const provider = normalizeProviderInput(requested ?? loadConfig().provider ?? "auto") ?? "auto";
|
|
178
|
-
|
|
179
|
-
if (provider === "auto") {
|
|
180
|
-
if (available.exa) return "exa";
|
|
181
|
-
if (available.perplexity) return "perplexity";
|
|
182
|
-
if (available.gemini) return "gemini";
|
|
183
|
-
return "exa";
|
|
184
|
-
}
|
|
185
|
-
if (provider === "exa" && !available.exa) {
|
|
186
|
-
if (available.perplexity) return "perplexity";
|
|
187
|
-
return available.gemini ? "gemini" : "exa";
|
|
188
|
-
}
|
|
189
|
-
if (provider === "perplexity" && !available.perplexity) {
|
|
190
|
-
if (available.exa) return "exa";
|
|
191
|
-
return available.gemini ? "gemini" : "perplexity";
|
|
192
|
-
}
|
|
193
|
-
if (provider === "gemini" && !available.gemini) {
|
|
194
|
-
if (available.exa) return "exa";
|
|
195
|
-
return available.perplexity ? "perplexity" : "gemini";
|
|
196
|
-
}
|
|
197
|
-
return provider;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
const pendingFetches = new Map<string, AbortController>();
|
|
201
|
-
let sessionActive = false;
|
|
202
|
-
let widgetVisible = false;
|
|
203
|
-
let widgetUnsubscribe: (() => void) | null = null;
|
|
204
|
-
let activeCurator: CuratorServerHandle | null = null;
|
|
205
|
-
let glimpseWin: GlimpseWindow | null = null;
|
|
206
|
-
|
|
207
|
-
interface PendingCurate {
|
|
208
|
-
phase: "searching" | "curating";
|
|
209
|
-
workflow: CuratorWorkflow;
|
|
210
|
-
summaryContext: SummaryGenerationContext;
|
|
211
|
-
searchResults: Map<number, QueryResultData>;
|
|
212
|
-
allInlineContent: ExtractedContent[];
|
|
213
|
-
queryList: string[];
|
|
214
|
-
includeContent: boolean;
|
|
215
|
-
numResults?: number;
|
|
216
|
-
recencyFilter?: "day" | "week" | "month" | "year";
|
|
217
|
-
domainFilter?: string[];
|
|
218
|
-
availableProviders: ProviderAvailability;
|
|
219
|
-
defaultProvider: ResolvedSearchProvider;
|
|
220
|
-
summaryModels: Array<{ value: string; label: string }>;
|
|
221
|
-
defaultSummaryModel: string | null;
|
|
222
|
-
timeoutSeconds: number;
|
|
223
|
-
onUpdate: ((update: { content: Array<{ type: string; text: string }>; details?: Record<string, unknown> }) => void) | undefined;
|
|
224
|
-
signal: AbortSignal | undefined;
|
|
225
|
-
abortSearches: () => void;
|
|
226
|
-
finish: (value: unknown) => void;
|
|
227
|
-
cancel: (reason?: "user" | "stale") => void;
|
|
228
|
-
browserPromise?: Promise<void>;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
let pendingCurate: PendingCurate | null = null;
|
|
232
|
-
|
|
233
|
-
function cancelPendingCurate(reason: "user" | "stale" = "stale"): void {
|
|
234
|
-
pendingCurate?.cancel(reason);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
const MAX_INLINE_CONTENT = 30000; // Content returned directly to agent
|
|
238
|
-
|
|
239
|
-
function stripThumbnails(results: ExtractedContent[]): ExtractedContent[] {
|
|
240
|
-
return results.map(({ thumbnail, frames, ...rest }) => rest);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
function formatSearchSummary(results: SearchResult[], answer: string): string {
|
|
244
|
-
let output = answer ? `${answer}\n\n---\n\n**Sources:**\n` : "";
|
|
245
|
-
output += results.map((r, i) => `${i + 1}. ${r.title}\n ${r.url}`).join("\n\n");
|
|
246
|
-
return output;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function duplicateQuerySet(results: QueryResultData[]): Set<string> {
|
|
250
|
-
const counts = new Map<string, number>();
|
|
251
|
-
for (const result of results) {
|
|
252
|
-
counts.set(result.query, (counts.get(result.query) ?? 0) + 1);
|
|
253
|
-
}
|
|
254
|
-
const duplicates = new Set<string>();
|
|
255
|
-
for (const [query, count] of counts) {
|
|
256
|
-
if (count > 1) duplicates.add(query);
|
|
257
|
-
}
|
|
258
|
-
return duplicates;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
function formatQueryHeader(query: string, provider: string | undefined, duplicateQueries: Set<string>): string {
|
|
262
|
-
const suffix = duplicateQueries.has(query) && provider ? ` (${provider})` : "";
|
|
263
|
-
return `## Query: "${query}"${suffix}\n\n`;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
function hasFullInlineCoverage(urls: string[], inlineContent: ExtractedContent[] | undefined): boolean {
|
|
267
|
-
if (!inlineContent || inlineContent.length === 0) return false;
|
|
268
|
-
const coveredUrls = new Set(inlineContent.map(c => c.url));
|
|
269
|
-
return urls.every(url => coveredUrls.has(url));
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
function formatFullResults(queryData: QueryResultData): string {
|
|
273
|
-
let output = `## Results for: "${queryData.query}"\n\n`;
|
|
274
|
-
if (queryData.answer) {
|
|
275
|
-
output += `${queryData.answer}\n\n---\n\n`;
|
|
276
|
-
}
|
|
277
|
-
for (const r of queryData.results) {
|
|
278
|
-
output += `### ${r.title}\n${r.url}\n\n`;
|
|
279
|
-
}
|
|
280
|
-
return output;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
function abortPendingFetches(): void {
|
|
284
|
-
for (const controller of pendingFetches.values()) {
|
|
285
|
-
controller.abort();
|
|
286
|
-
}
|
|
287
|
-
pendingFetches.clear();
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
function closeCurator(): void {
|
|
291
|
-
const win = glimpseWin;
|
|
292
|
-
glimpseWin = null;
|
|
293
|
-
try { win?.close(); } catch {}
|
|
294
|
-
cancelPendingCurate();
|
|
295
|
-
if (activeCurator) {
|
|
296
|
-
activeCurator.close();
|
|
297
|
-
activeCurator = null;
|
|
298
|
-
}
|
|
5
|
+
import { Text } from "@mariozechner/pi-tui";
|
|
6
|
+
import { Type } from "typebox";
|
|
7
|
+
import { renderWebAccessToolResult } from "./result-renderers.js";
|
|
8
|
+
|
|
9
|
+
type CapturedCommand = Omit<RegisteredCommand, "name" | "sourceInfo">;
|
|
10
|
+
type CapturedShortcut = Parameters<ExtensionAPI["registerShortcut"]>[1];
|
|
11
|
+
type ToolRenderResultArgs = Parameters<NonNullable<ToolDefinition["renderResult"]>>;
|
|
12
|
+
type CapturedHeavy = {
|
|
13
|
+
tools: Map<string, ToolDefinition>;
|
|
14
|
+
commands: Map<string, CapturedCommand>;
|
|
15
|
+
handlers: Map<string, HandlerFn[]>;
|
|
16
|
+
shortcuts: Map<string, CapturedShortcut>;
|
|
17
|
+
};
|
|
18
|
+
type SessionSnapshot = {
|
|
19
|
+
eventName: "session_start" | "session_tree";
|
|
20
|
+
event: unknown;
|
|
21
|
+
ctx: ExtensionContext;
|
|
22
|
+
generation: number;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function addHandler(captured: CapturedHeavy, event: string, handler: HandlerFn): void {
|
|
26
|
+
const handlers = captured.handlers.get(event) ?? [];
|
|
27
|
+
handlers.push(handler);
|
|
28
|
+
captured.handlers.set(event, handlers);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function dispatchHandlers(captured: CapturedHeavy, eventName: string, event: unknown, ctx: ExtensionContext): Promise<void> {
|
|
32
|
+
for (const handler of captured.handlers.get(eventName) ?? []) {
|
|
33
|
+
await handler(event, ctx);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function createHeavyProxy(pi: ExtensionAPI, captured: CapturedHeavy): ExtensionAPI {
|
|
38
|
+
return new Proxy(pi, {
|
|
39
|
+
get(target, prop, receiver) {
|
|
40
|
+
if (prop === "registerTool") {
|
|
41
|
+
return (tool: ToolDefinition) => {
|
|
42
|
+
captured.tools.set(tool.name, tool);
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
if (prop === "registerCommand") {
|
|
46
|
+
return (name: string, options: CapturedCommand) => {
|
|
47
|
+
captured.commands.set(name, options);
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
if (prop === "on") {
|
|
51
|
+
return (event: string, handler: HandlerFn) => {
|
|
52
|
+
addHandler(captured, event, handler);
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
if (prop === "registerShortcut") {
|
|
56
|
+
return (shortcut: string, options: CapturedShortcut) => {
|
|
57
|
+
captured.shortcuts.set(shortcut, options);
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
if (prop === "registerMessageRenderer") {
|
|
61
|
+
return (customType: string, renderer: MessageRenderer) => pi.registerMessageRenderer(customType, renderer);
|
|
62
|
+
}
|
|
63
|
+
return Reflect.get(target, prop, receiver);
|
|
64
|
+
},
|
|
65
|
+
}) as ExtensionAPI;
|
|
299
66
|
}
|
|
300
67
|
|
|
301
|
-
async function
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
if (
|
|
309
|
-
|
|
310
|
-
}
|
|
68
|
+
async function executeHeavyTool(
|
|
69
|
+
loadHeavy: () => Promise<CapturedHeavy>,
|
|
70
|
+
name: string,
|
|
71
|
+
args: Parameters<NonNullable<ToolDefinition["execute"]>>,
|
|
72
|
+
): Promise<ReturnType<NonNullable<ToolDefinition["execute"]>>> {
|
|
73
|
+
const heavy = await loadHeavy();
|
|
74
|
+
const tool = heavy.tools.get(name);
|
|
75
|
+
if (!tool?.execute) throw new Error(`Web access tool implementation not found: ${name}`);
|
|
76
|
+
return tool.execute(...args) as ReturnType<NonNullable<ToolDefinition["execute"]>>;
|
|
311
77
|
}
|
|
312
78
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
_write(obj: Record<string, unknown>): void;
|
|
79
|
+
async function runHeavyCommand(loadHeavy: () => Promise<CapturedHeavy>, name: string, args: string | undefined, ctx: ExtensionContext): Promise<void> {
|
|
80
|
+
const heavy = await loadHeavy();
|
|
81
|
+
const command = heavy.commands.get(name);
|
|
82
|
+
if (!command) throw new Error(`Web access command implementation not found: ${name}`);
|
|
83
|
+
await command.handler(args, ctx);
|
|
319
84
|
}
|
|
320
85
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
const req = createRequire(import.meta.url);
|
|
326
|
-
return req.resolve("glimpseui");
|
|
327
|
-
} catch {
|
|
328
|
-
// Optional dependency.
|
|
329
|
-
}
|
|
330
|
-
try {
|
|
331
|
-
const globalRoot = execFileSync("npm", ["root", "-g"], { encoding: "utf-8" }).trim();
|
|
332
|
-
const entry = join(globalRoot, "glimpseui", "src", "glimpse.mjs");
|
|
333
|
-
if (existsSync(entry)) return entry;
|
|
334
|
-
} catch {
|
|
335
|
-
// npm may be unavailable.
|
|
336
|
-
}
|
|
337
|
-
return null;
|
|
86
|
+
function renderHeavyToolResult(loadedHeavy: CapturedHeavy | null, name: string, args: ToolRenderResultArgs): ReturnType<NonNullable<ToolDefinition["renderResult"]>> {
|
|
87
|
+
const renderer = loadedHeavy?.tools.get(name)?.renderResult;
|
|
88
|
+
if (renderer) return renderer(...args);
|
|
89
|
+
return renderWebAccessToolResult(name, args);
|
|
338
90
|
}
|
|
339
91
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
const
|
|
343
|
-
if (resolved) {
|
|
92
|
+
function getInitialShortcutConfig(): { curate: string; activity: string } {
|
|
93
|
+
const defaults = { curate: "ctrl+shift+s", activity: "ctrl+shift+w" };
|
|
94
|
+
for (const configPath of [join(homedir(), ".atomic", "web-search.json"), join(homedir(), ".pi", "web-search.json")]) {
|
|
344
95
|
try {
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
} catch {}
|
|
348
|
-
}
|
|
349
|
-
glimpseOpen = null;
|
|
350
|
-
return glimpseOpen;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
function openInGlimpse(
|
|
354
|
-
open: (html: string, opts: Record<string, unknown>) => GlimpseWindow,
|
|
355
|
-
url: string,
|
|
356
|
-
title: string,
|
|
357
|
-
): GlimpseWindow {
|
|
358
|
-
const shellHTML = `<!DOCTYPE html>
|
|
359
|
-
<html>
|
|
360
|
-
<head><meta charset="UTF-8"><title>${title}</title></head>
|
|
361
|
-
<body style="margin:0; background:#1a1a2e;">
|
|
362
|
-
<script>window.location.replace(${JSON.stringify(url)});</script>
|
|
363
|
-
</body>
|
|
364
|
-
</html>`;
|
|
365
|
-
const win = open(shellHTML, {
|
|
366
|
-
width: 800,
|
|
367
|
-
height: 900,
|
|
368
|
-
title,
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
let maxHeight = 1200;
|
|
372
|
-
win.on("ready", (info) => {
|
|
373
|
-
const visibleHeight = info?.screen?.visibleHeight;
|
|
374
|
-
if (typeof visibleHeight === "number" && visibleHeight > 0) {
|
|
375
|
-
maxHeight = Math.floor(visibleHeight * 0.85);
|
|
376
|
-
}
|
|
377
|
-
});
|
|
378
|
-
win.on("message", (data) => {
|
|
379
|
-
if (!data || typeof data !== "object") return;
|
|
380
|
-
const msg = data as Record<string, unknown>;
|
|
381
|
-
if (msg.type !== "resize" || typeof msg.height !== "number") return;
|
|
382
|
-
const clamped = Math.max(400, Math.min(Math.round(msg.height), maxHeight));
|
|
383
|
-
win._write({ type: "resize", width: 800, height: clamped });
|
|
384
|
-
});
|
|
385
|
-
|
|
386
|
-
return win;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
function extractDomain(url: string): string {
|
|
390
|
-
try { return new URL(url).hostname; }
|
|
391
|
-
catch { return url; }
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
function updateWidget(ctx: ExtensionContext): void {
|
|
395
|
-
const theme = ctx.ui.theme;
|
|
396
|
-
const entries = activityMonitor.getEntries();
|
|
397
|
-
const lines: string[] = [];
|
|
398
|
-
|
|
399
|
-
lines.push(theme.fg("accent", "─── Web Search Activity " + "─".repeat(36)));
|
|
400
|
-
|
|
401
|
-
if (entries.length === 0) {
|
|
402
|
-
lines.push(theme.fg("muted", " No activity yet"));
|
|
403
|
-
} else {
|
|
404
|
-
for (const e of entries) {
|
|
405
|
-
lines.push(" " + formatEntryLine(e, theme));
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
lines.push(theme.fg("accent", "─".repeat(60)));
|
|
410
|
-
|
|
411
|
-
const rateInfo = activityMonitor.getRateLimitInfo();
|
|
412
|
-
const resetMs = rateInfo.oldestTimestamp ? Math.max(0, rateInfo.oldestTimestamp + rateInfo.windowMs - Date.now()) : 0;
|
|
413
|
-
const resetSec = Math.ceil(resetMs / 1000);
|
|
414
|
-
lines.push(
|
|
415
|
-
theme.fg("muted", `Rate: ${rateInfo.used}/${rateInfo.max}`) +
|
|
416
|
-
(resetMs > 0 ? theme.fg("dim", ` (resets in ${resetSec}s)`) : ""),
|
|
417
|
-
);
|
|
418
|
-
|
|
419
|
-
ctx.ui.setWidget("web-activity", new Text(lines.join("\n"), 0, 0));
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
function formatEntryLine(
|
|
423
|
-
entry: ActivityEntry,
|
|
424
|
-
theme: { fg: (color: string, text: string) => string },
|
|
425
|
-
): string {
|
|
426
|
-
const typeStr = entry.type === "api" ? "API" : "GET";
|
|
427
|
-
const target =
|
|
428
|
-
entry.type === "api"
|
|
429
|
-
? `"${truncateToWidth(entry.query || "", 28, "")}"`
|
|
430
|
-
: truncateToWidth(entry.url?.replace(/^https?:\/\//, "") || "", 30, "");
|
|
431
|
-
|
|
432
|
-
const duration = entry.endTime
|
|
433
|
-
? `${((entry.endTime - entry.startTime) / 1000).toFixed(1)}s`
|
|
434
|
-
: `${((Date.now() - entry.startTime) / 1000).toFixed(1)}s`;
|
|
435
|
-
|
|
436
|
-
let statusStr: string;
|
|
437
|
-
let indicator: string;
|
|
438
|
-
if (entry.error) {
|
|
439
|
-
statusStr = "err";
|
|
440
|
-
indicator = theme.fg("error", "✗");
|
|
441
|
-
} else if (entry.status === null) {
|
|
442
|
-
statusStr = "...";
|
|
443
|
-
indicator = theme.fg("warning", "⋯");
|
|
444
|
-
} else if (entry.status === 0) {
|
|
445
|
-
statusStr = "abort";
|
|
446
|
-
indicator = theme.fg("muted", "○");
|
|
447
|
-
} else {
|
|
448
|
-
statusStr = String(entry.status);
|
|
449
|
-
indicator = entry.status >= 200 && entry.status < 300 ? theme.fg("success", "✓") : theme.fg("error", "✗");
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
return `${typeStr.padEnd(4)} ${target.padEnd(32)} ${statusStr.padStart(5)} ${duration.padStart(5)} ${indicator}`;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
function handleSessionChange(ctx: ExtensionContext): void {
|
|
456
|
-
abortPendingFetches();
|
|
457
|
-
closeCurator();
|
|
458
|
-
clearCloneCache();
|
|
459
|
-
sessionActive = true;
|
|
460
|
-
restoreFromSession(ctx);
|
|
461
|
-
// Unsubscribe before clear() to avoid callback with stale ctx
|
|
462
|
-
widgetUnsubscribe?.();
|
|
463
|
-
widgetUnsubscribe = null;
|
|
464
|
-
activityMonitor.clear();
|
|
465
|
-
if (widgetVisible) {
|
|
466
|
-
// Re-subscribe with new ctx
|
|
467
|
-
widgetUnsubscribe = activityMonitor.onUpdate(() => updateWidget(ctx));
|
|
468
|
-
updateWidget(ctx);
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
export default function (pi: ExtensionAPI) {
|
|
473
|
-
const initConfig = loadConfigForExtensionInit();
|
|
474
|
-
const curateKey = initConfig.shortcuts?.curate || DEFAULT_SHORTCUTS.curate;
|
|
475
|
-
const activityKey = initConfig.shortcuts?.activity || DEFAULT_SHORTCUTS.activity;
|
|
476
|
-
|
|
477
|
-
function startBackgroundFetch(urls: string[]): string | null {
|
|
478
|
-
if (urls.length === 0) return null;
|
|
479
|
-
const fetchId = generateId();
|
|
480
|
-
const controller = new AbortController();
|
|
481
|
-
pendingFetches.set(fetchId, controller);
|
|
482
|
-
fetchAllContent(urls, controller.signal)
|
|
483
|
-
.then((fetched) => {
|
|
484
|
-
if (!sessionActive || !pendingFetches.has(fetchId)) return;
|
|
485
|
-
const data: StoredSearchData = {
|
|
486
|
-
id: fetchId,
|
|
487
|
-
type: "fetch",
|
|
488
|
-
timestamp: Date.now(),
|
|
489
|
-
urls: stripThumbnails(fetched),
|
|
490
|
-
};
|
|
491
|
-
storeResult(fetchId, data);
|
|
492
|
-
pi.appendEntry("web-search-results", data);
|
|
493
|
-
const ok = fetched.filter(f => !f.error).length;
|
|
494
|
-
pi.sendMessage(
|
|
495
|
-
{
|
|
496
|
-
customType: "web-search-content-ready",
|
|
497
|
-
content: `Content fetched for ${ok}/${fetched.length} URLs [${fetchId}]. Full page content now available.`,
|
|
498
|
-
display: true,
|
|
499
|
-
},
|
|
500
|
-
{ triggerTurn: true },
|
|
501
|
-
);
|
|
502
|
-
})
|
|
503
|
-
.catch((err) => {
|
|
504
|
-
if (!sessionActive || !pendingFetches.has(fetchId)) return;
|
|
505
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
506
|
-
const isAbort = (err instanceof Error && err.name === "AbortError") || message.toLowerCase().includes("abort");
|
|
507
|
-
if (!isAbort) {
|
|
508
|
-
pi.sendMessage(
|
|
509
|
-
{
|
|
510
|
-
customType: "web-search-error",
|
|
511
|
-
content: `Content fetch failed [${fetchId}]: ${message}`,
|
|
512
|
-
display: true,
|
|
513
|
-
},
|
|
514
|
-
{ triggerTurn: false },
|
|
515
|
-
);
|
|
516
|
-
}
|
|
517
|
-
})
|
|
518
|
-
.finally(() => { pendingFetches.delete(fetchId); });
|
|
519
|
-
return fetchId;
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
function storeAndPublishSearch(results: QueryResultData[]): string {
|
|
523
|
-
const id = generateId();
|
|
524
|
-
const data: StoredSearchData = {
|
|
525
|
-
id, type: "search", timestamp: Date.now(), queries: results,
|
|
526
|
-
};
|
|
527
|
-
storeResult(id, data);
|
|
528
|
-
pi.appendEntry("web-search-results", data);
|
|
529
|
-
return id;
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
interface SearchReturnOptions {
|
|
533
|
-
queryList: string[];
|
|
534
|
-
results: QueryResultData[];
|
|
535
|
-
urls: string[];
|
|
536
|
-
includeContent: boolean;
|
|
537
|
-
inlineContent?: ExtractedContent[];
|
|
538
|
-
curated?: boolean;
|
|
539
|
-
curatedFrom?: number;
|
|
540
|
-
workflow?: CuratorWorkflow;
|
|
541
|
-
approvedSummary?: string;
|
|
542
|
-
summaryMeta?: SummaryMeta;
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
function normalizeSummaryMeta(meta: SummaryMeta | undefined, summaryText: string): SummaryMeta {
|
|
546
|
-
const normalizedText = summaryText.trim();
|
|
547
|
-
if (!meta) {
|
|
96
|
+
if (!existsSync(configPath)) continue;
|
|
97
|
+
const parsed = JSON.parse(readFileSync(configPath, "utf8")) as { shortcuts?: { curate?: string; activity?: string } };
|
|
548
98
|
return {
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
tokenEstimate: normalizedText.length > 0 ? Math.max(1, Math.ceil(normalizedText.length / 4)) : 0,
|
|
552
|
-
fallbackUsed: false,
|
|
553
|
-
edited: false,
|
|
99
|
+
curate: parsed.shortcuts?.curate?.trim() || defaults.curate,
|
|
100
|
+
activity: parsed.shortcuts?.activity?.trim() || defaults.activity,
|
|
554
101
|
};
|
|
102
|
+
} catch (error) {
|
|
103
|
+
console.error(`[pi-web-access] Failed to inspect shortcuts in ${configPath}:`, error);
|
|
555
104
|
}
|
|
556
|
-
|
|
557
|
-
return {
|
|
558
|
-
model: meta.model,
|
|
559
|
-
durationMs: Number.isFinite(meta.durationMs) && meta.durationMs >= 0 ? meta.durationMs : 0,
|
|
560
|
-
tokenEstimate: Number.isFinite(meta.tokenEstimate) && meta.tokenEstimate >= 0
|
|
561
|
-
? meta.tokenEstimate
|
|
562
|
-
: (normalizedText.length > 0 ? Math.max(1, Math.ceil(normalizedText.length / 4)) : 0),
|
|
563
|
-
fallbackUsed: meta.fallbackUsed === true,
|
|
564
|
-
fallbackReason: meta.fallbackReason,
|
|
565
|
-
edited: meta.edited === true,
|
|
566
|
-
};
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
function buildCurationCancelledReturn(reason: "user" | "stale") {
|
|
570
|
-
const message = `Search curation cancelled (${reason}).`;
|
|
571
|
-
return {
|
|
572
|
-
content: [{ type: "text", text: message }],
|
|
573
|
-
details: {
|
|
574
|
-
error: message,
|
|
575
|
-
cancelled: true,
|
|
576
|
-
cancelReason: reason,
|
|
577
|
-
},
|
|
578
|
-
};
|
|
579
105
|
}
|
|
106
|
+
return defaults;
|
|
107
|
+
}
|
|
580
108
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
if (!model) continue;
|
|
588
|
-
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
589
|
-
if (auth.ok && auth.apiKey) return { model, apiKey: auth.apiKey, headers: auth.headers };
|
|
590
|
-
}
|
|
591
|
-
throw new Error(`No model available: ${candidates.map(c => `${c.provider}/${c.id}`).join(", ")}`);
|
|
592
|
-
}
|
|
109
|
+
export default function webAccess(pi: ExtensionAPI) {
|
|
110
|
+
let heavyPromise: Promise<CapturedHeavy> | null = null;
|
|
111
|
+
let loadedHeavy: CapturedHeavy | null = null;
|
|
112
|
+
let sessionSnapshot: SessionSnapshot | null = null;
|
|
113
|
+
let lifecycleGeneration = 0;
|
|
114
|
+
let replayedGeneration = 0;
|
|
593
115
|
|
|
594
|
-
async function
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
{ provider: "openai", id: "gpt-4.1-mini" },
|
|
599
|
-
]);
|
|
600
|
-
const response = await complete(
|
|
601
|
-
model,
|
|
602
|
-
{
|
|
603
|
-
messages: [{
|
|
604
|
-
role: "user",
|
|
605
|
-
content: [{ type: "text", text: `Rewrite this web search query to get better, more specific results. Add relevant year qualifiers, precise technical terms, and specificity. Return ONLY the improved query text, nothing else.\n\nQuery: ${query}` }],
|
|
606
|
-
timestamp: Date.now(),
|
|
607
|
-
}],
|
|
608
|
-
},
|
|
609
|
-
{ apiKey, headers, signal },
|
|
610
|
-
);
|
|
611
|
-
if (response.stopReason === "aborted") throw new Error("Aborted");
|
|
612
|
-
const contentParts = Array.isArray(response.content) ? response.content : [];
|
|
613
|
-
const text = contentParts
|
|
614
|
-
.map(p => {
|
|
615
|
-
if (!p || typeof p !== "object") return "";
|
|
616
|
-
const part = p as Record<string, unknown>;
|
|
617
|
-
return typeof part.text === "string" ? part.text : "";
|
|
618
|
-
})
|
|
619
|
-
.join("")
|
|
620
|
-
.trim();
|
|
621
|
-
if (!text) throw new Error("Rewrite returned empty response");
|
|
622
|
-
return text;
|
|
116
|
+
async function replayCurrentSession(heavy: CapturedHeavy): Promise<void> {
|
|
117
|
+
if (!sessionSnapshot || replayedGeneration === sessionSnapshot.generation) return;
|
|
118
|
+
replayedGeneration = sessionSnapshot.generation;
|
|
119
|
+
await dispatchHandlers(heavy, sessionSnapshot.eventName, sessionSnapshot.event, sessionSnapshot.ctx);
|
|
623
120
|
}
|
|
624
121
|
|
|
625
|
-
async function
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
const result = resultsByIndex.get(qi);
|
|
636
|
-
if (result) selectedResults.push(result);
|
|
637
|
-
}
|
|
638
|
-
if (selectedResults.length === 0) {
|
|
639
|
-
throw new Error("No selected results available for summary generation");
|
|
640
|
-
}
|
|
641
|
-
try {
|
|
642
|
-
return await generateSummaryDraft(selectedResults, summaryContext, signal, modelOverride, feedback);
|
|
643
|
-
} catch (err) {
|
|
644
|
-
const isEmptyResponse = err instanceof Error && err.message.includes("Summary model returned empty response");
|
|
645
|
-
if (!isEmptyResponse) throw err;
|
|
646
|
-
const deterministic = buildDeterministicSummary(selectedResults);
|
|
647
|
-
return {
|
|
648
|
-
summary: deterministic.summary,
|
|
649
|
-
meta: {
|
|
650
|
-
...deterministic.meta,
|
|
651
|
-
fallbackReason: "summary-model-empty-response",
|
|
652
|
-
},
|
|
653
|
-
};
|
|
122
|
+
async function loadHeavy(): Promise<CapturedHeavy> {
|
|
123
|
+
if (!heavyPromise) {
|
|
124
|
+
heavyPromise = (async () => {
|
|
125
|
+
const captured: CapturedHeavy = { tools: new Map(), commands: new Map(), handlers: new Map(), shortcuts: new Map() };
|
|
126
|
+
const mod = await import("./index-heavy.js");
|
|
127
|
+
await mod.default(createHeavyProxy(pi, captured));
|
|
128
|
+
loadedHeavy = captured;
|
|
129
|
+
await replayCurrentSession(captured);
|
|
130
|
+
return captured;
|
|
131
|
+
})();
|
|
654
132
|
}
|
|
133
|
+
return heavyPromise;
|
|
655
134
|
}
|
|
656
135
|
|
|
657
|
-
async
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
const addModel = (provider: string, id: string) => {
|
|
665
|
-
const value = `${provider}/${id}`;
|
|
666
|
-
if (seen.has(value)) return;
|
|
667
|
-
seen.add(value);
|
|
668
|
-
summaryModels.push({ value, label: value });
|
|
669
|
-
};
|
|
670
|
-
|
|
671
|
-
try {
|
|
672
|
-
const availableModels = summaryContext.modelRegistry.getAvailable();
|
|
673
|
-
for (const model of availableModels) {
|
|
674
|
-
const value = `${model.provider}/${model.id}`;
|
|
675
|
-
availableValues.add(value);
|
|
676
|
-
addModel(model.provider, model.id);
|
|
677
|
-
}
|
|
678
|
-
} catch (err) {
|
|
679
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
680
|
-
console.error(`Failed to load summary models: ${message}`);
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
const currentModelValue = summaryContext.model
|
|
684
|
-
? `${summaryContext.model.provider}/${summaryContext.model.id}`
|
|
685
|
-
: null;
|
|
686
|
-
if (summaryContext.model && currentModelValue && !seen.has(currentModelValue)) {
|
|
687
|
-
addModel(summaryContext.model.provider, summaryContext.model.id);
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
const config = loadConfig();
|
|
691
|
-
const configuredSummaryModel = typeof config.summaryModel === "string" ? config.summaryModel.trim() : "";
|
|
692
|
-
const preferredDefaults = [
|
|
693
|
-
"anthropic/claude-haiku-4-5",
|
|
694
|
-
"openai-codex/gpt-5.3-codex-spark",
|
|
695
|
-
];
|
|
696
|
-
|
|
697
|
-
let defaultSummaryModel: string | null = null;
|
|
698
|
-
if (configuredSummaryModel.length > 0 && availableValues.has(configuredSummaryModel)) {
|
|
699
|
-
defaultSummaryModel = configuredSummaryModel;
|
|
136
|
+
pi.on("session_start", async (event, ctx) => {
|
|
137
|
+
const generation = ++lifecycleGeneration;
|
|
138
|
+
sessionSnapshot = { eventName: "session_start", event, ctx, generation };
|
|
139
|
+
if (loadedHeavy) {
|
|
140
|
+
replayedGeneration = generation;
|
|
141
|
+
await dispatchHandlers(loadedHeavy, "session_start", event, ctx);
|
|
700
142
|
}
|
|
701
|
-
|
|
702
|
-
for (const preferred of preferredDefaults) {
|
|
703
|
-
if (availableValues.has(preferred)) {
|
|
704
|
-
defaultSummaryModel = preferred;
|
|
705
|
-
break;
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
if (!defaultSummaryModel && summaryModels.length > 0) {
|
|
710
|
-
defaultSummaryModel = summaryModels[0].value;
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
return { summaryModels, defaultSummaryModel };
|
|
714
|
-
}
|
|
143
|
+
});
|
|
715
144
|
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
return {
|
|
723
|
-
approvedSummary: submittedSummary,
|
|
724
|
-
summaryMeta: normalizeSummaryMeta(payload.summaryMeta, submittedSummary),
|
|
725
|
-
};
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
const selected = filterByQueryIndices(payload.selectedQueryIndices, resultsByIndex).results;
|
|
729
|
-
const fallbackResults = selected.length > 0 ? selected : [...resultsByIndex.values()];
|
|
730
|
-
const deterministic = buildDeterministicSummary(fallbackResults);
|
|
731
|
-
return {
|
|
732
|
-
approvedSummary: deterministic.summary,
|
|
733
|
-
summaryMeta: deterministic.meta,
|
|
734
|
-
};
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
function buildSearchReturn(opts: SearchReturnOptions) {
|
|
738
|
-
const sc = opts.results.filter(r => !r.error).length;
|
|
739
|
-
const tr = opts.results.reduce((sum, r) => sum + r.results.length, 0);
|
|
740
|
-
|
|
741
|
-
const hasApprovedSummary = typeof opts.approvedSummary === "string" && opts.approvedSummary.trim().length > 0;
|
|
742
|
-
let output = "";
|
|
743
|
-
if (hasApprovedSummary) {
|
|
744
|
-
output = opts.approvedSummary!.trim();
|
|
745
|
-
} else {
|
|
746
|
-
if (opts.curated) {
|
|
747
|
-
output += "[These results were manually curated by the user in the browser. Use them as-is — do not re-search or discard.]\n\n";
|
|
748
|
-
}
|
|
749
|
-
const duplicateQueries = opts.curated ? duplicateQuerySet(opts.results) : new Set<string>();
|
|
750
|
-
for (const { query, answer, results, error, provider } of opts.results) {
|
|
751
|
-
if (opts.queryList.length > 1) {
|
|
752
|
-
output += opts.curated
|
|
753
|
-
? formatQueryHeader(query, provider, duplicateQueries)
|
|
754
|
-
: `## Query: "${query}"\n\n`;
|
|
755
|
-
}
|
|
756
|
-
if (error) output += `Error: ${error}\n\n`;
|
|
757
|
-
else if (results.length === 0) output += "No results found.\n\n";
|
|
758
|
-
else output += formatSearchSummary(results, answer) + "\n\n";
|
|
759
|
-
}
|
|
145
|
+
pi.on("session_tree", async (event, ctx) => {
|
|
146
|
+
const generation = ++lifecycleGeneration;
|
|
147
|
+
sessionSnapshot = { eventName: "session_tree", event, ctx, generation };
|
|
148
|
+
if (loadedHeavy) {
|
|
149
|
+
replayedGeneration = generation;
|
|
150
|
+
await dispatchHandlers(loadedHeavy, "session_tree", event, ctx);
|
|
760
151
|
}
|
|
152
|
+
});
|
|
761
153
|
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
if (
|
|
765
|
-
|
|
766
|
-
const data: StoredSearchData = {
|
|
767
|
-
id: fetchId,
|
|
768
|
-
type: "fetch",
|
|
769
|
-
timestamp: Date.now(),
|
|
770
|
-
urls: opts.inlineContent,
|
|
771
|
-
};
|
|
772
|
-
storeResult(fetchId, data);
|
|
773
|
-
pi.appendEntry("web-search-results", data);
|
|
774
|
-
if (!hasApprovedSummary) {
|
|
775
|
-
output += `---\nFull content for ${opts.inlineContent.length} sources available [${fetchId}].`;
|
|
776
|
-
}
|
|
777
|
-
} else if (opts.includeContent) {
|
|
778
|
-
fetchId = startBackgroundFetch(opts.urls);
|
|
779
|
-
if (fetchId && !hasApprovedSummary) {
|
|
780
|
-
output += `---\nContent fetching in background [${fetchId}]. Will notify when ready.`;
|
|
781
|
-
}
|
|
154
|
+
pi.on("session_shutdown", async (event, ctx) => {
|
|
155
|
+
++lifecycleGeneration;
|
|
156
|
+
if (loadedHeavy) {
|
|
157
|
+
await dispatchHandlers(loadedHeavy, "session_shutdown", event, ctx);
|
|
782
158
|
}
|
|
159
|
+
sessionSnapshot = null;
|
|
160
|
+
replayedGeneration = lifecycleGeneration;
|
|
161
|
+
});
|
|
783
162
|
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
totalResults: tr,
|
|
794
|
-
includeContent: opts.includeContent,
|
|
795
|
-
fetchId,
|
|
796
|
-
fetchUrls: isBackgroundFetch ? opts.urls : undefined,
|
|
797
|
-
searchId,
|
|
798
|
-
...(opts.curated ? {
|
|
799
|
-
curated: true,
|
|
800
|
-
curatedFrom: opts.curatedFrom,
|
|
801
|
-
curatedQueries: opts.results.map(r => ({
|
|
802
|
-
query: r.query,
|
|
803
|
-
provider: r.provider || null,
|
|
804
|
-
answer: r.answer || null,
|
|
805
|
-
sources: r.results.map(s => ({ title: s.title, url: s.url })),
|
|
806
|
-
error: r.error,
|
|
807
|
-
})),
|
|
808
|
-
} : {}),
|
|
809
|
-
...((opts.workflow && hasApprovedSummary)
|
|
810
|
-
? {
|
|
811
|
-
summary: {
|
|
812
|
-
text: opts.approvedSummary!.trim(),
|
|
813
|
-
workflow: opts.workflow,
|
|
814
|
-
model: opts.summaryMeta?.model ?? null,
|
|
815
|
-
durationMs: opts.summaryMeta?.durationMs ?? 0,
|
|
816
|
-
tokenEstimate: opts.summaryMeta?.tokenEstimate ?? 0,
|
|
817
|
-
fallbackUsed: opts.summaryMeta?.fallbackUsed === true,
|
|
818
|
-
fallbackReason: opts.summaryMeta?.fallbackReason,
|
|
819
|
-
edited: opts.summaryMeta?.edited === true,
|
|
820
|
-
},
|
|
821
|
-
}
|
|
822
|
-
: {}),
|
|
163
|
+
const shortcuts = getInitialShortcutConfig();
|
|
164
|
+
for (const [shortcut, name] of [[shortcuts.curate, "curate"], [shortcuts.activity, "activity"]] as const) {
|
|
165
|
+
pi.registerShortcut(shortcut, {
|
|
166
|
+
description: name === "curate" ? "Open web search curator" : "Show web search activity",
|
|
167
|
+
handler: async (ctx) => {
|
|
168
|
+
const heavy = await loadHeavy();
|
|
169
|
+
const handler = heavy.shortcuts.get(shortcut)?.handler;
|
|
170
|
+
if (!handler) throw new Error(`Web access shortcut implementation not found: ${shortcut}`);
|
|
171
|
+
await handler(ctx);
|
|
823
172
|
},
|
|
824
|
-
};
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
function filterByQueryIndices(selectedQueryIndices: number[], results: Map<number, QueryResultData>) {
|
|
828
|
-
const filteredResults: QueryResultData[] = [];
|
|
829
|
-
const filteredUrls: string[] = [];
|
|
830
|
-
for (const qi of selectedQueryIndices) {
|
|
831
|
-
const r = results.get(qi);
|
|
832
|
-
if (r) {
|
|
833
|
-
filteredResults.push(r);
|
|
834
|
-
for (const res of r.results) {
|
|
835
|
-
if (!filteredUrls.includes(res.url)) filteredUrls.push(res.url);
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
}
|
|
839
|
-
return { results: filteredResults, urls: filteredUrls };
|
|
173
|
+
});
|
|
840
174
|
}
|
|
841
175
|
|
|
842
|
-
function collectAllResultsAndUrls(resultsByIndex: Map<number, QueryResultData>) {
|
|
843
|
-
const results = [...resultsByIndex.values()];
|
|
844
|
-
const urls: string[] = [];
|
|
845
|
-
for (const result of results) {
|
|
846
|
-
for (const source of result.results) {
|
|
847
|
-
if (!urls.includes(source.url)) urls.push(source.url);
|
|
848
|
-
}
|
|
849
|
-
}
|
|
850
|
-
return { results, urls };
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
async function openCuratorBrowser(pc: PendingCurate, searchesComplete = true): Promise<void> {
|
|
854
|
-
let handle: CuratorServerHandle | null = null;
|
|
855
|
-
try {
|
|
856
|
-
pc.phase = "curating";
|
|
857
|
-
|
|
858
|
-
const searchAbort = new AbortController();
|
|
859
|
-
const addSearchSignal = pc.signal
|
|
860
|
-
? AbortSignal.any([pc.signal, searchAbort.signal])
|
|
861
|
-
: searchAbort.signal;
|
|
862
|
-
|
|
863
|
-
const sessionToken = randomUUID();
|
|
864
|
-
handle = await startCuratorServer(
|
|
865
|
-
{
|
|
866
|
-
queries: pc.queryList,
|
|
867
|
-
sessionToken,
|
|
868
|
-
timeout: pc.timeoutSeconds,
|
|
869
|
-
availableProviders: pc.availableProviders,
|
|
870
|
-
defaultProvider: pc.defaultProvider,
|
|
871
|
-
summaryModels: pc.summaryModels,
|
|
872
|
-
defaultSummaryModel: pc.defaultSummaryModel,
|
|
873
|
-
},
|
|
874
|
-
{
|
|
875
|
-
async onSummarize(selectedQueryIndices, summarizeSignal, model, feedback) {
|
|
876
|
-
if (pendingCurate !== pc) throw new Error("Curator session is no longer active.");
|
|
877
|
-
pc.onUpdate?.({
|
|
878
|
-
content: [{ type: "text", text: "Generating summary draft..." }],
|
|
879
|
-
details: { phase: "generating-summary", progress: 0.9 },
|
|
880
|
-
});
|
|
881
|
-
const draft = await generateSummaryForSelectedIndices(
|
|
882
|
-
selectedQueryIndices,
|
|
883
|
-
pc.searchResults,
|
|
884
|
-
pc.summaryContext,
|
|
885
|
-
summarizeSignal,
|
|
886
|
-
model,
|
|
887
|
-
feedback,
|
|
888
|
-
);
|
|
889
|
-
if (pendingCurate !== pc) throw new Error("Curator session is no longer active.");
|
|
890
|
-
pc.onUpdate?.({
|
|
891
|
-
content: [{ type: "text", text: "Summary draft ready — waiting for approval..." }],
|
|
892
|
-
details: { phase: "waiting-for-approval", progress: 1 },
|
|
893
|
-
});
|
|
894
|
-
return draft;
|
|
895
|
-
},
|
|
896
|
-
onSubmit(payload) {
|
|
897
|
-
if (pendingCurate !== pc) return;
|
|
898
|
-
searchAbort.abort();
|
|
899
|
-
const filtered = payload.selectedQueryIndices.length > 0
|
|
900
|
-
? filterByQueryIndices(payload.selectedQueryIndices, pc.searchResults)
|
|
901
|
-
: collectAllResultsAndUrls(pc.searchResults);
|
|
902
|
-
const filteredInline = pc.allInlineContent.filter(c => filtered.urls.includes(c.url));
|
|
903
|
-
const base: SearchReturnOptions = {
|
|
904
|
-
queryList: filtered.results.map(r => r.query),
|
|
905
|
-
results: filtered.results,
|
|
906
|
-
urls: filtered.urls,
|
|
907
|
-
includeContent: pc.includeContent,
|
|
908
|
-
inlineContent: filteredInline.length > 0 ? filteredInline : undefined,
|
|
909
|
-
curated: true,
|
|
910
|
-
curatedFrom: pc.searchResults.size,
|
|
911
|
-
};
|
|
912
|
-
if (!payload.rawResults) {
|
|
913
|
-
const resolvedSummary = resolveSummaryForSubmit(payload, pc.searchResults);
|
|
914
|
-
base.workflow = pc.workflow;
|
|
915
|
-
base.approvedSummary = resolvedSummary.approvedSummary;
|
|
916
|
-
base.summaryMeta = resolvedSummary.summaryMeta;
|
|
917
|
-
}
|
|
918
|
-
pc.finish(buildSearchReturn(base));
|
|
919
|
-
closeCurator();
|
|
920
|
-
},
|
|
921
|
-
onCancel(reason) {
|
|
922
|
-
if (pendingCurate !== pc) return;
|
|
923
|
-
searchAbort.abort();
|
|
924
|
-
if (reason === "timeout") {
|
|
925
|
-
const resolvedSummary = resolveSummaryForSubmit({ selectedQueryIndices: [], summary: undefined, summaryMeta: undefined }, pc.searchResults);
|
|
926
|
-
const all = collectAllResultsAndUrls(pc.searchResults);
|
|
927
|
-
const filteredInline = pc.allInlineContent.filter(c => all.urls.includes(c.url));
|
|
928
|
-
pc.finish(buildSearchReturn({
|
|
929
|
-
queryList: all.results.map(r => r.query),
|
|
930
|
-
results: all.results,
|
|
931
|
-
urls: all.urls,
|
|
932
|
-
includeContent: pc.includeContent,
|
|
933
|
-
inlineContent: filteredInline.length > 0 ? filteredInline : undefined,
|
|
934
|
-
curated: true,
|
|
935
|
-
curatedFrom: pc.searchResults.size,
|
|
936
|
-
workflow: pc.workflow,
|
|
937
|
-
approvedSummary: resolvedSummary.approvedSummary,
|
|
938
|
-
summaryMeta: resolvedSummary.summaryMeta,
|
|
939
|
-
}));
|
|
940
|
-
} else {
|
|
941
|
-
pc.finish(buildCurationCancelledReturn(reason));
|
|
942
|
-
}
|
|
943
|
-
closeCurator();
|
|
944
|
-
},
|
|
945
|
-
onProviderChange(provider) {
|
|
946
|
-
if (pendingCurate !== pc) return;
|
|
947
|
-
const normalized = normalizeProviderInput(provider);
|
|
948
|
-
if (!normalized || normalized === "auto") return;
|
|
949
|
-
pc.defaultProvider = normalized;
|
|
950
|
-
try {
|
|
951
|
-
saveConfig({ provider: normalized });
|
|
952
|
-
} catch (err) {
|
|
953
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
954
|
-
console.error(`Failed to persist default provider: ${message}`);
|
|
955
|
-
}
|
|
956
|
-
},
|
|
957
|
-
async onAddSearch(query, queryIndex, provider) {
|
|
958
|
-
if (pendingCurate !== pc) throw new Error("Curator session is no longer active.");
|
|
959
|
-
const normalizedProvider = normalizeProviderInput(provider);
|
|
960
|
-
const requestedProvider = !normalizedProvider || normalizedProvider === "auto"
|
|
961
|
-
? pc.defaultProvider
|
|
962
|
-
: normalizedProvider;
|
|
963
|
-
try {
|
|
964
|
-
const { answer, results, inlineContent, provider: actualProvider } = await search(query, {
|
|
965
|
-
provider: requestedProvider,
|
|
966
|
-
numResults: pc.numResults,
|
|
967
|
-
recencyFilter: pc.recencyFilter,
|
|
968
|
-
domainFilter: pc.domainFilter,
|
|
969
|
-
includeContent: pc.includeContent,
|
|
970
|
-
signal: addSearchSignal,
|
|
971
|
-
});
|
|
972
|
-
if (pendingCurate !== pc) throw new Error("Curator session is no longer active.");
|
|
973
|
-
pc.searchResults.set(queryIndex, { query, answer, results, error: null, provider: actualProvider });
|
|
974
|
-
if (inlineContent) pc.allInlineContent.push(...inlineContent);
|
|
975
|
-
return {
|
|
976
|
-
answer,
|
|
977
|
-
results: results.map(r => ({ title: r.title, url: r.url, domain: extractDomain(r.url) })),
|
|
978
|
-
provider: actualProvider,
|
|
979
|
-
};
|
|
980
|
-
} catch (err) {
|
|
981
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
982
|
-
if (pendingCurate === pc) {
|
|
983
|
-
pc.searchResults.set(queryIndex, { query, answer: "", results: [], error: message, provider: requestedProvider });
|
|
984
|
-
}
|
|
985
|
-
throw err;
|
|
986
|
-
}
|
|
987
|
-
},
|
|
988
|
-
async onRewriteQuery(query, rewriteSignal) {
|
|
989
|
-
if (pendingCurate !== pc) throw new Error("Curator session is no longer active.");
|
|
990
|
-
return rewriteSearchQuery(query, pc.summaryContext, rewriteSignal);
|
|
991
|
-
},
|
|
992
|
-
},
|
|
993
|
-
);
|
|
994
|
-
|
|
995
|
-
if (pendingCurate !== pc) {
|
|
996
|
-
handle.close();
|
|
997
|
-
return;
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
|
-
activeCurator = handle;
|
|
1001
|
-
|
|
1002
|
-
for (const [qi, data] of pc.searchResults) {
|
|
1003
|
-
if (data.error) {
|
|
1004
|
-
handle.pushError(qi, data.error, data.provider);
|
|
1005
|
-
} else {
|
|
1006
|
-
handle.pushResult(qi, {
|
|
1007
|
-
answer: data.answer,
|
|
1008
|
-
results: data.results.map(r => ({ title: r.title, url: r.url, domain: extractDomain(r.url) })),
|
|
1009
|
-
provider: data.provider || pc.defaultProvider,
|
|
1010
|
-
});
|
|
1011
|
-
}
|
|
1012
|
-
}
|
|
1013
|
-
if (searchesComplete) handle.searchesDone();
|
|
1014
|
-
|
|
1015
|
-
pc.onUpdate?.({
|
|
1016
|
-
content: [{ type: "text", text: searchesComplete ? "Waiting for summary approval in browser..." : "Searches streaming to browser..." }],
|
|
1017
|
-
details: { phase: "curating", progress: searchesComplete ? 1 : 0.5 },
|
|
1018
|
-
});
|
|
1019
|
-
|
|
1020
|
-
const open = platform() === "darwin" ? await getGlimpseOpen() : null;
|
|
1021
|
-
if (open) {
|
|
1022
|
-
try {
|
|
1023
|
-
const win = openInGlimpse(open, handle.url, "Search Curator");
|
|
1024
|
-
glimpseWin = win;
|
|
1025
|
-
win.on("closed", () => {
|
|
1026
|
-
if (glimpseWin === win) {
|
|
1027
|
-
glimpseWin = null;
|
|
1028
|
-
closeCurator();
|
|
1029
|
-
}
|
|
1030
|
-
});
|
|
1031
|
-
return;
|
|
1032
|
-
} catch (err) {
|
|
1033
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1034
|
-
console.error(`Failed to open Glimpse curator window: ${message}`);
|
|
1035
|
-
glimpseWin = null;
|
|
1036
|
-
}
|
|
1037
|
-
}
|
|
1038
|
-
await openInBrowser(pi, handle.url);
|
|
1039
|
-
} catch (err) {
|
|
1040
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1041
|
-
console.error(`Failed to open curator UI: ${message}`);
|
|
1042
|
-
if (pendingCurate === pc || (handle && activeCurator === handle)) {
|
|
1043
|
-
closeCurator();
|
|
1044
|
-
}
|
|
1045
|
-
}
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
pi.registerShortcut(curateKey, {
|
|
1049
|
-
description: "Review search results",
|
|
1050
|
-
handler: async (ctx) => {
|
|
1051
|
-
if (!pendingCurate) return;
|
|
1052
|
-
|
|
1053
|
-
if (pendingCurate.phase === "searching") {
|
|
1054
|
-
pendingCurate.browserPromise = openCuratorBrowser(pendingCurate, false);
|
|
1055
|
-
ctx.ui.notify("Opening curator — remaining searches will stream in", "info");
|
|
1056
|
-
return;
|
|
1057
|
-
}
|
|
1058
|
-
},
|
|
1059
|
-
});
|
|
1060
|
-
|
|
1061
|
-
pi.registerShortcut(activityKey, {
|
|
1062
|
-
description: "Toggle web search activity",
|
|
1063
|
-
handler: async (ctx) => {
|
|
1064
|
-
widgetVisible = !widgetVisible;
|
|
1065
|
-
if (widgetVisible) {
|
|
1066
|
-
widgetUnsubscribe = activityMonitor.onUpdate(() => updateWidget(ctx));
|
|
1067
|
-
updateWidget(ctx);
|
|
1068
|
-
} else {
|
|
1069
|
-
widgetUnsubscribe?.();
|
|
1070
|
-
widgetUnsubscribe = null;
|
|
1071
|
-
ctx.ui.setWidget("web-activity", null);
|
|
1072
|
-
}
|
|
1073
|
-
},
|
|
1074
|
-
});
|
|
1075
|
-
|
|
1076
|
-
pi.on("session_start", async (_event, ctx) => handleSessionChange(ctx));
|
|
1077
|
-
pi.on("session_tree", async (_event, ctx) => handleSessionChange(ctx));
|
|
1078
|
-
|
|
1079
|
-
pi.on("session_shutdown", () => {
|
|
1080
|
-
sessionActive = false;
|
|
1081
|
-
abortPendingFetches();
|
|
1082
|
-
closeCurator();
|
|
1083
|
-
clearCloneCache();
|
|
1084
|
-
clearResults();
|
|
1085
|
-
// Unsubscribe before clear() to avoid callback with stale ctx
|
|
1086
|
-
widgetUnsubscribe?.();
|
|
1087
|
-
widgetUnsubscribe = null;
|
|
1088
|
-
activityMonitor.clear();
|
|
1089
|
-
widgetVisible = false;
|
|
1090
|
-
});
|
|
1091
|
-
|
|
1092
176
|
pi.registerTool({
|
|
1093
177
|
name: "web_search",
|
|
1094
178
|
label: "Web Search",
|
|
1095
|
-
description:
|
|
1096
|
-
|
|
1097
|
-
promptSnippet:
|
|
1098
|
-
"Use for web research questions. Prefer {queries:[...]} with 2-4 varied angles over a single query for broader coverage.",
|
|
179
|
+
description: "Search the web using Perplexity AI, Exa, or Gemini. Returns an AI-synthesized answer with source citations. For comprehensive research, prefer queries (plural) with 2-4 varied angles over a single query — each query gets its own synthesized answer, so varying phrasing and scope gives much broader coverage. When includeContent is true, full page content is fetched in the background. Searches auto-open the interactive browser curator and stream results live; set workflow to \"none\" to skip curation. Provider auto-selects: Exa (direct API with key, MCP fallback without), else Perplexity (needs key), else Gemini API (needs key), else Gemini Web (needs a supported Chromium-based browser login).",
|
|
180
|
+
promptSnippet: "Use for web research questions. Prefer {queries:[...]} with 2-4 varied angles over a single query for broader coverage.",
|
|
1099
181
|
parameters: Type.Object({
|
|
1100
182
|
query: Type.Optional(Type.String({ description: "Single search query. For research tasks, prefer 'queries' with multiple varied angles instead." })),
|
|
1101
|
-
queries: Type.Optional(Type.Array(Type.String(), { description: "Multiple queries searched in sequence, each returning its own synthesized answer. Prefer this for research — vary phrasing, scope, and angle across 2-4 queries to maximize coverage.
|
|
183
|
+
queries: Type.Optional(Type.Array(Type.String(), { description: "Multiple queries searched in sequence, each returning its own synthesized answer. Prefer this for research — vary phrasing, scope, and angle across 2-4 queries to maximize coverage." })),
|
|
1102
184
|
numResults: Type.Optional(Type.Number({ description: "Results per query (default: 5, max: 20)" })),
|
|
1103
185
|
includeContent: Type.Optional(Type.Boolean({ description: "Fetch full page content (async)" })),
|
|
1104
|
-
recencyFilter: Type.Optional(
|
|
1105
|
-
StringEnum(["day", "week", "month", "year"], { description: "Filter by recency" }),
|
|
1106
|
-
),
|
|
186
|
+
recencyFilter: Type.Optional(Type.String({ enum: ["day", "week", "month", "year"], description: "Filter by recency" })),
|
|
1107
187
|
domainFilter: Type.Optional(Type.Array(Type.String(), { description: "Limit to domains (prefix with - to exclude)" })),
|
|
1108
|
-
provider: Type.Optional(
|
|
1109
|
-
|
|
1110
|
-
),
|
|
1111
|
-
workflow: Type.Optional(
|
|
1112
|
-
StringEnum(["none", "summary-review"], {
|
|
1113
|
-
description: "Search workflow mode: none = no curator, summary-review = open curator with auto summary draft (default)",
|
|
1114
|
-
}),
|
|
1115
|
-
),
|
|
188
|
+
provider: Type.Optional(Type.String({ enum: ["auto", "perplexity", "gemini", "exa"], description: "Search provider (default: auto)" })),
|
|
189
|
+
workflow: Type.Optional(Type.String({ enum: ["none", "summary-review"], description: "Search workflow mode: none = no curator, summary-review = open curator with auto summary draft (default)" })),
|
|
1116
190
|
}),
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
const rawQueryList: unknown[] = Array.isArray(params.queries)
|
|
1120
|
-
? params.queries
|
|
1121
|
-
: (params.query !== undefined ? [params.query] : []);
|
|
1122
|
-
const queryList = normalizeQueryList(rawQueryList);
|
|
1123
|
-
const configWorkflow = loadConfigForExtensionInit().workflow;
|
|
1124
|
-
const workflow = resolveWorkflow(params.workflow ?? configWorkflow, ctx?.hasUI !== false);
|
|
1125
|
-
const shouldCurate = workflow !== "none";
|
|
1126
|
-
|
|
1127
|
-
if (queryList.length === 0) {
|
|
1128
|
-
return {
|
|
1129
|
-
content: [{ type: "text", text: "Error: No query provided. Use 'query' or 'queries' parameter." }],
|
|
1130
|
-
details: { error: "No query provided" },
|
|
1131
|
-
};
|
|
1132
|
-
}
|
|
1133
|
-
|
|
1134
|
-
if (shouldCurate && !ctx) {
|
|
1135
|
-
return {
|
|
1136
|
-
content: [{ type: "text", text: "Error: Curation requires an active extension context." }],
|
|
1137
|
-
details: { error: "Missing extension context" },
|
|
1138
|
-
};
|
|
1139
|
-
}
|
|
1140
|
-
|
|
1141
|
-
if (shouldCurate) {
|
|
1142
|
-
closeCurator();
|
|
1143
|
-
|
|
1144
|
-
let resolvePromise: (value: unknown) => void = () => {};
|
|
1145
|
-
const promise = new Promise<unknown>((resolve) => {
|
|
1146
|
-
resolvePromise = resolve;
|
|
1147
|
-
});
|
|
1148
|
-
const includeContent = params.includeContent ?? false;
|
|
1149
|
-
const searchResults = new Map<number, QueryResultData>();
|
|
1150
|
-
const allInlineContent: ExtractedContent[] = [];
|
|
1151
|
-
const searchAbort = new AbortController();
|
|
1152
|
-
const searchSignal = signal
|
|
1153
|
-
? AbortSignal.any([signal, searchAbort.signal])
|
|
1154
|
-
: searchAbort.signal;
|
|
1155
|
-
let cancelled = false;
|
|
1156
|
-
|
|
1157
|
-
const bootstrap = await loadCuratorBootstrap(params.provider);
|
|
1158
|
-
const availableProviders = bootstrap.availableProviders;
|
|
1159
|
-
const defaultProvider = bootstrap.defaultProvider;
|
|
1160
|
-
const curatorTimeoutSeconds = bootstrap.timeoutSeconds;
|
|
1161
|
-
const curatorWorkflow: CuratorWorkflow = "summary-review";
|
|
1162
|
-
|
|
1163
|
-
const summaryContext: SummaryGenerationContext = {
|
|
1164
|
-
model: ctx.model,
|
|
1165
|
-
modelRegistry: ctx.modelRegistry,
|
|
1166
|
-
};
|
|
1167
|
-
const summaryModelChoices = await loadSummaryModelChoices(summaryContext);
|
|
1168
|
-
|
|
1169
|
-
const pc: PendingCurate = {
|
|
1170
|
-
phase: "searching",
|
|
1171
|
-
workflow: curatorWorkflow,
|
|
1172
|
-
summaryContext,
|
|
1173
|
-
searchResults,
|
|
1174
|
-
allInlineContent,
|
|
1175
|
-
queryList,
|
|
1176
|
-
includeContent,
|
|
1177
|
-
numResults: params.numResults,
|
|
1178
|
-
recencyFilter: params.recencyFilter,
|
|
1179
|
-
domainFilter: params.domainFilter,
|
|
1180
|
-
availableProviders,
|
|
1181
|
-
defaultProvider,
|
|
1182
|
-
summaryModels: summaryModelChoices.summaryModels,
|
|
1183
|
-
defaultSummaryModel: summaryModelChoices.defaultSummaryModel,
|
|
1184
|
-
timeoutSeconds: curatorTimeoutSeconds,
|
|
1185
|
-
onUpdate: onUpdate as PendingCurate["onUpdate"],
|
|
1186
|
-
signal,
|
|
1187
|
-
abortSearches: () => {
|
|
1188
|
-
if (!searchAbort.signal.aborted) searchAbort.abort();
|
|
1189
|
-
},
|
|
1190
|
-
finish: () => {},
|
|
1191
|
-
cancel: () => {},
|
|
1192
|
-
};
|
|
1193
|
-
|
|
1194
|
-
const finish = (value: unknown) => {
|
|
1195
|
-
if (cancelled) return;
|
|
1196
|
-
cancelled = true;
|
|
1197
|
-
pc.abortSearches();
|
|
1198
|
-
signal?.removeEventListener("abort", onAbort);
|
|
1199
|
-
pendingCurate = null;
|
|
1200
|
-
resolvePromise(value);
|
|
1201
|
-
};
|
|
1202
|
-
|
|
1203
|
-
const cancel = (reason: "user" | "stale" = "stale") => {
|
|
1204
|
-
if (cancelled) return;
|
|
1205
|
-
finish(buildCurationCancelledReturn(reason));
|
|
1206
|
-
};
|
|
1207
|
-
|
|
1208
|
-
pc.finish = finish;
|
|
1209
|
-
pc.cancel = cancel;
|
|
1210
|
-
|
|
1211
|
-
const onAbort = () => closeCurator();
|
|
1212
|
-
pendingCurate = pc;
|
|
1213
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1214
|
-
pc.browserPromise = openCuratorBrowser(pc, false);
|
|
1215
|
-
|
|
1216
|
-
for (let qi = 0; qi < queryList.length; qi++) {
|
|
1217
|
-
if (signal?.aborted || cancelled || searchAbort.signal.aborted) break;
|
|
1218
|
-
onUpdate?.({
|
|
1219
|
-
content: [{ type: "text", text: `Searching ${qi + 1}/${queryList.length}: "${queryList[qi]}"...` }],
|
|
1220
|
-
details: { phase: "searching", progress: qi / queryList.length, currentQuery: queryList[qi] },
|
|
1221
|
-
});
|
|
1222
|
-
const requestedProvider = pc.defaultProvider;
|
|
1223
|
-
try {
|
|
1224
|
-
const { answer, results, inlineContent, provider } = await search(queryList[qi], {
|
|
1225
|
-
provider: requestedProvider,
|
|
1226
|
-
numResults: params.numResults,
|
|
1227
|
-
recencyFilter: params.recencyFilter,
|
|
1228
|
-
domainFilter: params.domainFilter,
|
|
1229
|
-
includeContent: params.includeContent,
|
|
1230
|
-
signal: searchSignal,
|
|
1231
|
-
});
|
|
1232
|
-
if (signal?.aborted || cancelled || searchAbort.signal.aborted) break;
|
|
1233
|
-
searchResults.set(qi, { query: queryList[qi], answer, results, error: null, provider });
|
|
1234
|
-
if (inlineContent) allInlineContent.push(...inlineContent);
|
|
1235
|
-
if (activeCurator) {
|
|
1236
|
-
activeCurator.pushResult(qi, {
|
|
1237
|
-
answer,
|
|
1238
|
-
results: results.map(r => ({ title: r.title, url: r.url, domain: extractDomain(r.url) })),
|
|
1239
|
-
provider,
|
|
1240
|
-
});
|
|
1241
|
-
}
|
|
1242
|
-
} catch (err) {
|
|
1243
|
-
if (signal?.aborted || cancelled || searchAbort.signal.aborted) break;
|
|
1244
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1245
|
-
searchResults.set(qi, { query: queryList[qi], answer: "", results: [], error: message, provider: requestedProvider });
|
|
1246
|
-
if (activeCurator) {
|
|
1247
|
-
activeCurator.pushError(qi, message, requestedProvider);
|
|
1248
|
-
}
|
|
1249
|
-
}
|
|
1250
|
-
}
|
|
1251
|
-
|
|
1252
|
-
if (signal?.aborted || cancelled || searchAbort.signal.aborted) {
|
|
1253
|
-
cancel();
|
|
1254
|
-
return promise;
|
|
1255
|
-
}
|
|
1256
|
-
|
|
1257
|
-
await pc.browserPromise;
|
|
1258
|
-
if (activeCurator && !cancelled) {
|
|
1259
|
-
activeCurator.searchesDone();
|
|
1260
|
-
pc.onUpdate?.({
|
|
1261
|
-
content: [{ type: "text", text: "All searches complete — waiting for summary approval in browser..." }],
|
|
1262
|
-
details: { phase: "curating", progress: 1 },
|
|
1263
|
-
});
|
|
1264
|
-
}
|
|
1265
|
-
|
|
1266
|
-
return promise;
|
|
1267
|
-
}
|
|
1268
|
-
|
|
1269
|
-
const searchResults: QueryResultData[] = [];
|
|
1270
|
-
const allUrls: string[] = [];
|
|
1271
|
-
const allInlineContent: ExtractedContent[] = [];
|
|
1272
|
-
const resolvedProvider = normalizeProviderInput(params.provider ?? loadConfig().provider);
|
|
1273
|
-
|
|
1274
|
-
for (let i = 0; i < queryList.length; i++) {
|
|
1275
|
-
const query = queryList[i];
|
|
1276
|
-
|
|
1277
|
-
onUpdate?.({
|
|
1278
|
-
content: [{ type: "text", text: `Searching ${i + 1}/${queryList.length}: "${query}"...` }],
|
|
1279
|
-
details: { phase: "search", progress: i / queryList.length, currentQuery: query },
|
|
1280
|
-
});
|
|
1281
|
-
|
|
1282
|
-
try {
|
|
1283
|
-
const { answer, results, inlineContent, provider } = await search(query, {
|
|
1284
|
-
provider: resolvedProvider,
|
|
1285
|
-
numResults: params.numResults,
|
|
1286
|
-
recencyFilter: params.recencyFilter,
|
|
1287
|
-
domainFilter: params.domainFilter,
|
|
1288
|
-
includeContent: params.includeContent,
|
|
1289
|
-
signal,
|
|
1290
|
-
});
|
|
1291
|
-
|
|
1292
|
-
searchResults.push({ query, answer, results, error: null, provider });
|
|
1293
|
-
for (const r of results) {
|
|
1294
|
-
if (!allUrls.includes(r.url)) {
|
|
1295
|
-
allUrls.push(r.url);
|
|
1296
|
-
}
|
|
1297
|
-
}
|
|
1298
|
-
if (inlineContent) allInlineContent.push(...inlineContent);
|
|
1299
|
-
} catch (err) {
|
|
1300
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1301
|
-
const requestedProvider = typeof resolvedProvider === "string" && resolvedProvider !== "auto"
|
|
1302
|
-
? resolvedProvider
|
|
1303
|
-
: undefined;
|
|
1304
|
-
searchResults.push({ query, answer: "", results: [], error: message, provider: requestedProvider });
|
|
1305
|
-
}
|
|
1306
|
-
}
|
|
1307
|
-
|
|
1308
|
-
return buildSearchReturn({
|
|
1309
|
-
queryList,
|
|
1310
|
-
results: searchResults,
|
|
1311
|
-
urls: allUrls,
|
|
1312
|
-
includeContent: params.includeContent ?? false,
|
|
1313
|
-
inlineContent: allInlineContent.length > 0 ? allInlineContent : undefined,
|
|
1314
|
-
});
|
|
1315
|
-
},
|
|
1316
|
-
|
|
191
|
+
execute: (...args) => executeHeavyTool(loadHeavy, "web_search", args),
|
|
192
|
+
renderResult: (...args) => renderHeavyToolResult(loadedHeavy, "web_search", args),
|
|
1317
193
|
renderCall(args, theme) {
|
|
1318
|
-
const input = args as { query?:
|
|
1319
|
-
const
|
|
1320
|
-
|
|
1321
|
-
: (input.query !== undefined ? [input.query] : []);
|
|
1322
|
-
const queryList = normalizeQueryList(rawQueryList);
|
|
1323
|
-
if (queryList.length === 0) {
|
|
1324
|
-
return new Text(theme.fg("toolTitle", theme.bold("search ")) + theme.fg("error", "(no query)"), 0, 0);
|
|
1325
|
-
}
|
|
1326
|
-
if (queryList.length === 1) {
|
|
1327
|
-
const q = queryList[0];
|
|
1328
|
-
const display = q.length > 60 ? q.slice(0, 57) + "..." : q;
|
|
1329
|
-
return new Text(theme.fg("toolTitle", theme.bold("search ")) + theme.fg("accent", `"${display}"`), 0, 0);
|
|
1330
|
-
}
|
|
1331
|
-
const lines = [theme.fg("toolTitle", theme.bold("search ")) + theme.fg("accent", `${queryList.length} queries`)];
|
|
1332
|
-
for (const q of queryList.slice(0, 5)) {
|
|
1333
|
-
const display = q.length > 50 ? q.slice(0, 47) + "..." : q;
|
|
1334
|
-
lines.push(theme.fg("muted", ` "${display}"`));
|
|
1335
|
-
}
|
|
1336
|
-
if (queryList.length > 5) {
|
|
1337
|
-
lines.push(theme.fg("muted", ` ... and ${queryList.length - 5} more`));
|
|
1338
|
-
}
|
|
1339
|
-
return new Text(lines.join("\n"), 0, 0);
|
|
1340
|
-
},
|
|
1341
|
-
|
|
1342
|
-
renderResult(result, { expanded, isPartial }, theme) {
|
|
1343
|
-
type QueryDetail = {
|
|
1344
|
-
query: string;
|
|
1345
|
-
provider: string | null;
|
|
1346
|
-
answer: string | null;
|
|
1347
|
-
sources: Array<{ title: string; url: string }>;
|
|
1348
|
-
error: string | null;
|
|
1349
|
-
};
|
|
1350
|
-
const details = result.details as {
|
|
1351
|
-
queryCount?: number;
|
|
1352
|
-
successfulQueries?: number;
|
|
1353
|
-
totalResults?: number;
|
|
1354
|
-
error?: string;
|
|
1355
|
-
fetchId?: string;
|
|
1356
|
-
fetchUrls?: string[];
|
|
1357
|
-
phase?: string;
|
|
1358
|
-
progress?: number;
|
|
1359
|
-
currentQuery?: string;
|
|
1360
|
-
curated?: boolean;
|
|
1361
|
-
curatedFrom?: number;
|
|
1362
|
-
curatedQueries?: QueryDetail[];
|
|
1363
|
-
cancelled?: boolean;
|
|
1364
|
-
cancelReason?: string;
|
|
1365
|
-
summary?: {
|
|
1366
|
-
text: string;
|
|
1367
|
-
workflow: CuratorWorkflow;
|
|
1368
|
-
model: string | null;
|
|
1369
|
-
durationMs: number;
|
|
1370
|
-
tokenEstimate: number;
|
|
1371
|
-
fallbackUsed: boolean;
|
|
1372
|
-
fallbackReason?: string;
|
|
1373
|
-
edited?: boolean;
|
|
1374
|
-
};
|
|
1375
|
-
};
|
|
1376
|
-
|
|
1377
|
-
if (isPartial) {
|
|
1378
|
-
if (details?.phase === "curating") {
|
|
1379
|
-
return new Text(theme.fg("accent", "waiting for summary approval..."), 0, 0);
|
|
1380
|
-
}
|
|
1381
|
-
if (details?.phase === "searching") {
|
|
1382
|
-
const progress = details?.progress ?? 0;
|
|
1383
|
-
const bar = "\u2588".repeat(Math.floor(progress * 10)) + "\u2591".repeat(10 - Math.floor(progress * 10));
|
|
1384
|
-
const query = details?.currentQuery || "";
|
|
1385
|
-
const display = query.length > 40 ? query.slice(0, 37) + "..." : query;
|
|
1386
|
-
return new Text(theme.fg("accent", `[${bar}] ${display}`), 0, 0);
|
|
1387
|
-
}
|
|
1388
|
-
const progress = details?.progress ?? 0;
|
|
1389
|
-
const bar = "\u2588".repeat(Math.floor(progress * 10)) + "\u2591".repeat(10 - Math.floor(progress * 10));
|
|
1390
|
-
return new Text(theme.fg("accent", `[${bar}] ${details?.phase || "searching"}`), 0, 0);
|
|
1391
|
-
}
|
|
1392
|
-
|
|
1393
|
-
if (details?.error) {
|
|
1394
|
-
return new Text(theme.fg("error", `Error: ${details.error}`), 0, 0);
|
|
1395
|
-
}
|
|
1396
|
-
|
|
1397
|
-
let statusLine: string;
|
|
1398
|
-
const queryInfo = details?.queryCount === 1 ? "" : `${details?.successfulQueries}/${details?.queryCount} queries, `;
|
|
1399
|
-
statusLine = theme.fg("success", `${queryInfo}${details?.totalResults ?? 0} sources`);
|
|
1400
|
-
if (details?.curated && details?.curatedFrom) {
|
|
1401
|
-
statusLine += theme.fg("muted", ` (${details.queryCount}/${details.curatedFrom} queries curated)`);
|
|
1402
|
-
}
|
|
1403
|
-
if (details?.fetchId && details?.fetchUrls) {
|
|
1404
|
-
statusLine += theme.fg("muted", ` (fetching ${details.fetchUrls.length} URLs)`);
|
|
1405
|
-
} else if (details?.fetchId) {
|
|
1406
|
-
statusLine += theme.fg("muted", " (content ready)");
|
|
1407
|
-
}
|
|
1408
|
-
|
|
1409
|
-
// Build expanded lines first so collapsed view can reference total count
|
|
1410
|
-
const lines = [statusLine];
|
|
1411
|
-
if (details?.summary?.text) {
|
|
1412
|
-
lines.push("");
|
|
1413
|
-
lines.push(theme.fg("accent", `── Summary (${details.summary.workflow}) ` + "─".repeat(32)));
|
|
1414
|
-
lines.push("");
|
|
1415
|
-
for (const line of details.summary.text.split("\n")) {
|
|
1416
|
-
lines.push(` ${line}`);
|
|
1417
|
-
}
|
|
1418
|
-
lines.push("");
|
|
1419
|
-
const metaParts = [
|
|
1420
|
-
details.summary.model ? `model=${details.summary.model}` : "model=deterministic",
|
|
1421
|
-
`duration=${details.summary.durationMs}ms`,
|
|
1422
|
-
`tokens~${details.summary.tokenEstimate}`,
|
|
1423
|
-
details.summary.fallbackUsed ? "fallback=true" : "fallback=false",
|
|
1424
|
-
details.summary.edited ? "edited=true" : "edited=false",
|
|
1425
|
-
];
|
|
1426
|
-
if (details.summary.fallbackReason) {
|
|
1427
|
-
metaParts.push(`reason=${details.summary.fallbackReason}`);
|
|
1428
|
-
}
|
|
1429
|
-
lines.push(theme.fg("dim", " " + metaParts.join(" · ")));
|
|
1430
|
-
}
|
|
1431
|
-
|
|
1432
|
-
const queryDetails = details?.curatedQueries;
|
|
1433
|
-
if (queryDetails?.length) {
|
|
1434
|
-
const kept = queryDetails.length;
|
|
1435
|
-
const from = details?.curatedFrom ?? kept;
|
|
1436
|
-
lines.push("");
|
|
1437
|
-
lines.push(theme.fg("accent", `\u2500\u2500 Curated Results (${kept} of ${from} queries kept) ` + "\u2500".repeat(24)));
|
|
1438
|
-
|
|
1439
|
-
for (const cq of queryDetails) {
|
|
1440
|
-
lines.push("");
|
|
1441
|
-
const dq = cq.query.length > 65 ? cq.query.slice(0, 62) + "..." : cq.query;
|
|
1442
|
-
const providerLabel = cq.provider ? ` (${cq.provider})` : "";
|
|
1443
|
-
lines.push(theme.fg("accent", ` "${dq}"${providerLabel}`));
|
|
1444
|
-
|
|
1445
|
-
if (cq.error) {
|
|
1446
|
-
lines.push(theme.fg("error", ` ${cq.error}`));
|
|
1447
|
-
} else if (cq.answer) {
|
|
1448
|
-
lines.push("");
|
|
1449
|
-
for (const line of cq.answer.split("\n")) {
|
|
1450
|
-
lines.push(` ${line}`);
|
|
1451
|
-
}
|
|
1452
|
-
}
|
|
1453
|
-
|
|
1454
|
-
if (cq.sources.length > 0) {
|
|
1455
|
-
lines.push("");
|
|
1456
|
-
for (const s of cq.sources) {
|
|
1457
|
-
const domain = s.url.replace(/^https?:\/\//, "").replace(/\/.*$/, "");
|
|
1458
|
-
const title = s.title.length > 50 ? s.title.slice(0, 47) + "..." : s.title;
|
|
1459
|
-
lines.push(theme.fg("muted", ` \u25b8 ${title}`) + theme.fg("dim", ` \u00b7 ${domain}`));
|
|
1460
|
-
}
|
|
1461
|
-
}
|
|
1462
|
-
}
|
|
1463
|
-
lines.push("");
|
|
1464
|
-
} else {
|
|
1465
|
-
const textContent = result.content.find((c) => c.type === "text")?.text || "";
|
|
1466
|
-
const preview = textContent.length > 500 ? textContent.slice(0, 500) + "..." : textContent;
|
|
1467
|
-
for (const line of preview.split("\n")) {
|
|
1468
|
-
lines.push(theme.fg("dim", line));
|
|
1469
|
-
}
|
|
1470
|
-
}
|
|
1471
|
-
|
|
1472
|
-
if (details?.fetchUrls && details.fetchUrls.length > 0) {
|
|
1473
|
-
if (details.curated) {
|
|
1474
|
-
lines.push(theme.fg("muted", `Fetching ${details.fetchUrls.length} URLs in background`));
|
|
1475
|
-
} else {
|
|
1476
|
-
lines.push(theme.fg("muted", "Fetching:"));
|
|
1477
|
-
for (const u of details.fetchUrls.slice(0, 5)) {
|
|
1478
|
-
const display = u.length > 60 ? u.slice(0, 57) + "..." : u;
|
|
1479
|
-
lines.push(theme.fg("dim", " " + display));
|
|
1480
|
-
}
|
|
1481
|
-
if (details.fetchUrls.length > 5) {
|
|
1482
|
-
lines.push(theme.fg("dim", ` ... and ${details.fetchUrls.length - 5} more`));
|
|
1483
|
-
}
|
|
1484
|
-
}
|
|
1485
|
-
}
|
|
1486
|
-
|
|
1487
|
-
const totalLines = lines.length;
|
|
1488
|
-
|
|
1489
|
-
if (!expanded) {
|
|
1490
|
-
const box = new Box(1, 0, (t) => theme.bg("toolSuccessBg", t));
|
|
1491
|
-
box.addChild(new Text(statusLine, 0, 0));
|
|
1492
|
-
|
|
1493
|
-
let collapsedLines = 1; // statusLine
|
|
1494
|
-
const summaryPreview = details?.summary?.text?.trim() || "";
|
|
1495
|
-
if (summaryPreview) {
|
|
1496
|
-
const preview = summaryPreview.length > 120 ? summaryPreview.slice(0, 117) + "..." : summaryPreview;
|
|
1497
|
-
box.addChild(new Text(theme.fg("dim", preview), 0, 0));
|
|
1498
|
-
collapsedLines++;
|
|
1499
|
-
} else if (details?.curatedQueries?.length) {
|
|
1500
|
-
for (const cq of details.curatedQueries.slice(0, 3)) {
|
|
1501
|
-
const dq = cq.query.length > 55 ? cq.query.slice(0, 52) + "..." : cq.query;
|
|
1502
|
-
const srcCount = cq.sources?.length ?? 0;
|
|
1503
|
-
const suffix = cq.error ? theme.fg("error", " (error)") : theme.fg("dim", ` · ${srcCount} sources`);
|
|
1504
|
-
box.addChild(new Text(theme.fg("accent", ` "${dq}"`) + suffix, 0, 0));
|
|
1505
|
-
collapsedLines++;
|
|
1506
|
-
}
|
|
1507
|
-
if (details.curatedQueries.length > 3) {
|
|
1508
|
-
box.addChild(new Text(theme.fg("dim", ` ... and ${details.curatedQueries.length - 3} more`), 0, 0));
|
|
1509
|
-
collapsedLines++;
|
|
1510
|
-
}
|
|
1511
|
-
} else {
|
|
1512
|
-
const textContent = result.content.find((c) => c.type === "text")?.text || "";
|
|
1513
|
-
const firstContentLine = textContent.split("\n").find(l => {
|
|
1514
|
-
const t = l.trim();
|
|
1515
|
-
return t && !t.startsWith("[") && !t.startsWith("#") && !t.startsWith("---");
|
|
1516
|
-
});
|
|
1517
|
-
const fallbackLine = (firstContentLine?.trim() || "").replace(/\*\*/g, "");
|
|
1518
|
-
if (fallbackLine) {
|
|
1519
|
-
const preview = fallbackLine.length > 120 ? fallbackLine.slice(0, 117) + "..." : fallbackLine;
|
|
1520
|
-
box.addChild(new Text(theme.fg("dim", preview), 0, 0));
|
|
1521
|
-
collapsedLines++;
|
|
1522
|
-
}
|
|
1523
|
-
}
|
|
1524
|
-
const moreLines = Math.max(0, totalLines - collapsedLines);
|
|
1525
|
-
if (moreLines > 0) {
|
|
1526
|
-
box.addChild(new Text(theme.fg("muted", `\n... (${moreLines} more lines, ${totalLines} total, CTRL+O Expand)`), 0, 0));
|
|
1527
|
-
}
|
|
1528
|
-
return box;
|
|
1529
|
-
}
|
|
1530
|
-
|
|
1531
|
-
return new Text(lines.join("\n"), 0, 0);
|
|
194
|
+
const input = args as { query?: string; queries?: string[] };
|
|
195
|
+
const label = input.queries?.length ? `${input.queries.length} queries` : input.query ?? "(no query)";
|
|
196
|
+
return new Text(theme.fg("toolTitle", theme.bold("web_search ")) + theme.fg("accent", label), 0, 0);
|
|
1532
197
|
},
|
|
1533
198
|
});
|
|
1534
199
|
|
|
@@ -1536,296 +201,38 @@ export default function (pi: ExtensionAPI) {
|
|
|
1536
201
|
name: "code_search",
|
|
1537
202
|
label: "Code Search",
|
|
1538
203
|
description: "Search for code examples, documentation, and API references. Returns relevant code snippets and docs from GitHub, Stack Overflow, and official documentation. Use for any programming question — API usage, library examples, debugging help.",
|
|
1539
|
-
promptSnippet:
|
|
1540
|
-
"Use for programming/API/library questions to retrieve concrete examples and docs before implementing or debugging code.",
|
|
204
|
+
promptSnippet: "Use for programming/API/library questions to retrieve concrete examples and docs before implementing or debugging code.",
|
|
1541
205
|
parameters: Type.Object({
|
|
1542
206
|
query: Type.String({ description: "Programming question, API, library, or debugging topic to search for" }),
|
|
1543
|
-
maxTokens: Type.Optional(Type.Integer({
|
|
1544
|
-
minimum: 1000,
|
|
1545
|
-
maximum: 50000,
|
|
1546
|
-
description: "Maximum tokens of code/documentation context to return (default: 5000)",
|
|
1547
|
-
})),
|
|
207
|
+
maxTokens: Type.Optional(Type.Integer({ minimum: 1000, maximum: 50000, description: "Maximum tokens of code/documentation context to return (default: 5000)" })),
|
|
1548
208
|
}),
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
return executeCodeSearch(toolCallId, params, signal);
|
|
1552
|
-
},
|
|
1553
|
-
|
|
1554
|
-
renderCall(args, theme) {
|
|
1555
|
-
const { query } = args as { query?: string };
|
|
1556
|
-
const display = !query
|
|
1557
|
-
? "(no query)"
|
|
1558
|
-
: query.length > 70 ? query.slice(0, 67) + "..." : query;
|
|
1559
|
-
return new Text(theme.fg("toolTitle", theme.bold("code_search ")) + theme.fg("accent", display), 0, 0);
|
|
1560
|
-
},
|
|
1561
|
-
|
|
1562
|
-
renderResult(result, { expanded }, theme) {
|
|
1563
|
-
const details = result.details as { query?: string; maxTokens?: number; error?: string };
|
|
1564
|
-
if (details?.error) {
|
|
1565
|
-
return new Text(theme.fg("error", `Error: ${details.error}`), 0, 0);
|
|
1566
|
-
}
|
|
1567
|
-
|
|
1568
|
-
const summary = theme.fg("success", "code context returned") +
|
|
1569
|
-
theme.fg("muted", ` (${details?.maxTokens ?? 5000} tokens max)`);
|
|
1570
|
-
if (!expanded) return new Text(summary, 0, 0);
|
|
1571
|
-
|
|
1572
|
-
const textContent = result.content.find((c) => c.type === "text")?.text || "";
|
|
1573
|
-
const preview = textContent.length > 500 ? textContent.slice(0, 500) + "..." : textContent;
|
|
1574
|
-
return new Text(summary + "\n" + theme.fg("dim", preview), 0, 0);
|
|
1575
|
-
},
|
|
209
|
+
execute: (...args) => executeHeavyTool(loadHeavy, "code_search", args),
|
|
210
|
+
renderResult: (...args) => renderHeavyToolResult(loadedHeavy, "code_search", args),
|
|
1576
211
|
});
|
|
1577
212
|
|
|
1578
213
|
pi.registerTool({
|
|
1579
214
|
name: "fetch_content",
|
|
1580
215
|
label: "Fetch Content",
|
|
1581
216
|
description: "Fetch URL(s) and extract readable content as markdown. Supports YouTube video transcripts (with thumbnail), GitHub repository contents, and local video files (with frame thumbnail). Video frames can be extracted via timestamp/range or sampled across the entire video with frames alone. Falls back to Gemini for pages that block bots or fail Readability extraction. For YouTube and video files: ALWAYS pass the user's specific question via the prompt parameter — this directs the AI to focus on that aspect of the video, producing much better results than a generic extraction. Content is always stored and can be retrieved with get_search_content.",
|
|
1582
|
-
promptSnippet:
|
|
1583
|
-
"Use to extract readable content from URL(s), YouTube, GitHub repos, or local videos. For video questions, pass the user's exact question in prompt.",
|
|
217
|
+
promptSnippet: "Use to extract readable content from URL(s), YouTube, GitHub repos, or local videos. For video questions, pass the user's exact question in prompt.",
|
|
1584
218
|
parameters: Type.Object({
|
|
1585
219
|
url: Type.Optional(Type.String({ description: "Single URL to fetch" })),
|
|
1586
220
|
urls: Type.Optional(Type.Array(Type.String(), { description: "Multiple URLs (parallel)" })),
|
|
1587
|
-
forceClone: Type.Optional(Type.Boolean({
|
|
1588
|
-
|
|
1589
|
-
})),
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
})),
|
|
1593
|
-
timestamp: Type.Optional(Type.String({
|
|
1594
|
-
description: "Extract video frame(s) at a timestamp or time range. Single: '1:23:45', '23:45', or '85' (seconds). Range: '23:41-25:00' extracts evenly-spaced frames across that span (default 6). Use frames with ranges to control density; single+frames uses a fixed 5s interval. YouTube requires yt-dlp + ffmpeg; local videos require ffmpeg. Use a range when you know the approximate area but not the exact moment — you'll get a contact sheet to visually identify the right frame.",
|
|
1595
|
-
})),
|
|
1596
|
-
frames: Type.Optional(Type.Integer({
|
|
1597
|
-
minimum: 1,
|
|
1598
|
-
maximum: 12,
|
|
1599
|
-
description: "Number of frames to extract. Use with timestamp range for custom density, with single timestamp to get N frames at 5s intervals, or alone to sample across the entire video. Requires yt-dlp + ffmpeg for YouTube, ffmpeg for local video.",
|
|
1600
|
-
})),
|
|
1601
|
-
model: Type.Optional(Type.String({
|
|
1602
|
-
description: "Override the Gemini model for video/YouTube analysis (e.g. 'gemini-2.5-flash', 'gemini-3-flash-preview'). Defaults to config or gemini-3-flash-preview.",
|
|
1603
|
-
})),
|
|
221
|
+
forceClone: Type.Optional(Type.Boolean({ description: "Force cloning large GitHub repositories that exceed the size threshold" })),
|
|
222
|
+
prompt: Type.Optional(Type.String({ description: "Question or instruction for video analysis (YouTube and video files)." })),
|
|
223
|
+
timestamp: Type.Optional(Type.String({ description: "Extract video frame(s) at a timestamp or time range." })),
|
|
224
|
+
frames: Type.Optional(Type.Integer({ minimum: 1, maximum: 12, description: "Number of frames to extract." })),
|
|
225
|
+
model: Type.Optional(Type.String({ description: "Override the Gemini model for video/YouTube analysis." })),
|
|
1604
226
|
}),
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
const urlList = params.urls ?? (params.url ? [params.url] : []);
|
|
1608
|
-
if (urlList.length === 0) {
|
|
1609
|
-
return {
|
|
1610
|
-
content: [{ type: "text", text: "Error: No URL provided." }],
|
|
1611
|
-
details: { error: "No URL provided" },
|
|
1612
|
-
};
|
|
1613
|
-
}
|
|
1614
|
-
|
|
1615
|
-
onUpdate?.({
|
|
1616
|
-
content: [{ type: "text", text: `Fetching ${urlList.length} URL(s)...` }],
|
|
1617
|
-
details: { phase: "fetch", progress: 0 },
|
|
1618
|
-
});
|
|
1619
|
-
|
|
1620
|
-
const fetchResults = await fetchAllContent(urlList, signal, {
|
|
1621
|
-
forceClone: params.forceClone,
|
|
1622
|
-
prompt: params.prompt,
|
|
1623
|
-
timestamp: params.timestamp,
|
|
1624
|
-
frames: params.frames,
|
|
1625
|
-
model: params.model,
|
|
1626
|
-
});
|
|
1627
|
-
const successful = fetchResults.filter((r) => !r.error).length;
|
|
1628
|
-
const totalChars = fetchResults.reduce((sum, r) => sum + r.content.length, 0);
|
|
1629
|
-
|
|
1630
|
-
// ALWAYS store results (even for single URL)
|
|
1631
|
-
const responseId = generateId();
|
|
1632
|
-
const data: StoredSearchData = {
|
|
1633
|
-
id: responseId,
|
|
1634
|
-
type: "fetch",
|
|
1635
|
-
timestamp: Date.now(),
|
|
1636
|
-
urls: stripThumbnails(fetchResults),
|
|
1637
|
-
};
|
|
1638
|
-
storeResult(responseId, data);
|
|
1639
|
-
pi.appendEntry("web-search-results", data);
|
|
1640
|
-
|
|
1641
|
-
// Single URL: return content directly (possibly truncated) with responseId
|
|
1642
|
-
if (urlList.length === 1) {
|
|
1643
|
-
const result = fetchResults[0];
|
|
1644
|
-
if (result.error) {
|
|
1645
|
-
return {
|
|
1646
|
-
content: [{ type: "text", text: `Error: ${result.error}` }],
|
|
1647
|
-
details: { urls: urlList, urlCount: 1, successful: 0, error: result.error, responseId, prompt: params.prompt, timestamp: params.timestamp, frames: params.frames },
|
|
1648
|
-
};
|
|
1649
|
-
}
|
|
1650
|
-
|
|
1651
|
-
const fullLength = result.content.length;
|
|
1652
|
-
const truncated = fullLength > MAX_INLINE_CONTENT;
|
|
1653
|
-
let output = truncated
|
|
1654
|
-
? result.content.slice(0, MAX_INLINE_CONTENT) + "\n\n[Content truncated...]"
|
|
1655
|
-
: result.content;
|
|
1656
|
-
|
|
1657
|
-
if (truncated) {
|
|
1658
|
-
output += `\n\n---\nShowing ${MAX_INLINE_CONTENT} of ${fullLength} chars. ` +
|
|
1659
|
-
`Use get_search_content({ responseId: "${responseId}", urlIndex: 0 }) for full content.`;
|
|
1660
|
-
}
|
|
1661
|
-
|
|
1662
|
-
const content: Array<{ type: string; text?: string; data?: string; mimeType?: string }> = [];
|
|
1663
|
-
if (result.frames?.length) {
|
|
1664
|
-
for (const frame of result.frames) {
|
|
1665
|
-
content.push({ type: "image", data: frame.data, mimeType: frame.mimeType });
|
|
1666
|
-
content.push({ type: "text", text: `Frame at ${frame.timestamp}` });
|
|
1667
|
-
}
|
|
1668
|
-
} else if (result.thumbnail) {
|
|
1669
|
-
content.push({ type: "image", data: result.thumbnail.data, mimeType: result.thumbnail.mimeType });
|
|
1670
|
-
}
|
|
1671
|
-
content.push({ type: "text", text: output });
|
|
1672
|
-
|
|
1673
|
-
const imageCount = (result.frames?.length ?? 0) + (result.thumbnail ? 1 : 0);
|
|
1674
|
-
return {
|
|
1675
|
-
content,
|
|
1676
|
-
details: {
|
|
1677
|
-
urls: urlList,
|
|
1678
|
-
urlCount: 1,
|
|
1679
|
-
successful: 1,
|
|
1680
|
-
totalChars: fullLength,
|
|
1681
|
-
title: result.title,
|
|
1682
|
-
responseId,
|
|
1683
|
-
truncated,
|
|
1684
|
-
hasImage: imageCount > 0,
|
|
1685
|
-
imageCount,
|
|
1686
|
-
prompt: params.prompt,
|
|
1687
|
-
timestamp: params.timestamp,
|
|
1688
|
-
frames: params.frames,
|
|
1689
|
-
duration: result.duration,
|
|
1690
|
-
},
|
|
1691
|
-
};
|
|
1692
|
-
}
|
|
1693
|
-
|
|
1694
|
-
// Multi-URL: existing behavior (summary + responseId)
|
|
1695
|
-
let output = "## Fetched URLs\n\n";
|
|
1696
|
-
for (const { url, title, content, error } of fetchResults) {
|
|
1697
|
-
if (error) {
|
|
1698
|
-
output += `- ${url}: Error - ${error}\n`;
|
|
1699
|
-
} else {
|
|
1700
|
-
output += `- ${title || url} (${content.length} chars)\n`;
|
|
1701
|
-
}
|
|
1702
|
-
}
|
|
1703
|
-
output += `\n---\nUse get_search_content({ responseId: "${responseId}", urlIndex: 0 }) to retrieve full content.`;
|
|
1704
|
-
|
|
1705
|
-
return {
|
|
1706
|
-
content: [{ type: "text", text: output }],
|
|
1707
|
-
details: { urls: urlList, urlCount: urlList.length, successful, totalChars, responseId },
|
|
1708
|
-
};
|
|
1709
|
-
},
|
|
1710
|
-
|
|
1711
|
-
renderCall(args, theme) {
|
|
1712
|
-
const { url, urls, prompt, timestamp, frames, model } = args as { url?: string; urls?: string[]; prompt?: string; timestamp?: string; frames?: number; model?: string };
|
|
1713
|
-
const urlList = urls ?? (url ? [url] : []);
|
|
1714
|
-
if (urlList.length === 0) {
|
|
1715
|
-
return new Text(theme.fg("toolTitle", theme.bold("fetch ")) + theme.fg("error", "(no URL)"), 0, 0);
|
|
1716
|
-
}
|
|
1717
|
-
const lines: string[] = [];
|
|
1718
|
-
if (urlList.length === 1) {
|
|
1719
|
-
const display = urlList[0].length > 60 ? urlList[0].slice(0, 57) + "..." : urlList[0];
|
|
1720
|
-
lines.push(theme.fg("toolTitle", theme.bold("fetch ")) + theme.fg("accent", display));
|
|
1721
|
-
} else {
|
|
1722
|
-
lines.push(theme.fg("toolTitle", theme.bold("fetch ")) + theme.fg("accent", `${urlList.length} URLs`));
|
|
1723
|
-
for (const u of urlList.slice(0, 5)) {
|
|
1724
|
-
const display = u.length > 60 ? u.slice(0, 57) + "..." : u;
|
|
1725
|
-
lines.push(theme.fg("muted", " " + display));
|
|
1726
|
-
}
|
|
1727
|
-
if (urlList.length > 5) {
|
|
1728
|
-
lines.push(theme.fg("muted", ` ... and ${urlList.length - 5} more`));
|
|
1729
|
-
}
|
|
1730
|
-
}
|
|
1731
|
-
if (timestamp) {
|
|
1732
|
-
lines.push(theme.fg("dim", " timestamp: ") + theme.fg("warning", timestamp));
|
|
1733
|
-
}
|
|
1734
|
-
if (typeof frames === "number") {
|
|
1735
|
-
lines.push(theme.fg("dim", " frames: ") + theme.fg("warning", String(frames)));
|
|
1736
|
-
}
|
|
1737
|
-
if (prompt) {
|
|
1738
|
-
const display = prompt.length > 250 ? prompt.slice(0, 247) + "..." : prompt;
|
|
1739
|
-
lines.push(theme.fg("dim", " prompt: ") + theme.fg("muted", `"${display}"`));
|
|
1740
|
-
}
|
|
1741
|
-
if (model) {
|
|
1742
|
-
lines.push(theme.fg("dim", " model: ") + theme.fg("warning", model));
|
|
1743
|
-
}
|
|
1744
|
-
return new Text(lines.join("\n"), 0, 0);
|
|
1745
|
-
},
|
|
1746
|
-
|
|
1747
|
-
renderResult(result, { expanded, isPartial }, theme) {
|
|
1748
|
-
const details = result.details as {
|
|
1749
|
-
urlCount?: number;
|
|
1750
|
-
successful?: number;
|
|
1751
|
-
totalChars?: number;
|
|
1752
|
-
error?: string;
|
|
1753
|
-
title?: string;
|
|
1754
|
-
truncated?: boolean;
|
|
1755
|
-
responseId?: string;
|
|
1756
|
-
phase?: string;
|
|
1757
|
-
progress?: number;
|
|
1758
|
-
hasImage?: boolean;
|
|
1759
|
-
imageCount?: number;
|
|
1760
|
-
prompt?: string;
|
|
1761
|
-
timestamp?: string;
|
|
1762
|
-
frames?: number;
|
|
1763
|
-
duration?: number;
|
|
1764
|
-
};
|
|
1765
|
-
|
|
1766
|
-
if (isPartial) {
|
|
1767
|
-
const progress = details?.progress ?? 0;
|
|
1768
|
-
const bar = "\u2588".repeat(Math.floor(progress * 10)) + "\u2591".repeat(10 - Math.floor(progress * 10));
|
|
1769
|
-
return new Text(theme.fg("accent", `[${bar}] ${details?.phase || "fetching"}`), 0, 0);
|
|
1770
|
-
}
|
|
1771
|
-
|
|
1772
|
-
if (details?.error) {
|
|
1773
|
-
return new Text(theme.fg("error", `Error: ${details.error}`), 0, 0);
|
|
1774
|
-
}
|
|
1775
|
-
|
|
1776
|
-
if (details?.urlCount === 1) {
|
|
1777
|
-
const title = details?.title || "Untitled";
|
|
1778
|
-
const imgCount = details?.imageCount ?? (details?.hasImage ? 1 : 0);
|
|
1779
|
-
const imageBadge = imgCount > 1
|
|
1780
|
-
? theme.fg("accent", ` [${imgCount} images]`)
|
|
1781
|
-
: imgCount === 1
|
|
1782
|
-
? theme.fg("accent", " [image]")
|
|
1783
|
-
: "";
|
|
1784
|
-
let statusLine = theme.fg("success", title) + theme.fg("muted", ` (${details?.totalChars ?? 0} chars)`) + imageBadge;
|
|
1785
|
-
if (details?.truncated) {
|
|
1786
|
-
statusLine += theme.fg("warning", " [truncated]");
|
|
1787
|
-
}
|
|
1788
|
-
if (typeof details?.duration === "number") {
|
|
1789
|
-
statusLine += theme.fg("muted", ` | ${formatSeconds(Math.floor(details.duration))} total`);
|
|
1790
|
-
}
|
|
1791
|
-
const textContent = result.content.find((c) => c.type === "text")?.text || "";
|
|
1792
|
-
if (!expanded) {
|
|
1793
|
-
const brief = textContent.length > 200 ? textContent.slice(0, 200) + "..." : textContent;
|
|
1794
|
-
return new Text(statusLine + "\n" + theme.fg("dim", brief), 0, 0);
|
|
1795
|
-
}
|
|
1796
|
-
const lines = [statusLine];
|
|
1797
|
-
if (details?.prompt) {
|
|
1798
|
-
const display = details.prompt.length > 250 ? details.prompt.slice(0, 247) + "..." : details.prompt;
|
|
1799
|
-
lines.push(theme.fg("dim", ` prompt: "${display}"`));
|
|
1800
|
-
}
|
|
1801
|
-
if (details?.timestamp) {
|
|
1802
|
-
lines.push(theme.fg("dim", ` timestamp: ${details.timestamp}`));
|
|
1803
|
-
}
|
|
1804
|
-
if (typeof details?.frames === "number") {
|
|
1805
|
-
lines.push(theme.fg("dim", ` frames: ${details.frames}`));
|
|
1806
|
-
}
|
|
1807
|
-
const preview = textContent.length > 500 ? textContent.slice(0, 500) + "..." : textContent;
|
|
1808
|
-
lines.push(theme.fg("dim", preview));
|
|
1809
|
-
return new Text(lines.join("\n"), 0, 0);
|
|
1810
|
-
}
|
|
1811
|
-
|
|
1812
|
-
const countColor = (details?.successful ?? 0) > 0 ? "success" : "error";
|
|
1813
|
-
const statusLine = theme.fg(countColor, `${details?.successful}/${details?.urlCount} URLs`) + theme.fg("muted", " (content stored)");
|
|
1814
|
-
if (!expanded) {
|
|
1815
|
-
return new Text(statusLine, 0, 0);
|
|
1816
|
-
}
|
|
1817
|
-
const textContent = result.content.find((c) => c.type === "text")?.text || "";
|
|
1818
|
-
const preview = textContent.length > 500 ? textContent.slice(0, 500) + "..." : textContent;
|
|
1819
|
-
return new Text(statusLine + "\n" + theme.fg("dim", preview), 0, 0);
|
|
1820
|
-
},
|
|
227
|
+
execute: (...args) => executeHeavyTool(loadHeavy, "fetch_content", args),
|
|
228
|
+
renderResult: (...args) => renderHeavyToolResult(loadedHeavy, "fetch_content", args),
|
|
1821
229
|
});
|
|
1822
230
|
|
|
1823
231
|
pi.registerTool({
|
|
1824
232
|
name: "get_search_content",
|
|
1825
233
|
label: "Get Search Content",
|
|
1826
234
|
description: "Retrieve full content from a previous web_search or fetch_content call.",
|
|
1827
|
-
promptSnippet:
|
|
1828
|
-
"Use after web_search/fetch_content when full stored content is needed via responseId plus query/url selectors.",
|
|
235
|
+
promptSnippet: "Use after web_search/fetch_content when full stored content is needed via responseId plus query/url selectors.",
|
|
1829
236
|
parameters: Type.Object({
|
|
1830
237
|
responseId: Type.String({ description: "The responseId from web_search or fetch_content" }),
|
|
1831
238
|
query: Type.Optional(Type.String({ description: "Get content for this query (web_search)" })),
|
|
@@ -1833,518 +240,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
1833
240
|
url: Type.Optional(Type.String({ description: "Get content for this URL" })),
|
|
1834
241
|
urlIndex: Type.Optional(Type.Number({ description: "Get content for URL at index" })),
|
|
1835
242
|
}),
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
const data = getResult(params.responseId);
|
|
1839
|
-
if (!data) {
|
|
1840
|
-
return {
|
|
1841
|
-
content: [{ type: "text", text: `Error: No stored results for "${params.responseId}"` }],
|
|
1842
|
-
details: { error: "Not found", responseId: params.responseId },
|
|
1843
|
-
};
|
|
1844
|
-
}
|
|
1845
|
-
|
|
1846
|
-
if (data.type === "search" && data.queries) {
|
|
1847
|
-
let queryData: QueryResultData | undefined;
|
|
1848
|
-
|
|
1849
|
-
if (params.query !== undefined) {
|
|
1850
|
-
queryData = data.queries.find((q) => q.query === params.query);
|
|
1851
|
-
if (!queryData) {
|
|
1852
|
-
const available = data.queries.map((q) => `"${q.query}"`).join(", ");
|
|
1853
|
-
return {
|
|
1854
|
-
content: [{ type: "text", text: `Query "${params.query}" not found. Available: ${available}` }],
|
|
1855
|
-
details: { error: "Query not found" },
|
|
1856
|
-
};
|
|
1857
|
-
}
|
|
1858
|
-
} else if (params.queryIndex !== undefined) {
|
|
1859
|
-
queryData = data.queries[params.queryIndex];
|
|
1860
|
-
if (!queryData) {
|
|
1861
|
-
return {
|
|
1862
|
-
content: [{ type: "text", text: `Index ${params.queryIndex} out of range (0-${data.queries.length - 1})` }],
|
|
1863
|
-
details: { error: "Index out of range" },
|
|
1864
|
-
};
|
|
1865
|
-
}
|
|
1866
|
-
} else {
|
|
1867
|
-
const available = data.queries.map((q, i) => `${i}: "${q.query}"`).join(", ");
|
|
1868
|
-
return {
|
|
1869
|
-
content: [{ type: "text", text: `Specify query or queryIndex. Available: ${available}` }],
|
|
1870
|
-
details: { error: "No query specified" },
|
|
1871
|
-
};
|
|
1872
|
-
}
|
|
1873
|
-
|
|
1874
|
-
if (queryData.error) {
|
|
1875
|
-
return {
|
|
1876
|
-
content: [{ type: "text", text: `Error for "${queryData.query}": ${queryData.error}` }],
|
|
1877
|
-
details: { error: queryData.error, query: queryData.query },
|
|
1878
|
-
};
|
|
1879
|
-
}
|
|
1880
|
-
|
|
1881
|
-
return {
|
|
1882
|
-
content: [{ type: "text", text: formatFullResults(queryData) }],
|
|
1883
|
-
details: { query: queryData.query, resultCount: queryData.results.length },
|
|
1884
|
-
};
|
|
1885
|
-
}
|
|
1886
|
-
|
|
1887
|
-
if (data.type === "fetch" && data.urls) {
|
|
1888
|
-
let urlData: ExtractedContent | undefined;
|
|
1889
|
-
|
|
1890
|
-
if (params.url !== undefined) {
|
|
1891
|
-
urlData = data.urls.find((u) => u.url === params.url);
|
|
1892
|
-
if (!urlData) {
|
|
1893
|
-
const available = data.urls.map((u) => u.url).join("\n ");
|
|
1894
|
-
return {
|
|
1895
|
-
content: [{ type: "text", text: `URL not found. Available:\n ${available}` }],
|
|
1896
|
-
details: { error: "URL not found" },
|
|
1897
|
-
};
|
|
1898
|
-
}
|
|
1899
|
-
} else if (params.urlIndex !== undefined) {
|
|
1900
|
-
urlData = data.urls[params.urlIndex];
|
|
1901
|
-
if (!urlData) {
|
|
1902
|
-
return {
|
|
1903
|
-
content: [{ type: "text", text: `Index ${params.urlIndex} out of range (0-${data.urls.length - 1})` }],
|
|
1904
|
-
details: { error: "Index out of range" },
|
|
1905
|
-
};
|
|
1906
|
-
}
|
|
1907
|
-
} else {
|
|
1908
|
-
const available = data.urls.map((u, i) => `${i}: ${u.url}`).join("\n ");
|
|
1909
|
-
return {
|
|
1910
|
-
content: [{ type: "text", text: `Specify url or urlIndex. Available:\n ${available}` }],
|
|
1911
|
-
details: { error: "No URL specified" },
|
|
1912
|
-
};
|
|
1913
|
-
}
|
|
1914
|
-
|
|
1915
|
-
if (urlData.error) {
|
|
1916
|
-
return {
|
|
1917
|
-
content: [{ type: "text", text: `Error for ${urlData.url}: ${urlData.error}` }],
|
|
1918
|
-
details: { error: urlData.error, url: urlData.url },
|
|
1919
|
-
};
|
|
1920
|
-
}
|
|
1921
|
-
|
|
1922
|
-
return {
|
|
1923
|
-
content: [{ type: "text", text: `# ${urlData.title}\n\n${urlData.content}` }],
|
|
1924
|
-
details: { url: urlData.url, title: urlData.title, contentLength: urlData.content.length },
|
|
1925
|
-
};
|
|
1926
|
-
}
|
|
1927
|
-
|
|
1928
|
-
return {
|
|
1929
|
-
content: [{ type: "text", text: "Invalid stored data format" }],
|
|
1930
|
-
details: { error: "Invalid data" },
|
|
1931
|
-
};
|
|
1932
|
-
},
|
|
1933
|
-
|
|
1934
|
-
renderCall(args, theme) {
|
|
1935
|
-
const { responseId, query, queryIndex, url, urlIndex } = args as {
|
|
1936
|
-
responseId: string;
|
|
1937
|
-
query?: string;
|
|
1938
|
-
queryIndex?: number;
|
|
1939
|
-
url?: string;
|
|
1940
|
-
urlIndex?: number;
|
|
1941
|
-
};
|
|
1942
|
-
let target = "";
|
|
1943
|
-
if (query) target = `query="${query}"`;
|
|
1944
|
-
else if (queryIndex !== undefined) target = `queryIndex=${queryIndex}`;
|
|
1945
|
-
else if (url) target = url.length > 30 ? url.slice(0, 27) + "..." : url;
|
|
1946
|
-
else if (urlIndex !== undefined) target = `urlIndex=${urlIndex}`;
|
|
1947
|
-
return new Text(theme.fg("toolTitle", theme.bold("get_content ")) + theme.fg("accent", target || responseId.slice(0, 8)), 0, 0);
|
|
1948
|
-
},
|
|
1949
|
-
|
|
1950
|
-
renderResult(result, { expanded }, theme) {
|
|
1951
|
-
const details = result.details as {
|
|
1952
|
-
error?: string;
|
|
1953
|
-
query?: string;
|
|
1954
|
-
url?: string;
|
|
1955
|
-
title?: string;
|
|
1956
|
-
resultCount?: number;
|
|
1957
|
-
contentLength?: number;
|
|
1958
|
-
};
|
|
1959
|
-
|
|
1960
|
-
if (details?.error) {
|
|
1961
|
-
return new Text(theme.fg("error", `Error: ${details.error}`), 0, 0);
|
|
1962
|
-
}
|
|
1963
|
-
|
|
1964
|
-
let statusLine: string;
|
|
1965
|
-
if (details?.query) {
|
|
1966
|
-
statusLine = theme.fg("success", `"${details.query}"`) + theme.fg("muted", ` (${details.resultCount} results)`);
|
|
1967
|
-
} else {
|
|
1968
|
-
statusLine = theme.fg("success", details?.title || "Content") + theme.fg("muted", ` (${details?.contentLength ?? 0} chars)`);
|
|
1969
|
-
}
|
|
1970
|
-
|
|
1971
|
-
if (!expanded) {
|
|
1972
|
-
return new Text(statusLine, 0, 0);
|
|
1973
|
-
}
|
|
1974
|
-
|
|
1975
|
-
const textContent = result.content.find((c) => c.type === "text")?.text || "";
|
|
1976
|
-
const preview = textContent.length > 500 ? textContent.slice(0, 500) + "..." : textContent;
|
|
1977
|
-
return new Text(statusLine + "\n" + theme.fg("dim", preview), 0, 0);
|
|
1978
|
-
},
|
|
243
|
+
execute: (...args) => executeHeavyTool(loadHeavy, "get_search_content", args),
|
|
244
|
+
renderResult: (...args) => renderHeavyToolResult(loadedHeavy, "get_search_content", args),
|
|
1979
245
|
});
|
|
1980
246
|
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
let bootstrap: CuratorBootstrap;
|
|
1993
|
-
try {
|
|
1994
|
-
bootstrap = await loadCuratorBootstrap(undefined);
|
|
1995
|
-
} catch (err) {
|
|
1996
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1997
|
-
ctx.ui.notify(`Failed to load web search config: ${message}`, "error");
|
|
1998
|
-
return;
|
|
1999
|
-
}
|
|
2000
|
-
const availableProviders = bootstrap.availableProviders;
|
|
2001
|
-
const initialProvider = bootstrap.defaultProvider;
|
|
2002
|
-
const curatorTimeoutSeconds = bootstrap.timeoutSeconds;
|
|
2003
|
-
let currentProvider = initialProvider;
|
|
2004
|
-
const summaryContext: SummaryGenerationContext = {
|
|
2005
|
-
model: ctx.model,
|
|
2006
|
-
modelRegistry: ctx.modelRegistry,
|
|
2007
|
-
};
|
|
2008
|
-
const summaryModelChoices = await loadSummaryModelChoices(summaryContext);
|
|
2009
|
-
|
|
2010
|
-
ctx.ui.notify("Opening web search curator...", "info");
|
|
2011
|
-
|
|
2012
|
-
const collected = new Map<number, QueryResultData>();
|
|
2013
|
-
const searchAbort = new AbortController();
|
|
2014
|
-
let aborted = false;
|
|
2015
|
-
let commandHandle: CuratorServerHandle | null = null;
|
|
2016
|
-
|
|
2017
|
-
function sendFollowUpFromReturn(payload: ReturnType<typeof buildSearchReturn>) {
|
|
2018
|
-
pi.sendMessage({
|
|
2019
|
-
customType: "web-search-results",
|
|
2020
|
-
content: payload.content,
|
|
2021
|
-
display: "tool",
|
|
2022
|
-
details: payload.details,
|
|
2023
|
-
}, { triggerTurn: true, deliverAs: "followUp" });
|
|
2024
|
-
}
|
|
2025
|
-
|
|
2026
|
-
try {
|
|
2027
|
-
const handle = await startCuratorServer(
|
|
2028
|
-
{
|
|
2029
|
-
queries,
|
|
2030
|
-
sessionToken,
|
|
2031
|
-
timeout: curatorTimeoutSeconds,
|
|
2032
|
-
availableProviders,
|
|
2033
|
-
defaultProvider: initialProvider,
|
|
2034
|
-
summaryModels: summaryModelChoices.summaryModels,
|
|
2035
|
-
defaultSummaryModel: summaryModelChoices.defaultSummaryModel,
|
|
2036
|
-
},
|
|
2037
|
-
{
|
|
2038
|
-
async onSummarize(selectedQueryIndices, summarizeSignal, model, feedback) {
|
|
2039
|
-
if (commandHandle && activeCurator !== commandHandle) {
|
|
2040
|
-
throw new Error("Curator session is no longer active.");
|
|
2041
|
-
}
|
|
2042
|
-
return generateSummaryForSelectedIndices(
|
|
2043
|
-
selectedQueryIndices,
|
|
2044
|
-
collected,
|
|
2045
|
-
summaryContext,
|
|
2046
|
-
summarizeSignal,
|
|
2047
|
-
model,
|
|
2048
|
-
feedback,
|
|
2049
|
-
);
|
|
2050
|
-
},
|
|
2051
|
-
onSubmit(payload) {
|
|
2052
|
-
if (commandHandle && activeCurator !== commandHandle) return;
|
|
2053
|
-
aborted = true;
|
|
2054
|
-
searchAbort.abort();
|
|
2055
|
-
const filtered = payload.selectedQueryIndices.length > 0
|
|
2056
|
-
? filterByQueryIndices(payload.selectedQueryIndices, collected)
|
|
2057
|
-
: collectAllResultsAndUrls(collected);
|
|
2058
|
-
const base: SearchReturnOptions = {
|
|
2059
|
-
queryList: filtered.results.map(r => r.query),
|
|
2060
|
-
results: filtered.results,
|
|
2061
|
-
urls: filtered.urls,
|
|
2062
|
-
includeContent: false,
|
|
2063
|
-
curated: true,
|
|
2064
|
-
curatedFrom: collected.size,
|
|
2065
|
-
};
|
|
2066
|
-
if (!payload.rawResults) {
|
|
2067
|
-
const resolvedSummary = resolveSummaryForSubmit(payload, collected);
|
|
2068
|
-
base.workflow = "summary-review";
|
|
2069
|
-
base.approvedSummary = resolvedSummary.approvedSummary;
|
|
2070
|
-
base.summaryMeta = resolvedSummary.summaryMeta;
|
|
2071
|
-
}
|
|
2072
|
-
sendFollowUpFromReturn(buildSearchReturn(base));
|
|
2073
|
-
closeCurator();
|
|
2074
|
-
},
|
|
2075
|
-
onCancel(reason) {
|
|
2076
|
-
if (commandHandle && activeCurator !== commandHandle) return;
|
|
2077
|
-
aborted = true;
|
|
2078
|
-
searchAbort.abort();
|
|
2079
|
-
if (reason === "timeout") {
|
|
2080
|
-
const all = collectAllResultsAndUrls(collected);
|
|
2081
|
-
const resolvedSummary = resolveSummaryForSubmit({ selectedQueryIndices: [], summary: undefined, summaryMeta: undefined }, collected);
|
|
2082
|
-
sendFollowUpFromReturn(buildSearchReturn({
|
|
2083
|
-
queryList: all.results.map(r => r.query),
|
|
2084
|
-
results: all.results,
|
|
2085
|
-
urls: all.urls,
|
|
2086
|
-
includeContent: false,
|
|
2087
|
-
curated: true,
|
|
2088
|
-
curatedFrom: collected.size,
|
|
2089
|
-
workflow: "summary-review",
|
|
2090
|
-
approvedSummary: resolvedSummary.approvedSummary,
|
|
2091
|
-
summaryMeta: resolvedSummary.summaryMeta,
|
|
2092
|
-
}));
|
|
2093
|
-
}
|
|
2094
|
-
closeCurator();
|
|
2095
|
-
},
|
|
2096
|
-
onProviderChange(provider) {
|
|
2097
|
-
if (commandHandle && activeCurator !== commandHandle) return;
|
|
2098
|
-
const normalized = normalizeProviderInput(provider);
|
|
2099
|
-
if (!normalized || normalized === "auto") return;
|
|
2100
|
-
currentProvider = normalized;
|
|
2101
|
-
try {
|
|
2102
|
-
saveConfig({ provider: normalized });
|
|
2103
|
-
} catch (err) {
|
|
2104
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
2105
|
-
console.error(`Failed to persist default provider: ${message}`);
|
|
2106
|
-
}
|
|
2107
|
-
},
|
|
2108
|
-
async onAddSearch(query, queryIndex, provider) {
|
|
2109
|
-
if (commandHandle && activeCurator !== commandHandle) {
|
|
2110
|
-
throw new Error("Curator session is no longer active.");
|
|
2111
|
-
}
|
|
2112
|
-
const normalizedProvider = normalizeProviderInput(provider);
|
|
2113
|
-
const requestedProvider = !normalizedProvider || normalizedProvider === "auto"
|
|
2114
|
-
? currentProvider
|
|
2115
|
-
: normalizedProvider;
|
|
2116
|
-
try {
|
|
2117
|
-
const { answer, results, provider: actualProvider } = await search(query, {
|
|
2118
|
-
provider: requestedProvider,
|
|
2119
|
-
signal: searchAbort.signal,
|
|
2120
|
-
});
|
|
2121
|
-
if (commandHandle && activeCurator !== commandHandle) {
|
|
2122
|
-
throw new Error("Curator session is no longer active.");
|
|
2123
|
-
}
|
|
2124
|
-
collected.set(queryIndex, { query, answer, results, error: null, provider: actualProvider });
|
|
2125
|
-
return {
|
|
2126
|
-
answer,
|
|
2127
|
-
results: results.map(r => ({ title: r.title, url: r.url, domain: extractDomain(r.url) })),
|
|
2128
|
-
provider: actualProvider,
|
|
2129
|
-
};
|
|
2130
|
-
} catch (err) {
|
|
2131
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
2132
|
-
if (!commandHandle || activeCurator === commandHandle) {
|
|
2133
|
-
collected.set(queryIndex, { query, answer: "", results: [], error: message, provider: requestedProvider });
|
|
2134
|
-
}
|
|
2135
|
-
throw err;
|
|
2136
|
-
}
|
|
2137
|
-
},
|
|
2138
|
-
async onRewriteQuery(query, rewriteSignal) {
|
|
2139
|
-
if (commandHandle && activeCurator !== commandHandle) {
|
|
2140
|
-
throw new Error("Curator session is no longer active.");
|
|
2141
|
-
}
|
|
2142
|
-
return rewriteSearchQuery(query, summaryContext, rewriteSignal);
|
|
2143
|
-
},
|
|
2144
|
-
},
|
|
2145
|
-
);
|
|
2146
|
-
|
|
2147
|
-
commandHandle = handle;
|
|
2148
|
-
activeCurator = handle;
|
|
2149
|
-
const open = platform() === "darwin" ? await getGlimpseOpen() : null;
|
|
2150
|
-
if (open) {
|
|
2151
|
-
try {
|
|
2152
|
-
const win = openInGlimpse(open, handle.url, "Search Curator");
|
|
2153
|
-
glimpseWin = win;
|
|
2154
|
-
win.on("closed", () => {
|
|
2155
|
-
if (glimpseWin === win) {
|
|
2156
|
-
glimpseWin = null;
|
|
2157
|
-
closeCurator();
|
|
2158
|
-
}
|
|
2159
|
-
});
|
|
2160
|
-
} catch (err) {
|
|
2161
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
2162
|
-
console.error(`Failed to open Glimpse curator window: ${message}`);
|
|
2163
|
-
glimpseWin = null;
|
|
2164
|
-
await openInBrowser(pi, handle.url);
|
|
2165
|
-
}
|
|
2166
|
-
} else {
|
|
2167
|
-
await openInBrowser(pi, handle.url);
|
|
2168
|
-
}
|
|
2169
|
-
|
|
2170
|
-
if (queries.length > 0) {
|
|
2171
|
-
(async () => {
|
|
2172
|
-
for (let qi = 0; qi < queries.length; qi++) {
|
|
2173
|
-
if (aborted || activeCurator !== handle) break;
|
|
2174
|
-
const requestedProvider = currentProvider;
|
|
2175
|
-
try {
|
|
2176
|
-
const { answer, results, provider } = await search(queries[qi], {
|
|
2177
|
-
provider: requestedProvider,
|
|
2178
|
-
signal: searchAbort.signal,
|
|
2179
|
-
});
|
|
2180
|
-
if (aborted || activeCurator !== handle) break;
|
|
2181
|
-
handle.pushResult(qi, {
|
|
2182
|
-
answer,
|
|
2183
|
-
results: results.map(r => ({ title: r.title, url: r.url, domain: extractDomain(r.url) })),
|
|
2184
|
-
provider,
|
|
2185
|
-
});
|
|
2186
|
-
collected.set(qi, { query: queries[qi], answer, results, error: null, provider });
|
|
2187
|
-
} catch (err) {
|
|
2188
|
-
if (aborted || activeCurator !== handle) break;
|
|
2189
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
2190
|
-
handle.pushError(qi, message, requestedProvider);
|
|
2191
|
-
collected.set(qi, { query: queries[qi], answer: "", results: [], error: message, provider: requestedProvider });
|
|
2192
|
-
}
|
|
2193
|
-
}
|
|
2194
|
-
if (!aborted && activeCurator === handle) handle.searchesDone();
|
|
2195
|
-
})();
|
|
2196
|
-
} else {
|
|
2197
|
-
if (activeCurator === handle) handle.searchesDone();
|
|
2198
|
-
}
|
|
2199
|
-
} catch (err) {
|
|
2200
|
-
closeCurator();
|
|
2201
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
2202
|
-
ctx.ui.notify(`Failed to open curator: ${message}`, "error");
|
|
2203
|
-
}
|
|
2204
|
-
},
|
|
2205
|
-
});
|
|
2206
|
-
|
|
2207
|
-
pi.registerCommand("curator", {
|
|
2208
|
-
description: "Toggle or configure the search curator workflow",
|
|
2209
|
-
handler: async (args, ctx) => {
|
|
2210
|
-
const arg = args.trim().toLowerCase();
|
|
2211
|
-
|
|
2212
|
-
let newWorkflow: WebSearchWorkflow;
|
|
2213
|
-
if (arg.length === 0) {
|
|
2214
|
-
const current = resolveWorkflow(loadConfigForExtensionInit().workflow, true);
|
|
2215
|
-
newWorkflow = current === "none" ? "summary-review" : "none";
|
|
2216
|
-
} else if (arg === "on") {
|
|
2217
|
-
newWorkflow = "summary-review";
|
|
2218
|
-
} else if (arg === "off") {
|
|
2219
|
-
newWorkflow = "none";
|
|
2220
|
-
} else if (arg === "none" || arg === "summary-review") {
|
|
2221
|
-
newWorkflow = arg;
|
|
2222
|
-
} else {
|
|
2223
|
-
ctx.ui.notify(`Unknown option: ${arg}. Use on, off, or summary-review.`, "error");
|
|
2224
|
-
return;
|
|
2225
|
-
}
|
|
2226
|
-
|
|
2227
|
-
try {
|
|
2228
|
-
saveConfig({ workflow: newWorkflow });
|
|
2229
|
-
} catch (err) {
|
|
2230
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
2231
|
-
ctx.ui.notify(`Failed to save config: ${message}`, "error");
|
|
2232
|
-
return;
|
|
2233
|
-
}
|
|
2234
|
-
|
|
2235
|
-
const label = newWorkflow === "none"
|
|
2236
|
-
? "Curator disabled — web_search will return raw results"
|
|
2237
|
-
: "Curator enabled — web_search will open curator and auto-generate a summary draft";
|
|
2238
|
-
pi.sendMessage({
|
|
2239
|
-
customType: "curator-config",
|
|
2240
|
-
content: [{ type: "text", text: label }],
|
|
2241
|
-
display: "tool",
|
|
2242
|
-
details: { workflow: newWorkflow },
|
|
2243
|
-
}, { triggerTurn: false, deliverAs: "followUp" });
|
|
2244
|
-
},
|
|
2245
|
-
});
|
|
2246
|
-
|
|
2247
|
-
pi.registerCommand("google-account", {
|
|
2248
|
-
description: "Show the active Google account for Gemini Web",
|
|
2249
|
-
handler: async () => {
|
|
2250
|
-
if (!isBrowserCookieAccessAllowed()) {
|
|
2251
|
-
pi.sendMessage({
|
|
2252
|
-
customType: "google-account",
|
|
2253
|
-
content: [{ type: "text", text: `Gemini Web browser cookie access is disabled. Set allowBrowserCookies: true in ~/${CONFIG_DIR_NAME}/web-search.json to enable it.` }],
|
|
2254
|
-
display: "tool",
|
|
2255
|
-
details: { available: false, cookieAccessAllowed: false },
|
|
2256
|
-
}, { triggerTurn: true, deliverAs: "followUp" });
|
|
2257
|
-
return;
|
|
2258
|
-
}
|
|
2259
|
-
|
|
2260
|
-
const cookies = await isGeminiWebAvailable();
|
|
2261
|
-
if (!cookies) {
|
|
2262
|
-
pi.sendMessage({
|
|
2263
|
-
customType: "google-account",
|
|
2264
|
-
content: [{ type: "text", text: "Gemini Web is unavailable. Sign into gemini.google.com in a supported Chromium-based browser." }],
|
|
2265
|
-
display: "tool",
|
|
2266
|
-
details: { available: false, cookieAccessAllowed: true },
|
|
2267
|
-
}, { triggerTurn: true, deliverAs: "followUp" });
|
|
2268
|
-
return;
|
|
2269
|
-
}
|
|
2270
|
-
|
|
2271
|
-
const email = await getActiveGoogleEmail(cookies);
|
|
2272
|
-
const text = email
|
|
2273
|
-
? `Active Google account: ${email}`
|
|
2274
|
-
: "Gemini Web is available, but the active Google account could not be determined.";
|
|
2275
|
-
|
|
2276
|
-
pi.sendMessage({
|
|
2277
|
-
customType: "google-account",
|
|
2278
|
-
content: [{ type: "text", text }],
|
|
2279
|
-
display: "tool",
|
|
2280
|
-
details: { available: true, email: email ?? null },
|
|
2281
|
-
}, { triggerTurn: true, deliverAs: "followUp" });
|
|
2282
|
-
},
|
|
2283
|
-
});
|
|
2284
|
-
|
|
2285
|
-
pi.registerCommand("search", {
|
|
2286
|
-
description: "Browse stored web search results",
|
|
2287
|
-
handler: async (_args, ctx) => {
|
|
2288
|
-
const results = getAllResults();
|
|
2289
|
-
|
|
2290
|
-
if (results.length === 0) {
|
|
2291
|
-
ctx.ui.notify("No stored search results", "info");
|
|
2292
|
-
return;
|
|
2293
|
-
}
|
|
2294
|
-
|
|
2295
|
-
const options = results.map((r) => {
|
|
2296
|
-
const age = Math.floor((Date.now() - r.timestamp) / 60000);
|
|
2297
|
-
const ageStr = age < 60 ? `${age}m ago` : `${Math.floor(age / 60)}h ago`;
|
|
2298
|
-
if (r.type === "search" && r.queries) {
|
|
2299
|
-
const query = r.queries[0]?.query || "unknown";
|
|
2300
|
-
return `[${r.id.slice(0, 6)}] "${query}" (${r.queries.length} queries) - ${ageStr}`;
|
|
2301
|
-
}
|
|
2302
|
-
if (r.type === "fetch" && r.urls) {
|
|
2303
|
-
return `[${r.id.slice(0, 6)}] ${r.urls.length} URLs fetched - ${ageStr}`;
|
|
2304
|
-
}
|
|
2305
|
-
return `[${r.id.slice(0, 6)}] ${r.type} - ${ageStr}`;
|
|
2306
|
-
});
|
|
2307
|
-
|
|
2308
|
-
const choice = await ctx.ui.select("Stored Search Results", options);
|
|
2309
|
-
if (!choice) return;
|
|
2310
|
-
|
|
2311
|
-
const match = choice.match(/^\[([a-z0-9]+)\]/);
|
|
2312
|
-
if (!match) return;
|
|
2313
|
-
|
|
2314
|
-
const selected = results.find((r) => r.id.startsWith(match[1]));
|
|
2315
|
-
if (!selected) return;
|
|
2316
|
-
|
|
2317
|
-
const actions = ["View details", "Delete"];
|
|
2318
|
-
const action = await ctx.ui.select(`Result ${selected.id.slice(0, 6)}`, actions);
|
|
2319
|
-
|
|
2320
|
-
if (action === "Delete") {
|
|
2321
|
-
deleteResult(selected.id);
|
|
2322
|
-
ctx.ui.notify(`Deleted ${selected.id.slice(0, 6)}`, "info");
|
|
2323
|
-
} else if (action === "View details") {
|
|
2324
|
-
let info = `ID: ${selected.id}\nType: ${selected.type}\nAge: ${Math.floor((Date.now() - selected.timestamp) / 60000)}m\n\n`;
|
|
2325
|
-
if (selected.type === "search" && selected.queries) {
|
|
2326
|
-
info += "Queries:\n";
|
|
2327
|
-
const queries = selected.queries.slice(0, 10);
|
|
2328
|
-
for (const q of queries) {
|
|
2329
|
-
info += `- "${q.query}" (${q.results.length} results)\n`;
|
|
2330
|
-
}
|
|
2331
|
-
if (selected.queries.length > 10) {
|
|
2332
|
-
info += `... and ${selected.queries.length - 10} more\n`;
|
|
2333
|
-
}
|
|
2334
|
-
}
|
|
2335
|
-
if (selected.type === "fetch" && selected.urls) {
|
|
2336
|
-
info += "URLs:\n";
|
|
2337
|
-
const urls = selected.urls.slice(0, 10);
|
|
2338
|
-
for (const u of urls) {
|
|
2339
|
-
const urlDisplay = u.url.length > 50 ? u.url.slice(0, 47) + "..." : u.url;
|
|
2340
|
-
info += `- ${urlDisplay} (${u.error || `${u.content.length} chars`})\n`;
|
|
2341
|
-
}
|
|
2342
|
-
if (selected.urls.length > 10) {
|
|
2343
|
-
info += `... and ${selected.urls.length - 10} more\n`;
|
|
2344
|
-
}
|
|
2345
|
-
}
|
|
2346
|
-
ctx.ui.notify(info, "info");
|
|
2347
|
-
}
|
|
2348
|
-
},
|
|
2349
|
-
});
|
|
247
|
+
for (const [name, description] of [
|
|
248
|
+
["websearch", "Configure web search"],
|
|
249
|
+
["curator", "Configure web search curator"],
|
|
250
|
+
["google-account", "Show the active Google account for Gemini Web"],
|
|
251
|
+
["search", "Browse stored web search results"],
|
|
252
|
+
] as const) {
|
|
253
|
+
pi.registerCommand(name, {
|
|
254
|
+
description,
|
|
255
|
+
handler: (args, ctx) => runHeavyCommand(loadHeavy, name, args, ctx),
|
|
256
|
+
});
|
|
257
|
+
}
|
|
2350
258
|
}
|