pi-web-providers 3.2.0 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +24 -3
  2. package/dist/index.js +1697 -626
  3. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -1,25 +1,25 @@
1
1
  // src/index.ts
2
- import { randomUUID } from "node:crypto";
3
- import { mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
2
+ import { mkdir as mkdir3, writeFile as writeFile3 } from "node:fs/promises";
4
3
  import { tmpdir } from "node:os";
5
- import { basename, dirname as dirname2, join as join2, relative } from "node:path";
4
+ import { basename, join as join3 } from "node:path";
6
5
  import {
6
+ copyToClipboard,
7
7
  DEFAULT_MAX_BYTES,
8
8
  DEFAULT_MAX_LINES,
9
9
  formatSize as formatSize2,
10
- getMarkdownTheme,
10
+ getMarkdownTheme as getMarkdownTheme2,
11
11
  truncateHead
12
12
  } from "@earendil-works/pi-coding-agent";
13
13
  import {
14
14
  Box,
15
15
  Editor,
16
- getKeybindings,
17
- Key,
18
- Markdown,
19
- matchesKey,
16
+ getKeybindings as getKeybindings2,
17
+ Key as Key2,
18
+ Markdown as Markdown2,
19
+ matchesKey as matchesKey2,
20
20
  Text,
21
- truncateToWidth,
22
- visibleWidth,
21
+ truncateToWidth as truncateToWidth2,
22
+ visibleWidth as visibleWidth2,
23
23
  wrapTextWithAnsi
24
24
  } from "@earendil-works/pi-tui";
25
25
  import { Type as Type16 } from "typebox";
@@ -2454,39 +2454,39 @@ function createDefaultExecutionSettings(overrides = {}) {
2454
2454
  function normalizeDiagnosticDetail(detail) {
2455
2455
  return detail.trim().replace(/[.\s]+$/u, "");
2456
2456
  }
2457
- function startsWithProviderLabel(providerLabel, detail) {
2458
- return detail.toLowerCase().startsWith(providerLabel.toLowerCase());
2457
+ function startsWithProviderLabel(providerLabel2, detail) {
2458
+ return detail.toLowerCase().startsWith(providerLabel2.toLowerCase());
2459
2459
  }
2460
2460
  function readsLikeProviderClause(detail) {
2461
2461
  return /^(is|has|was|returned|did|does|could|cannot|must|should|search\b|contents\b|answer\b|research\b|output\b|response\b|result\b|query\b|no\b|missing\b|deep research\b)/iu.test(
2462
2462
  detail
2463
2463
  );
2464
2464
  }
2465
- function formatProviderDiagnostic(providerLabel, detail) {
2465
+ function formatProviderDiagnostic(providerLabel2, detail) {
2466
2466
  const normalized = normalizeDiagnosticDetail(detail);
2467
2467
  if (!normalized) {
2468
- return `${providerLabel} failed.`;
2468
+ return `${providerLabel2} failed.`;
2469
2469
  }
2470
- if (startsWithProviderLabel(providerLabel, normalized)) {
2470
+ if (startsWithProviderLabel(providerLabel2, normalized)) {
2471
2471
  return `${normalized}.`;
2472
2472
  }
2473
2473
  if (readsLikeProviderClause(normalized)) {
2474
- return `${providerLabel} ${normalized}.`;
2474
+ return `${providerLabel2} ${normalized}.`;
2475
2475
  }
2476
- return `${providerLabel}: ${normalized}.`;
2476
+ return `${providerLabel2}: ${normalized}.`;
2477
2477
  }
2478
- function formatResearchTerminalDiagnostic(providerLabel, status, detail) {
2478
+ function formatResearchTerminalDiagnostic(providerLabel2, status, detail) {
2479
2479
  const normalized = detail ? normalizeDiagnosticDetail(detail) : "";
2480
2480
  if (!normalized) {
2481
- return status === "cancelled" ? `${providerLabel} research was canceled.` : `${providerLabel} research failed.`;
2481
+ return status === "cancelled" ? `${providerLabel2} research was canceled.` : `${providerLabel2} research failed.`;
2482
2482
  }
2483
- if (startsWithProviderLabel(providerLabel, normalized)) {
2483
+ if (startsWithProviderLabel(providerLabel2, normalized)) {
2484
2484
  return `${normalized}.`;
2485
2485
  }
2486
2486
  if (/^research\b/iu.test(normalized)) {
2487
- return `${providerLabel} ${normalized}.`;
2487
+ return `${providerLabel2} ${normalized}.`;
2488
2488
  }
2489
- return status === "cancelled" ? `${providerLabel} research was canceled: ${normalized}.` : `${providerLabel} research failed: ${normalized}.`;
2489
+ return status === "cancelled" ? `${providerLabel2} research was canceled: ${normalized}.` : `${providerLabel2} research failed: ${normalized}.`;
2490
2490
  }
2491
2491
 
2492
2492
  // src/execution-policy.ts
@@ -2532,7 +2532,7 @@ async function runWithExecutionPolicy(label, operation, settings, context) {
2532
2532
  throw new Error(`${label} failed.`);
2533
2533
  }
2534
2534
  async function executeAsyncResearch({
2535
- providerLabel,
2535
+ providerLabel: providerLabel2,
2536
2536
  providerId,
2537
2537
  context,
2538
2538
  pollIntervalMs = DEFAULT_RESEARCH_POLL_INTERVAL_MS,
@@ -2541,7 +2541,7 @@ async function executeAsyncResearch({
2541
2541
  start,
2542
2542
  poll
2543
2543
  }) {
2544
- const timeoutMessage = `${providerLabel} research exceeded ${formatDuration(timeoutMs)}.`;
2544
+ const timeoutMessage = `${providerLabel2} research exceeded ${formatDuration(timeoutMs)}.`;
2545
2545
  const deadline = createDeadlineSignal(
2546
2546
  context.signal,
2547
2547
  timeoutMs,
@@ -2555,7 +2555,7 @@ async function executeAsyncResearch({
2555
2555
  let lastProgressStatus;
2556
2556
  const startedAt = Date.now();
2557
2557
  try {
2558
- researchContext.onProgress?.(`Starting research via ${providerLabel}`);
2558
+ researchContext.onProgress?.(`Starting research via ${providerLabel2}`);
2559
2559
  const job = await withAbortAndOptionalTimeout(
2560
2560
  start(researchContext),
2561
2561
  void 0,
@@ -2564,14 +2564,14 @@ async function executeAsyncResearch({
2564
2564
  );
2565
2565
  const jobId = job.id;
2566
2566
  if (!jobId) {
2567
- throw new Error(`${providerLabel} research did not return a job id.`);
2567
+ throw new Error(`${providerLabel2} research did not return a job id.`);
2568
2568
  }
2569
- researchContext.onProgress?.(`${providerLabel} research started: ${jobId}`);
2569
+ researchContext.onProgress?.(`${providerLabel2} research started: ${jobId}`);
2570
2570
  let consecutivePollErrors = 0;
2571
2571
  while (true) {
2572
2572
  throwIfAborted(
2573
2573
  researchContext.signal,
2574
- `${providerLabel} research aborted.`
2574
+ `${providerLabel2} research aborted.`
2575
2575
  );
2576
2576
  try {
2577
2577
  const result = await withAbortAndOptionalTimeout(
@@ -2584,7 +2584,7 @@ async function executeAsyncResearch({
2584
2584
  const progressStatus = result.statusText ?? result.status;
2585
2585
  if (result.status !== lastStatus || progressStatus !== lastProgressStatus) {
2586
2586
  researchContext.onProgress?.(
2587
- `Research via ${providerLabel}: ${progressStatus} (${formatElapsed(Date.now() - startedAt)} elapsed)`
2587
+ `Research via ${providerLabel2}: ${progressStatus} (${formatElapsed(Date.now() - startedAt)} elapsed)`
2588
2588
  );
2589
2589
  lastStatus = result.status;
2590
2590
  lastProgressStatus = progressStatus;
@@ -2592,13 +2592,13 @@ async function executeAsyncResearch({
2592
2592
  if (result.status === "completed") {
2593
2593
  return result.output ?? {
2594
2594
  provider: providerId,
2595
- text: `${providerLabel} research completed without textual output.`
2595
+ text: `${providerLabel2} research completed without textual output.`
2596
2596
  };
2597
2597
  }
2598
2598
  if (result.status === "failed" || result.status === "cancelled") {
2599
2599
  throw new Error(
2600
2600
  formatResearchTerminalDiagnostic(
2601
- providerLabel,
2601
+ providerLabel2,
2602
2602
  result.status,
2603
2603
  result.error
2604
2604
  )
@@ -2614,11 +2614,11 @@ async function executeAsyncResearch({
2614
2614
  consecutivePollErrors += 1;
2615
2615
  if (consecutivePollErrors >= maxConsecutivePollErrors) {
2616
2616
  throw new Error(
2617
- `${providerLabel} research polling failed too many times in a row: ${formatErrorMessage(error)}`
2617
+ `${providerLabel2} research polling failed too many times in a row: ${formatErrorMessage(error)}`
2618
2618
  );
2619
2619
  }
2620
2620
  researchContext.onProgress?.(
2621
- `${providerLabel} research poll is still retrying after transient errors (${consecutivePollErrors}/${maxConsecutivePollErrors} consecutive poll failures). Background job id: ${jobId}`
2621
+ `${providerLabel2} research poll is still retrying after transient errors (${consecutivePollErrors}/${maxConsecutivePollErrors} consecutive poll failures). Background job id: ${jobId}`
2622
2622
  );
2623
2623
  }
2624
2624
  await sleep(pollIntervalMs, researchContext.signal);
@@ -2626,11 +2626,11 @@ async function executeAsyncResearch({
2626
2626
  } catch (error) {
2627
2627
  if (isAbortErrorFromSignal(researchContext.signal, error)) {
2628
2628
  throw new Error(
2629
- formatProviderDiagnostic(providerLabel, formatErrorMessage(error))
2629
+ formatProviderDiagnostic(providerLabel2, formatErrorMessage(error))
2630
2630
  );
2631
2631
  }
2632
2632
  throw new Error(
2633
- formatProviderDiagnostic(providerLabel, formatErrorMessage(error))
2633
+ formatProviderDiagnostic(providerLabel2, formatErrorMessage(error))
2634
2634
  );
2635
2635
  } finally {
2636
2636
  deadline.cleanup();
@@ -3097,6 +3097,8 @@ var exaProvider = defineProvider({
3097
3097
  import FirecrawlClient from "@mendable/firecrawl-js";
3098
3098
  import { Type as Type7 } from "typebox";
3099
3099
  var FIRECRAWL_CLOUD_HOST = "api.firecrawl.dev";
3100
+ var FIRECRAWL_DEFAULT_API_URL = "https://api.firecrawl.dev";
3101
+ var FIRECRAWL_QUESTION_LIMIT = 1e4;
3100
3102
  var firecrawlSearchOptionsSchema = Type7.Object(
3101
3103
  {
3102
3104
  lang: Type7.Optional(
@@ -3203,6 +3205,55 @@ var firecrawlScrapeOptionsSchema = Type7.Object(
3203
3205
  },
3204
3206
  { description: "Firecrawl scrape options." }
3205
3207
  );
3208
+ var firecrawlAnswerOptionsSchema = Type7.Object(
3209
+ {
3210
+ url: Type7.String({
3211
+ minLength: 1,
3212
+ description: "URL of the page to ask about."
3213
+ }),
3214
+ onlyMainContent: Type7.Optional(
3215
+ Type7.Boolean({ description: "Extract only the main content." })
3216
+ ),
3217
+ includeTags: Type7.Optional(
3218
+ Type7.Array(Type7.String(), { description: "CSS selectors to include." })
3219
+ ),
3220
+ excludeTags: Type7.Optional(
3221
+ Type7.Array(Type7.String(), { description: "CSS selectors to exclude." })
3222
+ ),
3223
+ waitFor: Type7.Optional(
3224
+ Type7.Integer({
3225
+ minimum: 0,
3226
+ description: "Milliseconds to wait before scraping."
3227
+ })
3228
+ ),
3229
+ headers: Type7.Optional(
3230
+ Type7.Record(Type7.String(), Type7.String(), {
3231
+ description: "Headers to send when scraping."
3232
+ })
3233
+ ),
3234
+ location: Type7.Optional(
3235
+ Type7.Object(
3236
+ {
3237
+ country: Type7.Optional(Type7.String({ description: "Country hint." })),
3238
+ region: Type7.Optional(Type7.String({ description: "Region hint." })),
3239
+ city: Type7.Optional(Type7.String({ description: "City hint." }))
3240
+ },
3241
+ { description: "Location hint for scraping." }
3242
+ )
3243
+ ),
3244
+ mobile: Type7.Optional(
3245
+ Type7.Boolean({ description: "Use a mobile browser profile." })
3246
+ ),
3247
+ proxy: Type7.Optional(
3248
+ Type7.String({
3249
+ description: "Proxy mode passed through to Firecrawl."
3250
+ })
3251
+ )
3252
+ },
3253
+ {
3254
+ description: "Firecrawl page-question options. The URL is required; the question comes from the web_answer query."
3255
+ }
3256
+ );
3206
3257
  var firecrawlImplementation = {
3207
3258
  id: "firecrawl",
3208
3259
  label: "Firecrawl",
@@ -3213,6 +3264,8 @@ var firecrawlImplementation = {
3213
3264
  return firecrawlSearchOptionsSchema;
3214
3265
  case "contents":
3215
3266
  return firecrawlScrapeOptionsSchema;
3267
+ case "answer":
3268
+ return firecrawlAnswerOptionsSchema;
3216
3269
  default:
3217
3270
  return void 0;
3218
3271
  }
@@ -3279,6 +3332,34 @@ var firecrawlImplementation = {
3279
3332
  })
3280
3333
  )
3281
3334
  };
3335
+ },
3336
+ async answer(query2, config, _context, options) {
3337
+ const question = validateQuestion(query2);
3338
+ const defaults = asJsonObject(config.options?.scrape);
3339
+ const answerDefaults = asJsonObject(config.options?.answer);
3340
+ const mergedOptions = {
3341
+ onlyMainContent: true,
3342
+ ...defaults,
3343
+ ...answerDefaults,
3344
+ ...options ?? {}
3345
+ };
3346
+ const url2 = validateUrl(mergedOptions.url);
3347
+ const scrapeOptions = stripAnswerOnlyOptions(mergedOptions);
3348
+ const response = await scrapeQuestion(config, url2, question, scrapeOptions);
3349
+ const document = getFirecrawlDocument(response);
3350
+ const answer = readString2(document.answer);
3351
+ if (!answer?.trim()) {
3352
+ throw new Error("No answer returned for this URL.");
3353
+ }
3354
+ return {
3355
+ provider: firecrawlImplementation.id,
3356
+ text: answer.trim(),
3357
+ itemCount: 1,
3358
+ metadata: {
3359
+ url: url2,
3360
+ ...asRecord(document.metadata) ? { metadata: document.metadata } : {}
3361
+ }
3362
+ };
3282
3363
  }
3283
3364
  };
3284
3365
  function createClient3(config) {
@@ -3302,6 +3383,88 @@ function getFirecrawlCapabilityStatus(config, options) {
3302
3383
  function isFirecrawlCloudApiUrl(apiUrl) {
3303
3384
  return !apiUrl || apiUrl.includes(FIRECRAWL_CLOUD_HOST);
3304
3385
  }
3386
+ function validateQuestion(query2) {
3387
+ const question = query2.trim();
3388
+ if (!question) {
3389
+ throw new Error("question must be a non-empty string.");
3390
+ }
3391
+ if (question.length > FIRECRAWL_QUESTION_LIMIT) {
3392
+ throw new Error(
3393
+ `Firecrawl question must be at most ${FIRECRAWL_QUESTION_LIMIT} characters.`
3394
+ );
3395
+ }
3396
+ return question;
3397
+ }
3398
+ function validateUrl(value) {
3399
+ if (typeof value !== "string" || !value.trim()) {
3400
+ throw new Error("Firecrawl answer requires options.url.");
3401
+ }
3402
+ return value.trim();
3403
+ }
3404
+ function stripAnswerOnlyOptions(options) {
3405
+ const { url: _url, formats: _formats, ...scrapeOptions } = options;
3406
+ return scrapeOptions;
3407
+ }
3408
+ async function scrapeQuestion(config, url2, question, options) {
3409
+ const apiUrl = resolveConfigValue(config.baseUrl) ?? FIRECRAWL_DEFAULT_API_URL;
3410
+ const apiKey = resolveConfigValue(config.credentials?.api);
3411
+ if (isFirecrawlCloudApiUrl(apiUrl) && !apiKey) {
3412
+ throw new Error("is missing an API key");
3413
+ }
3414
+ const response = await fetch(joinUrl(apiUrl, "/v2/scrape"), {
3415
+ method: "POST",
3416
+ headers: {
3417
+ "Content-Type": "application/json",
3418
+ ...apiKey ? { Authorization: `Bearer ${apiKey}` } : {}
3419
+ },
3420
+ body: JSON.stringify({
3421
+ ...options,
3422
+ url: url2,
3423
+ formats: [{ type: "question", question }]
3424
+ })
3425
+ });
3426
+ const payload = await readJsonResponse(response);
3427
+ if (!response.ok) {
3428
+ throw new Error(readFirecrawlError(payload, response.statusText));
3429
+ }
3430
+ if (isFirecrawlFailure(payload)) {
3431
+ throw new Error(readFirecrawlError(payload, "Firecrawl scrape failed."));
3432
+ }
3433
+ return payload;
3434
+ }
3435
+ function joinUrl(baseUrl, path) {
3436
+ return `${baseUrl.replace(/\/+$/g, "")}/${path.replace(/^\/+/g, "")}`;
3437
+ }
3438
+ async function readJsonResponse(response) {
3439
+ const text = await response.text();
3440
+ if (!text) {
3441
+ return {};
3442
+ }
3443
+ try {
3444
+ return JSON.parse(text);
3445
+ } catch {
3446
+ return text;
3447
+ }
3448
+ }
3449
+ function isFirecrawlFailure(value) {
3450
+ const record = asRecord(value);
3451
+ return record?.success === false || record?.error !== void 0;
3452
+ }
3453
+ function readFirecrawlError(value, fallback) {
3454
+ const record = asRecord(value);
3455
+ return readString2(record?.error) ?? readString2(record?.message) ?? (typeof value === "string" ? value : void 0) ?? fallback;
3456
+ }
3457
+ function getFirecrawlDocument(value) {
3458
+ const record = asRecord(value);
3459
+ const data = asRecord(record?.data);
3460
+ if (data) {
3461
+ return data;
3462
+ }
3463
+ if (record) {
3464
+ return record;
3465
+ }
3466
+ throw new Error(`Unexpected Firecrawl response: ${formatJson(value)}`);
3467
+ }
3305
3468
  function flattenSearchResults(response) {
3306
3469
  return ["web", "news", "images"].flatMap(
3307
3470
  (source) => (response[source] ?? []).map((entry) => toSearchResult(source, entry)).filter((entry) => entry !== null)
@@ -3389,6 +3552,21 @@ var firecrawlProvider = defineProvider({
3389
3552
  input.options
3390
3553
  );
3391
3554
  }
3555
+ }),
3556
+ answer: defineCapability({
3557
+ options: firecrawlImplementation.getToolOptionsSchema?.("answer"),
3558
+ promptGuidelines: [
3559
+ "Firecrawl web_answer is page-scoped: set options.url to the specific page URL to ask about.",
3560
+ "Do not use Firecrawl web_answer for general multi-source answers; use web_search plus web_contents or web_research instead."
3561
+ ],
3562
+ async execute(input, ctx) {
3563
+ return await firecrawlImplementation.answer(
3564
+ input.query,
3565
+ ctx.config,
3566
+ ctx,
3567
+ input.options
3568
+ );
3569
+ }
3392
3570
  })
3393
3571
  }
3394
3572
  });
@@ -3535,7 +3713,7 @@ var geminiImplementation = {
3535
3713
  context.signal
3536
3714
  );
3537
3715
  const results = await Promise.all(
3538
- extractGoogleSearchResults(interaction.outputs).slice(0, maxResults).map(async (result) => {
3716
+ extractGoogleSearchResults(readInteractionSteps(interaction)).slice(0, maxResults).map(async (result) => {
3539
3717
  const resolvedUrl = await resolveGoogleSearchUrl(
3540
3718
  result.url,
3541
3719
  context.signal
@@ -3622,7 +3800,7 @@ var geminiImplementation = {
3622
3800
  );
3623
3801
  const status = readNonEmptyString3(interaction.status) ?? "unknown";
3624
3802
  if (status === "completed") {
3625
- const text = formatInteractionOutputs(interaction.outputs);
3803
+ const text = formatInteractionSteps(readInteractionSteps(interaction));
3626
3804
  return {
3627
3805
  status: "completed",
3628
3806
  output: {
@@ -3652,7 +3830,7 @@ var geminiImplementation = {
3652
3830
  if (status === "requires_action") {
3653
3831
  return {
3654
3832
  status: "failed",
3655
- error: describeGeminiRequiredAction(interaction.outputs)
3833
+ error: describeGeminiRequiredAction(readInteractionSteps(interaction))
3656
3834
  };
3657
3835
  }
3658
3836
  return status === "in_progress" ? { status: "in_progress" } : { status: "in_progress", statusText: status };
@@ -3686,17 +3864,20 @@ function addAbortSignalToGeminiConfig(config, signal) {
3686
3864
  abortSignal: signal
3687
3865
  };
3688
3866
  }
3689
- function extractGoogleSearchResults(outputs) {
3867
+ function readInteractionSteps(interaction) {
3868
+ return typeof interaction === "object" && interaction !== null ? interaction.steps : void 0;
3869
+ }
3870
+ function extractGoogleSearchResults(steps) {
3690
3871
  const seen = /* @__PURE__ */ new Set();
3691
3872
  const results = [];
3692
- if (!Array.isArray(outputs)) {
3873
+ if (!Array.isArray(steps)) {
3693
3874
  return results;
3694
3875
  }
3695
- for (const output of outputs) {
3696
- if (typeof output !== "object" || output === null) {
3876
+ for (const step of steps) {
3877
+ if (typeof step !== "object" || step === null) {
3697
3878
  continue;
3698
3879
  }
3699
- const content = output;
3880
+ const content = step;
3700
3881
  if (content.type !== "google_search_result") {
3701
3882
  continue;
3702
3883
  }
@@ -3862,16 +4043,21 @@ function extractGroundingSources(chunks) {
3862
4043
  }
3863
4044
  return sources;
3864
4045
  }
3865
- function formatInteractionOutputs(outputs) {
4046
+ function formatInteractionSteps(steps) {
3866
4047
  const lines = [];
3867
- if (!Array.isArray(outputs)) {
4048
+ if (!Array.isArray(steps)) {
3868
4049
  return "";
3869
4050
  }
3870
- for (const output of outputs) {
3871
- if (typeof output === "object" && output !== null && "type" in output && output.type === "text" && "text" in output && typeof output.text === "string") {
3872
- const text = output.text.trim();
3873
- if (text) {
3874
- lines.push(text);
4051
+ for (const step of steps) {
4052
+ if (typeof step !== "object" || step === null || !("type" in step) || step.type !== "model_output" || !("content" in step) || !Array.isArray(step.content)) {
4053
+ continue;
4054
+ }
4055
+ for (const part of step.content) {
4056
+ if (typeof part === "object" && part !== null && "type" in part && part.type === "text" && "text" in part && typeof part.text === "string") {
4057
+ const text = part.text.trim();
4058
+ if (text) {
4059
+ lines.push(text);
4060
+ }
3875
4061
  }
3876
4062
  }
3877
4063
  }
@@ -4066,14 +4252,12 @@ function buildGeminiGenerateContentRequest({
4066
4252
  }
4067
4253
  };
4068
4254
  }
4069
- function describeGeminiRequiredAction(outputs) {
4070
- if (!Array.isArray(outputs) || outputs.length === 0) {
4255
+ function describeGeminiRequiredAction(steps) {
4256
+ if (!Array.isArray(steps) || steps.length === 0) {
4071
4257
  return "research requires additional action";
4072
4258
  }
4073
- const firstOutput = outputs.find(
4074
- (value) => typeof value === "object" && value !== null
4075
- );
4076
- const type = readNonEmptyString3(firstOutput?.type);
4259
+ const lastStep = [...steps].reverse().find((value) => typeof value === "object" && value !== null);
4260
+ const type = readNonEmptyString3(lastStep?.type);
4077
4261
  if (!type) {
4078
4262
  return "research requires additional action";
4079
4263
  }
@@ -6040,7 +6224,7 @@ var serperImplementation = {
6040
6224
  requestOptions
6041
6225
  );
6042
6226
  const response = await fetch(
6043
- joinUrl(resolveConfigValue(config.baseUrl), requestOptions.mode),
6227
+ joinUrl2(resolveConfigValue(config.baseUrl), requestOptions.mode),
6044
6228
  {
6045
6229
  method: "POST",
6046
6230
  headers: {
@@ -6075,7 +6259,7 @@ var serperImplementation = {
6075
6259
  };
6076
6260
  }
6077
6261
  };
6078
- function joinUrl(baseUrl, mode = "search") {
6262
+ function joinUrl2(baseUrl, mode = "search") {
6079
6263
  const base2 = (baseUrl ?? DEFAULT_BASE_URL3).replace(/\/+$/, "");
6080
6264
  if (mode === "webpage" && base2 === DEFAULT_BASE_URL3) {
6081
6265
  return DEFAULT_SCRAPE_URL;
@@ -7671,45 +7855,6 @@ ${JSON.stringify(value, null, 2).trim()}
7671
7855
  \`\`\``;
7672
7856
  }
7673
7857
 
7674
- // src/options.ts
7675
- function buildToolOptionsSchema(_capability, providerSchema) {
7676
- if (!providerSchema || Object.keys(providerSchema.properties).length === 0) {
7677
- return void 0;
7678
- }
7679
- return closeObjectSchemas(providerSchema);
7680
- }
7681
- function closeObjectSchemas(schema) {
7682
- if (!isSchemaRecord(schema)) {
7683
- return schema;
7684
- }
7685
- const properties = isSchemaRecord(schema.properties) ? Object.fromEntries(
7686
- Object.entries(schema.properties).map(([key, value]) => [
7687
- key,
7688
- closeObjectSchemas(value)
7689
- ])
7690
- ) : schema.properties;
7691
- const items = isSchemaRecord(schema.items) ? closeObjectSchemas(schema.items) : Array.isArray(schema.items) ? schema.items.map((item) => closeObjectSchemas(item)) : schema.items;
7692
- return {
7693
- ...schema,
7694
- ...properties ? { properties } : {},
7695
- ...items ? { items } : {},
7696
- ...mapSchemaArray(schema, "anyOf"),
7697
- ...mapSchemaArray(schema, "oneOf"),
7698
- ...mapSchemaArray(schema, "allOf"),
7699
- ...schema.type === "object" && isSchemaRecord(schema.properties) ? { additionalProperties: false } : {}
7700
- };
7701
- }
7702
- function mapSchemaArray(schema, key) {
7703
- const value = schema[key];
7704
- return Array.isArray(value) ? { [key]: value.map((entry) => closeObjectSchemas(entry)) } : {};
7705
- }
7706
- function isSchemaRecord(value) {
7707
- return typeof value === "object" && value !== null && !Array.isArray(value);
7708
- }
7709
-
7710
- // src/prefetch-manager.ts
7711
- import { createHash } from "node:crypto";
7712
-
7713
7858
  // src/provider-resolution.ts
7714
7859
  function supportsTool2(provider, tool) {
7715
7860
  return provider.capabilities[tool] !== void 0;
@@ -7851,58 +7996,192 @@ function resolveProviderForTool(config, cwd, tool, explicit) {
7851
7996
  return provider;
7852
7997
  }
7853
7998
 
7854
- // src/provider-runtime.ts
7855
- async function executeProviderRequest(provider, config, request, context) {
7856
- return await executeProviderExecution(
7999
+ // src/managed-tools.ts
8000
+ var CAPABILITY_TOOL_NAMES = {
8001
+ search: "web_search",
8002
+ contents: "web_contents",
8003
+ answer: "web_answer",
8004
+ research: "web_research"
8005
+ };
8006
+ var MANAGED_TOOL_NAMES = Object.values(CAPABILITY_TOOL_NAMES);
8007
+ function getAvailableProviderIdsForCapability(config, cwd, capability) {
8008
+ const providerId = getMappedProviderIdForTool(config, capability);
8009
+ if (!providerId) {
8010
+ return [];
8011
+ }
8012
+ const provider = PROVIDERS_BY_ID[providerId];
8013
+ if (!supportsTool2(provider, capability)) {
8014
+ return [];
8015
+ }
8016
+ const status = getProviderCapabilityStatus(
8017
+ config,
8018
+ cwd,
8019
+ providerId,
8020
+ capability,
7857
8021
  {
7858
- capability: request.capability,
7859
- providerLabel: provider.label,
7860
- settings: config.settings,
7861
- execute: (executionContext) => executeProviderCapability(
7862
- provider,
7863
- request.capability,
7864
- providerInputFromRequest(request),
7865
- {
7866
- ...executionContext,
7867
- config
7868
- }
7869
- )
7870
- },
7871
- context
8022
+ resolveSecrets: false
8023
+ }
7872
8024
  );
8025
+ return isProviderCapabilityExposable(status) ? [providerId] : [];
7873
8026
  }
7874
- async function executeProviderExecution(execution, context) {
7875
- if (execution.capability === "research") {
7876
- const deadline = createResearchDeadlineSignal(
7877
- context.signal,
7878
- execution.providerLabel,
7879
- execution.settings?.researchTimeoutMs
7880
- );
7881
- try {
7882
- const researchContext = deadline ? { ...context, signal: deadline.signal } : context;
7883
- return await withAbortSignal(
7884
- execution.execute(researchContext),
7885
- researchContext.signal
7886
- );
7887
- } catch (error) {
7888
- throw new Error(
7889
- formatProviderDiagnostic(
7890
- execution.providerLabel,
7891
- formatErrorMessage(error)
7892
- )
7893
- );
7894
- } finally {
7895
- deadline?.cleanup();
8027
+ function getProviderStatusForTool(config, cwd, providerId, capability) {
8028
+ return getProviderCapabilityStatus(config, cwd, providerId, capability);
8029
+ }
8030
+ function getAvailableManagedToolNames(config, cwd) {
8031
+ return Object.keys(CAPABILITY_TOOL_NAMES).filter(
8032
+ (capability) => getAvailableProviderIdsForCapability(config, cwd, capability).length > 0
8033
+ ).map((capability) => CAPABILITY_TOOL_NAMES[capability]);
8034
+ }
8035
+ function getSyncedActiveTools(config, cwd, activeToolNames, options) {
8036
+ const availableToolNames = new Set(getAvailableManagedToolNames(config, cwd));
8037
+ const nextActiveTools = new Set(activeToolNames);
8038
+ for (const toolName of MANAGED_TOOL_NAMES) {
8039
+ if (availableToolNames.has(toolName)) {
8040
+ if (options.addAvailable) {
8041
+ nextActiveTools.add(toolName);
8042
+ }
8043
+ continue;
7896
8044
  }
8045
+ nextActiveTools.delete(toolName);
7897
8046
  }
7898
- const requestPolicy = resolveExecutionPolicy(execution.settings);
8047
+ return nextActiveTools;
8048
+ }
8049
+ async function refreshManagedTools(pi, registerManagedTools2, cwd, options) {
8050
+ const config = await loadConfig();
8051
+ const nextActiveTools = getSyncedActiveTools(
8052
+ config,
8053
+ cwd,
8054
+ pi.getActiveTools(),
8055
+ options
8056
+ );
8057
+ registerManagedTools2({
8058
+ search: getAvailableProviderIdsForCapability(config, cwd, "search"),
8059
+ contents: getAvailableProviderIdsForCapability(config, cwd, "contents"),
8060
+ answer: getAvailableProviderIdsForCapability(config, cwd, "answer"),
8061
+ research: getAvailableProviderIdsForCapability(config, cwd, "research")
8062
+ });
8063
+ await syncManagedToolAvailability(pi, nextActiveTools);
8064
+ }
8065
+ async function refreshManagedToolsOnStartup(pi, registerManagedTools2, cwd, options) {
7899
8066
  try {
7900
- return await runWithExecutionPolicy(
7901
- `${execution.providerLabel} ${execution.capability} request`,
7902
- execution.execute,
7903
- requestPolicy,
7904
- context
7905
- );
8067
+ await refreshManagedTools(pi, registerManagedTools2, cwd, options);
8068
+ } catch (error) {
8069
+ pi.sendMessage({
8070
+ customType: "web-providers-config-error",
8071
+ content: formatStartupConfigError(error),
8072
+ display: true
8073
+ });
8074
+ await syncManagedToolAvailability(
8075
+ pi,
8076
+ new Set(
8077
+ pi.getActiveTools().filter((toolName) => !MANAGED_TOOL_NAMES.includes(toolName))
8078
+ )
8079
+ );
8080
+ }
8081
+ }
8082
+ function formatStartupConfigError(error) {
8083
+ const detail = error instanceof Error ? error.message : String(error);
8084
+ return `web-providers config error: ${detail.replace(getConfigPath(), "~/.pi/agent/web-providers.json")}`;
8085
+ }
8086
+ async function syncManagedToolAvailability(pi, nextActiveTools) {
8087
+ const activeTools = pi.getActiveTools();
8088
+ const changed = activeTools.length !== nextActiveTools.size || activeTools.some((toolName) => !nextActiveTools.has(toolName));
8089
+ if (changed) {
8090
+ pi.setActiveTools(Array.from(nextActiveTools));
8091
+ }
8092
+ }
8093
+
8094
+ // src/options.ts
8095
+ function buildToolOptionsSchema(_capability, providerSchema) {
8096
+ if (!providerSchema || Object.keys(providerSchema.properties).length === 0) {
8097
+ return void 0;
8098
+ }
8099
+ return closeObjectSchemas(providerSchema);
8100
+ }
8101
+ function closeObjectSchemas(schema) {
8102
+ if (!isSchemaRecord(schema)) {
8103
+ return schema;
8104
+ }
8105
+ const properties = isSchemaRecord(schema.properties) ? Object.fromEntries(
8106
+ Object.entries(schema.properties).map(([key, value]) => [
8107
+ key,
8108
+ closeObjectSchemas(value)
8109
+ ])
8110
+ ) : schema.properties;
8111
+ const items = isSchemaRecord(schema.items) ? closeObjectSchemas(schema.items) : Array.isArray(schema.items) ? schema.items.map((item) => closeObjectSchemas(item)) : schema.items;
8112
+ return {
8113
+ ...schema,
8114
+ ...properties ? { properties } : {},
8115
+ ...items ? { items } : {},
8116
+ ...mapSchemaArray(schema, "anyOf"),
8117
+ ...mapSchemaArray(schema, "oneOf"),
8118
+ ...mapSchemaArray(schema, "allOf"),
8119
+ ...schema.type === "object" && isSchemaRecord(schema.properties) ? { additionalProperties: false } : {}
8120
+ };
8121
+ }
8122
+ function mapSchemaArray(schema, key) {
8123
+ const value = schema[key];
8124
+ return Array.isArray(value) ? { [key]: value.map((entry) => closeObjectSchemas(entry)) } : {};
8125
+ }
8126
+ function isSchemaRecord(value) {
8127
+ return typeof value === "object" && value !== null && !Array.isArray(value);
8128
+ }
8129
+
8130
+ // src/prefetch-manager.ts
8131
+ import { createHash } from "node:crypto";
8132
+
8133
+ // src/provider-runtime.ts
8134
+ async function executeProviderRequest(provider, config, request, context) {
8135
+ return await executeProviderExecution(
8136
+ {
8137
+ capability: request.capability,
8138
+ providerLabel: provider.label,
8139
+ settings: config.settings,
8140
+ execute: (executionContext) => executeProviderCapability(
8141
+ provider,
8142
+ request.capability,
8143
+ providerInputFromRequest(request),
8144
+ {
8145
+ ...executionContext,
8146
+ config
8147
+ }
8148
+ )
8149
+ },
8150
+ context
8151
+ );
8152
+ }
8153
+ async function executeProviderExecution(execution, context) {
8154
+ if (execution.capability === "research") {
8155
+ const deadline = createResearchDeadlineSignal(
8156
+ context.signal,
8157
+ execution.providerLabel,
8158
+ execution.settings?.researchTimeoutMs
8159
+ );
8160
+ try {
8161
+ const researchContext = deadline ? { ...context, signal: deadline.signal } : context;
8162
+ return await withAbortSignal(
8163
+ execution.execute(researchContext),
8164
+ researchContext.signal
8165
+ );
8166
+ } catch (error) {
8167
+ throw new Error(
8168
+ formatProviderDiagnostic(
8169
+ execution.providerLabel,
8170
+ formatErrorMessage(error)
8171
+ )
8172
+ );
8173
+ } finally {
8174
+ deadline?.cleanup();
8175
+ }
8176
+ }
8177
+ const requestPolicy = resolveExecutionPolicy(execution.settings);
8178
+ try {
8179
+ return await runWithExecutionPolicy(
8180
+ `${execution.providerLabel} ${execution.capability} request`,
8181
+ execution.execute,
8182
+ requestPolicy,
8183
+ context
8184
+ );
7906
8185
  } catch (error) {
7907
8186
  throw new Error(
7908
8187
  formatProviderDiagnostic(
@@ -7944,7 +8223,7 @@ function resolveExecutionPolicy(defaults) {
7944
8223
  retryDelayMs: defaults?.retryDelayMs ?? 2e3
7945
8224
  };
7946
8225
  }
7947
- function createResearchDeadlineSignal(signal, providerLabel, timeoutMs) {
8226
+ function createResearchDeadlineSignal(signal, providerLabel2, timeoutMs) {
7948
8227
  if (timeoutMs === void 0) {
7949
8228
  return void 0;
7950
8229
  }
@@ -7959,7 +8238,7 @@ function createResearchDeadlineSignal(signal, providerLabel, timeoutMs) {
7959
8238
  const timer = setTimeout(() => {
7960
8239
  controller.abort(
7961
8240
  new Error(
7962
- `${providerLabel} research exceeded ${formatDuration(timeoutMs)}.`
8241
+ `${providerLabel2} research exceeded ${formatDuration(timeoutMs)}.`
7963
8242
  )
7964
8243
  );
7965
8244
  }, timeoutMs);
@@ -9614,93 +9893,1107 @@ function getFirstLine(text) {
9614
9893
  return text?.split("\n").map((line) => line.trim()).find((line) => line.length > 0);
9615
9894
  }
9616
9895
 
9617
- // src/index.ts
9618
- var DEFAULT_MAX_RESULTS = 5;
9619
- var MAX_ALLOWED_RESULTS = 20;
9620
- var MAX_SEARCH_QUERIES = 10;
9621
- var RESEARCH_HEARTBEAT_MS = 15e3;
9622
- var WEB_RESEARCH_RESULT_MESSAGE_TYPE = "web-research-result";
9623
- var WEB_RESEARCH_WIDGET_KEY = "web-research-jobs";
9896
+ // src/web-research-lifecycle.ts
9897
+ import { randomUUID } from "node:crypto";
9898
+ import { mkdir as mkdir2, readdir, readFile as readFile2, stat, writeFile as writeFile2 } from "node:fs/promises";
9899
+ import { dirname as dirname2, join as join2 } from "node:path";
9624
9900
  var RESEARCH_ARTIFACTS_DIR = join2(".pi", "artifacts", "research");
9901
+ var MAX_RESEARCH_HISTORY_ITEMS = 20;
9902
+ var RESEARCH_PREVIEW_MAX_BYTES = 5e4;
9903
+ var RESEARCH_REPORT_MAX_BYTES = 2e5;
9625
9904
  var pendingResearchTasks = /* @__PURE__ */ new Set();
9626
- var CAPABILITY_TOOL_NAMES = {
9627
- search: "web_search",
9628
- contents: "web_contents",
9629
- answer: "web_answer",
9630
- research: "web_research"
9631
- };
9632
- var MANAGED_TOOL_NAMES = Object.values(CAPABILITY_TOOL_NAMES);
9633
- var DEFAULT_SUMMARY_SYMBOLS = {
9634
- success: "\u2714",
9635
- failure: "\u2718"
9636
- };
9637
- function webProvidersExtension(pi) {
9638
- const activeWebResearchRequests = /* @__PURE__ */ new Map();
9639
- let latestWidgetContext;
9640
- let webResearchWidgetTimer;
9641
- const stopWebResearchWidgetTimer = () => {
9642
- if (webResearchWidgetTimer) {
9643
- clearInterval(webResearchWidgetTimer);
9644
- webResearchWidgetTimer = void 0;
9645
- }
9646
- };
9647
- const ensureWebResearchWidgetTimer = () => {
9648
- if (webResearchWidgetTimer || activeWebResearchRequests.size === 0) {
9649
- return;
9905
+ async function dispatchWebResearch({
9906
+ activeWebResearchRequests,
9907
+ config,
9908
+ explicitProvider,
9909
+ ctx,
9910
+ options,
9911
+ input,
9912
+ executionOverride,
9913
+ executeResearch,
9914
+ deliverResult,
9915
+ onJobsChanged,
9916
+ resultMessageType
9917
+ }) {
9918
+ await cleanupContentStore();
9919
+ const provider = resolveProviderForTool(
9920
+ config,
9921
+ ctx.cwd,
9922
+ "research",
9923
+ explicitProvider
9924
+ );
9925
+ const request = createWebResearchRequest(ctx.cwd, provider.id, input);
9926
+ const abortController = new AbortController();
9927
+ const task = { request, abortController };
9928
+ const providerConfig = getEffectiveProviderConfig(config, provider.id);
9929
+ activeWebResearchRequests.set(request.id, task);
9930
+ onJobsChanged();
9931
+ trackPendingResearchTask(
9932
+ runDispatchedWebResearch({
9933
+ activeWebResearchRequests,
9934
+ task,
9935
+ config,
9936
+ provider,
9937
+ providerConfig,
9938
+ ctx,
9939
+ options,
9940
+ executionOverride,
9941
+ executeResearch,
9942
+ deliverResult,
9943
+ onJobsChanged,
9944
+ resultMessageType
9945
+ })
9946
+ );
9947
+ return {
9948
+ content: [
9949
+ {
9950
+ type: "text",
9951
+ text: `Started web research via ${provider.label}.`
9952
+ }
9953
+ ],
9954
+ details: request,
9955
+ display: {
9956
+ provider: { id: provider.id, label: provider.label },
9957
+ outcome: { success: "started" }
9650
9958
  }
9651
- webResearchWidgetTimer = setInterval(() => {
9652
- updateWebResearchWidget();
9653
- }, 1e3);
9654
9959
  };
9655
- const updateWebResearchWidget = (ctx) => {
9656
- const widgetContext = ctx ?? latestWidgetContext;
9657
- if (!widgetContext) {
9658
- return;
9659
- }
9660
- latestWidgetContext = widgetContext;
9661
- if (!widgetContext.hasUI) {
9662
- stopWebResearchWidgetTimer();
9663
- return;
9664
- }
9665
- if (activeWebResearchRequests.size === 0) {
9666
- stopWebResearchWidgetTimer();
9667
- widgetContext.ui.setWidget(WEB_RESEARCH_WIDGET_KEY, void 0);
9668
- return;
9960
+ }
9961
+ async function runDispatchedWebResearch({
9962
+ activeWebResearchRequests,
9963
+ task,
9964
+ config,
9965
+ provider,
9966
+ providerConfig,
9967
+ ctx,
9968
+ options,
9969
+ executionOverride,
9970
+ executeResearch,
9971
+ deliverResult,
9972
+ onJobsChanged,
9973
+ resultMessageType
9974
+ }) {
9975
+ const { request, abortController } = task;
9976
+ let result;
9977
+ let reportText = "";
9978
+ try {
9979
+ const response = await executeResearch({
9980
+ config,
9981
+ provider,
9982
+ providerConfig,
9983
+ ctx,
9984
+ signal: abortController.signal,
9985
+ options,
9986
+ input: request.input,
9987
+ onProgress: (message) => {
9988
+ request.progress = summarizeWebResearchProgress(
9989
+ message,
9990
+ provider.label
9991
+ );
9992
+ onJobsChanged();
9993
+ },
9994
+ executionOverride
9995
+ });
9996
+ result = buildWebResearchResult(request, abortController, task, response);
9997
+ if (result.status === "completed") {
9998
+ reportText = response.text;
9669
9999
  }
9670
- ensureWebResearchWidgetTimer();
9671
- widgetContext.ui.setWidget(
9672
- WEB_RESEARCH_WIDGET_KEY,
9673
- buildWebResearchWidgetLines(
9674
- [...activeWebResearchRequests.values()],
9675
- widgetContext.ui.theme
9676
- )
9677
- );
9678
- };
9679
- if ("registerMessageRenderer" in pi) {
9680
- pi.registerMessageRenderer(
9681
- WEB_RESEARCH_RESULT_MESSAGE_TYPE,
9682
- (message, state, theme) => renderWebResearchResultMessage(message, state, theme)
10000
+ } catch (error) {
10001
+ result = buildFailedWebResearchResult(
10002
+ request,
10003
+ abortController,
10004
+ task,
10005
+ error
9683
10006
  );
9684
10007
  }
9685
- pi.registerCommand("web-providers", {
9686
- description: "Configure web search providers",
9687
- handler: async (_args, ctx) => {
9688
- if (!ctx.hasUI) {
9689
- ctx.ui.notify("web-providers requires interactive mode", "error");
9690
- return;
9691
- }
9692
- await runWebProvidersConfig(
9693
- pi,
9694
- { activeWebResearchRequests, updateWebResearchWidget },
9695
- ctx
9696
- );
9697
- }
10008
+ try {
10009
+ await writeWebResearchArtifact(result, reportText);
10010
+ deliverResult({
10011
+ customType: resultMessageType,
10012
+ content: formatWebResearchResultMessage(result, reportText),
10013
+ display: true,
10014
+ details: result
10015
+ });
10016
+ } finally {
10017
+ activeWebResearchRequests.delete(request.id);
10018
+ onJobsChanged();
10019
+ }
10020
+ }
10021
+ function buildWebResearchResult(request, abortController, task, response) {
10022
+ const completedAt = (/* @__PURE__ */ new Date()).toISOString();
10023
+ if (abortController.signal.aborted && task.cancelRequestedAt !== void 0) {
10024
+ return {
10025
+ ...request,
10026
+ status: "cancelled",
10027
+ completedAt,
10028
+ elapsedMs: elapsedMs(request.startedAt, completedAt),
10029
+ error: "web research was cancelled by the user."
10030
+ };
10031
+ }
10032
+ return {
10033
+ ...request,
10034
+ status: "completed",
10035
+ completedAt,
10036
+ elapsedMs: elapsedMs(request.startedAt, completedAt),
10037
+ itemCount: response.itemCount
10038
+ };
10039
+ }
10040
+ function buildFailedWebResearchResult(request, abortController, task, error) {
10041
+ const completedAt = (/* @__PURE__ */ new Date()).toISOString();
10042
+ const cancelled = abortController.signal.aborted && task.cancelRequestedAt !== void 0;
10043
+ return {
10044
+ ...request,
10045
+ status: cancelled ? "cancelled" : "failed",
10046
+ completedAt,
10047
+ elapsedMs: elapsedMs(request.startedAt, completedAt),
10048
+ error: cancelled ? "web research was cancelled by the user." : formatErrorMessage(error)
10049
+ };
10050
+ }
10051
+ function elapsedMs(startedAt, completedAt) {
10052
+ return Math.max(0, Date.parse(completedAt) - Date.parse(startedAt));
10053
+ }
10054
+ function getActiveWebResearchRequests(tasks) {
10055
+ return [...tasks.values()].map((task) => task.request);
10056
+ }
10057
+ function getWebResearchTaskSnapshots(tasks) {
10058
+ return [...tasks.values()].map((task) => ({
10059
+ request: task.request,
10060
+ cancelRequestedAt: task.cancelRequestedAt
10061
+ }));
10062
+ }
10063
+ function cancelWebResearchTask(tasks, id) {
10064
+ const task = tasks.get(id);
10065
+ if (!task || task.abortController.signal.aborted) return false;
10066
+ task.cancelRequestedAt = (/* @__PURE__ */ new Date()).toISOString();
10067
+ task.request.progress = "cancelling";
10068
+ task.abortController.abort(
10069
+ new Error("web research was cancelled by the user.")
10070
+ );
10071
+ return true;
10072
+ }
10073
+ function createWebResearchRequest(cwd, provider, input) {
10074
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
10075
+ return {
10076
+ tool: "web_research",
10077
+ id: randomUUID(),
10078
+ provider,
10079
+ input,
10080
+ outputPath: buildWebResearchArtifactPath(cwd, input, startedAt),
10081
+ startedAt
10082
+ };
10083
+ }
10084
+ function buildWebResearchArtifactPath(cwd, input, startedAt) {
10085
+ const timestamp = startedAt.replaceAll(":", "-").replace(".", "-");
10086
+ const slug = slugifyWebResearchInput(input);
10087
+ return join2(cwd, RESEARCH_ARTIFACTS_DIR, `${timestamp}-${slug}.md`);
10088
+ }
10089
+ function slugifyWebResearchInput(input) {
10090
+ const slug = input.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60).replace(/-+$/g, "");
10091
+ return slug.length > 0 ? slug : "research";
10092
+ }
10093
+ function getWebResearchProgressIcon(request) {
10094
+ if (request.progress === "poll retrying after transient errors") {
10095
+ return "\u27F3";
10096
+ }
10097
+ if (request.progress === "queued" || request.progress === "cancelling") {
10098
+ return "\u25CC";
10099
+ }
10100
+ if (request.progress === "starting") {
10101
+ return "\u25D4";
10102
+ }
10103
+ if (request.progress?.startsWith("started:")) {
10104
+ return "\u25D1";
10105
+ }
10106
+ return "\u25CF";
10107
+ }
10108
+ function summarizeWebResearchProgress(message, providerLabel2) {
10109
+ const startingMessage = `Starting research via ${providerLabel2}`;
10110
+ if (message === startingMessage) {
10111
+ return "starting";
10112
+ }
10113
+ const startedPrefix = `${providerLabel2} research started: `;
10114
+ if (message.startsWith(startedPrefix)) {
10115
+ return `started: ${message.slice(startedPrefix.length)}`;
10116
+ }
10117
+ const statusPrefix = `Research via ${providerLabel2}: `;
10118
+ if (message.startsWith(statusPrefix)) {
10119
+ return message.slice(statusPrefix.length).replace(/\s+\([^)]* elapsed\)$/u, "").trim();
10120
+ }
10121
+ const retryPrefix = `${providerLabel2} research poll is still retrying after transient errors`;
10122
+ if (message.startsWith(retryPrefix)) {
10123
+ return "poll retrying after transient errors";
10124
+ }
10125
+ return message.trim();
10126
+ }
10127
+ function formatWebResearchResultMessage(result, reportText) {
10128
+ const text = reportText.trim();
10129
+ if (text.length > 0) {
10130
+ return `${text}
10131
+ `;
10132
+ }
10133
+ if (result.error) {
10134
+ return `${result.error}
10135
+ `;
10136
+ }
10137
+ return "";
10138
+ }
10139
+ async function writeWebResearchArtifact(result, reportText) {
10140
+ await mkdir2(dirname2(result.outputPath), { recursive: true });
10141
+ await writeFile2(
10142
+ result.outputPath,
10143
+ formatWebResearchArtifact(result, reportText),
10144
+ "utf-8"
10145
+ );
10146
+ }
10147
+ function formatWebResearchArtifact(result, reportText) {
10148
+ const providerLabel2 = PROVIDERS_BY_ID[result.provider]?.label ?? result.provider;
10149
+ const metadata = {
10150
+ query: result.input,
10151
+ provider: providerLabel2,
10152
+ providerId: result.provider,
10153
+ status: result.status,
10154
+ startedAt: result.startedAt,
10155
+ completedAt: result.completedAt,
10156
+ elapsedMs: result.elapsedMs,
10157
+ itemCount: result.itemCount,
10158
+ error: result.error
10159
+ };
10160
+ const lines = [
10161
+ "---",
10162
+ ...Object.entries(metadata).flatMap(
10163
+ ([key, value]) => value === void 0 ? [] : [`${key}: ${formatYamlScalar(value)}`]
10164
+ ),
10165
+ "---",
10166
+ "",
10167
+ "# Web research report"
10168
+ ];
10169
+ if (reportText) {
10170
+ lines.push("", reportText);
10171
+ }
10172
+ return `${lines.join("\n")}
10173
+ `;
10174
+ }
10175
+ function formatYamlScalar(value) {
10176
+ if (typeof value === "number") {
10177
+ return String(value);
10178
+ }
10179
+ return JSON.stringify(value);
10180
+ }
10181
+ async function loadWebResearchHistory(cwd, maxItems = MAX_RESEARCH_HISTORY_ITEMS) {
10182
+ const dir = join2(cwd, RESEARCH_ARTIFACTS_DIR);
10183
+ let entries;
10184
+ try {
10185
+ entries = await readdir(dir);
10186
+ } catch {
10187
+ return [];
10188
+ }
10189
+ const markdown = entries.filter((name) => name.endsWith(".md"));
10190
+ const withStats = await Promise.all(
10191
+ markdown.map(async (fileName) => {
10192
+ const outputPath = join2(dir, fileName);
10193
+ try {
10194
+ return { fileName, outputPath, stat: await stat(outputPath) };
10195
+ } catch {
10196
+ return void 0;
10197
+ }
10198
+ })
10199
+ );
10200
+ const newest = withStats.filter((item) => item !== void 0).sort((left, right) => right.stat.mtimeMs - left.stat.mtimeMs).slice(0, maxItems);
10201
+ return Promise.all(
10202
+ newest.map(async ({ fileName, outputPath, stat: stat2 }) => {
10203
+ let content = "";
10204
+ try {
10205
+ content = await readFile2(outputPath, "utf-8");
10206
+ } catch {
10207
+ }
10208
+ const metadata = parseWebResearchArtifactMetadata(content);
10209
+ return {
10210
+ outputPath,
10211
+ fileName,
10212
+ query: metadata.query ?? "",
10213
+ title: deriveWebResearchTitle(content, metadata.query ?? ""),
10214
+ provider: metadata.provider ?? "",
10215
+ status: metadata.status ?? "unknown",
10216
+ startedAt: metadata.startedAt ?? "",
10217
+ completedAt: metadata.completedAt ?? "",
10218
+ elapsedMs: computeHistoryElapsedMs(metadata),
10219
+ mtimeMs: stat2.mtimeMs
10220
+ };
10221
+ })
10222
+ );
10223
+ }
10224
+ function parseWebResearchArtifactMetadata(content) {
10225
+ return parseWebResearchFrontmatter(content) ?? parseLegacyWebResearchArtifactMetadata(content);
10226
+ }
10227
+ function parseWebResearchFrontmatter(content) {
10228
+ if (!content.startsWith("---\n")) {
10229
+ return void 0;
10230
+ }
10231
+ const end = content.indexOf("\n---", 4);
10232
+ if (end === -1) {
10233
+ return void 0;
10234
+ }
10235
+ const result = {};
10236
+ const frontmatter = content.slice(4, end);
10237
+ for (const line of frontmatter.split(/\r?\n/u)) {
10238
+ const match = /^([A-Za-z][A-Za-z0-9_-]*):\s*(.*)$/u.exec(line);
10239
+ if (!match) {
10240
+ continue;
10241
+ }
10242
+ result[match[1] ?? ""] = parseYamlScalar(match[2] ?? "");
10243
+ }
10244
+ return result;
10245
+ }
10246
+ function parseYamlScalar(value) {
10247
+ const trimmed = value.trim();
10248
+ if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
10249
+ try {
10250
+ const parsed = JSON.parse(trimmed);
10251
+ return typeof parsed === "string" ? parsed : String(parsed);
10252
+ } catch {
10253
+ }
10254
+ }
10255
+ return trimmed;
10256
+ }
10257
+ function parseLegacyWebResearchArtifactMetadata(content) {
10258
+ const result = {};
10259
+ const metadataHeadings = /* @__PURE__ */ new Map([
10260
+ ["Query", "query"],
10261
+ ["Provider", "provider"],
10262
+ ["Status", "status"],
10263
+ ["Started", "startedAt"],
10264
+ ["Completed", "completedAt"]
10265
+ ]);
10266
+ const artifactBodyHeadings = /* @__PURE__ */ new Set(["Elapsed", "Items", "Error", "Report"]);
10267
+ const lines = content.split(/\r?\n/u);
10268
+ for (let index = 0; index < lines.length; index++) {
10269
+ const match = /^##\s+(.+)$/u.exec(lines[index] ?? "");
10270
+ if (!match) continue;
10271
+ const heading = match[1] ?? "";
10272
+ if (artifactBodyHeadings.has(heading)) break;
10273
+ const key = metadataHeadings.get(heading);
10274
+ if (key === void 0 || result[key] !== void 0) continue;
10275
+ const values = [];
10276
+ for (let next = index + 1; next < lines.length; next++) {
10277
+ if (/^##\s+/u.test(lines[next] ?? "")) break;
10278
+ if ((lines[next] ?? "").trim() || values.length > 0)
10279
+ values.push(lines[next] ?? "");
10280
+ }
10281
+ result[key] = values.join("\n").trim();
10282
+ }
10283
+ return result;
10284
+ }
10285
+ var RESEARCH_TITLE_MAX_LENGTH = 80;
10286
+ var NON_TITLE_HEADINGS = /* @__PURE__ */ new Set([
10287
+ "Web research report",
10288
+ "Query",
10289
+ "Provider",
10290
+ "Status",
10291
+ "Started",
10292
+ "Completed",
10293
+ "Elapsed",
10294
+ "Items",
10295
+ "Error",
10296
+ "Report"
10297
+ ]);
10298
+ function deriveWebResearchTitle(content, query2) {
10299
+ const body = getWebResearchBody(content);
10300
+ for (const line of body.split(/\r?\n/u)) {
10301
+ const match = /^#{1,2}\s+(.+?)\s*$/u.exec(line);
10302
+ if (!match) continue;
10303
+ const heading = (match[1] ?? "").trim();
10304
+ if (heading.length > 0 && !NON_TITLE_HEADINGS.has(heading)) {
10305
+ return heading;
10306
+ }
10307
+ }
10308
+ const fallback = query2.replace(/\s+/g, " ").trim();
10309
+ if (fallback.length <= RESEARCH_TITLE_MAX_LENGTH) {
10310
+ return fallback;
10311
+ }
10312
+ return `${fallback.slice(0, RESEARCH_TITLE_MAX_LENGTH - 1)}\u2026`;
10313
+ }
10314
+ function computeHistoryElapsedMs(metadata) {
10315
+ if (metadata.elapsedMs !== void 0) {
10316
+ const parsed = Number(metadata.elapsedMs);
10317
+ if (Number.isFinite(parsed) && parsed >= 0) {
10318
+ return parsed;
10319
+ }
10320
+ }
10321
+ const started = Date.parse(metadata.startedAt ?? "");
10322
+ const completed = Date.parse(metadata.completedAt ?? "");
10323
+ if (Number.isFinite(started) && Number.isFinite(completed)) {
10324
+ return Math.max(0, completed - started);
10325
+ }
10326
+ return void 0;
10327
+ }
10328
+ function getWebResearchBody(content) {
10329
+ if (!content.startsWith("---\n")) {
10330
+ return content;
10331
+ }
10332
+ const end = content.indexOf("\n---", 4);
10333
+ if (end === -1) {
10334
+ return content;
10335
+ }
10336
+ const afterClose = content.indexOf("\n", end + 4);
10337
+ return afterClose === -1 ? "" : content.slice(afterClose + 1);
10338
+ }
10339
+ async function loadWebResearchReport(outputPath) {
10340
+ const buffer = await readFile2(outputPath);
10341
+ const truncated = buffer.byteLength > RESEARCH_REPORT_MAX_BYTES;
10342
+ const content = buffer.subarray(0, RESEARCH_REPORT_MAX_BYTES).toString("utf-8");
10343
+ return {
10344
+ body: getWebResearchBody(content).trim(),
10345
+ metadata: parseWebResearchArtifactMetadata(content),
10346
+ truncated
10347
+ };
10348
+ }
10349
+ async function loadWebResearchPreview(outputPath) {
10350
+ const buffer = await readFile2(outputPath);
10351
+ const truncated = buffer.byteLength > RESEARCH_PREVIEW_MAX_BYTES;
10352
+ const text = buffer.subarray(0, RESEARCH_PREVIEW_MAX_BYTES).toString("utf-8");
10353
+ return truncated ? `${text}
10354
+
10355
+ ---
10356
+ Preview truncated. Open the full report at \`${outputPath}\`.` : `${text}
10357
+
10358
+ ---
10359
+ Full report: \`${outputPath}\``;
10360
+ }
10361
+ function trackPendingResearchTask(task) {
10362
+ const tracked = task.catch(() => {
10363
+ }).finally(() => {
10364
+ pendingResearchTasks.delete(tracked);
10365
+ });
10366
+ pendingResearchTasks.add(tracked);
10367
+ }
10368
+ async function waitForPendingResearchTasks() {
10369
+ await Promise.all([...pendingResearchTasks]);
10370
+ }
10371
+
10372
+ // src/web-research-view.ts
10373
+ import { getMarkdownTheme } from "@earendil-works/pi-coding-agent";
10374
+ import {
10375
+ getKeybindings,
10376
+ Key,
10377
+ Markdown,
10378
+ matchesKey,
10379
+ truncateToWidth,
10380
+ visibleWidth
10381
+ } from "@earendil-works/pi-tui";
10382
+ var STATUS_MESSAGE_TTL_MS = 1500;
10383
+ var PAGE_JUMP = 10;
10384
+ var WebResearchManagerView = class {
10385
+ constructor(tui, theme, done, cwd, tasks, onChange, actions, loadHistory = loadWebResearchHistory) {
10386
+ this.tui = tui;
10387
+ this.theme = theme;
10388
+ this.done = done;
10389
+ this.cwd = cwd;
10390
+ this.tasks = tasks;
10391
+ this.onChange = onChange;
10392
+ this.actions = actions;
10393
+ this.loadHistory = loadHistory;
10394
+ void this.reloadHistory();
10395
+ }
10396
+ tui;
10397
+ theme;
10398
+ done;
10399
+ cwd;
10400
+ tasks;
10401
+ onChange;
10402
+ actions;
10403
+ loadHistory;
10404
+ selectedIndex = 0;
10405
+ history = [];
10406
+ confirmCancelId;
10407
+ open;
10408
+ scrollOffset = 0;
10409
+ statusMessage;
10410
+ statusMessageTimer;
10411
+ isReportOpen() {
10412
+ return this.open !== void 0;
10413
+ }
10414
+ render(width) {
10415
+ if (this.open?.kind === "report") {
10416
+ return this.renderReport(this.open, width);
10417
+ }
10418
+ if (this.open?.kind === "detail") {
10419
+ const snapshot = this.findSnapshot(this.open.taskId);
10420
+ if (snapshot) return this.renderDetail(snapshot, width);
10421
+ this.open = void 0;
10422
+ }
10423
+ return this.renderTable(width);
10424
+ }
10425
+ invalidate() {
10426
+ }
10427
+ dispose() {
10428
+ if (this.statusMessageTimer) clearTimeout(this.statusMessageTimer);
10429
+ }
10430
+ handleInput(data) {
10431
+ if (this.open) {
10432
+ this.handleOpenInput(data);
10433
+ } else {
10434
+ this.handleTableInput(data);
10435
+ }
10436
+ this.tui.requestRender();
10437
+ }
10438
+ refresh() {
10439
+ if (this.open?.kind === "detail" && !this.findSnapshot(this.open.taskId)) {
10440
+ this.open = void 0;
10441
+ this.confirmCancelId = void 0;
10442
+ }
10443
+ void this.reloadHistory();
10444
+ this.tui.requestRender();
10445
+ }
10446
+ // --- table mode ---
10447
+ getRows() {
10448
+ const snapshots = getWebResearchTaskSnapshots(this.tasks).sort(
10449
+ (a, b) => a.request.startedAt.localeCompare(b.request.startedAt)
10450
+ );
10451
+ const runningPaths = new Set(
10452
+ snapshots.map((snapshot) => snapshot.request.outputPath)
10453
+ );
10454
+ const rows = snapshots.map((snapshot) => ({
10455
+ kind: "running",
10456
+ snapshot
10457
+ }));
10458
+ for (const item of this.history) {
10459
+ if (runningPaths.has(item.outputPath)) continue;
10460
+ rows.push({ kind: "history", item });
10461
+ }
10462
+ return rows;
10463
+ }
10464
+ border(width) {
10465
+ return this.theme.fg("border", "\u2500".repeat(Math.max(1, width)));
10466
+ }
10467
+ renderTable(width) {
10468
+ const rows = this.getRows();
10469
+ this.selectedIndex = clamp2(this.selectedIndex, 0, rows.length - 1);
10470
+ const lines = [
10471
+ this.border(width),
10472
+ this.theme.fg("accent", " Web research"),
10473
+ ""
10474
+ ];
10475
+ if (rows.length === 0) {
10476
+ lines.push(this.theme.fg("muted", " No research jobs or reports"));
10477
+ } else {
10478
+ const layout = computeTableLayout(rows, width);
10479
+ const now = Date.now();
10480
+ rows.forEach((row, index) => {
10481
+ lines.push(
10482
+ formatResearchTableRow(
10483
+ row,
10484
+ layout,
10485
+ this.theme,
10486
+ index === this.selectedIndex,
10487
+ now
10488
+ )
10489
+ );
10490
+ if (row.kind === "running" && this.confirmCancelId === row.snapshot.request.id) {
10491
+ lines.push(
10492
+ this.theme.fg(
10493
+ "warning",
10494
+ truncateToWidth(
10495
+ ` Press c again to cancel this ${providerLabel(row.snapshot.request.provider)} research`,
10496
+ width
10497
+ )
10498
+ )
10499
+ );
10500
+ }
10501
+ });
10502
+ }
10503
+ lines.push("");
10504
+ if (this.statusMessage) {
10505
+ lines.push(this.theme.fg("accent", ` ${this.statusMessage}`));
10506
+ }
10507
+ lines.push(
10508
+ this.theme.fg(
10509
+ "dim",
10510
+ " \u2191\u2193 move \xB7 Enter open \xB7 c cancel running \xB7 Esc close"
10511
+ ),
10512
+ this.border(width)
10513
+ );
10514
+ return lines;
10515
+ }
10516
+ handleTableInput(data) {
10517
+ const kb = getKeybindings();
10518
+ const rows = this.getRows();
10519
+ if (kb.matches(data, "tui.select.up")) this.move(rows.length, -1);
10520
+ else if (kb.matches(data, "tui.select.down")) this.move(rows.length, 1);
10521
+ else if (kb.matches(data, "tui.select.pageUp"))
10522
+ this.move(rows.length, -PAGE_JUMP, false);
10523
+ else if (kb.matches(data, "tui.select.pageDown"))
10524
+ this.move(rows.length, PAGE_JUMP, false);
10525
+ else if (kb.matches(data, "tui.select.confirm"))
10526
+ void this.openRow(rows[this.selectedIndex]);
10527
+ else if (data === "c") this.cancelRow(rows[this.selectedIndex]);
10528
+ else if (kb.matches(data, "tui.select.cancel")) {
10529
+ if (this.confirmCancelId) this.confirmCancelId = void 0;
10530
+ else this.done(void 0);
10531
+ }
10532
+ }
10533
+ move(count, delta, wrap = true) {
10534
+ this.confirmCancelId = void 0;
10535
+ if (count === 0) return;
10536
+ const next = this.selectedIndex + delta;
10537
+ this.selectedIndex = wrap ? (next + count) % count : clamp2(next, 0, count - 1);
10538
+ }
10539
+ async openRow(row) {
10540
+ if (!row) return;
10541
+ this.confirmCancelId = void 0;
10542
+ if (row.kind === "running") {
10543
+ this.open = { kind: "detail", taskId: row.snapshot.request.id };
10544
+ this.scrollOffset = 0;
10545
+ return;
10546
+ }
10547
+ try {
10548
+ const report = await loadWebResearchReport(row.item.outputPath);
10549
+ this.open = {
10550
+ kind: "report",
10551
+ title: row.item.title || row.item.query,
10552
+ body: report.body,
10553
+ truncated: report.truncated,
10554
+ item: row.item,
10555
+ markdown: new Markdown(report.body, 1, 0, getMarkdownTheme())
10556
+ };
10557
+ this.scrollOffset = 0;
10558
+ } catch (error) {
10559
+ this.showStatusMessage(
10560
+ `Failed to read ${row.item.outputPath}: ${formatErrorMessage(error)}`
10561
+ );
10562
+ }
10563
+ this.tui.requestRender();
10564
+ }
10565
+ cancelRow(row) {
10566
+ if (!row || row.kind !== "running") return;
10567
+ const id = row.snapshot.request.id;
10568
+ if (this.confirmCancelId !== id) {
10569
+ this.confirmCancelId = id;
10570
+ return;
10571
+ }
10572
+ cancelWebResearchTask(this.tasks, id);
10573
+ this.confirmCancelId = void 0;
10574
+ this.onChange();
10575
+ }
10576
+ // --- report / detail mode ---
10577
+ renderReport(open, width) {
10578
+ const lines = [
10579
+ this.border(width),
10580
+ this.theme.fg("accent", truncateToWidth(` ${open.title}`, width)),
10581
+ this.theme.fg(
10582
+ "dim",
10583
+ " c copy markdown \xB7 i inject into context \xB7 \u2191\u2193/PgUp/PgDn scroll \xB7 Esc back"
10584
+ ),
10585
+ this.border(width),
10586
+ ""
10587
+ ];
10588
+ let body;
10589
+ try {
10590
+ body = open.markdown.render(Math.max(20, width - 2));
10591
+ } catch {
10592
+ body = open.body.split("\n").map((line) => truncateToWidth(line, width));
10593
+ }
10594
+ const viewport = this.viewportHeight();
10595
+ const maxOffset = Math.max(0, body.length - viewport);
10596
+ this.scrollOffset = clamp2(this.scrollOffset, 0, maxOffset);
10597
+ lines.push(...body.slice(this.scrollOffset, this.scrollOffset + viewport));
10598
+ lines.push("");
10599
+ const footer = [];
10600
+ if (body.length > viewport) {
10601
+ const end = Math.min(body.length, this.scrollOffset + viewport);
10602
+ footer.push(`lines ${this.scrollOffset + 1}\u2013${end} of ${body.length}`);
10603
+ }
10604
+ if (open.truncated) {
10605
+ footer.push(`report truncated \xB7 full text: ${open.item.outputPath}`);
10606
+ }
10607
+ if (this.statusMessage) {
10608
+ lines.push(this.theme.fg("accent", ` ${this.statusMessage}`));
10609
+ }
10610
+ if (footer.length > 0) {
10611
+ lines.push(
10612
+ this.theme.fg("dim", truncateToWidth(` ${footer.join(" \xB7 ")}`, width))
10613
+ );
10614
+ }
10615
+ lines.push(this.border(width));
10616
+ return lines;
10617
+ }
10618
+ renderDetail(snapshot, width) {
10619
+ const request = snapshot.request;
10620
+ const elapsed = formatCompactElapsed(
10621
+ Date.now() - Date.parse(request.startedAt)
10622
+ );
10623
+ const progress = snapshot.cancelRequestedAt ? "cancelling" : request.progress ?? "running";
10624
+ const lines = [
10625
+ this.border(width),
10626
+ this.theme.fg(
10627
+ "accent",
10628
+ truncateToWidth(
10629
+ ` Running research via ${providerLabel(request.provider)}`,
10630
+ width
10631
+ )
10632
+ ),
10633
+ this.theme.fg("dim", " c cancel \xB7 Esc back"),
10634
+ this.border(width),
10635
+ "",
10636
+ truncateToWidth(` Status: ${progress} (${elapsed} elapsed)`, width),
10637
+ truncateToWidth(` Report path: ${request.outputPath}`, width),
10638
+ "",
10639
+ this.theme.fg("accent", " Research brief"),
10640
+ ""
10641
+ ];
10642
+ for (const line of request.input.split("\n").slice(0, 100)) {
10643
+ lines.push(truncateToWidth(` ${line}`, width));
10644
+ }
10645
+ if (this.confirmCancelId === request.id) {
10646
+ lines.push(
10647
+ "",
10648
+ this.theme.fg("warning", " Press c again to cancel this research")
10649
+ );
10650
+ }
10651
+ if (this.statusMessage) {
10652
+ lines.push("", this.theme.fg("accent", ` ${this.statusMessage}`));
10653
+ }
10654
+ lines.push(this.border(width));
10655
+ return lines;
10656
+ }
10657
+ handleOpenInput(data) {
10658
+ const kb = getKeybindings();
10659
+ const open = this.open;
10660
+ if (!open) return;
10661
+ if (kb.matches(data, "tui.select.cancel")) {
10662
+ if (this.confirmCancelId) {
10663
+ this.confirmCancelId = void 0;
10664
+ return;
10665
+ }
10666
+ this.open = void 0;
10667
+ this.scrollOffset = 0;
10668
+ return;
10669
+ }
10670
+ if (open.kind === "detail") {
10671
+ if (data === "c") {
10672
+ const snapshot = this.findSnapshot(open.taskId);
10673
+ if (!snapshot) return;
10674
+ if (this.confirmCancelId !== open.taskId) {
10675
+ this.confirmCancelId = open.taskId;
10676
+ return;
10677
+ }
10678
+ cancelWebResearchTask(this.tasks, open.taskId);
10679
+ this.confirmCancelId = void 0;
10680
+ this.onChange();
10681
+ this.showStatusMessage("Cancellation requested");
10682
+ }
10683
+ return;
10684
+ }
10685
+ if (kb.matches(data, "tui.select.up")) this.scrollOffset -= 1;
10686
+ else if (kb.matches(data, "tui.select.down")) this.scrollOffset += 1;
10687
+ else if (kb.matches(data, "tui.select.pageUp"))
10688
+ this.scrollOffset -= this.viewportHeight();
10689
+ else if (kb.matches(data, "tui.select.pageDown"))
10690
+ this.scrollOffset += this.viewportHeight();
10691
+ else if (matchesKey(data, Key.home)) this.scrollOffset = 0;
10692
+ else if (matchesKey(data, Key.end))
10693
+ this.scrollOffset = Number.MAX_SAFE_INTEGER;
10694
+ else if (data === "c") {
10695
+ void this.actions.copyToClipboard(open.body).then(() => this.showStatusMessage("Copied report to clipboard")).catch((error) => {
10696
+ const message = `Copy failed: ${formatErrorMessage(error)}`;
10697
+ this.showStatusMessage(message);
10698
+ this.actions.notify(message, "error");
10699
+ });
10700
+ } else if (data === "i") {
10701
+ this.actions.injectReport({
10702
+ title: open.title,
10703
+ body: open.body,
10704
+ item: open.item
10705
+ });
10706
+ this.showStatusMessage("Report added to conversation context");
10707
+ }
10708
+ }
10709
+ viewportHeight() {
10710
+ return Math.max(5, Math.floor(this.tui.terminal.rows * 0.85) - 6);
10711
+ }
10712
+ showStatusMessage(message) {
10713
+ this.statusMessage = message;
10714
+ if (this.statusMessageTimer) clearTimeout(this.statusMessageTimer);
10715
+ this.statusMessageTimer = setTimeout(() => {
10716
+ this.statusMessage = void 0;
10717
+ this.statusMessageTimer = void 0;
10718
+ this.tui.requestRender();
10719
+ }, STATUS_MESSAGE_TTL_MS);
10720
+ }
10721
+ findSnapshot(taskId) {
10722
+ return getWebResearchTaskSnapshots(this.tasks).find(
10723
+ (snapshot) => snapshot.request.id === taskId
10724
+ );
10725
+ }
10726
+ async reloadHistory() {
10727
+ this.history = await this.loadHistory(this.cwd);
10728
+ this.tui.requestRender();
10729
+ }
10730
+ };
10731
+ function computeTableLayout(rows, width) {
10732
+ const providerWidth = clamp2(
10733
+ Math.max(0, ...rows.map((row) => visibleWidth(rowProvider(row)))),
10734
+ 4,
10735
+ 12
10736
+ );
10737
+ const date = 10;
10738
+ const duration = 7;
10739
+ const fixed = 2 + 2 + date + 1 + providerWidth + 1 + duration + 1;
10740
+ return {
10741
+ date,
10742
+ provider: providerWidth,
10743
+ duration,
10744
+ title: Math.max(10, width - fixed)
10745
+ };
10746
+ }
10747
+ function formatResearchTableRow(row, layout, theme, selected, now = Date.now()) {
10748
+ const cursor = selected ? "\u203A " : " ";
10749
+ const glyph = statusGlyph(row, theme);
10750
+ const date = padCell(
10751
+ formatRelativeDate(rowTimestampMs(row), now),
10752
+ layout.date
10753
+ );
10754
+ const provider = padCell(
10755
+ truncateToWidth(rowProvider(row), layout.provider),
10756
+ layout.provider
10757
+ );
10758
+ const duration = padCell(rowDuration(row, now), layout.duration, "right");
10759
+ const title = truncateToWidth(rowTitle(row), layout.title);
10760
+ const dim = (text) => row.kind === "history" ? text : theme.fg("dim", text);
10761
+ return `${cursor}${glyph} ${dim(date)} ${provider} ${theme.fg("muted", duration)} ${title}`;
10762
+ }
10763
+ function statusGlyph(row, theme) {
10764
+ if (row.kind === "running") {
10765
+ const icon = row.snapshot.cancelRequestedAt ? "\u25CC" : getWebResearchProgressIcon(row.snapshot.request);
10766
+ return theme.fg("accent", icon);
10767
+ }
10768
+ switch (row.item.status) {
10769
+ case "completed":
10770
+ return theme.fg("success", "\u2714");
10771
+ case "failed":
10772
+ return theme.fg("error", "\u2718");
10773
+ case "cancelled":
10774
+ return theme.fg("warning", "\u2718");
10775
+ default:
10776
+ return theme.fg("dim", "?");
10777
+ }
10778
+ }
10779
+ function rowProvider(row) {
10780
+ return row.kind === "running" ? providerLabel(row.snapshot.request.provider) : row.item.provider || "?";
10781
+ }
10782
+ function rowTimestampMs(row) {
10783
+ if (row.kind === "running") {
10784
+ return Date.parse(row.snapshot.request.startedAt);
10785
+ }
10786
+ const completed = Date.parse(row.item.completedAt);
10787
+ if (Number.isFinite(completed)) return completed;
10788
+ const started = Date.parse(row.item.startedAt);
10789
+ if (Number.isFinite(started)) return started;
10790
+ return row.item.mtimeMs;
10791
+ }
10792
+ function rowDuration(row, now) {
10793
+ if (row.kind === "running") {
10794
+ return formatCompactElapsed(
10795
+ now - Date.parse(row.snapshot.request.startedAt)
10796
+ );
10797
+ }
10798
+ return row.item.elapsedMs === void 0 ? "\u2014" : formatCompactElapsed(row.item.elapsedMs);
10799
+ }
10800
+ var REDUNDANT_PROGRESS = /* @__PURE__ */ new Set([
10801
+ "in_progress",
10802
+ "running",
10803
+ "starting",
10804
+ "queued",
10805
+ "cancelling"
10806
+ ]);
10807
+ function rowTitle(row) {
10808
+ if (row.kind === "running") {
10809
+ const request = row.snapshot.request;
10810
+ const progress = row.snapshot.cancelRequestedAt ? void 0 : request.progress;
10811
+ const prefix = progress && !REDUNDANT_PROGRESS.has(progress) ? `${progress} \u2014 ` : "";
10812
+ return `${prefix}${cleanSingleLine(request.input)}`;
10813
+ }
10814
+ return row.item.title || cleanSingleLine(row.item.query);
10815
+ }
10816
+ function formatRelativeDate(timestampMs, now) {
10817
+ if (!Number.isFinite(timestampMs)) return "?";
10818
+ const diff = Math.max(0, now - timestampMs);
10819
+ if (diff < 6e4) return "now";
10820
+ if (diff < 36e5) return `${Math.floor(diff / 6e4)}m ago`;
10821
+ if (diff < 864e5) return `${Math.floor(diff / 36e5)}h ago`;
10822
+ if (diff < 7 * 864e5) return `${Math.floor(diff / 864e5)}d ago`;
10823
+ const date = new Date(timestampMs);
10824
+ const month = String(date.getMonth() + 1).padStart(2, "0");
10825
+ const day = String(date.getDate()).padStart(2, "0");
10826
+ if (date.getFullYear() === new Date(now).getFullYear()) {
10827
+ return `${month}-${day}`;
10828
+ }
10829
+ return `${date.getFullYear()}-${month}-${day}`;
10830
+ }
10831
+ function providerLabel(providerId) {
10832
+ return PROVIDERS_BY_ID[providerId]?.label ?? providerId;
10833
+ }
10834
+ function padCell(text, width, align = "left") {
10835
+ const pad = " ".repeat(Math.max(0, width - visibleWidth(text)));
10836
+ return align === "left" ? `${text}${pad}` : `${pad}${text}`;
10837
+ }
10838
+ function clamp2(value, min, max) {
10839
+ return Math.min(Math.max(value, min), Math.max(min, max));
10840
+ }
10841
+ function formatCompactElapsed(ms) {
10842
+ const totalSeconds = Math.max(0, Math.floor(ms / 1e3));
10843
+ const minutes = Math.floor(totalSeconds / 60);
10844
+ const seconds = totalSeconds % 60;
10845
+ if (minutes > 0) {
10846
+ return `${minutes}m${seconds}s`;
10847
+ }
10848
+ return `${totalSeconds}s`;
10849
+ }
10850
+ function cleanSingleLine(text) {
10851
+ return text.replace(/\s+/g, " ").trim();
10852
+ }
10853
+
10854
+ // src/index.ts
10855
+ var DEFAULT_MAX_RESULTS = 5;
10856
+ var MAX_ALLOWED_RESULTS = 20;
10857
+ var MAX_SEARCH_QUERIES = 10;
10858
+ var RESEARCH_HEARTBEAT_MS = 15e3;
10859
+ var WEB_RESEARCH_RESULT_MESSAGE_TYPE = "web-research-result";
10860
+ var WEB_RESEARCH_REPORT_MESSAGE_TYPE = "web-research-report";
10861
+ var WEB_RESEARCH_WIDGET_KEY = "web-research-jobs";
10862
+ var DEFAULT_SUMMARY_SYMBOLS = {
10863
+ success: "\u2714",
10864
+ failure: "\u2718"
10865
+ };
10866
+ function webProvidersExtension(pi) {
10867
+ const activeWebResearchRequests = /* @__PURE__ */ new Map();
10868
+ let latestWidgetContext;
10869
+ let webResearchWidgetTimer;
10870
+ const stopWebResearchWidgetTimer = () => {
10871
+ if (webResearchWidgetTimer) {
10872
+ clearInterval(webResearchWidgetTimer);
10873
+ webResearchWidgetTimer = void 0;
10874
+ }
10875
+ };
10876
+ const ensureWebResearchWidgetTimer = () => {
10877
+ if (webResearchWidgetTimer || activeWebResearchRequests.size === 0) {
10878
+ return;
10879
+ }
10880
+ webResearchWidgetTimer = setInterval(() => {
10881
+ updateWebResearchWidget();
10882
+ }, 1e3);
10883
+ };
10884
+ const updateWebResearchWidget = (ctx) => {
10885
+ const widgetContext = ctx ?? latestWidgetContext;
10886
+ if (!widgetContext) {
10887
+ return;
10888
+ }
10889
+ latestWidgetContext = widgetContext;
10890
+ if (!widgetContext.hasUI) {
10891
+ stopWebResearchWidgetTimer();
10892
+ return;
10893
+ }
10894
+ if (activeWebResearchRequests.size === 0) {
10895
+ stopWebResearchWidgetTimer();
10896
+ widgetContext.ui.setWidget(WEB_RESEARCH_WIDGET_KEY, void 0);
10897
+ return;
10898
+ }
10899
+ ensureWebResearchWidgetTimer();
10900
+ widgetContext.ui.setWidget(
10901
+ WEB_RESEARCH_WIDGET_KEY,
10902
+ buildWebResearchWidgetLines(
10903
+ getActiveWebResearchRequests(activeWebResearchRequests),
10904
+ widgetContext.ui.theme
10905
+ )
10906
+ );
10907
+ };
10908
+ if ("registerMessageRenderer" in pi) {
10909
+ pi.registerMessageRenderer(
10910
+ WEB_RESEARCH_RESULT_MESSAGE_TYPE,
10911
+ (message, state, theme) => renderWebResearchResultMessage(message, state, theme)
10912
+ );
10913
+ pi.registerMessageRenderer(
10914
+ WEB_RESEARCH_REPORT_MESSAGE_TYPE,
10915
+ (message, state, theme) => renderWebResearchReportMessage(message, state, theme)
10916
+ );
10917
+ }
10918
+ pi.registerCommand("web-providers", {
10919
+ description: "Configure web search providers",
10920
+ handler: async (_args, ctx) => {
10921
+ if (!ctx.hasUI) {
10922
+ ctx.ui.notify("web-providers requires interactive mode", "error");
10923
+ return;
10924
+ }
10925
+ await runWebProvidersConfig(
10926
+ pi,
10927
+ { activeWebResearchRequests, updateWebResearchWidget },
10928
+ ctx
10929
+ );
10930
+ }
10931
+ });
10932
+ pi.registerCommand("web-research", {
10933
+ description: "Browse, inspect, and manage web researches",
10934
+ handler: async (_args, ctx) => {
10935
+ if (!ctx.hasUI) {
10936
+ ctx.ui.notify("web-research requires interactive mode", "error");
10937
+ return;
10938
+ }
10939
+ const actions = {
10940
+ copyToClipboard,
10941
+ injectReport: ({ title, body, item }) => {
10942
+ pi.sendMessage(
10943
+ {
10944
+ customType: WEB_RESEARCH_REPORT_MESSAGE_TYPE,
10945
+ content: formatWebResearchReportMessage(title, body, item),
10946
+ display: true,
10947
+ details: {
10948
+ title,
10949
+ outputPath: item.outputPath,
10950
+ provider: item.provider,
10951
+ query: item.query,
10952
+ status: item.status
10953
+ }
10954
+ },
10955
+ { triggerTurn: false }
10956
+ );
10957
+ },
10958
+ notify: (message, type) => ctx.ui.notify(message, type ?? "info")
10959
+ };
10960
+ let timer;
10961
+ let view;
10962
+ try {
10963
+ await ctx.ui.custom(
10964
+ (tui, theme, _keybindings, done) => {
10965
+ view = new WebResearchManagerView(
10966
+ tui,
10967
+ theme,
10968
+ done,
10969
+ ctx.cwd,
10970
+ activeWebResearchRequests,
10971
+ () => updateWebResearchWidget(ctx),
10972
+ actions
10973
+ );
10974
+ timer = setInterval(() => view?.refresh(), 1e3);
10975
+ return view;
10976
+ },
10977
+ {
10978
+ overlay: true,
10979
+ overlayOptions: () => view?.isReportOpen() ? { anchor: "center", width: "85%", maxHeight: "85%" } : {
10980
+ anchor: "center",
10981
+ width: "75%",
10982
+ maxHeight: "60%",
10983
+ minWidth: 60
10984
+ }
10985
+ }
10986
+ );
10987
+ } finally {
10988
+ if (timer) clearInterval(timer);
10989
+ }
10990
+ }
9698
10991
  });
9699
10992
  pi.on("session_start", async (_event, ctx) => {
9700
10993
  latestWidgetContext = ctx;
9701
10994
  resetContentStore();
9702
10995
  updateWebResearchWidget(ctx);
9703
- await refreshManagedToolsOnStartup(
10996
+ await refreshManagedToolsOnStartup2(
9704
10997
  pi,
9705
10998
  { activeWebResearchRequests, updateWebResearchWidget },
9706
10999
  ctx.cwd,
@@ -9711,7 +11004,7 @@ function webProvidersExtension(pi) {
9711
11004
  latestWidgetContext = ctx;
9712
11005
  await cleanupContentStore();
9713
11006
  updateWebResearchWidget(ctx);
9714
- await refreshManagedToolsOnStartup(
11007
+ await refreshManagedToolsOnStartup2(
9715
11008
  pi,
9716
11009
  { activeWebResearchRequests, updateWebResearchWidget },
9717
11010
  ctx.cwd,
@@ -9934,7 +11227,7 @@ function registerWebResearchTool(pi, webResearchLifecycle, providerIds) {
9934
11227
  "Do not expect the final report in the same turn; tell the user that web research has started and wait for the completion message with the saved report path."
9935
11228
  ]),
9936
11229
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
9937
- return dispatchWebResearch({
11230
+ return dispatchWebResearch2({
9938
11231
  pi,
9939
11232
  activeWebResearchRequests: webResearchLifecycle.activeWebResearchRequests,
9940
11233
  updateWebResearchWidget: webResearchLifecycle.updateWebResearchWidget,
@@ -9959,109 +11252,38 @@ function registerWebResearchTool(pi, webResearchLifecycle, providerIds) {
9959
11252
  }
9960
11253
  });
9961
11254
  }
9962
- async function runWebProvidersConfig(pi, webResearchLifecycle, ctx) {
9963
- const config = await loadConfig();
9964
- const activeProvider = getInitialProviderSelection(config);
9965
- await ctx.ui.custom(
9966
- (tui, theme, _keybindings, done) => new WebProvidersSettingsView(
9967
- tui,
9968
- theme,
9969
- done,
9970
- ctx,
9971
- config,
9972
- activeProvider
9973
- )
9974
- );
9975
- await refreshManagedTools(pi, webResearchLifecycle, ctx.cwd, {
9976
- addAvailable: true
9977
- });
9978
- }
9979
- function formatStartupConfigError(error) {
9980
- const detail = error instanceof Error ? error.message : String(error);
9981
- return `web-providers config error: ${detail.replace(getConfigPath(), "~/.pi/agent/web-providers.json")}`;
9982
- }
9983
- function getAvailableProviderIdsForCapability(config, cwd, capability) {
9984
- const providerId = getMappedProviderIdForTool(config, capability);
9985
- if (!providerId) {
9986
- return [];
9987
- }
9988
- const provider = PROVIDERS_BY_ID[providerId];
9989
- if (!supportsTool2(provider, capability)) {
9990
- return [];
9991
- }
9992
- const status = getProviderCapabilityStatus(
9993
- config,
9994
- cwd,
9995
- providerId,
9996
- capability,
9997
- {
9998
- resolveSecrets: false
9999
- }
10000
- );
10001
- return isProviderCapabilityExposable(status) ? [providerId] : [];
10002
- }
10003
- function getProviderStatusForTool(config, cwd, providerId, capability) {
10004
- return getProviderCapabilityStatus(config, cwd, providerId, capability);
10005
- }
10006
- function getAvailableManagedToolNames(config, cwd) {
10007
- return Object.keys(CAPABILITY_TOOL_NAMES).filter(
10008
- (capability) => getAvailableProviderIdsForCapability(config, cwd, capability).length > 0
10009
- ).map((capability) => CAPABILITY_TOOL_NAMES[capability]);
10010
- }
10011
- function getSyncedActiveTools(config, cwd, activeToolNames, options) {
10012
- const availableToolNames = new Set(getAvailableManagedToolNames(config, cwd));
10013
- const nextActiveTools = new Set(activeToolNames);
10014
- for (const toolName of MANAGED_TOOL_NAMES) {
10015
- if (availableToolNames.has(toolName)) {
10016
- if (options.addAvailable) {
10017
- nextActiveTools.add(toolName);
10018
- }
10019
- continue;
10020
- }
10021
- nextActiveTools.delete(toolName);
10022
- }
10023
- return nextActiveTools;
10024
- }
10025
- async function refreshManagedTools(pi, webResearchLifecycle, cwd, options) {
11255
+ async function runWebProvidersConfig(pi, webResearchLifecycle, ctx) {
10026
11256
  const config = await loadConfig();
10027
- const nextActiveTools = getSyncedActiveTools(
10028
- config,
10029
- cwd,
10030
- pi.getActiveTools(),
10031
- options
11257
+ const activeProvider = getInitialProviderSelection(config);
11258
+ await ctx.ui.custom(
11259
+ (tui, theme, _keybindings, done) => new WebProvidersSettingsView(
11260
+ tui,
11261
+ theme,
11262
+ done,
11263
+ ctx,
11264
+ config,
11265
+ activeProvider
11266
+ )
10032
11267
  );
10033
- registerManagedTools(pi, webResearchLifecycle, {
10034
- search: getAvailableProviderIdsForCapability(config, cwd, "search"),
10035
- contents: getAvailableProviderIdsForCapability(config, cwd, "contents"),
10036
- answer: getAvailableProviderIdsForCapability(config, cwd, "answer"),
10037
- research: getAvailableProviderIdsForCapability(config, cwd, "research")
11268
+ await refreshManagedTools2(pi, webResearchLifecycle, ctx.cwd, {
11269
+ addAvailable: true
10038
11270
  });
10039
- await syncManagedToolAvailability(pi, nextActiveTools);
10040
11271
  }
10041
- async function refreshManagedToolsOnStartup(pi, webResearchLifecycle, cwd, options) {
10042
- try {
10043
- await refreshManagedTools(pi, webResearchLifecycle, cwd, options);
10044
- } catch (error) {
10045
- const message = formatStartupConfigError(error);
10046
- pi.sendMessage({
10047
- customType: "web-providers-config-error",
10048
- content: message,
10049
- display: true
10050
- });
10051
- await syncManagedToolAvailability(
10052
- pi,
10053
- new Set(
10054
- pi.getActiveTools().filter((toolName) => !MANAGED_TOOL_NAMES.includes(toolName))
10055
- )
10056
- );
10057
- }
11272
+ async function refreshManagedTools2(pi, webResearchLifecycle, cwd, options) {
11273
+ await refreshManagedTools(
11274
+ pi,
11275
+ (providerIdsByCapability) => registerManagedTools(pi, webResearchLifecycle, providerIdsByCapability),
11276
+ cwd,
11277
+ options
11278
+ );
10058
11279
  }
10059
- async function syncManagedToolAvailability(pi, nextActiveTools) {
10060
- const activeTools = pi.getActiveTools();
10061
- const changed = activeTools.length !== nextActiveTools.size || activeTools.some((toolName) => !nextActiveTools.has(toolName));
10062
- if (changed) {
10063
- pi.setActiveTools(Array.from(nextActiveTools));
10064
- }
11280
+ async function refreshManagedToolsOnStartup2(pi, webResearchLifecycle, cwd, options) {
11281
+ await refreshManagedToolsOnStartup(
11282
+ pi,
11283
+ (providerIdsByCapability) => registerManagedTools(pi, webResearchLifecycle, providerIdsByCapability),
11284
+ cwd,
11285
+ options
11286
+ );
10065
11287
  }
10066
11288
  function getSearchMaxResultsLimit(providerId) {
10067
11289
  const capabilities = PROVIDERS_BY_ID[providerId].capabilities;
@@ -10278,12 +11500,12 @@ async function executeRawProviderRequest({
10278
11500
  input
10279
11501
  });
10280
11502
  }
10281
- function buildSearchBatchError(outcomes, providerLabel) {
11503
+ function buildSearchBatchError(outcomes, providerLabel2) {
10282
11504
  const failed = outcomes.filter((outcome) => outcome.error !== void 0);
10283
11505
  if (failed.length === 1) {
10284
11506
  return new Error(
10285
11507
  formatProviderCapabilityFailure(
10286
- providerLabel,
11508
+ providerLabel2,
10287
11509
  "search",
10288
11510
  failed[0]?.error ?? ""
10289
11511
  )
@@ -10293,7 +11515,7 @@ function buildSearchBatchError(outcomes, providerLabel) {
10293
11515
  (outcome, index) => `${index + 1}. ${formatQuotedPreview(outcome.query, 40)} \u2014 ${outcome.error}`
10294
11516
  ).join("; ");
10295
11517
  return new Error(
10296
- `${providerLabel} search failed for ${failed.length} queries: ${summary}`
11518
+ `${providerLabel2} search failed for ${failed.length} queries: ${summary}`
10297
11519
  );
10298
11520
  }
10299
11521
  async function executeSingleSearchQuery({
@@ -10430,12 +11652,12 @@ async function executeAnswerToolInternal({
10430
11652
  })
10431
11653
  };
10432
11654
  }
10433
- function buildAnswerBatchError(outcomes, providerLabel) {
11655
+ function buildAnswerBatchError(outcomes, providerLabel2) {
10434
11656
  const failed = outcomes.filter((outcome) => outcome.error !== void 0);
10435
11657
  if (failed.length === 1) {
10436
11658
  return new Error(
10437
11659
  formatProviderCapabilityFailure(
10438
- providerLabel,
11660
+ providerLabel2,
10439
11661
  "answer",
10440
11662
  failed[0]?.error ?? ""
10441
11663
  )
@@ -10445,7 +11667,7 @@ function buildAnswerBatchError(outcomes, providerLabel) {
10445
11667
  (outcome, index) => `${index + 1}. ${formatQuotedPreview(outcome.query, 40)} \u2014 ${outcome.error}`
10446
11668
  ).join("; ");
10447
11669
  return new Error(
10448
- `${providerLabel} answer failed for ${failed.length} questions: ${summary}`
11670
+ `${providerLabel2} answer failed for ${failed.length} questions: ${summary}`
10449
11671
  );
10450
11672
  }
10451
11673
  function formatAnswerResponses(outcomes) {
@@ -10661,7 +11883,7 @@ async function executeProviderToolInternal({
10661
11883
  })
10662
11884
  };
10663
11885
  }
10664
- async function dispatchWebResearch({
11886
+ async function dispatchWebResearch2({
10665
11887
  pi,
10666
11888
  activeWebResearchRequests,
10667
11889
  updateWebResearchWidget,
@@ -10690,188 +11912,59 @@ async function dispatchWebResearchInternal({
10690
11912
  input,
10691
11913
  executionOverride
10692
11914
  }) {
10693
- await cleanupContentStore();
10694
- const provider = resolveProviderForTool(
11915
+ return dispatchWebResearch({
11916
+ activeWebResearchRequests,
10695
11917
  config,
10696
- ctx.cwd,
10697
- "research",
10698
- explicitProvider
10699
- );
10700
- const request = createWebResearchRequest(ctx.cwd, provider.id, input);
10701
- const providerConfig = getEffectiveProviderConfig(config, provider.id);
10702
- activeWebResearchRequests.set(request.id, request);
10703
- updateWebResearchWidget(ctx);
10704
- trackPendingResearchTask(
10705
- runDispatchedWebResearch({
10706
- pi,
10707
- activeWebResearchRequests,
10708
- updateWebResearchWidget,
10709
- request,
10710
- config,
11918
+ explicitProvider,
11919
+ ctx: { cwd: ctx.cwd },
11920
+ options: providerOptions,
11921
+ input,
11922
+ executionOverride,
11923
+ executeResearch: async ({
11924
+ config: config2,
10711
11925
  provider,
10712
11926
  providerConfig,
10713
- ctx,
10714
- options: providerOptions,
10715
- executionOverride
10716
- })
10717
- );
10718
- return {
10719
- content: [
10720
- {
10721
- type: "text",
10722
- text: `Started web research via ${provider.label}.`
10723
- }
10724
- ],
10725
- details: request,
10726
- display: buildProviderToolDisplay2({
10727
- capability: "research",
10728
- providerId: provider.id,
10729
- details: { tool: "web_research", provider: provider.id },
10730
- text: "started"
10731
- })
10732
- };
10733
- }
10734
- async function runDispatchedWebResearch({
10735
- pi,
10736
- activeWebResearchRequests,
10737
- updateWebResearchWidget,
10738
- request,
10739
- config,
10740
- provider,
10741
- providerConfig,
10742
- ctx,
10743
- options,
10744
- executionOverride
10745
- }) {
10746
- let result;
10747
- let reportText = "";
10748
- try {
10749
- const response = await executeProviderOperation({
11927
+ ctx: ctx2,
11928
+ signal,
11929
+ options,
11930
+ input: input2,
11931
+ onProgress,
11932
+ executionOverride: executionOverride2
11933
+ }) => executeProviderOperation({
10750
11934
  capability: "research",
10751
- config,
11935
+ config: config2,
10752
11936
  provider,
10753
11937
  providerConfig,
10754
- ctx,
10755
- signal: void 0,
11938
+ ctx: ctx2,
11939
+ signal,
10756
11940
  options,
10757
- input: request.input,
10758
- onProgress: (message) => {
10759
- request.progress = summarizeWebResearchProgress(
10760
- message,
10761
- provider.label
10762
- );
10763
- updateWebResearchWidget();
10764
- },
10765
- executionOverride
10766
- });
10767
- const completedAt = (/* @__PURE__ */ new Date()).toISOString();
10768
- result = {
10769
- ...request,
10770
- status: "completed",
10771
- completedAt,
10772
- elapsedMs: Math.max(
10773
- 0,
10774
- Date.parse(completedAt) - Date.parse(request.startedAt)
10775
- ),
10776
- itemCount: response.itemCount
10777
- };
10778
- reportText = response.text;
10779
- } catch (error) {
10780
- const completedAt = (/* @__PURE__ */ new Date()).toISOString();
10781
- result = {
10782
- ...request,
10783
- status: "failed",
10784
- completedAt,
10785
- elapsedMs: Math.max(
10786
- 0,
10787
- Date.parse(completedAt) - Date.parse(request.startedAt)
10788
- ),
10789
- error: formatErrorMessage(error)
10790
- };
10791
- }
10792
- try {
10793
- await writeWebResearchArtifact(result, reportText);
10794
- pi.sendMessage({
10795
- customType: WEB_RESEARCH_RESULT_MESSAGE_TYPE,
10796
- content: formatWebResearchResultMessage(result, reportText),
10797
- display: true,
10798
- details: result
10799
- });
10800
- } finally {
10801
- activeWebResearchRequests.delete(request.id);
10802
- updateWebResearchWidget();
10803
- }
10804
- }
10805
- function createWebResearchRequest(cwd, provider, input) {
10806
- const startedAt = (/* @__PURE__ */ new Date()).toISOString();
10807
- return {
10808
- tool: "web_research",
10809
- id: randomUUID(),
10810
- provider,
10811
- input,
10812
- outputPath: buildWebResearchArtifactPath(cwd, input, startedAt),
10813
- startedAt
10814
- };
10815
- }
10816
- function buildWebResearchArtifactPath(cwd, input, startedAt) {
10817
- const timestamp = startedAt.replaceAll(":", "-").replace(".", "-");
10818
- const slug = slugifyWebResearchInput(input);
10819
- return join2(cwd, RESEARCH_ARTIFACTS_DIR, `${timestamp}-${slug}.md`);
10820
- }
10821
- function slugifyWebResearchInput(input) {
10822
- const slug = input.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60).replace(/-+$/g, "");
10823
- return slug.length > 0 ? slug : "research";
11941
+ input: input2,
11942
+ onProgress,
11943
+ executionOverride: executionOverride2
11944
+ }),
11945
+ deliverResult: (message) => pi.sendMessage(message),
11946
+ onJobsChanged: () => updateWebResearchWidget(ctx),
11947
+ resultMessageType: WEB_RESEARCH_RESULT_MESSAGE_TYPE
11948
+ });
10824
11949
  }
10825
11950
  function buildWebResearchWidgetLines(requests, theme, now = Date.now()) {
10826
- const lines = [theme.fg("accent", "Research jobs:")];
10827
- for (const request of requests.slice().sort((left, right) => left.startedAt.localeCompare(right.startedAt)).slice(0, 3)) {
10828
- const providerLabel = PROVIDERS_BY_ID[request.provider]?.label ?? request.provider;
10829
- const elapsed = formatCompactElapsed(now - Date.parse(request.startedAt));
10830
- const icon = getWebResearchWidgetIcon(request, now);
10831
- lines.push(
10832
- `${icon}${providerLabel} ${theme.fg("muted", `(${elapsed}): `)}${truncateInline(cleanSingleLine(request.input), 70)}`
10833
- );
10834
- }
10835
- if (requests.length > 3) {
10836
- lines.push(theme.fg("muted", `+${requests.length - 3} more`));
10837
- }
10838
- return lines;
10839
- }
10840
- function getWebResearchWidgetIcon(request, _now) {
10841
- if (request.progress === "poll retrying after transient errors") {
10842
- return "\u27F3 ";
10843
- }
10844
- if (request.progress === "queued") {
10845
- return "\u25CC ";
10846
- }
10847
- if (request.progress === "starting") {
10848
- return "\u25D4 ";
10849
- }
10850
- if (request.progress?.startsWith("started:")) {
10851
- return "\u25D1 ";
10852
- }
10853
- return "\u25CF ";
10854
- }
10855
- function summarizeWebResearchProgress(message, providerLabel) {
10856
- const startingMessage = `Starting research via ${providerLabel}`;
10857
- if (message === startingMessage) {
10858
- return "starting";
10859
- }
10860
- const startedPrefix = `${providerLabel} research started: `;
10861
- if (message.startsWith(startedPrefix)) {
10862
- return `started: ${message.slice(startedPrefix.length)}`;
10863
- }
10864
- const statusPrefix = `Research via ${providerLabel}: `;
10865
- if (message.startsWith(statusPrefix)) {
10866
- return message.slice(statusPrefix.length).replace(/\s+\([^)]* elapsed\)$/u, "").trim();
10867
- }
10868
- const retryPrefix = `${providerLabel} research poll is still retrying after transient errors`;
10869
- if (message.startsWith(retryPrefix)) {
10870
- return "poll retrying after transient errors";
11951
+ const sorted = requests.slice().sort((left, right) => left.startedAt.localeCompare(right.startedAt));
11952
+ const jobs = sorted.slice(0, 3).map((request) => {
11953
+ const providerLabel2 = PROVIDERS_BY_ID[request.provider]?.label ?? request.provider;
11954
+ const elapsed = formatCompactElapsed2(now - Date.parse(request.startedAt));
11955
+ const icon = getWebResearchProgressIcon(request);
11956
+ const status = request.progress === "cancelling" ? " cancelling" : "";
11957
+ return `${icon} ${providerLabel2} ${elapsed}${status}`;
11958
+ });
11959
+ if (sorted.length > 3) {
11960
+ jobs.push(`+${sorted.length - 3} more`);
10871
11961
  }
10872
- return message.trim();
11962
+ const count = sorted.length === 1 ? "1 research" : `${sorted.length} researches`;
11963
+ return [
11964
+ `${theme.fg("accent", `${count} running`)}${theme.fg("muted", ` \xB7 ${jobs.join(" \xB7 ")} \xB7 /web-research`)}`
11965
+ ];
10873
11966
  }
10874
- function formatCompactElapsed(ms) {
11967
+ function formatCompactElapsed2(ms) {
10875
11968
  const totalSeconds = Math.max(0, Math.floor(ms / 1e3));
10876
11969
  const minutes = Math.floor(totalSeconds / 60);
10877
11970
  const seconds = totalSeconds % 60;
@@ -10880,68 +11973,6 @@ function formatCompactElapsed(ms) {
10880
11973
  }
10881
11974
  return `${totalSeconds}s`;
10882
11975
  }
10883
- function formatWebResearchResultMessage(result, reportText) {
10884
- const text = reportText.trim();
10885
- if (text.length > 0) {
10886
- return `${text}
10887
- `;
10888
- }
10889
- if (result.error) {
10890
- return `${result.error}
10891
- `;
10892
- }
10893
- return "";
10894
- }
10895
- async function writeWebResearchArtifact(result, reportText) {
10896
- await mkdir2(dirname2(result.outputPath), { recursive: true });
10897
- await writeFile2(
10898
- result.outputPath,
10899
- formatWebResearchArtifact(result, reportText),
10900
- "utf-8"
10901
- );
10902
- }
10903
- function formatWebResearchArtifact(result, reportText) {
10904
- const providerLabel = PROVIDERS_BY_ID[result.provider]?.label ?? result.provider;
10905
- const lines = [
10906
- "# Web research report",
10907
- "",
10908
- "## Query",
10909
- result.input,
10910
- "",
10911
- "## Provider",
10912
- providerLabel,
10913
- "",
10914
- "## Status",
10915
- result.status,
10916
- "",
10917
- "## Started",
10918
- result.startedAt,
10919
- "",
10920
- "## Completed",
10921
- result.completedAt,
10922
- "",
10923
- "## Elapsed",
10924
- formatElapsed(result.elapsedMs)
10925
- ];
10926
- if (typeof result.itemCount === "number") {
10927
- lines.push("", "## Items", String(result.itemCount));
10928
- }
10929
- if (result.error) {
10930
- lines.push("", "## Error", result.error);
10931
- }
10932
- if (reportText) {
10933
- lines.push("", "## Report", reportText);
10934
- }
10935
- return `${lines.join("\n")}
10936
- `;
10937
- }
10938
- function trackPendingResearchTask(task) {
10939
- const tracked = task.catch(() => {
10940
- }).finally(() => {
10941
- pendingResearchTasks.delete(tracked);
10942
- });
10943
- pendingResearchTasks.add(tracked);
10944
- }
10945
11976
  async function executeBatchedContentsTool({
10946
11977
  config,
10947
11978
  provider,
@@ -11103,10 +12134,10 @@ function createToolProgressReporter(capability, providerId, progress) {
11103
12134
  if (Date.now() - lastUpdateAt < RESEARCH_HEARTBEAT_MS) {
11104
12135
  return;
11105
12136
  }
11106
- const providerLabel = PROVIDERS_BY_ID[providerId]?.label ?? providerId;
12137
+ const providerLabel2 = PROVIDERS_BY_ID[providerId]?.label ?? providerId;
11107
12138
  const elapsed = formatElapsed(Date.now() - startedAt);
11108
12139
  emit(
11109
- `Researching via ${providerLabel} (${elapsed} elapsed)`,
12140
+ `Researching via ${providerLabel2} (${elapsed} elapsed)`,
11110
12141
  buildProgressDisplay2(providerId, `Researching ${elapsed}`)
11111
12142
  );
11112
12143
  lastUpdateAt = Date.now();
@@ -11129,38 +12160,38 @@ function renderListCallHeader(toolName, items, theme, suffix, options = {}) {
11129
12160
  invalidate() {
11130
12161
  },
11131
12162
  render(width) {
11132
- const normalizedItems = items.map((item) => cleanSingleLine(item)).filter((item) => item.length > 0);
12163
+ const normalizedItems = items.map((item) => cleanSingleLine2(item)).filter((item) => item.length > 0);
11133
12164
  const toolTitle = theme.fg("toolTitle", theme.bold(toolName));
11134
12165
  const mutedSuffix = suffix ? theme.fg("muted", suffix) : "";
11135
12166
  if (!options.forceMultiline && normalizedItems.length === 1) {
11136
12167
  const singleItem = options.quoteSingleItem ? formatQuotedPreview(normalizedItems[0], 80) : truncateInline(normalizedItems[0], 120);
11137
12168
  const inline = `${toolTitle} ${theme.fg("accent", singleItem)}${mutedSuffix}`;
11138
- const line = truncateToWidth(inline.trimEnd(), width);
11139
- return [line + " ".repeat(Math.max(0, width - visibleWidth(line)))];
12169
+ const line = truncateToWidth2(inline.trimEnd(), width);
12170
+ return [line + " ".repeat(Math.max(0, width - visibleWidth2(line)))];
11140
12171
  }
11141
12172
  let header = toolTitle;
11142
12173
  if (mutedSuffix) {
11143
12174
  header += mutedSuffix;
11144
12175
  }
11145
12176
  const lines = [];
11146
- const headerLine = truncateToWidth(header.trimEnd(), width);
12177
+ const headerLine = truncateToWidth2(header.trimEnd(), width);
11147
12178
  lines.push(
11148
- headerLine + " ".repeat(Math.max(0, width - visibleWidth(headerLine)))
12179
+ headerLine + " ".repeat(Math.max(0, width - visibleWidth2(headerLine)))
11149
12180
  );
11150
12181
  for (const item of normalizedItems) {
11151
12182
  const itemLines = options.forceMultiline ? wrapTextWithAnsi(
11152
12183
  theme.fg("accent", item),
11153
12184
  Math.max(1, width - 2)
11154
12185
  ).map((line) => ` ${line}`) : [
11155
- truncateToWidth(
12186
+ truncateToWidth2(
11156
12187
  ` ${theme.fg("accent", truncateInline(item, 120))}`,
11157
12188
  width
11158
12189
  )
11159
12190
  ];
11160
12191
  for (const itemLine of itemLines) {
11161
- const line = truncateToWidth(itemLine, width);
12192
+ const line = truncateToWidth2(itemLine, width);
11162
12193
  lines.push(
11163
- line + " ".repeat(Math.max(0, width - visibleWidth(line)))
12194
+ line + " ".repeat(Math.max(0, width - visibleWidth2(line)))
11164
12195
  );
11165
12196
  }
11166
12197
  }
@@ -11255,7 +12286,8 @@ function renderWebResearchResultMessage(message, { expanded }, theme, symbols =
11255
12286
  const text = typeof message.content === "string" ? message.content : extractTextContent(message.content);
11256
12287
  const details = isWebResearchResult(message.details) ? message.details : void 0;
11257
12288
  const isSuccess = details?.status === "completed";
11258
- const accent = isSuccess ? "success" : "error";
12289
+ const isCancelled = details?.status === "cancelled";
12290
+ const accent = isSuccess ? "success" : isCancelled ? "warning" : "error";
11259
12291
  const box = new Box(1, 1, (value) => theme.bg("customMessageBg", value));
11260
12292
  if (!expanded) {
11261
12293
  const summary = details ? buildWebResearchResultSummaryLine(details, theme, symbols) : theme.fg(accent, "Web research update");
@@ -11265,10 +12297,42 @@ function renderWebResearchResultMessage(message, { expanded }, theme, symbols =
11265
12297
  return box;
11266
12298
  }
11267
12299
  box.addChild(
11268
- details ? renderMarkdownBlock(renderWebResearchResultMarkdown(details)) : isSuccess ? renderMarkdownBlock(text ?? "") : renderBlockText(text ?? "", theme, "error")
12300
+ details ? renderMarkdownBlock(renderWebResearchResultMarkdown(details)) : isSuccess ? renderMarkdownBlock(text ?? "") : renderBlockText(
12301
+ text ?? "",
12302
+ theme,
12303
+ isCancelled ? "toolOutput" : "error"
12304
+ )
11269
12305
  );
11270
12306
  return box;
11271
12307
  }
12308
+ function formatWebResearchReportMessage(title, body, item) {
12309
+ const provenance = `Saved web research report "${title}" (provider: ${item.provider || "unknown"}, status: ${item.status}, artifact: \`${item.outputPath}\`):`;
12310
+ return `${provenance}
12311
+
12312
+ ${body}
12313
+ `;
12314
+ }
12315
+ function renderWebResearchReportMessage(message, { expanded }, theme) {
12316
+ const text = typeof message.content === "string" ? message.content : extractTextContent(message.content);
12317
+ const details = isWebResearchReportDetails(message.details) ? message.details : void 0;
12318
+ const box = new Box(1, 1, (value) => theme.bg("customMessageBg", value));
12319
+ if (!expanded) {
12320
+ const title = details?.title ?? "saved report";
12321
+ box.addChild(
12322
+ new Text(
12323
+ `${theme.fg("success", `Injected research report: ${title}`)}${theme.fg("muted", ` (${getExpandHint()})`)}`,
12324
+ 0,
12325
+ 0
12326
+ )
12327
+ );
12328
+ return box;
12329
+ }
12330
+ box.addChild(renderMarkdownBlock(text ?? ""));
12331
+ return box;
12332
+ }
12333
+ function isWebResearchReportDetails(details) {
12334
+ return typeof details === "object" && details !== null && "title" in details && "outputPath" in details && !("tool" in details);
12335
+ }
11272
12336
  function renderWebResearchRequestMarkdown(request) {
11273
12337
  return [
11274
12338
  "### Web research",
@@ -11294,7 +12358,7 @@ function renderWebResearchResultMarkdown(result) {
11294
12358
  ].join("\n");
11295
12359
  }
11296
12360
  function buildWebResearchResultSummaryLine(result, theme, symbols) {
11297
- const providerLabel = PROVIDERS_BY_ID[result.provider]?.label ?? result.provider;
12361
+ const providerLabel2 = PROVIDERS_BY_ID[result.provider]?.label ?? result.provider;
11298
12362
  if (result.status === "completed") {
11299
12363
  return renderSuccessSummary(
11300
12364
  `${formatSummaryElapsed(result.elapsedMs)} \xB7 ${basename(result.outputPath)}`,
@@ -11302,8 +12366,8 @@ function buildWebResearchResultSummaryLine(result, theme, symbols) {
11302
12366
  symbols
11303
12367
  );
11304
12368
  }
11305
- const statusText = result.status === "cancelled" ? `${providerLabel} research canceled after ${formatSummaryElapsed(result.elapsedMs)}` : `${providerLabel} research failed after ${formatSummaryElapsed(result.elapsedMs)}`;
11306
- const errorSuffix = result.error ? `: ${normalizeProviderFailureDetail(providerLabel, result.error)}` : "";
12369
+ const statusText = result.status === "cancelled" ? `${providerLabel2} research canceled after ${formatSummaryElapsed(result.elapsedMs)}` : `${providerLabel2} research failed after ${formatSummaryElapsed(result.elapsedMs)}`;
12370
+ const errorSuffix = result.error ? `: ${normalizeProviderFailureDetail(providerLabel2, result.error)}` : "";
11307
12371
  return renderFailureSummary(`${statusText}${errorSuffix}`, theme, symbols);
11308
12372
  }
11309
12373
  function isWebResearchRequest(details) {
@@ -11402,7 +12466,7 @@ function renderEntryList(width, theme, entries, selection) {
11402
12466
  const paddedLabel = entry.label.padEnd(labelWidth, " ");
11403
12467
  const label = selected ? theme.fg("accent", paddedLabel) : paddedLabel;
11404
12468
  const value = selected ? theme.fg("accent", entry.currentValue) : theme.fg("muted", entry.currentValue);
11405
- return truncateToWidth(`${prefix}${label} ${value}`, width);
12469
+ return truncateToWidth2(`${prefix}${label} ${value}`, width);
11406
12470
  });
11407
12471
  }
11408
12472
  function renderSelectedEntryDescription(width, theme, entry) {
@@ -11410,7 +12474,7 @@ function renderSelectedEntryDescription(width, theme, entry) {
11410
12474
  return [];
11411
12475
  }
11412
12476
  return wrapTextWithAnsi(entry.description, Math.max(10, width - 2)).map(
11413
- (line) => truncateToWidth(theme.fg("dim", line), width)
12477
+ (line) => truncateToWidth2(theme.fg("dim", line), width)
11414
12478
  );
11415
12479
  }
11416
12480
  function formatProviderCapabilityChecks(providerId, theme) {
@@ -11591,7 +12655,7 @@ var WebProvidersSettingsView = class {
11591
12655
  }
11592
12656
  lines.push("");
11593
12657
  lines.push(
11594
- truncateToWidth(
12658
+ truncateToWidth2(
11595
12659
  this.theme.fg(
11596
12660
  "dim",
11597
12661
  "\u2191\u2193 move \xB7 Tab/Shift+Tab switch section \xB7 Enter edit/open \xB7 Esc close"
@@ -11610,7 +12674,7 @@ var WebProvidersSettingsView = class {
11610
12674
  this.tui.requestRender();
11611
12675
  return;
11612
12676
  }
11613
- const kb = getKeybindings();
12677
+ const kb = getKeybindings2();
11614
12678
  const entries = this.getActiveSectionEntries();
11615
12679
  if (kb.matches(data, "tui.select.up")) {
11616
12680
  if (entries.length > 0) {
@@ -11620,9 +12684,9 @@ var WebProvidersSettingsView = class {
11620
12684
  if (entries.length > 0) {
11621
12685
  this.moveSelection(1);
11622
12686
  }
11623
- } else if (matchesKey(data, Key.tab)) {
12687
+ } else if (matchesKey2(data, Key2.tab)) {
11624
12688
  this.moveSection(1);
11625
- } else if (matchesKey(data, Key.shift("tab"))) {
12689
+ } else if (matchesKey2(data, Key2.shift("tab"))) {
11626
12690
  this.moveSection(-1);
11627
12691
  } else if (kb.matches(data, "tui.select.confirm") || data === " ") {
11628
12692
  void this.activateCurrentEntry();
@@ -11755,14 +12819,14 @@ var WebProvidersSettingsView = class {
11755
12819
  Math.max(20, Math.floor(width * 0.45))
11756
12820
  );
11757
12821
  const lines = [
11758
- truncateToWidth(
12822
+ truncateToWidth2(
11759
12823
  this.activeSection === section ? this.theme.fg("accent", this.theme.bold(title)) : this.theme.bold(title),
11760
12824
  width
11761
12825
  )
11762
12826
  ];
11763
12827
  if (section === "provider") {
11764
12828
  lines.push(
11765
- truncateToWidth(
12829
+ truncateToWidth2(
11766
12830
  this.theme.fg(
11767
12831
  "dim",
11768
12832
  ` ${"Provider".padEnd(labelWidth, " ")} S C A R Status`
@@ -11777,15 +12841,15 @@ var WebProvidersSettingsView = class {
11777
12841
  const paddedLabel = entry.label.padEnd(labelWidth, " ");
11778
12842
  const label = selected ? this.theme.fg("accent", paddedLabel) : paddedLabel;
11779
12843
  if (entry.currentValue.trim().length === 0) {
11780
- lines.push(truncateToWidth(`${prefix}${label}`, width));
12844
+ lines.push(truncateToWidth2(`${prefix}${label}`, width));
11781
12845
  continue;
11782
12846
  }
11783
12847
  const value = entry.preserveValueStyle ? entry.currentValue : selected ? this.theme.fg("accent", entry.currentValue) : this.theme.fg("muted", entry.currentValue);
11784
- lines.push(truncateToWidth(`${prefix}${label} ${value}`, width));
12848
+ lines.push(truncateToWidth2(`${prefix}${label} ${value}`, width));
11785
12849
  }
11786
12850
  if (section === "provider") {
11787
12851
  lines.push(
11788
- truncateToWidth(
12852
+ truncateToWidth2(
11789
12853
  this.theme.fg("dim", " S=Search C=Contents A=Answer R=Research"),
11790
12854
  width
11791
12855
  )
@@ -11920,7 +12984,7 @@ var ToolSettingsSubmenu = class {
11920
12984
  }
11921
12985
  const entries = this.getEntries();
11922
12986
  const lines = [
11923
- truncateToWidth(
12987
+ truncateToWidth2(
11924
12988
  this.theme.fg("accent", TOOL_INFO[this.toolId].label),
11925
12989
  width
11926
12990
  ),
@@ -11936,7 +13000,7 @@ var ToolSettingsSubmenu = class {
11936
13000
  }
11937
13001
  lines.push("");
11938
13002
  lines.push(
11939
- truncateToWidth(
13003
+ truncateToWidth2(
11940
13004
  this.theme.fg("dim", "\u2191\u2193 move \xB7 Enter edit/toggle \xB7 Esc back"),
11941
13005
  width
11942
13006
  )
@@ -11952,7 +13016,7 @@ var ToolSettingsSubmenu = class {
11952
13016
  this.tui.requestRender();
11953
13017
  return;
11954
13018
  }
11955
- const kb = getKeybindings();
13019
+ const kb = getKeybindings2();
11956
13020
  const entries = this.getEntries();
11957
13021
  if (kb.matches(data, "tui.select.up")) {
11958
13022
  if (this.selection > 0) {
@@ -12155,7 +13219,7 @@ var ProviderSettingsSubmenu = class {
12155
13219
  const providerConfig = this.getProviderConfig();
12156
13220
  const entries = this.getEntries();
12157
13221
  const lines = [
12158
- truncateToWidth(this.theme.fg("accent", provider.label), width),
13222
+ truncateToWidth2(this.theme.fg("accent", provider.label), width),
12159
13223
  "",
12160
13224
  ...renderEntryList(width, this.theme, entries, this.selection)
12161
13225
  ];
@@ -12172,10 +13236,10 @@ var ProviderSettingsSubmenu = class {
12172
13236
  );
12173
13237
  lines.push("");
12174
13238
  lines.push(
12175
- truncateToWidth(this.theme.fg("dim", `Status: ${status}`), width)
13239
+ truncateToWidth2(this.theme.fg("dim", `Status: ${status}`), width)
12176
13240
  );
12177
13241
  lines.push(
12178
- truncateToWidth(
13242
+ truncateToWidth2(
12179
13243
  this.theme.fg("dim", "\u2191\u2193 move \xB7 Enter edit/toggle \xB7 Esc back"),
12180
13244
  width
12181
13245
  )
@@ -12191,7 +13255,7 @@ var ProviderSettingsSubmenu = class {
12191
13255
  this.tui.requestRender();
12192
13256
  return;
12193
13257
  }
12194
- const kb = getKeybindings();
13258
+ const kb = getKeybindings2();
12195
13259
  const entries = this.getEntries();
12196
13260
  if (kb.matches(data, "tui.select.up")) {
12197
13261
  if (this.selection > 0) {
@@ -12326,12 +13390,12 @@ var TextValueSubmenu = class {
12326
13390
  editor;
12327
13391
  render(width) {
12328
13392
  return [
12329
- truncateToWidth(this.theme.fg("accent", this.title), width),
13393
+ truncateToWidth2(this.theme.fg("accent", this.title), width),
12330
13394
  "",
12331
13395
  ...this.editor.render(width),
12332
13396
  "",
12333
- truncateToWidth(this.theme.fg("dim", this.help), width),
12334
- truncateToWidth(
13397
+ truncateToWidth2(this.theme.fg("dim", this.help), width),
13398
+ truncateToWidth2(
12335
13399
  this.theme.fg(
12336
13400
  "dim",
12337
13401
  "Enter to save \xB7 Shift+Enter for newline \xB7 Esc to cancel"
@@ -12344,7 +13408,7 @@ var TextValueSubmenu = class {
12344
13408
  this.editor.invalidate();
12345
13409
  }
12346
13410
  handleInput(data) {
12347
- if (matchesKey(data, Key.escape)) {
13411
+ if (matchesKey2(data, Key2.escape)) {
12348
13412
  this.done(void 0);
12349
13413
  return;
12350
13414
  }
@@ -12468,7 +13532,7 @@ function getSearchQueriesForDisplay(queries) {
12468
13532
  function getAnswerQueriesForDisplay(queries) {
12469
13533
  return getSearchQueriesForDisplay(queries);
12470
13534
  }
12471
- function createBatchCompletionReporter(verb, providerId, providerLabel, total, report) {
13535
+ function createBatchCompletionReporter(verb, providerId, providerLabel2, total, report) {
12472
13536
  if (!report) {
12473
13537
  return {
12474
13538
  start: () => {
@@ -12482,7 +13546,7 @@ function createBatchCompletionReporter(verb, providerId, providerLabel, total, r
12482
13546
  let completedCount = 0;
12483
13547
  let failedCount = 0;
12484
13548
  const emit = () => {
12485
- let message = `${verb} via ${providerLabel}: ${completedCount}/${total} completed`;
13549
+ let message = `${verb} via ${providerLabel2}: ${completedCount}/${total} completed`;
12486
13550
  if (failedCount > 0) {
12487
13551
  message += `, ${failedCount} failed`;
12488
13552
  }
@@ -12534,8 +13598,8 @@ function renderMarkdownBlock(text) {
12534
13598
  if (!text) {
12535
13599
  return new Text("", 0, 0);
12536
13600
  }
12537
- return new Markdown(`
12538
- ${text}`, 0, 0, getMarkdownTheme());
13601
+ return new Markdown2(`
13602
+ ${text}`, 0, 0, getMarkdownTheme2());
12539
13603
  }
12540
13604
  function renderBlockText(text, theme, color) {
12541
13605
  if (!text) {
@@ -12569,12 +13633,12 @@ function prefixWithSymbol(text, symbol) {
12569
13633
  }
12570
13634
  function renderToolProgress(display, fallbackText, theme) {
12571
13635
  const progress = display?.progress;
12572
- const providerLabel = display?.provider?.label;
12573
- if (!progress || !providerLabel) {
13636
+ const providerLabel2 = display?.provider?.label;
13637
+ if (!progress || !providerLabel2) {
12574
13638
  return renderSimpleText(fallbackText ?? "Working\u2026", theme, "warning");
12575
13639
  }
12576
13640
  return new Text(
12577
- `${theme.fg("warning", progress.action)} ${theme.fg("muted", `via ${providerLabel}`)}`,
13641
+ `${theme.fg("warning", progress.action)} ${theme.fg("muted", `via ${providerLabel2}`)}`,
12578
13642
  0,
12579
13643
  0
12580
13644
  );
@@ -12626,15 +13690,15 @@ function buildFailureSummary({
12626
13690
  fallback
12627
13691
  }) {
12628
13692
  const detail = stripTrailingSentencePunctuation(getFirstLine2(text) ?? "");
12629
- const providerLabel = details?.provider !== void 0 ? PROVIDERS_BY_ID[details.provider]?.label ?? details.provider : void 0;
12630
- if (!providerLabel) {
13693
+ const providerLabel2 = details?.provider !== void 0 ? PROVIDERS_BY_ID[details.provider]?.label ?? details.provider : void 0;
13694
+ if (!providerLabel2) {
12631
13695
  return detail || fallback;
12632
13696
  }
12633
- return formatProviderCapabilityFailure(providerLabel, capability, detail);
13697
+ return formatProviderCapabilityFailure(providerLabel2, capability, detail);
12634
13698
  }
12635
- function formatProviderCapabilityFailure(providerLabel, capability, detail) {
13699
+ function formatProviderCapabilityFailure(providerLabel2, capability, detail) {
12636
13700
  const action = getFailureAction(capability);
12637
- const base2 = `${providerLabel} ${action} failed`;
13701
+ const base2 = `${providerLabel2} ${action} failed`;
12638
13702
  if (!detail || detail === base2) {
12639
13703
  return base2;
12640
13704
  }
@@ -12642,14 +13706,14 @@ function formatProviderCapabilityFailure(providerLabel, capability, detail) {
12642
13706
  return detail;
12643
13707
  }
12644
13708
  const normalizedDetail = normalizeProviderFailureDetail(
12645
- providerLabel,
13709
+ providerLabel2,
12646
13710
  detail
12647
13711
  );
12648
13712
  return `${base2}: ${normalizedDetail}`;
12649
13713
  }
12650
- function normalizeProviderFailureDetail(providerLabel, detail) {
13714
+ function normalizeProviderFailureDetail(providerLabel2, detail) {
12651
13715
  const normalized = stripTrailingSentencePunctuation(detail);
12652
- const providerPrefix = `${providerLabel}:`;
13716
+ const providerPrefix = `${providerLabel2}:`;
12653
13717
  return normalized.toLowerCase().startsWith(providerPrefix.toLowerCase()) ? normalized.slice(providerPrefix.length).trim() : normalized;
12654
13718
  }
12655
13719
  function getFailureAction(capability) {
@@ -12698,7 +13762,7 @@ function getFirstLine2(text) {
12698
13762
  }
12699
13763
  function getExpandHint() {
12700
13764
  try {
12701
- const keys = getKeybindings().getKeys("app.tools.expand");
13765
+ const keys = getKeybindings2().getKeys("app.tools.expand");
12702
13766
  if (keys.length > 0) {
12703
13767
  return `${keys.join("/")} to expand`;
12704
13768
  }
@@ -12706,11 +13770,11 @@ function getExpandHint() {
12706
13770
  }
12707
13771
  return "ctrl+o to expand";
12708
13772
  }
12709
- function cleanSingleLine(text) {
13773
+ function cleanSingleLine2(text) {
12710
13774
  return text.replace(/\s+/g, " ").trim();
12711
13775
  }
12712
13776
  function formatQuotedPreview(text, maxLength = 80) {
12713
- return `"${truncateInline(cleanSingleLine(text), maxLength)}"`;
13777
+ return `"${truncateInline(cleanSingleLine2(text), maxLength)}"`;
12714
13778
  }
12715
13779
  function formatSearchResponses(outcomes, prefetch) {
12716
13780
  const body = outcomes.map(
@@ -12736,10 +13800,10 @@ function formatSearchOutcomeSection(outcome, index, total) {
12736
13800
  ${body}`;
12737
13801
  }
12738
13802
  function formatSearchHeading(query2) {
12739
- return `"${escapeMarkdownText(cleanSingleLine(query2))}"`;
13803
+ return `"${escapeMarkdownText(cleanSingleLine2(query2))}"`;
12740
13804
  }
12741
13805
  function formatAnswerHeading(query2) {
12742
- return `"${escapeMarkdownText(cleanSingleLine(query2))}"`;
13806
+ return `"${escapeMarkdownText(cleanSingleLine2(query2))}"`;
12743
13807
  }
12744
13808
  function collectSearchResultUrls(outcomes) {
12745
13809
  return outcomes.flatMap(
@@ -12755,7 +13819,7 @@ function formatSearchResponseMarkdown(response) {
12755
13819
  `${index + 1}. ${formatMarkdownLink(result.title, result.url)}`
12756
13820
  ];
12757
13821
  if (result.snippet) {
12758
- lines.push(` ${escapeMarkdownText(cleanSingleLine(result.snippet))}`);
13822
+ lines.push(` ${escapeMarkdownText(cleanSingleLine2(result.snippet))}`);
12759
13823
  }
12760
13824
  return lines.join("\n");
12761
13825
  }).join("\n\n");
@@ -12764,7 +13828,7 @@ function formatMarkdownLink(label, url2) {
12764
13828
  return `[${escapeMarkdownLinkLabel(label)}](<${url2}>)`;
12765
13829
  }
12766
13830
  function escapeMarkdownLinkLabel(text) {
12767
- return cleanSingleLine(text).replaceAll("\\", "\\\\").replaceAll("]", "\\]");
13831
+ return cleanSingleLine2(text).replaceAll("\\", "\\\\").replaceAll("]", "\\]");
12768
13832
  }
12769
13833
  function escapeMarkdownText(text) {
12770
13834
  return text.replaceAll("\\", "\\\\").replaceAll("*", "\\*").replaceAll("_", "\\_").replaceAll("`", "\\`").replaceAll("#", "\\#").replaceAll("[", "\\[").replaceAll("]", "\\]");
@@ -12785,10 +13849,10 @@ async function truncateAndSaveWithMetadata(text, prefix) {
12785
13849
  truncated: false
12786
13850
  };
12787
13851
  }
12788
- const dir = join2(tmpdir(), `pi-web-providers-${prefix}-${Date.now()}`);
12789
- await mkdir2(dir, { recursive: true });
12790
- const fullPath = join2(dir, "output.txt");
12791
- await writeFile2(fullPath, text, "utf-8");
13852
+ const dir = join3(tmpdir(), `pi-web-providers-${prefix}-${Date.now()}`);
13853
+ await mkdir3(dir, { recursive: true });
13854
+ const fullPath = join3(dir, "output.txt");
13855
+ await writeFile3(fullPath, text, "utf-8");
12792
13856
  return {
12793
13857
  text: truncation.content + `
12794
13858
 
@@ -12895,6 +13959,12 @@ var __test__ = {
12895
13959
  }),
12896
13960
  extractTextContent,
12897
13961
  formatWebResearchResultMessage,
13962
+ getActiveWebResearchRequests,
13963
+ getWebResearchTaskSnapshots,
13964
+ cancelWebResearchTask,
13965
+ loadWebResearchHistory,
13966
+ loadWebResearchPreview,
13967
+ loadWebResearchReport,
12898
13968
  getAvailableManagedToolNames,
12899
13969
  getReadyCompatibleProvidersForTool,
12900
13970
  getEnabledCompatibleProvidersForTool: getReadyCompatibleProvidersForTool,
@@ -12912,9 +13982,10 @@ var __test__ = {
12912
13982
  renderProviderToolResult,
12913
13983
  renderWebResearchDispatchResult,
12914
13984
  renderWebResearchResultMessage,
12915
- waitForPendingResearchTasks: async () => {
12916
- await Promise.all([...pendingResearchTasks]);
12917
- },
13985
+ renderWebResearchReportMessage,
13986
+ formatWebResearchReportMessage,
13987
+ buildWebResearchWidgetLines,
13988
+ waitForPendingResearchTasks,
12918
13989
  formatSearchResponses,
12919
13990
  formatAnswerResponses
12920
13991
  };