pi-web-providers 3.3.0 → 3.5.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 +48 -17
  2. package/dist/index.js +2305 -794
  3. package/package.json +17 -17
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";
@@ -1276,6 +1276,12 @@ function literalUnion(values, options) {
1276
1276
  options
1277
1277
  );
1278
1278
  }
1279
+ function boolOrConfig(options) {
1280
+ return Type2.Union(
1281
+ [Type2.Boolean(), Type2.Record(Type2.String(), Type2.Any())],
1282
+ options
1283
+ );
1284
+ }
1279
1285
 
1280
1286
  // src/providers/claude.ts
1281
1287
  var SEARCH_OUTPUT_SCHEMA = {
@@ -1358,6 +1364,11 @@ var claudeOptionsSchema = Type3.Object(
1358
1364
  },
1359
1365
  { description: "Claude options." }
1360
1366
  );
1367
+ var claudeSearchPromptGuidelines = [
1368
+ "Use Claude search when Claude Code's agentic web-browsing and synthesis are useful for finding likely sources.",
1369
+ "Increase effort or maxTurns only for difficult, ambiguous, or multi-step source discovery; keep defaults for simple searches.",
1370
+ "Prefer web_contents after Claude search when the task requires direct inspection of a small set of selected URLs."
1371
+ ];
1361
1372
  var claudeImplementation = {
1362
1373
  id: "claude",
1363
1374
  label: "Claude",
@@ -1615,6 +1626,7 @@ var claudeProvider = defineProvider({
1615
1626
  capabilities: {
1616
1627
  search: defineCapability({
1617
1628
  options: claudeImplementation.getToolOptionsSchema?.("search"),
1629
+ promptGuidelines: claudeSearchPromptGuidelines,
1618
1630
  async execute(input, ctx) {
1619
1631
  const { query: query2, maxResults, options } = input;
1620
1632
  return await claudeImplementation.search(
@@ -1833,6 +1845,11 @@ var codexSearchOptionsSchema = Type5.Object(
1833
1845
  },
1834
1846
  { description: "Codex search options." }
1835
1847
  );
1848
+ var codexSearchPromptGuidelines = [
1849
+ "Use Codex search when the local Codex SDK should perform web-backed source discovery, especially for coding or developer-oriented investigations.",
1850
+ "Use webSearchMode='live' for current information and 'cached' when freshness is less important; do not set 'disabled' for normal web_search calls.",
1851
+ "Increase modelReasoningEffort only for difficult or ambiguous searches where deeper reasoning is worth the extra latency."
1852
+ ];
1836
1853
  var codexImplementation = {
1837
1854
  id: "codex",
1838
1855
  label: "Codex",
@@ -2013,6 +2030,7 @@ var codexProvider = defineProvider({
2013
2030
  capabilities: {
2014
2031
  search: defineCapability({
2015
2032
  options: codexImplementation.getToolOptionsSchema?.("search"),
2033
+ promptGuidelines: codexSearchPromptGuidelines,
2016
2034
  async execute(input, ctx) {
2017
2035
  const { query: query2, maxResults, options } = input;
2018
2036
  return await codexImplementation.search(
@@ -2454,39 +2472,39 @@ function createDefaultExecutionSettings(overrides = {}) {
2454
2472
  function normalizeDiagnosticDetail(detail) {
2455
2473
  return detail.trim().replace(/[.\s]+$/u, "");
2456
2474
  }
2457
- function startsWithProviderLabel(providerLabel, detail) {
2458
- return detail.toLowerCase().startsWith(providerLabel.toLowerCase());
2475
+ function startsWithProviderLabel(providerLabel2, detail) {
2476
+ return detail.toLowerCase().startsWith(providerLabel2.toLowerCase());
2459
2477
  }
2460
2478
  function readsLikeProviderClause(detail) {
2461
2479
  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
2480
  detail
2463
2481
  );
2464
2482
  }
2465
- function formatProviderDiagnostic(providerLabel, detail) {
2483
+ function formatProviderDiagnostic(providerLabel2, detail) {
2466
2484
  const normalized = normalizeDiagnosticDetail(detail);
2467
2485
  if (!normalized) {
2468
- return `${providerLabel} failed.`;
2486
+ return `${providerLabel2} failed.`;
2469
2487
  }
2470
- if (startsWithProviderLabel(providerLabel, normalized)) {
2488
+ if (startsWithProviderLabel(providerLabel2, normalized)) {
2471
2489
  return `${normalized}.`;
2472
2490
  }
2473
2491
  if (readsLikeProviderClause(normalized)) {
2474
- return `${providerLabel} ${normalized}.`;
2492
+ return `${providerLabel2} ${normalized}.`;
2475
2493
  }
2476
- return `${providerLabel}: ${normalized}.`;
2494
+ return `${providerLabel2}: ${normalized}.`;
2477
2495
  }
2478
- function formatResearchTerminalDiagnostic(providerLabel, status, detail) {
2496
+ function formatResearchTerminalDiagnostic(providerLabel2, status, detail) {
2479
2497
  const normalized = detail ? normalizeDiagnosticDetail(detail) : "";
2480
2498
  if (!normalized) {
2481
- return status === "cancelled" ? `${providerLabel} research was canceled.` : `${providerLabel} research failed.`;
2499
+ return status === "cancelled" ? `${providerLabel2} research was canceled.` : `${providerLabel2} research failed.`;
2482
2500
  }
2483
- if (startsWithProviderLabel(providerLabel, normalized)) {
2501
+ if (startsWithProviderLabel(providerLabel2, normalized)) {
2484
2502
  return `${normalized}.`;
2485
2503
  }
2486
2504
  if (/^research\b/iu.test(normalized)) {
2487
- return `${providerLabel} ${normalized}.`;
2505
+ return `${providerLabel2} ${normalized}.`;
2488
2506
  }
2489
- return status === "cancelled" ? `${providerLabel} research was canceled: ${normalized}.` : `${providerLabel} research failed: ${normalized}.`;
2507
+ return status === "cancelled" ? `${providerLabel2} research was canceled: ${normalized}.` : `${providerLabel2} research failed: ${normalized}.`;
2490
2508
  }
2491
2509
 
2492
2510
  // src/execution-policy.ts
@@ -2532,7 +2550,7 @@ async function runWithExecutionPolicy(label, operation, settings, context) {
2532
2550
  throw new Error(`${label} failed.`);
2533
2551
  }
2534
2552
  async function executeAsyncResearch({
2535
- providerLabel,
2553
+ providerLabel: providerLabel2,
2536
2554
  providerId,
2537
2555
  context,
2538
2556
  pollIntervalMs = DEFAULT_RESEARCH_POLL_INTERVAL_MS,
@@ -2541,7 +2559,7 @@ async function executeAsyncResearch({
2541
2559
  start,
2542
2560
  poll
2543
2561
  }) {
2544
- const timeoutMessage = `${providerLabel} research exceeded ${formatDuration(timeoutMs)}.`;
2562
+ const timeoutMessage = `${providerLabel2} research exceeded ${formatDuration(timeoutMs)}.`;
2545
2563
  const deadline = createDeadlineSignal(
2546
2564
  context.signal,
2547
2565
  timeoutMs,
@@ -2555,7 +2573,7 @@ async function executeAsyncResearch({
2555
2573
  let lastProgressStatus;
2556
2574
  const startedAt = Date.now();
2557
2575
  try {
2558
- researchContext.onProgress?.(`Starting research via ${providerLabel}`);
2576
+ researchContext.onProgress?.(`Starting research via ${providerLabel2}`);
2559
2577
  const job = await withAbortAndOptionalTimeout(
2560
2578
  start(researchContext),
2561
2579
  void 0,
@@ -2564,14 +2582,14 @@ async function executeAsyncResearch({
2564
2582
  );
2565
2583
  const jobId = job.id;
2566
2584
  if (!jobId) {
2567
- throw new Error(`${providerLabel} research did not return a job id.`);
2585
+ throw new Error(`${providerLabel2} research did not return a job id.`);
2568
2586
  }
2569
- researchContext.onProgress?.(`${providerLabel} research started: ${jobId}`);
2587
+ researchContext.onProgress?.(`${providerLabel2} research started: ${jobId}`);
2570
2588
  let consecutivePollErrors = 0;
2571
2589
  while (true) {
2572
2590
  throwIfAborted(
2573
2591
  researchContext.signal,
2574
- `${providerLabel} research aborted.`
2592
+ `${providerLabel2} research aborted.`
2575
2593
  );
2576
2594
  try {
2577
2595
  const result = await withAbortAndOptionalTimeout(
@@ -2584,7 +2602,7 @@ async function executeAsyncResearch({
2584
2602
  const progressStatus = result.statusText ?? result.status;
2585
2603
  if (result.status !== lastStatus || progressStatus !== lastProgressStatus) {
2586
2604
  researchContext.onProgress?.(
2587
- `Research via ${providerLabel}: ${progressStatus} (${formatElapsed(Date.now() - startedAt)} elapsed)`
2605
+ `Research via ${providerLabel2}: ${progressStatus} (${formatElapsed(Date.now() - startedAt)} elapsed)`
2588
2606
  );
2589
2607
  lastStatus = result.status;
2590
2608
  lastProgressStatus = progressStatus;
@@ -2592,13 +2610,13 @@ async function executeAsyncResearch({
2592
2610
  if (result.status === "completed") {
2593
2611
  return result.output ?? {
2594
2612
  provider: providerId,
2595
- text: `${providerLabel} research completed without textual output.`
2613
+ text: `${providerLabel2} research completed without textual output.`
2596
2614
  };
2597
2615
  }
2598
2616
  if (result.status === "failed" || result.status === "cancelled") {
2599
2617
  throw new Error(
2600
2618
  formatResearchTerminalDiagnostic(
2601
- providerLabel,
2619
+ providerLabel2,
2602
2620
  result.status,
2603
2621
  result.error
2604
2622
  )
@@ -2614,11 +2632,11 @@ async function executeAsyncResearch({
2614
2632
  consecutivePollErrors += 1;
2615
2633
  if (consecutivePollErrors >= maxConsecutivePollErrors) {
2616
2634
  throw new Error(
2617
- `${providerLabel} research polling failed too many times in a row: ${formatErrorMessage(error)}`
2635
+ `${providerLabel2} research polling failed too many times in a row: ${formatErrorMessage(error)}`
2618
2636
  );
2619
2637
  }
2620
2638
  researchContext.onProgress?.(
2621
- `${providerLabel} research poll is still retrying after transient errors (${consecutivePollErrors}/${maxConsecutivePollErrors} consecutive poll failures). Background job id: ${jobId}`
2639
+ `${providerLabel2} research poll is still retrying after transient errors (${consecutivePollErrors}/${maxConsecutivePollErrors} consecutive poll failures). Background job id: ${jobId}`
2622
2640
  );
2623
2641
  }
2624
2642
  await sleep(pollIntervalMs, researchContext.signal);
@@ -2626,11 +2644,11 @@ async function executeAsyncResearch({
2626
2644
  } catch (error) {
2627
2645
  if (isAbortErrorFromSignal(researchContext.signal, error)) {
2628
2646
  throw new Error(
2629
- formatProviderDiagnostic(providerLabel, formatErrorMessage(error))
2647
+ formatProviderDiagnostic(providerLabel2, formatErrorMessage(error))
2630
2648
  );
2631
2649
  }
2632
2650
  throw new Error(
2633
- formatProviderDiagnostic(providerLabel, formatErrorMessage(error))
2651
+ formatProviderDiagnostic(providerLabel2, formatErrorMessage(error))
2634
2652
  );
2635
2653
  } finally {
2636
2654
  deadline.cleanup();
@@ -2806,9 +2824,9 @@ var exaSearchOptionsSchema = Type6.Object(
2806
2824
  "hybrid",
2807
2825
  "fast",
2808
2826
  "instant",
2827
+ "deep-lite",
2809
2828
  "deep",
2810
- "deep-reasoning",
2811
- "deep-max"
2829
+ "deep-reasoning"
2812
2830
  ],
2813
2831
  { description: "Exa search mode." }
2814
2832
  )
@@ -2826,6 +2844,14 @@ var exaSearchOptionsSchema = Type6.Object(
2826
2844
  excludeDomains: Type6.Optional(
2827
2845
  Type6.Array(Type6.String(), { description: "Exclude these domains." })
2828
2846
  ),
2847
+ startCrawlDate: Type6.Optional(
2848
+ Type6.String({
2849
+ description: "ISO date string for earliest crawl date."
2850
+ })
2851
+ ),
2852
+ endCrawlDate: Type6.Optional(
2853
+ Type6.String({ description: "ISO date string for latest crawl date." })
2854
+ ),
2829
2855
  startPublishedDate: Type6.Optional(
2830
2856
  Type6.String({
2831
2857
  description: "ISO date string for earliest publish date."
@@ -2834,48 +2860,173 @@ var exaSearchOptionsSchema = Type6.Object(
2834
2860
  endPublishedDate: Type6.Optional(
2835
2861
  Type6.String({ description: "ISO date string for latest publish date." })
2836
2862
  ),
2863
+ includeText: Type6.Optional(
2864
+ Type6.Array(Type6.String(), {
2865
+ description: "Require result page text to contain these terms. Exa currently supports one short phrase."
2866
+ })
2867
+ ),
2868
+ excludeText: Type6.Optional(
2869
+ Type6.Array(Type6.String(), {
2870
+ description: "Require result page text not to contain these terms. Exa currently supports one short phrase."
2871
+ })
2872
+ ),
2873
+ systemPrompt: Type6.Optional(
2874
+ Type6.String({
2875
+ description: "Additional Exa instructions for deep search source selection and synthesis."
2876
+ })
2877
+ ),
2878
+ additionalQueries: Type6.Optional(
2879
+ Type6.Array(Type6.String(), {
2880
+ maxItems: 5,
2881
+ description: "Alternative query formulations for Exa deep search variants."
2882
+ })
2883
+ ),
2837
2884
  userLocation: Type6.Optional(
2838
- Type6.Object(
2839
- {
2840
- country: Type6.Optional(
2841
- Type6.String({ description: "Country hint for the user location." })
2842
- ),
2843
- region: Type6.Optional(
2844
- Type6.String({ description: "Region hint for the user location." })
2845
- ),
2846
- city: Type6.Optional(
2847
- Type6.String({ description: "City hint for the user location." })
2848
- ),
2849
- timezone: Type6.Optional(
2850
- Type6.String({
2851
- description: "Timezone hint for the user location."
2852
- })
2853
- )
2854
- },
2855
- {
2856
- description: "User location hint passed through to the Exa SDK."
2857
- }
2858
- )
2885
+ Type6.String({
2886
+ description: "Two-letter ISO country code for the user location, such as 'US'."
2887
+ })
2859
2888
  ),
2860
2889
  contents: Type6.Optional(
2861
2890
  Type6.Object(
2862
2891
  {
2863
2892
  text: Type6.Optional(
2864
- Type6.Boolean({ description: "Include text content." })
2893
+ Type6.Union(
2894
+ [
2895
+ Type6.Boolean(),
2896
+ Type6.Object(
2897
+ {
2898
+ maxCharacters: Type6.Optional(
2899
+ Type6.Integer({
2900
+ minimum: 1,
2901
+ description: "Maximum text characters per result."
2902
+ })
2903
+ ),
2904
+ includeHtmlTags: Type6.Optional(
2905
+ Type6.Boolean({
2906
+ description: "Include HTML tags in returned text."
2907
+ })
2908
+ ),
2909
+ verbosity: Type6.Optional(
2910
+ literalUnion(["compact", "standard", "full"], {
2911
+ description: "Verbosity level for returned text."
2912
+ })
2913
+ )
2914
+ },
2915
+ { additionalProperties: false }
2916
+ )
2917
+ ],
2918
+ { description: "Include text content." }
2919
+ )
2865
2920
  ),
2866
2921
  highlights: Type6.Optional(
2867
- Type6.Boolean({ description: "Include highlighted excerpts." })
2922
+ Type6.Union(
2923
+ [
2924
+ Type6.Boolean(),
2925
+ Type6.Object(
2926
+ {
2927
+ query: Type6.Optional(
2928
+ Type6.String({
2929
+ description: "Query to use for highlights."
2930
+ })
2931
+ ),
2932
+ maxCharacters: Type6.Optional(
2933
+ Type6.Integer({
2934
+ minimum: 1,
2935
+ description: "Maximum highlight characters."
2936
+ })
2937
+ )
2938
+ },
2939
+ { additionalProperties: false }
2940
+ )
2941
+ ],
2942
+ { description: "Include highlighted excerpts." }
2943
+ )
2868
2944
  ),
2869
2945
  summary: Type6.Optional(
2870
- Type6.Boolean({ description: "Include AI-generated summary." })
2946
+ Type6.Union(
2947
+ [
2948
+ Type6.Boolean(),
2949
+ Type6.Object(
2950
+ {
2951
+ query: Type6.Optional(
2952
+ Type6.String({
2953
+ description: "Query to guide summary generation."
2954
+ })
2955
+ )
2956
+ },
2957
+ { additionalProperties: false }
2958
+ )
2959
+ ],
2960
+ { description: "Include AI-generated summary." }
2961
+ )
2962
+ ),
2963
+ livecrawl: Type6.Optional(
2964
+ literalUnion(["never", "fallback", "always", "auto", "preferred"], {
2965
+ description: "Livecrawl mode for fetching fresh content."
2966
+ })
2967
+ ),
2968
+ livecrawlTimeout: Type6.Optional(
2969
+ Type6.Integer({
2970
+ minimum: 0,
2971
+ description: "Livecrawl timeout in milliseconds."
2972
+ })
2973
+ ),
2974
+ maxAgeHours: Type6.Optional(
2975
+ Type6.Number({
2976
+ description: "Maximum age of cached content in hours. Use 0 to always fetch fresh content."
2977
+ })
2978
+ ),
2979
+ filterEmptyResults: Type6.Optional(
2980
+ Type6.Boolean({ description: "Filter results with no contents." })
2981
+ ),
2982
+ subpages: Type6.Optional(
2983
+ Type6.Integer({
2984
+ minimum: 0,
2985
+ description: "Number of subpages to return for each result."
2986
+ })
2987
+ ),
2988
+ subpageTarget: Type6.Optional(
2989
+ Type6.Union([Type6.String(), Type6.Array(Type6.String())], {
2990
+ description: "Text used to match/rank returned subpages."
2991
+ })
2992
+ ),
2993
+ extras: Type6.Optional(
2994
+ Type6.Object(
2995
+ {
2996
+ links: Type6.Optional(
2997
+ Type6.Integer({
2998
+ minimum: 0,
2999
+ description: "Number of page links to include."
3000
+ })
3001
+ ),
3002
+ imageLinks: Type6.Optional(
3003
+ Type6.Integer({
3004
+ minimum: 0,
3005
+ description: "Number of image links to include."
3006
+ })
3007
+ )
3008
+ },
3009
+ { additionalProperties: false }
3010
+ )
2871
3011
  )
2872
3012
  },
2873
- { description: "What content to include in results." }
3013
+ {
3014
+ additionalProperties: false,
3015
+ description: "What content to include in results."
3016
+ }
2874
3017
  )
2875
3018
  )
2876
3019
  },
2877
3020
  { description: "Exa search options." }
2878
3021
  );
3022
+ var exaSearchPromptGuidelines = [
3023
+ "Use Exa's neural/auto search modes for semantic source discovery where exact keywords are uncertain; use keyword mode when exact terms, names, or identifiers matter.",
3024
+ "Use Exa category filters such as 'research paper' or 'company' when the user asks for a specific source type.",
3025
+ "Set includeDomains or excludeDomains when the task names preferred sources, requires primary sources, or needs noisy domains filtered out.",
3026
+ "Use startCrawlDate/endCrawlDate or contents.maxAgeHours when freshness of Exa's crawled content matters.",
3027
+ "Use includeText/excludeText for short required or forbidden phrases in page text.",
3028
+ "Request contents.text, contents.highlights, or contents.summary only when snippets are insufficient and richer source context is needed directly in search results."
3029
+ ];
2879
3030
  var exaImplementation = {
2880
3031
  id: "exa",
2881
3032
  label: "Exa",
@@ -3046,6 +3197,7 @@ var exaProvider = defineProvider({
3046
3197
  capabilities: {
3047
3198
  search: defineCapability({
3048
3199
  options: exaImplementation.getToolOptionsSchema?.("search"),
3200
+ promptGuidelines: exaSearchPromptGuidelines,
3049
3201
  async execute(input, ctx) {
3050
3202
  const { query: query2, maxResults, options } = input;
3051
3203
  return await exaImplementation.search(
@@ -3121,6 +3273,26 @@ var firecrawlSearchOptionsSchema = Type7.Object(
3121
3273
  description: "Search categories to include."
3122
3274
  })
3123
3275
  ),
3276
+ includeDomains: Type7.Optional(
3277
+ Type7.Array(Type7.String(), {
3278
+ description: "Restrict results to these domains."
3279
+ })
3280
+ ),
3281
+ excludeDomains: Type7.Optional(
3282
+ Type7.Array(Type7.String(), {
3283
+ description: "Exclude these domains."
3284
+ })
3285
+ ),
3286
+ tbs: Type7.Optional(
3287
+ Type7.String({
3288
+ description: "Google-style time-based search filter."
3289
+ })
3290
+ ),
3291
+ ignoreInvalidURLs: Type7.Optional(
3292
+ Type7.Boolean({
3293
+ description: "Ignore invalid result URLs returned by search."
3294
+ })
3295
+ ),
3124
3296
  location: Type7.Optional(
3125
3297
  Type7.Object(
3126
3298
  {
@@ -3141,9 +3313,22 @@ var firecrawlSearchOptionsSchema = Type7.Object(
3141
3313
  Type7.Object(
3142
3314
  {
3143
3315
  formats: Type7.Optional(
3144
- Type7.Array(literalUnion(["markdown", "html", "rawHtml"]), {
3145
- description: "Output formats."
3146
- })
3316
+ Type7.Array(
3317
+ literalUnion([
3318
+ "markdown",
3319
+ "html",
3320
+ "rawHtml",
3321
+ "links",
3322
+ "images",
3323
+ "screenshot",
3324
+ "summary",
3325
+ "json",
3326
+ "attributes"
3327
+ ]),
3328
+ {
3329
+ description: "Output formats."
3330
+ }
3331
+ )
3147
3332
  ),
3148
3333
  onlyMainContent: Type7.Optional(
3149
3334
  Type7.Boolean({ description: "Extract only the main content." })
@@ -3157,51 +3342,133 @@ var firecrawlSearchOptionsSchema = Type7.Object(
3157
3342
  },
3158
3343
  { description: "Firecrawl search options." }
3159
3344
  );
3345
+ var firecrawlSearchPromptGuidelines = [
3346
+ "Use Firecrawl search when the task benefits from searchable results that can also include scraped page content through scrapeOptions.",
3347
+ "Set scrapeOptions.formats=['markdown'] and onlyMainContent=true when source snippets are not enough and the user needs extracted page context in the search results.",
3348
+ "Use includeDomains/excludeDomains when search should stay within or avoid specific sites.",
3349
+ "Use lang, country, or location when the user asks for language-specific, country-specific, or local results.",
3350
+ "Prefer web_contents with Firecrawl scrape options after search when only a small set of known URLs needs full extraction."
3351
+ ];
3352
+ var firecrawlScrapeTuningProperties = {
3353
+ onlyMainContent: Type7.Optional(
3354
+ Type7.Boolean({ description: "Extract only the main content." })
3355
+ ),
3356
+ includeTags: Type7.Optional(
3357
+ Type7.Array(Type7.String(), { description: "CSS selectors to include." })
3358
+ ),
3359
+ excludeTags: Type7.Optional(
3360
+ Type7.Array(Type7.String(), { description: "CSS selectors to exclude." })
3361
+ ),
3362
+ waitFor: Type7.Optional(
3363
+ Type7.Integer({
3364
+ minimum: 0,
3365
+ description: "Milliseconds to wait before scraping."
3366
+ })
3367
+ ),
3368
+ headers: Type7.Optional(
3369
+ Type7.Record(Type7.String(), Type7.String(), {
3370
+ description: "Headers to send when scraping."
3371
+ })
3372
+ ),
3373
+ location: Type7.Optional(
3374
+ Type7.Object(
3375
+ {
3376
+ country: Type7.Optional(Type7.String({ description: "Country hint." })),
3377
+ region: Type7.Optional(Type7.String({ description: "Region hint." })),
3378
+ city: Type7.Optional(Type7.String({ description: "City hint." }))
3379
+ },
3380
+ { description: "Location hint for scraping." }
3381
+ )
3382
+ ),
3383
+ mobile: Type7.Optional(
3384
+ Type7.Boolean({ description: "Use a mobile browser profile." })
3385
+ ),
3386
+ proxy: Type7.Optional(
3387
+ Type7.String({
3388
+ description: "Proxy mode passed through to the Firecrawl SDK."
3389
+ })
3390
+ ),
3391
+ fastMode: Type7.Optional(
3392
+ Type7.Boolean({ description: "Use Firecrawl fast mode." })
3393
+ ),
3394
+ blockAds: Type7.Optional(
3395
+ Type7.Boolean({ description: "Block ads while scraping." })
3396
+ ),
3397
+ removeBase64Images: Type7.Optional(
3398
+ Type7.Boolean({
3399
+ description: "Remove base64 image data from scraped output."
3400
+ })
3401
+ ),
3402
+ redactPII: Type7.Optional(
3403
+ Type7.Union(
3404
+ [
3405
+ Type7.Boolean(),
3406
+ Type7.Object(
3407
+ {
3408
+ entities: Type7.Optional(
3409
+ Type7.Array(
3410
+ literalUnion([
3411
+ "PERSON",
3412
+ "EMAIL",
3413
+ "PHONE",
3414
+ "LOCATION",
3415
+ "FINANCIAL",
3416
+ "SECRET"
3417
+ ])
3418
+ )
3419
+ )
3420
+ },
3421
+ { additionalProperties: false }
3422
+ )
3423
+ ],
3424
+ { description: "Redact personal or sensitive data from output." }
3425
+ )
3426
+ ),
3427
+ maxAge: Type7.Optional(
3428
+ Type7.Number({
3429
+ description: "Maximum age of cached scrape data in milliseconds."
3430
+ })
3431
+ ),
3432
+ minAge: Type7.Optional(
3433
+ Type7.Number({
3434
+ description: "Minimum age of cached scrape data in milliseconds."
3435
+ })
3436
+ ),
3437
+ storeInCache: Type7.Optional(
3438
+ Type7.Boolean({ description: "Store scrape result in Firecrawl cache." })
3439
+ ),
3440
+ skipTlsVerification: Type7.Optional(
3441
+ Type7.Boolean({ description: "Skip TLS certificate verification." })
3442
+ )
3443
+ };
3160
3444
  var firecrawlScrapeOptionsSchema = Type7.Object(
3161
3445
  {
3162
3446
  formats: Type7.Optional(
3163
- Type7.Array(literalUnion(["markdown", "html", "rawHtml"]), {
3164
- description: "Output formats for scraping."
3165
- })
3166
- ),
3167
- onlyMainContent: Type7.Optional(
3168
- Type7.Boolean({ description: "Extract only the main content." })
3169
- ),
3170
- includeTags: Type7.Optional(
3171
- Type7.Array(Type7.String(), { description: "CSS selectors to include." })
3172
- ),
3173
- excludeTags: Type7.Optional(
3174
- Type7.Array(Type7.String(), { description: "CSS selectors to exclude." })
3447
+ Type7.Array(
3448
+ literalUnion([
3449
+ "markdown",
3450
+ "html",
3451
+ "rawHtml",
3452
+ "links",
3453
+ "images",
3454
+ "screenshot",
3455
+ "summary",
3456
+ "json",
3457
+ "attributes",
3458
+ "changeTracking"
3459
+ ]),
3460
+ {
3461
+ description: "Output formats for scraping."
3462
+ }
3463
+ )
3175
3464
  ),
3176
- waitFor: Type7.Optional(
3465
+ timeout: Type7.Optional(
3177
3466
  Type7.Integer({
3178
3467
  minimum: 0,
3179
- description: "Milliseconds to wait before scraping."
3180
- })
3181
- ),
3182
- headers: Type7.Optional(
3183
- Type7.Record(Type7.String(), Type7.String(), {
3184
- description: "Headers to send when scraping."
3468
+ description: "Request timeout in milliseconds."
3185
3469
  })
3186
3470
  ),
3187
- location: Type7.Optional(
3188
- Type7.Object(
3189
- {
3190
- country: Type7.Optional(Type7.String({ description: "Country hint." })),
3191
- region: Type7.Optional(Type7.String({ description: "Region hint." })),
3192
- city: Type7.Optional(Type7.String({ description: "City hint." }))
3193
- },
3194
- { description: "Location hint for scraping." }
3195
- )
3196
- ),
3197
- mobile: Type7.Optional(
3198
- Type7.Boolean({ description: "Use a mobile browser profile." })
3199
- ),
3200
- proxy: Type7.Optional(
3201
- Type7.String({
3202
- description: "Proxy mode passed through to the Firecrawl SDK."
3203
- })
3204
- )
3471
+ ...firecrawlScrapeTuningProperties
3205
3472
  },
3206
3473
  { description: "Firecrawl scrape options." }
3207
3474
  );
@@ -3211,44 +3478,7 @@ var firecrawlAnswerOptionsSchema = Type7.Object(
3211
3478
  minLength: 1,
3212
3479
  description: "URL of the page to ask about."
3213
3480
  }),
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
- )
3481
+ ...firecrawlScrapeTuningProperties
3252
3482
  },
3253
3483
  {
3254
3484
  description: "Firecrawl page-question options. The URL is required; the question comes from the web_answer query."
@@ -3531,6 +3761,7 @@ var firecrawlProvider = defineProvider({
3531
3761
  capabilities: {
3532
3762
  search: defineCapability({
3533
3763
  options: firecrawlImplementation.getToolOptionsSchema?.("search"),
3764
+ promptGuidelines: firecrawlSearchPromptGuidelines,
3534
3765
  async execute(input, ctx) {
3535
3766
  const { query: query2, maxResults, options } = input;
3536
3767
  return await firecrawlImplementation.search(
@@ -3653,6 +3884,12 @@ var geminiSearchOptionsSchema = Type8.Object(
3653
3884
  },
3654
3885
  { description: "Gemini search options." }
3655
3886
  );
3887
+ var geminiSearchPromptGuidelines = [
3888
+ "Use Gemini search when a grounded model should perform web-backed source discovery and return likely sources.",
3889
+ "Change model only when the user requests a specific Gemini model or when project configuration requires it.",
3890
+ "Tune generation_config only for explicit output-control needs; avoid disabling tool use for search tasks.",
3891
+ "Prefer web_contents after Gemini search when selected sources need direct extraction or closer reading."
3892
+ ];
3656
3893
  var geminiAnswerOptionsSchema = Type8.Object(
3657
3894
  {
3658
3895
  model: Type8.Optional(
@@ -3713,7 +3950,7 @@ var geminiImplementation = {
3713
3950
  context.signal
3714
3951
  );
3715
3952
  const results = await Promise.all(
3716
- extractGoogleSearchResults(interaction.outputs).slice(0, maxResults).map(async (result) => {
3953
+ extractGoogleSearchResults(readInteractionSteps(interaction)).slice(0, maxResults).map(async (result) => {
3717
3954
  const resolvedUrl = await resolveGoogleSearchUrl(
3718
3955
  result.url,
3719
3956
  context.signal
@@ -3800,7 +4037,7 @@ var geminiImplementation = {
3800
4037
  );
3801
4038
  const status = readNonEmptyString3(interaction.status) ?? "unknown";
3802
4039
  if (status === "completed") {
3803
- const text = formatInteractionOutputs(interaction.outputs);
4040
+ const text = formatInteractionSteps(readInteractionSteps(interaction));
3804
4041
  return {
3805
4042
  status: "completed",
3806
4043
  output: {
@@ -3830,7 +4067,7 @@ var geminiImplementation = {
3830
4067
  if (status === "requires_action") {
3831
4068
  return {
3832
4069
  status: "failed",
3833
- error: describeGeminiRequiredAction(interaction.outputs)
4070
+ error: describeGeminiRequiredAction(readInteractionSteps(interaction))
3834
4071
  };
3835
4072
  }
3836
4073
  return status === "in_progress" ? { status: "in_progress" } : { status: "in_progress", statusText: status };
@@ -3864,17 +4101,20 @@ function addAbortSignalToGeminiConfig(config, signal) {
3864
4101
  abortSignal: signal
3865
4102
  };
3866
4103
  }
3867
- function extractGoogleSearchResults(outputs) {
4104
+ function readInteractionSteps(interaction) {
4105
+ return typeof interaction === "object" && interaction !== null ? interaction.steps : void 0;
4106
+ }
4107
+ function extractGoogleSearchResults(steps) {
3868
4108
  const seen = /* @__PURE__ */ new Set();
3869
4109
  const results = [];
3870
- if (!Array.isArray(outputs)) {
4110
+ if (!Array.isArray(steps)) {
3871
4111
  return results;
3872
4112
  }
3873
- for (const output of outputs) {
3874
- if (typeof output !== "object" || output === null) {
4113
+ for (const step of steps) {
4114
+ if (typeof step !== "object" || step === null) {
3875
4115
  continue;
3876
4116
  }
3877
- const content = output;
4117
+ const content = step;
3878
4118
  if (content.type !== "google_search_result") {
3879
4119
  continue;
3880
4120
  }
@@ -4040,16 +4280,21 @@ function extractGroundingSources(chunks) {
4040
4280
  }
4041
4281
  return sources;
4042
4282
  }
4043
- function formatInteractionOutputs(outputs) {
4283
+ function formatInteractionSteps(steps) {
4044
4284
  const lines = [];
4045
- if (!Array.isArray(outputs)) {
4285
+ if (!Array.isArray(steps)) {
4046
4286
  return "";
4047
4287
  }
4048
- for (const output of outputs) {
4049
- if (typeof output === "object" && output !== null && "type" in output && output.type === "text" && "text" in output && typeof output.text === "string") {
4050
- const text = output.text.trim();
4051
- if (text) {
4052
- lines.push(text);
4288
+ for (const step of steps) {
4289
+ if (typeof step !== "object" || step === null || !("type" in step) || step.type !== "model_output" || !("content" in step) || !Array.isArray(step.content)) {
4290
+ continue;
4291
+ }
4292
+ for (const part of step.content) {
4293
+ if (typeof part === "object" && part !== null && "type" in part && part.type === "text" && "text" in part && typeof part.text === "string") {
4294
+ const text = part.text.trim();
4295
+ if (text) {
4296
+ lines.push(text);
4297
+ }
4053
4298
  }
4054
4299
  }
4055
4300
  }
@@ -4244,14 +4489,12 @@ function buildGeminiGenerateContentRequest({
4244
4489
  }
4245
4490
  };
4246
4491
  }
4247
- function describeGeminiRequiredAction(outputs) {
4248
- if (!Array.isArray(outputs) || outputs.length === 0) {
4492
+ function describeGeminiRequiredAction(steps) {
4493
+ if (!Array.isArray(steps) || steps.length === 0) {
4249
4494
  return "research requires additional action";
4250
4495
  }
4251
- const firstOutput = outputs.find(
4252
- (value) => typeof value === "object" && value !== null
4253
- );
4254
- const type = readNonEmptyString3(firstOutput?.type);
4496
+ const lastStep = [...steps].reverse().find((value) => typeof value === "object" && value !== null);
4497
+ const type = readNonEmptyString3(lastStep?.type);
4255
4498
  if (!type) {
4256
4499
  return "research requires additional action";
4257
4500
  }
@@ -4335,6 +4578,7 @@ var geminiProvider = defineProvider({
4335
4578
  capabilities: {
4336
4579
  search: defineCapability({
4337
4580
  options: geminiImplementation.getToolOptionsSchema?.("search"),
4581
+ promptGuidelines: geminiSearchPromptGuidelines,
4338
4582
  async execute(input, ctx) {
4339
4583
  const { query: query2, maxResults, options } = input;
4340
4584
  return await geminiImplementation.search(
@@ -4403,6 +4647,12 @@ var linkupSearchOptionsSchema = Type9.Object(
4403
4647
  },
4404
4648
  { description: "Linkup search options." }
4405
4649
  );
4650
+ var linkupSearchPromptGuidelines = [
4651
+ "Use Linkup depth='deep' for exploratory or high-recall source discovery, and 'standard' for quick direct searches.",
4652
+ "Use includeDomains or excludeDomains when the user names source constraints or when limiting the search space improves precision.",
4653
+ "Use fromDate and toDate when the user asks for recent, historical, or bounded-by-date results.",
4654
+ "Enable includeImages only when images are directly useful; otherwise keep search focused on textual source discovery."
4655
+ ];
4406
4656
  var linkupContentsOptionsSchema = Type9.Object(
4407
4657
  {
4408
4658
  renderJs: Type9.Optional(
@@ -4772,6 +5022,7 @@ var linkupProvider = defineProvider({
4772
5022
  capabilities: {
4773
5023
  search: defineCapability({
4774
5024
  options: linkupImplementation.getToolOptionsSchema?.("search"),
5025
+ promptGuidelines: linkupSearchPromptGuidelines,
4775
5026
  async execute(input, ctx) {
4776
5027
  const { query: query2, maxResults, options } = input;
4777
5028
  return await linkupImplementation.search(
@@ -4981,11 +5232,40 @@ function buildFetchMetadata(data) {
4981
5232
  }
4982
5233
 
4983
5234
  // src/providers/openai.ts
4984
- import { Type as Type10 } from "typebox";
4985
5235
  import OpenAI from "openai";
5236
+ import { Type as Type10 } from "typebox";
4986
5237
  var DEFAULT_SEARCH_MODEL2 = "gpt-4.1";
4987
5238
  var DEFAULT_ANSWER_MODEL2 = "gpt-4.1";
4988
5239
  var DEFAULT_RESEARCH_MODEL = "o4-mini-deep-research";
5240
+ var openaiWebSearchToolProperties = {
5241
+ searchContextSize: Type10.Optional(
5242
+ literalUnion(["low", "medium", "high"], {
5243
+ description: "Amount of context OpenAI web search should retrieve per search."
5244
+ })
5245
+ ),
5246
+ allowedDomains: Type10.Optional(
5247
+ Type10.Array(Type10.String(), {
5248
+ description: "Restrict OpenAI web search to these domains."
5249
+ })
5250
+ ),
5251
+ userLocation: Type10.Optional(
5252
+ Type10.Object(
5253
+ {
5254
+ city: Type10.Optional(Type10.String({ description: "User city hint." })),
5255
+ country: Type10.Optional(
5256
+ Type10.String({ description: "Two-letter user country code." })
5257
+ ),
5258
+ region: Type10.Optional(
5259
+ Type10.String({ description: "User region hint." })
5260
+ ),
5261
+ timezone: Type10.Optional(
5262
+ Type10.String({ description: "IANA timezone hint." })
5263
+ )
5264
+ },
5265
+ { description: "Approximate user location for OpenAI web search." }
5266
+ )
5267
+ )
5268
+ };
4989
5269
  var openaiSearchOptionsSchema = Type10.Object(
4990
5270
  {
4991
5271
  model: Type10.Optional(
@@ -4997,10 +5277,19 @@ var openaiSearchOptionsSchema = Type10.Object(
4997
5277
  Type10.String({
4998
5278
  description: "Optional instructions that shape source selection and result style."
4999
5279
  })
5000
- )
5280
+ ),
5281
+ ...openaiWebSearchToolProperties
5001
5282
  },
5002
5283
  { description: "OpenAI search options." }
5003
5284
  );
5285
+ var openaiSearchPromptGuidelines = [
5286
+ "Use OpenAI web search when an LLM-mediated search pass should identify likely sources from the live web.",
5287
+ "Use instructions to constrain source selection, freshness, geography, or output style only when the user explicitly needs that control.",
5288
+ "Use allowedDomains when the user asks to search only specific sites or primary-source domains.",
5289
+ "Use searchContextSize='high' only when the query needs richer source context; use 'low' for quick source discovery.",
5290
+ "Use userLocation for local, regional, or jurisdiction-specific searches.",
5291
+ "Prefer web_contents after OpenAI search when the task requires direct inspection of selected primary sources."
5292
+ ];
5004
5293
  var openaiAnswerOptionsSchema = Type10.Object(
5005
5294
  {
5006
5295
  model: Type10.Optional(
@@ -5012,7 +5301,8 @@ var openaiAnswerOptionsSchema = Type10.Object(
5012
5301
  Type10.String({
5013
5302
  description: "Optional instructions that shape the answer structure, tone, and source selection."
5014
5303
  })
5015
- )
5304
+ ),
5305
+ ...openaiWebSearchToolProperties
5016
5306
  },
5017
5307
  { description: "OpenAI answer options." }
5018
5308
  );
@@ -5033,7 +5323,8 @@ var openaiResearchOptionsSchema = Type10.Object(
5033
5323
  minimum: 1,
5034
5324
  description: "Maximum number of built-in tool calls the model may make during the research run."
5035
5325
  })
5036
- )
5326
+ ),
5327
+ ...openaiWebSearchToolProperties
5037
5328
  },
5038
5329
  { description: "OpenAI deep research options." }
5039
5330
  );
@@ -5194,7 +5485,7 @@ function buildOpenAISearchRequest(query2, maxResults, config, options) {
5194
5485
  "",
5195
5486
  `User query: ${query2}`
5196
5487
  ].join("\n"),
5197
- tools: [{ type: "web_search_preview" }],
5488
+ tools: [buildOpenAIWebSearchTool(mergedOptions)],
5198
5489
  text: {
5199
5490
  format: {
5200
5491
  type: "json_schema",
@@ -5213,7 +5504,7 @@ function buildOpenAIAnswerRequest(query2, config, options) {
5213
5504
  return {
5214
5505
  model,
5215
5506
  input: query2,
5216
- tools: [{ type: "web_search_preview" }],
5507
+ tools: [buildOpenAIWebSearchTool(mergedOptions)],
5217
5508
  ...instructions ? { instructions } : {}
5218
5509
  };
5219
5510
  }
@@ -5226,11 +5517,41 @@ function buildOpenAIResearchRequest(input, config, options) {
5226
5517
  model,
5227
5518
  input,
5228
5519
  background: true,
5229
- tools: [{ type: "web_search_preview" }],
5520
+ tools: [buildOpenAIWebSearchTool(mergedOptions)],
5230
5521
  ...instructions ? { instructions } : {},
5231
5522
  ...maxToolCalls ? { max_tool_calls: maxToolCalls } : {}
5232
5523
  };
5233
5524
  }
5525
+ function buildOpenAIWebSearchTool(options) {
5526
+ const tool = { type: "web_search" };
5527
+ if (options.searchContextSize) {
5528
+ tool.search_context_size = options.searchContextSize;
5529
+ }
5530
+ if (options.allowedDomains && options.allowedDomains.length > 0) {
5531
+ tool.filters = { allowed_domains: options.allowedDomains };
5532
+ }
5533
+ if (options.userLocation) {
5534
+ tool.user_location = {
5535
+ type: "approximate",
5536
+ ...options.userLocation
5537
+ };
5538
+ }
5539
+ return tool;
5540
+ }
5541
+ function resolveOpenAIWebSearchToolOptions(merged) {
5542
+ const searchContextSize = readStringUnion(merged.searchContextSize, [
5543
+ "low",
5544
+ "medium",
5545
+ "high"
5546
+ ]);
5547
+ const allowedDomains = readStringArray(merged.allowedDomains);
5548
+ const userLocation = readUserLocation(merged.userLocation);
5549
+ return {
5550
+ ...searchContextSize ? { searchContextSize } : {},
5551
+ ...allowedDomains ? { allowedDomains } : {},
5552
+ ...userLocation ? { userLocation } : {}
5553
+ };
5554
+ }
5234
5555
  function resolveOpenAISearchOptions(config, options) {
5235
5556
  const mergedOptions = {
5236
5557
  ...config.options?.search ?? {},
@@ -5240,7 +5561,8 @@ function resolveOpenAISearchOptions(config, options) {
5240
5561
  const instructions = readNonEmptyString4(mergedOptions.instructions);
5241
5562
  return {
5242
5563
  ...model ? { model } : {},
5243
- ...instructions ? { instructions } : {}
5564
+ ...instructions ? { instructions } : {},
5565
+ ...resolveOpenAIWebSearchToolOptions(mergedOptions)
5244
5566
  };
5245
5567
  }
5246
5568
  function resolveOpenAIAnswerOptions(config, options) {
@@ -5252,7 +5574,8 @@ function resolveOpenAIAnswerOptions(config, options) {
5252
5574
  const instructions = readNonEmptyString4(mergedOptions.instructions);
5253
5575
  return {
5254
5576
  ...model ? { model } : {},
5255
- ...instructions ? { instructions } : {}
5577
+ ...instructions ? { instructions } : {},
5578
+ ...resolveOpenAIWebSearchToolOptions(mergedOptions)
5256
5579
  };
5257
5580
  }
5258
5581
  function resolveOpenAIResearchOptions(config, options) {
@@ -5266,7 +5589,8 @@ function resolveOpenAIResearchOptions(config, options) {
5266
5589
  return {
5267
5590
  ...model ? { model } : {},
5268
5591
  ...instructions ? { instructions } : {},
5269
- ...maxToolCalls ? { max_tool_calls: maxToolCalls } : {}
5592
+ ...maxToolCalls ? { max_tool_calls: maxToolCalls } : {},
5593
+ ...resolveOpenAIWebSearchToolOptions(mergedOptions)
5270
5594
  };
5271
5595
  }
5272
5596
  function buildRequestOptions2(signal, idempotencyKey) {
@@ -5431,6 +5755,35 @@ function formatIncompleteError(response, operation) {
5431
5755
  function readNonEmptyString4(value) {
5432
5756
  return typeof value === "string" && value.trim().length > 0 ? value : void 0;
5433
5757
  }
5758
+ function readStringArray(value) {
5759
+ if (!Array.isArray(value)) {
5760
+ return void 0;
5761
+ }
5762
+ const strings = value.filter(
5763
+ (item) => typeof item === "string" && item.trim() !== ""
5764
+ );
5765
+ return strings.length > 0 ? strings : void 0;
5766
+ }
5767
+ function readStringUnion(value, values) {
5768
+ return typeof value === "string" && values.includes(value) ? value : void 0;
5769
+ }
5770
+ function readUserLocation(value) {
5771
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
5772
+ return void 0;
5773
+ }
5774
+ const location = value;
5775
+ const city = readNonEmptyString4(location.city);
5776
+ const country = readNonEmptyString4(location.country);
5777
+ const region = readNonEmptyString4(location.region);
5778
+ const timezone = readNonEmptyString4(location.timezone);
5779
+ const result = {
5780
+ ...city ? { city } : {},
5781
+ ...country ? { country } : {},
5782
+ ...region ? { region } : {},
5783
+ ...timezone ? { timezone } : {}
5784
+ };
5785
+ return Object.keys(result).length > 0 ? result : void 0;
5786
+ }
5434
5787
  function readPositiveInteger2(value) {
5435
5788
  return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : void 0;
5436
5789
  }
@@ -5455,6 +5808,7 @@ var openaiProvider = defineProvider({
5455
5808
  capabilities: {
5456
5809
  search: defineCapability({
5457
5810
  options: openaiImplementation.getToolOptionsSchema?.("search"),
5811
+ promptGuidelines: openaiSearchPromptGuidelines,
5458
5812
  async execute(input, ctx) {
5459
5813
  const { query: query2, maxResults, options } = input;
5460
5814
  return await openaiImplementation.search(
@@ -5497,13 +5851,19 @@ import { Type as Type11 } from "typebox";
5497
5851
  var parallelSearchOptionsSchema = Type11.Object(
5498
5852
  {
5499
5853
  mode: Type11.Optional(
5500
- literalUnion(["agentic", "one-shot"], {
5501
- description: "Parallel search mode. Use 'agentic' for exploratory or multi-step source discovery and 'one-shot' for direct, simple searches."
5854
+ literalUnion(["advanced", "basic", "turbo"], {
5855
+ description: "Parallel search mode. Use 'advanced' for higher quality, 'basic' for lower latency, or 'turbo' for the fastest responses."
5502
5856
  })
5503
5857
  )
5504
5858
  },
5505
5859
  { description: "Parallel search options." }
5506
5860
  );
5861
+ var parallelSearchPromptGuidelines = [
5862
+ "Use Parallel mode='advanced' for exploratory, ambiguous, or multi-hop source discovery where the provider should plan the search.",
5863
+ "Use Parallel mode='basic' for direct factual lookups and simple source finding where low latency is preferred.",
5864
+ "Use Parallel mode='turbo' only when fastest responses matter more than recall or depth.",
5865
+ "Prefer web_contents with Parallel extraction when a URL set is already known and the task needs full page content rather than more source discovery."
5866
+ ];
5507
5867
  var parallelExtractOptionsSchema = Type11.Object(
5508
5868
  {
5509
5869
  excerpts: Type11.Optional(
@@ -5536,7 +5896,7 @@ var parallelImplementation = {
5536
5896
  credentials: { api: "PARALLEL_API_KEY" },
5537
5897
  options: {
5538
5898
  search: {
5539
- mode: "agentic"
5899
+ mode: "advanced"
5540
5900
  },
5541
5901
  extract: {
5542
5902
  excerpts: false,
@@ -5551,13 +5911,11 @@ var parallelImplementation = {
5551
5911
  async search(query2, maxResults, config, context, options) {
5552
5912
  const client = createClient6(config);
5553
5913
  const defaults = asJsonObject(config.options?.search) ?? {};
5554
- const response = await client.beta.search(
5555
- {
5914
+ const response = await client.search(
5915
+ buildParallelSearchParams(query2, maxResults, {
5556
5916
  ...defaults,
5557
- ...options ?? {},
5558
- objective: query2,
5559
- max_results: maxResults
5560
- },
5917
+ ...options ?? {}
5918
+ }),
5561
5919
  buildRequestOptions3(context)
5562
5920
  );
5563
5921
  return {
@@ -5572,12 +5930,11 @@ var parallelImplementation = {
5572
5930
  async contents(urls, config, context, options) {
5573
5931
  const client = createClient6(config);
5574
5932
  const defaults = asJsonObject(config.options?.extract) ?? {};
5575
- const response = await client.beta.extract(
5576
- {
5933
+ const response = await client.extract(
5934
+ buildParallelExtractParams(urls, {
5577
5935
  ...defaults,
5578
- ...options ?? {},
5579
- urls
5580
- },
5936
+ ...options ?? {}
5937
+ }),
5581
5938
  buildRequestOptions3(context)
5582
5939
  );
5583
5940
  const resultsByUrl = new Map(
@@ -5609,6 +5966,62 @@ var parallelImplementation = {
5609
5966
  };
5610
5967
  }
5611
5968
  };
5969
+ function buildParallelSearchParams(query2, maxResults, options) {
5970
+ const {
5971
+ advanced_settings: advancedSettingsValue,
5972
+ max_results: _legacyMaxResults,
5973
+ mode: modeValue,
5974
+ objective: objectiveValue,
5975
+ search_queries: _searchQueries,
5976
+ ...rest
5977
+ } = options;
5978
+ const advancedSettings = readObjectOption(advancedSettingsValue);
5979
+ const mode = normalizeParallelSearchMode(modeValue);
5980
+ const objective = typeof objectiveValue === "string" && objectiveValue.trim() ? objectiveValue.trim() : query2;
5981
+ return {
5982
+ ...rest,
5983
+ search_queries: [query2],
5984
+ objective,
5985
+ ...mode ? { mode } : {},
5986
+ advanced_settings: {
5987
+ ...advancedSettings,
5988
+ max_results: maxResults
5989
+ }
5990
+ };
5991
+ }
5992
+ function buildParallelExtractParams(urls, options) {
5993
+ const {
5994
+ advanced_settings: advancedSettingsValue,
5995
+ excerpts: excerptsValue,
5996
+ full_content: fullContentValue,
5997
+ ...rest
5998
+ } = options;
5999
+ const advancedSettings = readObjectOption(advancedSettingsValue);
6000
+ if (typeof fullContentValue === "boolean") {
6001
+ advancedSettings.full_content = fullContentValue;
6002
+ }
6003
+ if (typeof excerptsValue === "boolean" && advancedSettings.excerpt_settings === void 0) {
6004
+ advancedSettings.excerpt_settings = excerptsValue ? {} : { max_chars_per_result: 0 };
6005
+ }
6006
+ return {
6007
+ ...rest,
6008
+ urls,
6009
+ advanced_settings: advancedSettings
6010
+ };
6011
+ }
6012
+ function normalizeParallelSearchMode(value) {
6013
+ switch (value) {
6014
+ case "advanced":
6015
+ case "basic":
6016
+ case "turbo":
6017
+ return value;
6018
+ default:
6019
+ return void 0;
6020
+ }
6021
+ }
6022
+ function readObjectOption(value) {
6023
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? { ...value } : {};
6024
+ }
5612
6025
  function createClient6(config) {
5613
6026
  const apiKey = resolveConfigValue(config.credentials?.api);
5614
6027
  if (!apiKey) {
@@ -5639,6 +6052,7 @@ var parallelProvider = defineProvider({
5639
6052
  capabilities: {
5640
6053
  search: defineCapability({
5641
6054
  options: parallelImplementation.getToolOptionsSchema?.("search"),
6055
+ promptGuidelines: parallelSearchPromptGuidelines,
5642
6056
  async execute(input, ctx) {
5643
6057
  const { query: query2, maxResults, options } = input;
5644
6058
  return await parallelImplementation.search(
@@ -5690,6 +6104,12 @@ var perplexitySearchOptionsSchema = Type12.Object(
5690
6104
  },
5691
6105
  { description: "Perplexity search options." }
5692
6106
  );
6107
+ var perplexitySearchPromptGuidelines = [
6108
+ "Use Perplexity search for concise web result retrieval with recency and domain filters, not for synthesized answers.",
6109
+ "Set search_recency_filter when freshness matters, such as breaking news, recently updated documentation, prices, or current events.",
6110
+ "Set search_domain_filter when the user asks for primary sources, official documentation, or domain-scoped retrieval.",
6111
+ "Use web_answer or web_research with Perplexity when the user wants synthesis rather than a list of candidate sources."
6112
+ ];
5693
6113
  var perplexityAnswerOptionsSchema = Type12.Object(
5694
6114
  {
5695
6115
  model: Type12.Optional(
@@ -5959,6 +6379,7 @@ var perplexityProvider = defineProvider({
5959
6379
  capabilities: {
5960
6380
  search: defineCapability({
5961
6381
  options: perplexityImplementation.getToolOptionsSchema?.("search"),
6382
+ promptGuidelines: perplexitySearchPromptGuidelines,
5962
6383
  async execute(input, ctx) {
5963
6384
  const { query: query2, maxResults, options } = input;
5964
6385
  return await perplexityImplementation.search(
@@ -6687,6 +7108,12 @@ var tavilySearchOptionsSchema = Type14.Object(
6687
7108
  },
6688
7109
  { description: "Tavily search options." }
6689
7110
  );
7111
+ var tavilySearchPromptGuidelines = [
7112
+ "Use Tavily topic='news' for recent journalism or current events and topic='finance' for market or company-finance research; otherwise leave topic as general.",
7113
+ "Use searchDepth='advanced' for broader or higher-recall source discovery, and 'basic' for quick direct lookups.",
7114
+ "Set timeRange, days, or country when the user asks for freshness, recency, or geography-specific results.",
7115
+ "Set includeRawContent or includeAnswer only when the search response itself should carry more context; prefer web_contents for selected source inspection."
7116
+ ];
6690
7117
  var tavilyExtractOptionsSchema = Type14.Object(
6691
7118
  {
6692
7119
  extractDepth: Type14.Optional(
@@ -6848,6 +7275,7 @@ var tavilyProvider = defineProvider({
6848
7275
  capabilities: {
6849
7276
  search: defineCapability({
6850
7277
  options: tavilyImplementation.getToolOptionsSchema?.("search"),
7278
+ promptGuidelines: tavilySearchPromptGuidelines,
6851
7279
  async execute(input, ctx) {
6852
7280
  const { query: query2, maxResults, options } = input;
6853
7281
  return await tavilyImplementation.search(
@@ -6890,32 +7318,230 @@ var valyuSearchOptionsSchema = Type15.Object(
6890
7318
  ),
6891
7319
  countryCode: Type15.Optional(
6892
7320
  Type15.String({ description: "Country code to scope search results." })
6893
- )
6894
- },
6895
- { description: "Valyu search options." }
6896
- );
6897
- var valyuAnswerOptionsSchema = Type15.Object(
6898
- {
6899
- responseLength: Type15.Optional(
6900
- literalUnion(["short", "medium", "large", "max"], {
6901
- description: "Response length for answers."
6902
- })
6903
7321
  ),
6904
- countryCode: Type15.Optional(
6905
- Type15.String({ description: "Country code to scope answer results." })
6906
- )
6907
- },
6908
- { description: "Valyu answer options." }
6909
- );
6910
- var valyuResearchOptionsSchema = Type15.Object(
6911
- {
6912
- responseLength: Type15.Optional(
6913
- literalUnion(["short", "medium", "large", "max"], {
6914
- description: "Response length for research."
7322
+ maxPrice: Type15.Optional(
7323
+ Type15.Number({
7324
+ minimum: 0,
7325
+ description: "Maximum price per thousand characters (CPM)."
7326
+ })
7327
+ ),
7328
+ relevanceThreshold: Type15.Optional(
7329
+ Type15.Number({
7330
+ minimum: 0,
7331
+ maximum: 1,
7332
+ description: "Minimum result relevance score."
7333
+ })
7334
+ ),
7335
+ includedSources: Type15.Optional(
7336
+ Type15.Array(Type15.String(), {
7337
+ description: "Restrict retrieval to these Valyu sources."
7338
+ })
7339
+ ),
7340
+ excludeSources: Type15.Optional(
7341
+ Type15.Array(Type15.String(), {
7342
+ description: "Exclude these Valyu sources."
7343
+ })
7344
+ ),
7345
+ sourceBiases: Type15.Optional(
7346
+ Type15.Record(Type15.String(), Type15.Number(), {
7347
+ description: "Per-source relevance bias weights."
7348
+ })
7349
+ ),
7350
+ category: Type15.Optional(
7351
+ Type15.String({ description: "Valyu source category to search." })
7352
+ ),
7353
+ startDate: Type15.Optional(
7354
+ Type15.String({ description: "ISO date string for earliest result date." })
7355
+ ),
7356
+ endDate: Type15.Optional(
7357
+ Type15.String({ description: "ISO date string for latest result date." })
7358
+ ),
7359
+ historicalCache: Type15.Optional(
7360
+ Type15.Boolean({
7361
+ description: "Allow Valyu historical cache usage when supported."
7362
+ })
7363
+ ),
7364
+ fastMode: Type15.Optional(
7365
+ Type15.Boolean({
7366
+ description: "Use Valyu fast mode when lower latency is preferred."
7367
+ })
7368
+ ),
7369
+ urlOnly: Type15.Optional(
7370
+ Type15.Boolean({
7371
+ description: "Return URL-focused results with less content."
7372
+ })
7373
+ ),
7374
+ instructions: Type15.Optional(
7375
+ Type15.String({
7376
+ description: "Provider instructions for retrieval and result selection."
7377
+ })
7378
+ )
7379
+ },
7380
+ { description: "Valyu search options." }
7381
+ );
7382
+ var valyuSearchPromptGuidelines = [
7383
+ "Use Valyu searchType='news' for recent journalism or current events, 'web' for public web results, and 'proprietary' when proprietary Valyu sources are required.",
7384
+ "Use includedSources, excludeSources, category, or source biases from configuration when the user asks for source-specific retrieval.",
7385
+ "Use startDate/endDate and countryCode when the task requires temporal or geographic scoping.",
7386
+ "Set responseLength higher only when search results need richer inline context; otherwise prefer concise results and follow up with web_contents."
7387
+ ];
7388
+ var valyuContentsOptionsSchema = Type15.Object(
7389
+ {
7390
+ summary: Type15.Optional(
7391
+ Type15.Union(
7392
+ [Type15.Boolean(), Type15.String(), Type15.Record(Type15.String(), Type15.Any())],
7393
+ {
7394
+ description: "Whether to include a summary, or instructions for the summary."
7395
+ }
7396
+ )
7397
+ ),
7398
+ extractEffort: Type15.Optional(
7399
+ literalUnion(["normal", "high", "auto"], {
7400
+ description: "Extraction effort. Use 'high' for difficult pages and 'normal' for faster extraction."
7401
+ })
7402
+ ),
7403
+ responseLength: Type15.Optional(
7404
+ Type15.Union(
7405
+ [
7406
+ literalUnion(["short", "medium", "large", "max"]),
7407
+ Type15.Number({ minimum: 0 })
7408
+ ],
7409
+ {
7410
+ description: "Content response length."
7411
+ }
7412
+ )
7413
+ ),
7414
+ maxPriceDollars: Type15.Optional(
7415
+ Type15.Number({
7416
+ minimum: 0,
7417
+ description: "Maximum extraction cost in USD."
7418
+ })
7419
+ ),
7420
+ screenshot: Type15.Optional(
7421
+ Type15.Boolean({
7422
+ description: "Include screenshot capture when supported."
7423
+ })
7424
+ ),
7425
+ startDate: Type15.Optional(
7426
+ Type15.String({
7427
+ description: "ISO date string for earliest content date."
7428
+ })
7429
+ ),
7430
+ endDate: Type15.Optional(
7431
+ Type15.String({ description: "ISO date string for latest content date." })
7432
+ ),
7433
+ historicalCache: Type15.Optional(
7434
+ Type15.Boolean({
7435
+ description: "Allow Valyu historical cache usage when supported."
7436
+ })
7437
+ )
7438
+ },
7439
+ { description: "Valyu contents options." }
7440
+ );
7441
+ var valyuAnswerOptionsSchema = Type15.Object(
7442
+ {
7443
+ structuredOutput: Type15.Optional(
7444
+ Type15.Record(Type15.String(), Type15.Any(), {
7445
+ description: "JSON schema-like structured output specification."
7446
+ })
7447
+ ),
7448
+ systemInstructions: Type15.Optional(
7449
+ Type15.String({
7450
+ description: "System instructions that guide Valyu answer generation."
7451
+ })
7452
+ ),
7453
+ searchType: Type15.Optional(
7454
+ literalUnion(["all", "web", "proprietary", "news"], {
7455
+ description: "Valyu search type for answer grounding."
7456
+ })
7457
+ ),
7458
+ dataMaxPrice: Type15.Optional(
7459
+ Type15.Number({
7460
+ minimum: 0,
7461
+ description: "Maximum data retrieval price for answer grounding."
6915
7462
  })
6916
7463
  ),
6917
7464
  countryCode: Type15.Optional(
6918
- Type15.String({ description: "Country code to scope research results." })
7465
+ Type15.String({ description: "Country code to scope answer results." })
7466
+ ),
7467
+ includedSources: Type15.Optional(
7468
+ Type15.Array(Type15.String(), {
7469
+ description: "Restrict answer grounding to these Valyu sources."
7470
+ })
7471
+ ),
7472
+ excludedSources: Type15.Optional(
7473
+ Type15.Array(Type15.String(), {
7474
+ description: "Exclude these Valyu sources from answer grounding."
7475
+ })
7476
+ ),
7477
+ startDate: Type15.Optional(
7478
+ Type15.String({ description: "ISO date string for earliest source date." })
7479
+ ),
7480
+ endDate: Type15.Optional(
7481
+ Type15.String({ description: "ISO date string for latest source date." })
7482
+ ),
7483
+ fastMode: Type15.Optional(
7484
+ Type15.Boolean({
7485
+ description: "Use Valyu fast mode when lower latency is preferred."
7486
+ })
7487
+ )
7488
+ },
7489
+ { description: "Valyu answer options." }
7490
+ );
7491
+ var valyuResearchOptionsSchema = Type15.Object(
7492
+ {
7493
+ mode: Type15.Optional(
7494
+ literalUnion(["fast", "standard", "lite", "heavy", "max"], {
7495
+ description: "Valyu deep research mode."
7496
+ })
7497
+ ),
7498
+ outputFormats: Type15.Optional(
7499
+ Type15.Array(
7500
+ Type15.Union([
7501
+ literalUnion(["markdown", "pdf", "toon"]),
7502
+ Type15.Record(Type15.String(), Type15.Any())
7503
+ ]),
7504
+ { description: "Requested Valyu research output formats." }
7505
+ )
7506
+ ),
7507
+ search: Type15.Optional(
7508
+ Type15.Object(
7509
+ {
7510
+ searchType: Type15.Optional(
7511
+ literalUnion(["all", "web", "proprietary"], {
7512
+ description: "Valyu source pool for research."
7513
+ })
7514
+ ),
7515
+ includedSources: Type15.Optional(Type15.Array(Type15.String())),
7516
+ excludedSources: Type15.Optional(Type15.Array(Type15.String())),
7517
+ sourceBiases: Type15.Optional(
7518
+ Type15.Record(Type15.String(), Type15.Number())
7519
+ ),
7520
+ startDate: Type15.Optional(Type15.String()),
7521
+ endDate: Type15.Optional(Type15.String()),
7522
+ historicalCache: Type15.Optional(Type15.Boolean()),
7523
+ category: Type15.Optional(Type15.String()),
7524
+ countryCode: Type15.Optional(Type15.String())
7525
+ },
7526
+ {
7527
+ additionalProperties: false,
7528
+ description: "Valyu deep research search configuration."
7529
+ }
7530
+ )
7531
+ ),
7532
+ tools: Type15.Optional(
7533
+ Type15.Object(
7534
+ {
7535
+ code_execution: Type15.Optional(boolOrConfig()),
7536
+ screenshots: Type15.Optional(boolOrConfig()),
7537
+ browser_use: Type15.Optional(boolOrConfig()),
7538
+ charts: Type15.Optional(boolOrConfig())
7539
+ },
7540
+ {
7541
+ additionalProperties: false,
7542
+ description: "Valyu deep research tool configuration."
7543
+ }
7544
+ )
6919
7545
  )
6920
7546
  },
6921
7547
  { description: "Valyu research options." }
@@ -6928,6 +7554,8 @@ var valyuImplementation = {
6928
7554
  switch (capability) {
6929
7555
  case "search":
6930
7556
  return valyuSearchOptionsSchema;
7557
+ case "contents":
7558
+ return valyuContentsOptionsSchema;
6931
7559
  case "answer":
6932
7560
  return valyuAnswerOptionsSchema;
6933
7561
  case "research":
@@ -6975,7 +7603,10 @@ var valyuImplementation = {
6975
7603
  },
6976
7604
  async contents(urls, config, _context, options) {
6977
7605
  const client = createClient9(config);
6978
- const response = await client.contents(urls, options);
7606
+ const response = await client.contents(urls, {
7607
+ ...asJsonObject(config.options?.contents) ?? {},
7608
+ ...options ?? {}
7609
+ });
6979
7610
  const finalResponse = "jobId" in response ? await client.waitForJob(response.jobId, {}) : response;
6980
7611
  if (!finalResponse.success) {
6981
7612
  throw new Error(finalResponse.error || "contents failed");
@@ -7121,7 +7752,7 @@ var valyuProvider = defineProvider({
7121
7752
  config: {
7122
7753
  createTemplate: () => valyuImplementation.createTemplate(),
7123
7754
  fields: ["credentials", "baseUrl", "options", "settings"],
7124
- optionCapabilities: ["search", "answer", "research"]
7755
+ optionCapabilities: ["search", "contents", "answer", "research"]
7125
7756
  },
7126
7757
  getCapabilityStatus: (config, cwd, tool, options) => valyuImplementation.getCapabilityStatus(
7127
7758
  config,
@@ -7132,6 +7763,7 @@ var valyuProvider = defineProvider({
7132
7763
  capabilities: {
7133
7764
  search: defineCapability({
7134
7765
  options: valyuImplementation.getToolOptionsSchema?.("search"),
7766
+ promptGuidelines: valyuSearchPromptGuidelines,
7135
7767
  async execute(input, ctx) {
7136
7768
  const { query: query2, maxResults, options } = input;
7137
7769
  return await valyuImplementation.search(
@@ -7849,45 +8481,6 @@ ${JSON.stringify(value, null, 2).trim()}
7849
8481
  \`\`\``;
7850
8482
  }
7851
8483
 
7852
- // src/options.ts
7853
- function buildToolOptionsSchema(_capability, providerSchema) {
7854
- if (!providerSchema || Object.keys(providerSchema.properties).length === 0) {
7855
- return void 0;
7856
- }
7857
- return closeObjectSchemas(providerSchema);
7858
- }
7859
- function closeObjectSchemas(schema) {
7860
- if (!isSchemaRecord(schema)) {
7861
- return schema;
7862
- }
7863
- const properties = isSchemaRecord(schema.properties) ? Object.fromEntries(
7864
- Object.entries(schema.properties).map(([key, value]) => [
7865
- key,
7866
- closeObjectSchemas(value)
7867
- ])
7868
- ) : schema.properties;
7869
- const items = isSchemaRecord(schema.items) ? closeObjectSchemas(schema.items) : Array.isArray(schema.items) ? schema.items.map((item) => closeObjectSchemas(item)) : schema.items;
7870
- return {
7871
- ...schema,
7872
- ...properties ? { properties } : {},
7873
- ...items ? { items } : {},
7874
- ...mapSchemaArray(schema, "anyOf"),
7875
- ...mapSchemaArray(schema, "oneOf"),
7876
- ...mapSchemaArray(schema, "allOf"),
7877
- ...schema.type === "object" && isSchemaRecord(schema.properties) ? { additionalProperties: false } : {}
7878
- };
7879
- }
7880
- function mapSchemaArray(schema, key) {
7881
- const value = schema[key];
7882
- return Array.isArray(value) ? { [key]: value.map((entry) => closeObjectSchemas(entry)) } : {};
7883
- }
7884
- function isSchemaRecord(value) {
7885
- return typeof value === "object" && value !== null && !Array.isArray(value);
7886
- }
7887
-
7888
- // src/prefetch-manager.ts
7889
- import { createHash } from "node:crypto";
7890
-
7891
8484
  // src/provider-resolution.ts
7892
8485
  function supportsTool2(provider, tool) {
7893
8486
  return provider.capabilities[tool] !== void 0;
@@ -8029,68 +8622,202 @@ function resolveProviderForTool(config, cwd, tool, explicit) {
8029
8622
  return provider;
8030
8623
  }
8031
8624
 
8032
- // src/provider-runtime.ts
8033
- async function executeProviderRequest(provider, config, request, context) {
8034
- return await executeProviderExecution(
8625
+ // src/managed-tools.ts
8626
+ var CAPABILITY_TOOL_NAMES = {
8627
+ search: "web_search",
8628
+ contents: "web_contents",
8629
+ answer: "web_answer",
8630
+ research: "web_research"
8631
+ };
8632
+ var MANAGED_TOOL_NAMES = Object.values(CAPABILITY_TOOL_NAMES);
8633
+ function getAvailableProviderIdsForCapability(config, cwd, capability) {
8634
+ const providerId = getMappedProviderIdForTool(config, capability);
8635
+ if (!providerId) {
8636
+ return [];
8637
+ }
8638
+ const provider = PROVIDERS_BY_ID[providerId];
8639
+ if (!supportsTool2(provider, capability)) {
8640
+ return [];
8641
+ }
8642
+ const status = getProviderCapabilityStatus(
8643
+ config,
8644
+ cwd,
8645
+ providerId,
8646
+ capability,
8035
8647
  {
8036
- capability: request.capability,
8037
- providerLabel: provider.label,
8038
- settings: config.settings,
8039
- execute: (executionContext) => executeProviderCapability(
8040
- provider,
8041
- request.capability,
8042
- providerInputFromRequest(request),
8043
- {
8044
- ...executionContext,
8045
- config
8046
- }
8047
- )
8048
- },
8049
- context
8648
+ resolveSecrets: false
8649
+ }
8050
8650
  );
8651
+ return isProviderCapabilityExposable(status) ? [providerId] : [];
8051
8652
  }
8052
- async function executeProviderExecution(execution, context) {
8053
- if (execution.capability === "research") {
8054
- const deadline = createResearchDeadlineSignal(
8055
- context.signal,
8056
- execution.providerLabel,
8057
- execution.settings?.researchTimeoutMs
8058
- );
8059
- try {
8060
- const researchContext = deadline ? { ...context, signal: deadline.signal } : context;
8061
- return await withAbortSignal(
8062
- execution.execute(researchContext),
8063
- researchContext.signal
8064
- );
8065
- } catch (error) {
8066
- throw new Error(
8067
- formatProviderDiagnostic(
8068
- execution.providerLabel,
8069
- formatErrorMessage(error)
8070
- )
8071
- );
8072
- } finally {
8073
- deadline?.cleanup();
8653
+ function getProviderStatusForTool(config, cwd, providerId, capability) {
8654
+ return getProviderCapabilityStatus(config, cwd, providerId, capability);
8655
+ }
8656
+ function getAvailableManagedToolNames(config, cwd) {
8657
+ return Object.keys(CAPABILITY_TOOL_NAMES).filter(
8658
+ (capability) => getAvailableProviderIdsForCapability(config, cwd, capability).length > 0
8659
+ ).map((capability) => CAPABILITY_TOOL_NAMES[capability]);
8660
+ }
8661
+ function getSyncedActiveTools(config, cwd, activeToolNames, options) {
8662
+ const availableToolNames = new Set(getAvailableManagedToolNames(config, cwd));
8663
+ const nextActiveTools = new Set(activeToolNames);
8664
+ for (const toolName of MANAGED_TOOL_NAMES) {
8665
+ if (availableToolNames.has(toolName)) {
8666
+ if (options.addAvailable) {
8667
+ nextActiveTools.add(toolName);
8668
+ }
8669
+ continue;
8074
8670
  }
8671
+ nextActiveTools.delete(toolName);
8075
8672
  }
8076
- const requestPolicy = resolveExecutionPolicy(execution.settings);
8673
+ return nextActiveTools;
8674
+ }
8675
+ async function refreshManagedTools(pi, registerManagedTools2, cwd, options) {
8676
+ const config = await loadConfig();
8677
+ const nextActiveTools = getSyncedActiveTools(
8678
+ config,
8679
+ cwd,
8680
+ pi.getActiveTools(),
8681
+ options
8682
+ );
8683
+ registerManagedTools2({
8684
+ search: getAvailableProviderIdsForCapability(config, cwd, "search"),
8685
+ contents: getAvailableProviderIdsForCapability(config, cwd, "contents"),
8686
+ answer: getAvailableProviderIdsForCapability(config, cwd, "answer"),
8687
+ research: getAvailableProviderIdsForCapability(config, cwd, "research")
8688
+ });
8689
+ await syncManagedToolAvailability(pi, nextActiveTools);
8690
+ }
8691
+ async function refreshManagedToolsOnStartup(pi, registerManagedTools2, cwd, options) {
8077
8692
  try {
8078
- return await runWithExecutionPolicy(
8079
- `${execution.providerLabel} ${execution.capability} request`,
8080
- execution.execute,
8081
- requestPolicy,
8082
- context
8083
- );
8693
+ await refreshManagedTools(pi, registerManagedTools2, cwd, options);
8084
8694
  } catch (error) {
8085
- throw new Error(
8086
- formatProviderDiagnostic(
8087
- execution.providerLabel,
8088
- formatErrorMessage(error)
8695
+ pi.sendMessage({
8696
+ customType: "web-providers-config-error",
8697
+ content: formatStartupConfigError(error),
8698
+ display: true
8699
+ });
8700
+ await syncManagedToolAvailability(
8701
+ pi,
8702
+ new Set(
8703
+ pi.getActiveTools().filter((toolName) => !MANAGED_TOOL_NAMES.includes(toolName))
8089
8704
  )
8090
8705
  );
8091
8706
  }
8092
8707
  }
8093
- function providerInputFromRequest(request) {
8708
+ function formatStartupConfigError(error) {
8709
+ const detail = error instanceof Error ? error.message : String(error);
8710
+ return `web-providers config error: ${detail.replace(getConfigPath(), "~/.pi/agent/web-providers.json")}`;
8711
+ }
8712
+ async function syncManagedToolAvailability(pi, nextActiveTools) {
8713
+ const activeTools = pi.getActiveTools();
8714
+ const changed = activeTools.length !== nextActiveTools.size || activeTools.some((toolName) => !nextActiveTools.has(toolName));
8715
+ if (changed) {
8716
+ pi.setActiveTools(Array.from(nextActiveTools));
8717
+ }
8718
+ }
8719
+
8720
+ // src/options.ts
8721
+ function buildToolOptionsSchema(_capability, providerSchema) {
8722
+ if (!providerSchema || Object.keys(providerSchema.properties).length === 0) {
8723
+ return void 0;
8724
+ }
8725
+ return closeObjectSchemas(providerSchema);
8726
+ }
8727
+ function closeObjectSchemas(schema) {
8728
+ if (!isSchemaRecord(schema)) {
8729
+ return schema;
8730
+ }
8731
+ const properties = isSchemaRecord(schema.properties) ? Object.fromEntries(
8732
+ Object.entries(schema.properties).map(([key, value]) => [
8733
+ key,
8734
+ closeObjectSchemas(value)
8735
+ ])
8736
+ ) : schema.properties;
8737
+ const items = isSchemaRecord(schema.items) ? closeObjectSchemas(schema.items) : Array.isArray(schema.items) ? schema.items.map((item) => closeObjectSchemas(item)) : schema.items;
8738
+ return {
8739
+ ...schema,
8740
+ ...properties ? { properties } : {},
8741
+ ...items ? { items } : {},
8742
+ ...mapSchemaArray(schema, "anyOf"),
8743
+ ...mapSchemaArray(schema, "oneOf"),
8744
+ ...mapSchemaArray(schema, "allOf"),
8745
+ ...schema.type === "object" && isSchemaRecord(schema.properties) ? { additionalProperties: false } : {}
8746
+ };
8747
+ }
8748
+ function mapSchemaArray(schema, key) {
8749
+ const value = schema[key];
8750
+ return Array.isArray(value) ? { [key]: value.map((entry) => closeObjectSchemas(entry)) } : {};
8751
+ }
8752
+ function isSchemaRecord(value) {
8753
+ return typeof value === "object" && value !== null && !Array.isArray(value);
8754
+ }
8755
+
8756
+ // src/prefetch-manager.ts
8757
+ import { createHash } from "node:crypto";
8758
+
8759
+ // src/provider-runtime.ts
8760
+ async function executeProviderRequest(provider, config, request, context) {
8761
+ return await executeProviderExecution(
8762
+ {
8763
+ capability: request.capability,
8764
+ providerLabel: provider.label,
8765
+ settings: config.settings,
8766
+ execute: (executionContext) => executeProviderCapability(
8767
+ provider,
8768
+ request.capability,
8769
+ providerInputFromRequest(request),
8770
+ {
8771
+ ...executionContext,
8772
+ config
8773
+ }
8774
+ )
8775
+ },
8776
+ context
8777
+ );
8778
+ }
8779
+ async function executeProviderExecution(execution, context) {
8780
+ if (execution.capability === "research") {
8781
+ const deadline = createResearchDeadlineSignal(
8782
+ context.signal,
8783
+ execution.providerLabel,
8784
+ execution.settings?.researchTimeoutMs
8785
+ );
8786
+ try {
8787
+ const researchContext = deadline ? { ...context, signal: deadline.signal } : context;
8788
+ return await withAbortSignal(
8789
+ execution.execute(researchContext),
8790
+ researchContext.signal
8791
+ );
8792
+ } catch (error) {
8793
+ throw new Error(
8794
+ formatProviderDiagnostic(
8795
+ execution.providerLabel,
8796
+ formatErrorMessage(error)
8797
+ )
8798
+ );
8799
+ } finally {
8800
+ deadline?.cleanup();
8801
+ }
8802
+ }
8803
+ const requestPolicy = resolveExecutionPolicy(execution.settings);
8804
+ try {
8805
+ return await runWithExecutionPolicy(
8806
+ `${execution.providerLabel} ${execution.capability} request`,
8807
+ execution.execute,
8808
+ requestPolicy,
8809
+ context
8810
+ );
8811
+ } catch (error) {
8812
+ throw new Error(
8813
+ formatProviderDiagnostic(
8814
+ execution.providerLabel,
8815
+ formatErrorMessage(error)
8816
+ )
8817
+ );
8818
+ }
8819
+ }
8820
+ function providerInputFromRequest(request) {
8094
8821
  switch (request.capability) {
8095
8822
  case "search":
8096
8823
  return {
@@ -8122,7 +8849,7 @@ function resolveExecutionPolicy(defaults) {
8122
8849
  retryDelayMs: defaults?.retryDelayMs ?? 2e3
8123
8850
  };
8124
8851
  }
8125
- function createResearchDeadlineSignal(signal, providerLabel, timeoutMs) {
8852
+ function createResearchDeadlineSignal(signal, providerLabel2, timeoutMs) {
8126
8853
  if (timeoutMs === void 0) {
8127
8854
  return void 0;
8128
8855
  }
@@ -8137,7 +8864,7 @@ function createResearchDeadlineSignal(signal, providerLabel, timeoutMs) {
8137
8864
  const timer = setTimeout(() => {
8138
8865
  controller.abort(
8139
8866
  new Error(
8140
- `${providerLabel} research exceeded ${formatDuration(timeoutMs)}.`
8867
+ `${providerLabel2} research exceeded ${formatDuration(timeoutMs)}.`
8141
8868
  )
8142
8869
  );
8143
8870
  }, timeoutMs);
@@ -8961,9 +9688,9 @@ var PROVIDER_SETTINGS = {
8961
9688
  "hybrid",
8962
9689
  "fast",
8963
9690
  "instant",
9691
+ "deep-lite",
8964
9692
  "deep",
8965
- "deep-reasoning",
8966
- "deep-max"
9693
+ "deep-reasoning"
8967
9694
  ],
8968
9695
  getValue: (config) => readString5(getExaSearchOptions(config)?.type) ?? "default",
8969
9696
  setValue: (config, value) => {
@@ -9191,7 +9918,7 @@ var PROVIDER_SETTINGS = {
9191
9918
  id: "parallelSearchMode",
9192
9919
  label: "Search mode",
9193
9920
  help: "Parallel search mode. 'default' uses the SDK default.",
9194
- values: ["default", "agentic", "one-shot"],
9921
+ values: ["default", "advanced", "basic", "turbo"],
9195
9922
  getValue: (config) => readString5(getParallelOptions(config)?.search?.mode) ?? "default",
9196
9923
  setValue: (config, value) => {
9197
9924
  const options = ensureParallelOptions(config);
@@ -9285,27 +10012,19 @@ var PROVIDER_SETTINGS = {
9285
10012
  }
9286
10013
  }),
9287
10014
  valuesSetting({
9288
- id: "valyuAnswerResponseLength",
9289
- label: "Answer response length",
9290
- help: "Valyu answer response length. 'default' uses the SDK default.",
9291
- values: ["default", "short", "medium", "large", "max"],
9292
- getValue: (config) => readString5(
9293
- getValyuCapabilityOptions(config, "answer")?.responseLength
9294
- ) ?? "default",
9295
- setValue: (config, value) => {
9296
- setValyuResponseLength(config, "answer", value);
9297
- }
9298
- }),
9299
- valuesSetting({
9300
- id: "valyuResearchResponseLength",
9301
- label: "Research response length",
9302
- help: "Valyu research response length. 'default' uses the SDK default.",
9303
- values: ["default", "short", "medium", "large", "max"],
9304
- getValue: (config) => readString5(
9305
- getValyuCapabilityOptions(config, "research")?.responseLength
9306
- ) ?? "default",
10015
+ id: "valyuResearchMode",
10016
+ label: "Research mode",
10017
+ help: "Valyu research mode. 'default' uses the SDK default.",
10018
+ values: ["default", "fast", "standard", "lite", "heavy", "max"],
10019
+ getValue: (config) => readString5(getValyuCapabilityOptions(config, "research")?.mode) ?? "default",
9307
10020
  setValue: (config, value) => {
9308
- setValyuResponseLength(config, "research", value);
10021
+ const options = ensureValyuCapabilityOptions(config, "research");
10022
+ if (value === "default") {
10023
+ delete options.mode;
10024
+ } else {
10025
+ options.mode = value;
10026
+ }
10027
+ cleanupCapabilityOptions(config, ["search", "answer", "research"]);
9309
10028
  }
9310
10029
  })
9311
10030
  ]
@@ -9792,76 +10511,1030 @@ function getFirstLine(text) {
9792
10511
  return text?.split("\n").map((line) => line.trim()).find((line) => line.length > 0);
9793
10512
  }
9794
10513
 
9795
- // src/index.ts
9796
- var DEFAULT_MAX_RESULTS = 5;
9797
- var MAX_ALLOWED_RESULTS = 20;
9798
- var MAX_SEARCH_QUERIES = 10;
9799
- var RESEARCH_HEARTBEAT_MS = 15e3;
9800
- var WEB_RESEARCH_RESULT_MESSAGE_TYPE = "web-research-result";
9801
- var WEB_RESEARCH_WIDGET_KEY = "web-research-jobs";
10514
+ // src/web-research-lifecycle.ts
10515
+ import { randomUUID } from "node:crypto";
10516
+ import { mkdir as mkdir2, readdir, readFile as readFile2, stat, writeFile as writeFile2 } from "node:fs/promises";
10517
+ import { dirname as dirname2, join as join2 } from "node:path";
9802
10518
  var RESEARCH_ARTIFACTS_DIR = join2(".pi", "artifacts", "research");
10519
+ var MAX_RESEARCH_HISTORY_ITEMS = 20;
10520
+ var RESEARCH_PREVIEW_MAX_BYTES = 5e4;
10521
+ var RESEARCH_REPORT_MAX_BYTES = 2e5;
9803
10522
  var pendingResearchTasks = /* @__PURE__ */ new Set();
9804
- var CAPABILITY_TOOL_NAMES = {
9805
- search: "web_search",
9806
- contents: "web_contents",
9807
- answer: "web_answer",
9808
- research: "web_research"
9809
- };
9810
- var MANAGED_TOOL_NAMES = Object.values(CAPABILITY_TOOL_NAMES);
9811
- var DEFAULT_SUMMARY_SYMBOLS = {
9812
- success: "\u2714",
9813
- failure: "\u2718"
9814
- };
9815
- function webProvidersExtension(pi) {
9816
- const activeWebResearchRequests = /* @__PURE__ */ new Map();
9817
- let latestWidgetContext;
9818
- let webResearchWidgetTimer;
9819
- const stopWebResearchWidgetTimer = () => {
9820
- if (webResearchWidgetTimer) {
9821
- clearInterval(webResearchWidgetTimer);
9822
- webResearchWidgetTimer = void 0;
9823
- }
9824
- };
9825
- const ensureWebResearchWidgetTimer = () => {
9826
- if (webResearchWidgetTimer || activeWebResearchRequests.size === 0) {
9827
- return;
10523
+ async function dispatchWebResearch({
10524
+ activeWebResearchRequests,
10525
+ config,
10526
+ explicitProvider,
10527
+ ctx,
10528
+ options,
10529
+ input,
10530
+ executionOverride,
10531
+ executeResearch,
10532
+ deliverResult,
10533
+ onJobsChanged,
10534
+ resultMessageType
10535
+ }) {
10536
+ await cleanupContentStore();
10537
+ const provider = resolveProviderForTool(
10538
+ config,
10539
+ ctx.cwd,
10540
+ "research",
10541
+ explicitProvider
10542
+ );
10543
+ const request = createWebResearchRequest(ctx.cwd, provider.id, input);
10544
+ const abortController = new AbortController();
10545
+ const task = { request, abortController };
10546
+ const providerConfig = getEffectiveProviderConfig(config, provider.id);
10547
+ activeWebResearchRequests.set(request.id, task);
10548
+ onJobsChanged();
10549
+ trackPendingResearchTask(
10550
+ runDispatchedWebResearch({
10551
+ activeWebResearchRequests,
10552
+ task,
10553
+ config,
10554
+ provider,
10555
+ providerConfig,
10556
+ ctx,
10557
+ options,
10558
+ executionOverride,
10559
+ executeResearch,
10560
+ deliverResult,
10561
+ onJobsChanged,
10562
+ resultMessageType
10563
+ })
10564
+ );
10565
+ return {
10566
+ content: [
10567
+ {
10568
+ type: "text",
10569
+ text: `Started web research via ${provider.label}.`
10570
+ }
10571
+ ],
10572
+ details: request,
10573
+ display: {
10574
+ provider: { id: provider.id, label: provider.label },
10575
+ outcome: { success: "started" }
9828
10576
  }
9829
- webResearchWidgetTimer = setInterval(() => {
9830
- updateWebResearchWidget();
9831
- }, 1e3);
9832
10577
  };
9833
- const updateWebResearchWidget = (ctx) => {
9834
- const widgetContext = ctx ?? latestWidgetContext;
9835
- if (!widgetContext) {
9836
- return;
9837
- }
9838
- latestWidgetContext = widgetContext;
9839
- if (!widgetContext.hasUI) {
9840
- stopWebResearchWidgetTimer();
9841
- return;
9842
- }
9843
- if (activeWebResearchRequests.size === 0) {
9844
- stopWebResearchWidgetTimer();
9845
- widgetContext.ui.setWidget(WEB_RESEARCH_WIDGET_KEY, void 0);
9846
- return;
10578
+ }
10579
+ async function runDispatchedWebResearch({
10580
+ activeWebResearchRequests,
10581
+ task,
10582
+ config,
10583
+ provider,
10584
+ providerConfig,
10585
+ ctx,
10586
+ options,
10587
+ executionOverride,
10588
+ executeResearch,
10589
+ deliverResult,
10590
+ onJobsChanged,
10591
+ resultMessageType
10592
+ }) {
10593
+ const { request, abortController } = task;
10594
+ let result;
10595
+ let reportText = "";
10596
+ try {
10597
+ const response = await executeResearch({
10598
+ config,
10599
+ provider,
10600
+ providerConfig,
10601
+ ctx,
10602
+ signal: abortController.signal,
10603
+ options,
10604
+ input: request.input,
10605
+ onProgress: (message) => {
10606
+ request.progress = summarizeWebResearchProgress(
10607
+ message,
10608
+ provider.label
10609
+ );
10610
+ onJobsChanged();
10611
+ },
10612
+ executionOverride
10613
+ });
10614
+ result = buildWebResearchResult(request, abortController, task, response);
10615
+ if (result.status === "completed") {
10616
+ reportText = response.text;
9847
10617
  }
9848
- ensureWebResearchWidgetTimer();
9849
- widgetContext.ui.setWidget(
9850
- WEB_RESEARCH_WIDGET_KEY,
9851
- buildWebResearchWidgetLines(
9852
- [...activeWebResearchRequests.values()],
9853
- widgetContext.ui.theme
9854
- )
9855
- );
9856
- };
9857
- if ("registerMessageRenderer" in pi) {
9858
- pi.registerMessageRenderer(
9859
- WEB_RESEARCH_RESULT_MESSAGE_TYPE,
9860
- (message, state, theme) => renderWebResearchResultMessage(message, state, theme)
10618
+ } catch (error) {
10619
+ result = buildFailedWebResearchResult(
10620
+ request,
10621
+ abortController,
10622
+ task,
10623
+ error
9861
10624
  );
9862
10625
  }
9863
- pi.registerCommand("web-providers", {
9864
- description: "Configure web search providers",
10626
+ try {
10627
+ await writeWebResearchArtifact(result, reportText);
10628
+ deliverResult({
10629
+ customType: resultMessageType,
10630
+ content: formatWebResearchResultMessage(result, reportText),
10631
+ display: true,
10632
+ details: result
10633
+ });
10634
+ } finally {
10635
+ activeWebResearchRequests.delete(request.id);
10636
+ onJobsChanged();
10637
+ }
10638
+ }
10639
+ function buildWebResearchResult(request, abortController, task, response) {
10640
+ const completedAt = (/* @__PURE__ */ new Date()).toISOString();
10641
+ if (abortController.signal.aborted && task.cancelRequestedAt !== void 0) {
10642
+ return {
10643
+ ...request,
10644
+ status: "cancelled",
10645
+ completedAt,
10646
+ elapsedMs: elapsedMs(request.startedAt, completedAt),
10647
+ error: "web research was cancelled by the user."
10648
+ };
10649
+ }
10650
+ return {
10651
+ ...request,
10652
+ status: "completed",
10653
+ completedAt,
10654
+ elapsedMs: elapsedMs(request.startedAt, completedAt),
10655
+ itemCount: response.itemCount
10656
+ };
10657
+ }
10658
+ function buildFailedWebResearchResult(request, abortController, task, error) {
10659
+ const completedAt = (/* @__PURE__ */ new Date()).toISOString();
10660
+ const cancelled = abortController.signal.aborted && task.cancelRequestedAt !== void 0;
10661
+ return {
10662
+ ...request,
10663
+ status: cancelled ? "cancelled" : "failed",
10664
+ completedAt,
10665
+ elapsedMs: elapsedMs(request.startedAt, completedAt),
10666
+ error: cancelled ? "web research was cancelled by the user." : formatErrorMessage(error)
10667
+ };
10668
+ }
10669
+ function elapsedMs(startedAt, completedAt) {
10670
+ return Math.max(0, Date.parse(completedAt) - Date.parse(startedAt));
10671
+ }
10672
+ function getActiveWebResearchRequests(tasks) {
10673
+ return [...tasks.values()].map((task) => task.request);
10674
+ }
10675
+ function getWebResearchTaskSnapshots(tasks) {
10676
+ return [...tasks.values()].map((task) => ({
10677
+ request: task.request,
10678
+ cancelRequestedAt: task.cancelRequestedAt
10679
+ }));
10680
+ }
10681
+ function cancelWebResearchTask(tasks, id) {
10682
+ const task = tasks.get(id);
10683
+ if (!task || task.abortController.signal.aborted) return false;
10684
+ task.cancelRequestedAt = (/* @__PURE__ */ new Date()).toISOString();
10685
+ task.request.progress = "cancelling";
10686
+ task.abortController.abort(
10687
+ new Error("web research was cancelled by the user.")
10688
+ );
10689
+ return true;
10690
+ }
10691
+ function createWebResearchRequest(cwd, provider, input) {
10692
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
10693
+ return {
10694
+ tool: "web_research",
10695
+ id: randomUUID(),
10696
+ provider,
10697
+ input,
10698
+ outputPath: buildWebResearchArtifactPath(cwd, input, startedAt),
10699
+ startedAt
10700
+ };
10701
+ }
10702
+ function buildWebResearchArtifactPath(cwd, input, startedAt) {
10703
+ const timestamp = startedAt.replaceAll(":", "-").replace(".", "-");
10704
+ const slug = slugifyWebResearchInput(input);
10705
+ return join2(cwd, RESEARCH_ARTIFACTS_DIR, `${timestamp}-${slug}.md`);
10706
+ }
10707
+ function slugifyWebResearchInput(input) {
10708
+ const slug = input.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60).replace(/-+$/g, "");
10709
+ return slug.length > 0 ? slug : "research";
10710
+ }
10711
+ function getWebResearchProgressIcon(request) {
10712
+ if (request.progress === "poll retrying after transient errors") {
10713
+ return "\u27F3";
10714
+ }
10715
+ if (request.progress === "queued" || request.progress === "cancelling") {
10716
+ return "\u25CC";
10717
+ }
10718
+ if (request.progress === "starting") {
10719
+ return "\u25D4";
10720
+ }
10721
+ if (request.progress?.startsWith("started:")) {
10722
+ return "\u25D1";
10723
+ }
10724
+ return "\u25CF";
10725
+ }
10726
+ function summarizeWebResearchProgress(message, providerLabel2) {
10727
+ const startingMessage = `Starting research via ${providerLabel2}`;
10728
+ if (message === startingMessage) {
10729
+ return "starting";
10730
+ }
10731
+ const startedPrefix = `${providerLabel2} research started: `;
10732
+ if (message.startsWith(startedPrefix)) {
10733
+ return `started: ${message.slice(startedPrefix.length)}`;
10734
+ }
10735
+ const statusPrefix = `Research via ${providerLabel2}: `;
10736
+ if (message.startsWith(statusPrefix)) {
10737
+ return message.slice(statusPrefix.length).replace(/\s+\([^)]* elapsed\)$/u, "").trim();
10738
+ }
10739
+ const retryPrefix = `${providerLabel2} research poll is still retrying after transient errors`;
10740
+ if (message.startsWith(retryPrefix)) {
10741
+ return "poll retrying after transient errors";
10742
+ }
10743
+ return message.trim();
10744
+ }
10745
+ function formatWebResearchResultMessage(result, reportText) {
10746
+ const text = reportText.trim();
10747
+ if (text.length > 0) {
10748
+ return `${text}
10749
+ `;
10750
+ }
10751
+ if (result.error) {
10752
+ return `${result.error}
10753
+ `;
10754
+ }
10755
+ return "";
10756
+ }
10757
+ async function writeWebResearchArtifact(result, reportText) {
10758
+ await mkdir2(dirname2(result.outputPath), { recursive: true });
10759
+ await writeFile2(
10760
+ result.outputPath,
10761
+ formatWebResearchArtifact(result, reportText),
10762
+ "utf-8"
10763
+ );
10764
+ }
10765
+ function formatWebResearchArtifact(result, reportText) {
10766
+ const providerLabel2 = PROVIDERS_BY_ID[result.provider]?.label ?? result.provider;
10767
+ const metadata = {
10768
+ query: result.input,
10769
+ provider: providerLabel2,
10770
+ providerId: result.provider,
10771
+ status: result.status,
10772
+ startedAt: result.startedAt,
10773
+ completedAt: result.completedAt,
10774
+ elapsedMs: result.elapsedMs,
10775
+ itemCount: result.itemCount,
10776
+ error: result.error
10777
+ };
10778
+ const lines = [
10779
+ "---",
10780
+ ...Object.entries(metadata).flatMap(
10781
+ ([key, value]) => value === void 0 ? [] : [`${key}: ${formatYamlScalar(value)}`]
10782
+ ),
10783
+ "---",
10784
+ "",
10785
+ "# Web research report"
10786
+ ];
10787
+ if (reportText) {
10788
+ lines.push("", reportText);
10789
+ }
10790
+ return `${lines.join("\n")}
10791
+ `;
10792
+ }
10793
+ function formatYamlScalar(value) {
10794
+ if (typeof value === "number") {
10795
+ return String(value);
10796
+ }
10797
+ return JSON.stringify(value);
10798
+ }
10799
+ async function loadWebResearchHistory(cwd, maxItems = MAX_RESEARCH_HISTORY_ITEMS) {
10800
+ const dir = join2(cwd, RESEARCH_ARTIFACTS_DIR);
10801
+ let entries;
10802
+ try {
10803
+ entries = await readdir(dir);
10804
+ } catch {
10805
+ return [];
10806
+ }
10807
+ const markdown = entries.filter((name) => name.endsWith(".md"));
10808
+ const withStats = await Promise.all(
10809
+ markdown.map(async (fileName) => {
10810
+ const outputPath = join2(dir, fileName);
10811
+ try {
10812
+ return { fileName, outputPath, stat: await stat(outputPath) };
10813
+ } catch {
10814
+ return void 0;
10815
+ }
10816
+ })
10817
+ );
10818
+ const newest = withStats.filter((item) => item !== void 0).sort((left, right) => right.stat.mtimeMs - left.stat.mtimeMs).slice(0, maxItems);
10819
+ return Promise.all(
10820
+ newest.map(async ({ fileName, outputPath, stat: stat2 }) => {
10821
+ let content = "";
10822
+ try {
10823
+ content = await readFile2(outputPath, "utf-8");
10824
+ } catch {
10825
+ }
10826
+ const metadata = parseWebResearchArtifactMetadata(content);
10827
+ return {
10828
+ outputPath,
10829
+ fileName,
10830
+ query: metadata.query ?? "",
10831
+ title: deriveWebResearchTitle(content, metadata.query ?? ""),
10832
+ provider: metadata.provider ?? "",
10833
+ status: metadata.status ?? "unknown",
10834
+ startedAt: metadata.startedAt ?? "",
10835
+ completedAt: metadata.completedAt ?? "",
10836
+ elapsedMs: computeHistoryElapsedMs(metadata),
10837
+ mtimeMs: stat2.mtimeMs
10838
+ };
10839
+ })
10840
+ );
10841
+ }
10842
+ function parseWebResearchArtifactMetadata(content) {
10843
+ return parseWebResearchFrontmatter(content) ?? parseLegacyWebResearchArtifactMetadata(content);
10844
+ }
10845
+ function parseWebResearchFrontmatter(content) {
10846
+ if (!content.startsWith("---\n")) {
10847
+ return void 0;
10848
+ }
10849
+ const end = content.indexOf("\n---", 4);
10850
+ if (end === -1) {
10851
+ return void 0;
10852
+ }
10853
+ const result = {};
10854
+ const frontmatter = content.slice(4, end);
10855
+ for (const line of frontmatter.split(/\r?\n/u)) {
10856
+ const match = /^([A-Za-z][A-Za-z0-9_-]*):\s*(.*)$/u.exec(line);
10857
+ if (!match) {
10858
+ continue;
10859
+ }
10860
+ result[match[1] ?? ""] = parseYamlScalar(match[2] ?? "");
10861
+ }
10862
+ return result;
10863
+ }
10864
+ function parseYamlScalar(value) {
10865
+ const trimmed = value.trim();
10866
+ if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
10867
+ try {
10868
+ const parsed = JSON.parse(trimmed);
10869
+ return typeof parsed === "string" ? parsed : String(parsed);
10870
+ } catch {
10871
+ }
10872
+ }
10873
+ return trimmed;
10874
+ }
10875
+ function parseLegacyWebResearchArtifactMetadata(content) {
10876
+ const result = {};
10877
+ const metadataHeadings = /* @__PURE__ */ new Map([
10878
+ ["Query", "query"],
10879
+ ["Provider", "provider"],
10880
+ ["Status", "status"],
10881
+ ["Started", "startedAt"],
10882
+ ["Completed", "completedAt"]
10883
+ ]);
10884
+ const artifactBodyHeadings = /* @__PURE__ */ new Set(["Elapsed", "Items", "Error", "Report"]);
10885
+ const lines = content.split(/\r?\n/u);
10886
+ for (let index = 0; index < lines.length; index++) {
10887
+ const match = /^##\s+(.+)$/u.exec(lines[index] ?? "");
10888
+ if (!match) continue;
10889
+ const heading = match[1] ?? "";
10890
+ if (artifactBodyHeadings.has(heading)) break;
10891
+ const key = metadataHeadings.get(heading);
10892
+ if (key === void 0 || result[key] !== void 0) continue;
10893
+ const values = [];
10894
+ for (let next = index + 1; next < lines.length; next++) {
10895
+ if (/^##\s+/u.test(lines[next] ?? "")) break;
10896
+ if ((lines[next] ?? "").trim() || values.length > 0)
10897
+ values.push(lines[next] ?? "");
10898
+ }
10899
+ result[key] = values.join("\n").trim();
10900
+ }
10901
+ return result;
10902
+ }
10903
+ var RESEARCH_TITLE_MAX_LENGTH = 80;
10904
+ var NON_TITLE_HEADINGS = /* @__PURE__ */ new Set([
10905
+ "Web research report",
10906
+ "Query",
10907
+ "Provider",
10908
+ "Status",
10909
+ "Started",
10910
+ "Completed",
10911
+ "Elapsed",
10912
+ "Items",
10913
+ "Error",
10914
+ "Report"
10915
+ ]);
10916
+ function deriveWebResearchTitle(content, query2) {
10917
+ const body = getWebResearchBody(content);
10918
+ for (const line of body.split(/\r?\n/u)) {
10919
+ const match = /^#{1,2}\s+(.+?)\s*$/u.exec(line);
10920
+ if (!match) continue;
10921
+ const heading = (match[1] ?? "").trim();
10922
+ if (heading.length > 0 && !NON_TITLE_HEADINGS.has(heading)) {
10923
+ return heading;
10924
+ }
10925
+ }
10926
+ const fallback = query2.replace(/\s+/g, " ").trim();
10927
+ if (fallback.length <= RESEARCH_TITLE_MAX_LENGTH) {
10928
+ return fallback;
10929
+ }
10930
+ return `${fallback.slice(0, RESEARCH_TITLE_MAX_LENGTH - 1)}\u2026`;
10931
+ }
10932
+ function computeHistoryElapsedMs(metadata) {
10933
+ if (metadata.elapsedMs !== void 0) {
10934
+ const parsed = Number(metadata.elapsedMs);
10935
+ if (Number.isFinite(parsed) && parsed >= 0) {
10936
+ return parsed;
10937
+ }
10938
+ }
10939
+ const started = Date.parse(metadata.startedAt ?? "");
10940
+ const completed = Date.parse(metadata.completedAt ?? "");
10941
+ if (Number.isFinite(started) && Number.isFinite(completed)) {
10942
+ return Math.max(0, completed - started);
10943
+ }
10944
+ return void 0;
10945
+ }
10946
+ function getWebResearchBody(content) {
10947
+ if (!content.startsWith("---\n")) {
10948
+ return content;
10949
+ }
10950
+ const end = content.indexOf("\n---", 4);
10951
+ if (end === -1) {
10952
+ return content;
10953
+ }
10954
+ const afterClose = content.indexOf("\n", end + 4);
10955
+ return afterClose === -1 ? "" : content.slice(afterClose + 1);
10956
+ }
10957
+ async function loadWebResearchReport(outputPath) {
10958
+ const buffer = await readFile2(outputPath);
10959
+ const truncated = buffer.byteLength > RESEARCH_REPORT_MAX_BYTES;
10960
+ const content = buffer.subarray(0, RESEARCH_REPORT_MAX_BYTES).toString("utf-8");
10961
+ return {
10962
+ body: getWebResearchBody(content).trim(),
10963
+ metadata: parseWebResearchArtifactMetadata(content),
10964
+ truncated
10965
+ };
10966
+ }
10967
+ async function loadWebResearchPreview(outputPath) {
10968
+ const buffer = await readFile2(outputPath);
10969
+ const truncated = buffer.byteLength > RESEARCH_PREVIEW_MAX_BYTES;
10970
+ const text = buffer.subarray(0, RESEARCH_PREVIEW_MAX_BYTES).toString("utf-8");
10971
+ return truncated ? `${text}
10972
+
10973
+ ---
10974
+ Preview truncated. Open the full report at \`${outputPath}\`.` : `${text}
10975
+
10976
+ ---
10977
+ Full report: \`${outputPath}\``;
10978
+ }
10979
+ function trackPendingResearchTask(task) {
10980
+ const tracked = task.catch(() => {
10981
+ }).finally(() => {
10982
+ pendingResearchTasks.delete(tracked);
10983
+ });
10984
+ pendingResearchTasks.add(tracked);
10985
+ }
10986
+ async function waitForPendingResearchTasks() {
10987
+ await Promise.all([...pendingResearchTasks]);
10988
+ }
10989
+
10990
+ // src/web-research-view.ts
10991
+ import { getMarkdownTheme } from "@earendil-works/pi-coding-agent";
10992
+ import {
10993
+ getKeybindings,
10994
+ Key,
10995
+ Markdown,
10996
+ matchesKey,
10997
+ truncateToWidth,
10998
+ visibleWidth
10999
+ } from "@earendil-works/pi-tui";
11000
+ var STATUS_MESSAGE_TTL_MS = 1500;
11001
+ var PAGE_JUMP = 10;
11002
+ var WebResearchManagerView = class {
11003
+ constructor(tui, theme, done, cwd, tasks, onChange, actions, loadHistory = loadWebResearchHistory) {
11004
+ this.tui = tui;
11005
+ this.theme = theme;
11006
+ this.done = done;
11007
+ this.cwd = cwd;
11008
+ this.tasks = tasks;
11009
+ this.onChange = onChange;
11010
+ this.actions = actions;
11011
+ this.loadHistory = loadHistory;
11012
+ void this.reloadHistory();
11013
+ }
11014
+ tui;
11015
+ theme;
11016
+ done;
11017
+ cwd;
11018
+ tasks;
11019
+ onChange;
11020
+ actions;
11021
+ loadHistory;
11022
+ selectedIndex = 0;
11023
+ history = [];
11024
+ confirmCancelId;
11025
+ open;
11026
+ scrollOffset = 0;
11027
+ statusMessage;
11028
+ statusMessageTimer;
11029
+ isReportOpen() {
11030
+ return this.open !== void 0;
11031
+ }
11032
+ render(width) {
11033
+ if (this.open?.kind === "report") {
11034
+ return this.renderReport(this.open, width);
11035
+ }
11036
+ if (this.open?.kind === "detail") {
11037
+ const snapshot = this.findSnapshot(this.open.taskId);
11038
+ if (snapshot) return this.renderDetail(snapshot, width);
11039
+ this.open = void 0;
11040
+ }
11041
+ return this.renderTable(width);
11042
+ }
11043
+ invalidate() {
11044
+ }
11045
+ dispose() {
11046
+ if (this.statusMessageTimer) clearTimeout(this.statusMessageTimer);
11047
+ }
11048
+ handleInput(data) {
11049
+ if (this.open) {
11050
+ this.handleOpenInput(data);
11051
+ } else {
11052
+ this.handleTableInput(data);
11053
+ }
11054
+ this.tui.requestRender();
11055
+ }
11056
+ refresh() {
11057
+ if (this.open?.kind === "detail" && !this.findSnapshot(this.open.taskId)) {
11058
+ this.open = void 0;
11059
+ this.confirmCancelId = void 0;
11060
+ }
11061
+ void this.reloadHistory();
11062
+ this.tui.requestRender();
11063
+ }
11064
+ // --- table mode ---
11065
+ getRows() {
11066
+ const snapshots = getWebResearchTaskSnapshots(this.tasks).sort(
11067
+ (a, b) => a.request.startedAt.localeCompare(b.request.startedAt)
11068
+ );
11069
+ const runningPaths = new Set(
11070
+ snapshots.map((snapshot) => snapshot.request.outputPath)
11071
+ );
11072
+ const rows = snapshots.map((snapshot) => ({
11073
+ kind: "running",
11074
+ snapshot
11075
+ }));
11076
+ for (const item of this.history) {
11077
+ if (runningPaths.has(item.outputPath)) continue;
11078
+ rows.push({ kind: "history", item });
11079
+ }
11080
+ return rows;
11081
+ }
11082
+ border(width) {
11083
+ return this.theme.fg("border", "\u2500".repeat(Math.max(1, width)));
11084
+ }
11085
+ renderTable(width) {
11086
+ const rows = this.getRows();
11087
+ this.selectedIndex = clamp2(this.selectedIndex, 0, rows.length - 1);
11088
+ const lines = [
11089
+ this.border(width),
11090
+ this.theme.fg("accent", " Web research"),
11091
+ ""
11092
+ ];
11093
+ if (rows.length === 0) {
11094
+ lines.push(this.theme.fg("muted", " No research jobs or reports"));
11095
+ } else {
11096
+ const layout = computeTableLayout(rows, width);
11097
+ const now = Date.now();
11098
+ rows.forEach((row, index) => {
11099
+ lines.push(
11100
+ formatResearchTableRow(
11101
+ row,
11102
+ layout,
11103
+ this.theme,
11104
+ index === this.selectedIndex,
11105
+ now
11106
+ )
11107
+ );
11108
+ if (row.kind === "running" && this.confirmCancelId === row.snapshot.request.id) {
11109
+ lines.push(
11110
+ this.theme.fg(
11111
+ "warning",
11112
+ truncateToWidth(
11113
+ ` Press c again to cancel this ${providerLabel(row.snapshot.request.provider)} research`,
11114
+ width
11115
+ )
11116
+ )
11117
+ );
11118
+ }
11119
+ });
11120
+ }
11121
+ lines.push("");
11122
+ if (this.statusMessage) {
11123
+ lines.push(this.theme.fg("accent", ` ${this.statusMessage}`));
11124
+ }
11125
+ lines.push(
11126
+ this.theme.fg(
11127
+ "dim",
11128
+ " \u2191\u2193 move \xB7 Enter open \xB7 c cancel running \xB7 Esc close"
11129
+ ),
11130
+ this.border(width)
11131
+ );
11132
+ return lines;
11133
+ }
11134
+ handleTableInput(data) {
11135
+ const kb = getKeybindings();
11136
+ const rows = this.getRows();
11137
+ if (kb.matches(data, "tui.select.up")) this.move(rows.length, -1);
11138
+ else if (kb.matches(data, "tui.select.down")) this.move(rows.length, 1);
11139
+ else if (kb.matches(data, "tui.select.pageUp"))
11140
+ this.move(rows.length, -PAGE_JUMP, false);
11141
+ else if (kb.matches(data, "tui.select.pageDown"))
11142
+ this.move(rows.length, PAGE_JUMP, false);
11143
+ else if (kb.matches(data, "tui.select.confirm"))
11144
+ void this.openRow(rows[this.selectedIndex]);
11145
+ else if (data === "c") this.cancelRow(rows[this.selectedIndex]);
11146
+ else if (kb.matches(data, "tui.select.cancel")) {
11147
+ if (this.confirmCancelId) this.confirmCancelId = void 0;
11148
+ else this.done(void 0);
11149
+ }
11150
+ }
11151
+ move(count, delta, wrap = true) {
11152
+ this.confirmCancelId = void 0;
11153
+ if (count === 0) return;
11154
+ const next = this.selectedIndex + delta;
11155
+ this.selectedIndex = wrap ? (next + count) % count : clamp2(next, 0, count - 1);
11156
+ }
11157
+ async openRow(row) {
11158
+ if (!row) return;
11159
+ this.confirmCancelId = void 0;
11160
+ if (row.kind === "running") {
11161
+ this.open = { kind: "detail", taskId: row.snapshot.request.id };
11162
+ this.scrollOffset = 0;
11163
+ return;
11164
+ }
11165
+ try {
11166
+ const report = await loadWebResearchReport(row.item.outputPath);
11167
+ this.open = {
11168
+ kind: "report",
11169
+ title: row.item.title || row.item.query,
11170
+ body: report.body,
11171
+ truncated: report.truncated,
11172
+ item: row.item,
11173
+ markdown: new Markdown(report.body, 1, 0, getMarkdownTheme())
11174
+ };
11175
+ this.scrollOffset = 0;
11176
+ } catch (error) {
11177
+ this.showStatusMessage(
11178
+ `Failed to read ${row.item.outputPath}: ${formatErrorMessage(error)}`
11179
+ );
11180
+ }
11181
+ this.tui.requestRender();
11182
+ }
11183
+ cancelRow(row) {
11184
+ if (!row || row.kind !== "running") return;
11185
+ const id = row.snapshot.request.id;
11186
+ if (this.confirmCancelId !== id) {
11187
+ this.confirmCancelId = id;
11188
+ return;
11189
+ }
11190
+ cancelWebResearchTask(this.tasks, id);
11191
+ this.confirmCancelId = void 0;
11192
+ this.onChange();
11193
+ }
11194
+ // --- report / detail mode ---
11195
+ renderReport(open, width) {
11196
+ const lines = [
11197
+ this.border(width),
11198
+ this.theme.fg("accent", truncateToWidth(` ${open.title}`, width)),
11199
+ this.theme.fg(
11200
+ "dim",
11201
+ " c copy markdown \xB7 i inject into context \xB7 \u2191\u2193/PgUp/PgDn scroll \xB7 Esc back"
11202
+ ),
11203
+ this.border(width),
11204
+ ""
11205
+ ];
11206
+ let body;
11207
+ try {
11208
+ body = open.markdown.render(Math.max(20, width - 2));
11209
+ } catch {
11210
+ body = open.body.split("\n").map((line) => truncateToWidth(line, width));
11211
+ }
11212
+ const viewport = this.viewportHeight();
11213
+ const maxOffset = Math.max(0, body.length - viewport);
11214
+ this.scrollOffset = clamp2(this.scrollOffset, 0, maxOffset);
11215
+ lines.push(...body.slice(this.scrollOffset, this.scrollOffset + viewport));
11216
+ lines.push("");
11217
+ const footer = [];
11218
+ if (body.length > viewport) {
11219
+ const end = Math.min(body.length, this.scrollOffset + viewport);
11220
+ footer.push(`lines ${this.scrollOffset + 1}\u2013${end} of ${body.length}`);
11221
+ }
11222
+ if (open.truncated) {
11223
+ footer.push(`report truncated \xB7 full text: ${open.item.outputPath}`);
11224
+ }
11225
+ if (this.statusMessage) {
11226
+ lines.push(this.theme.fg("accent", ` ${this.statusMessage}`));
11227
+ }
11228
+ if (footer.length > 0) {
11229
+ lines.push(
11230
+ this.theme.fg("dim", truncateToWidth(` ${footer.join(" \xB7 ")}`, width))
11231
+ );
11232
+ }
11233
+ lines.push(this.border(width));
11234
+ return lines;
11235
+ }
11236
+ renderDetail(snapshot, width) {
11237
+ const request = snapshot.request;
11238
+ const elapsed = formatCompactElapsed(
11239
+ Date.now() - Date.parse(request.startedAt)
11240
+ );
11241
+ const progress = snapshot.cancelRequestedAt ? "cancelling" : request.progress ?? "running";
11242
+ const lines = [
11243
+ this.border(width),
11244
+ this.theme.fg(
11245
+ "accent",
11246
+ truncateToWidth(
11247
+ ` Running research via ${providerLabel(request.provider)}`,
11248
+ width
11249
+ )
11250
+ ),
11251
+ this.theme.fg("dim", " c cancel \xB7 Esc back"),
11252
+ this.border(width),
11253
+ "",
11254
+ truncateToWidth(` Status: ${progress} (${elapsed} elapsed)`, width),
11255
+ truncateToWidth(` Report path: ${request.outputPath}`, width),
11256
+ "",
11257
+ this.theme.fg("accent", " Research brief"),
11258
+ ""
11259
+ ];
11260
+ for (const line of request.input.split("\n").slice(0, 100)) {
11261
+ lines.push(truncateToWidth(` ${line}`, width));
11262
+ }
11263
+ if (this.confirmCancelId === request.id) {
11264
+ lines.push(
11265
+ "",
11266
+ this.theme.fg("warning", " Press c again to cancel this research")
11267
+ );
11268
+ }
11269
+ if (this.statusMessage) {
11270
+ lines.push("", this.theme.fg("accent", ` ${this.statusMessage}`));
11271
+ }
11272
+ lines.push(this.border(width));
11273
+ return lines;
11274
+ }
11275
+ handleOpenInput(data) {
11276
+ const kb = getKeybindings();
11277
+ const open = this.open;
11278
+ if (!open) return;
11279
+ if (kb.matches(data, "tui.select.cancel")) {
11280
+ if (this.confirmCancelId) {
11281
+ this.confirmCancelId = void 0;
11282
+ return;
11283
+ }
11284
+ this.open = void 0;
11285
+ this.scrollOffset = 0;
11286
+ return;
11287
+ }
11288
+ if (open.kind === "detail") {
11289
+ if (data === "c") {
11290
+ const snapshot = this.findSnapshot(open.taskId);
11291
+ if (!snapshot) return;
11292
+ if (this.confirmCancelId !== open.taskId) {
11293
+ this.confirmCancelId = open.taskId;
11294
+ return;
11295
+ }
11296
+ cancelWebResearchTask(this.tasks, open.taskId);
11297
+ this.confirmCancelId = void 0;
11298
+ this.onChange();
11299
+ this.showStatusMessage("Cancellation requested");
11300
+ }
11301
+ return;
11302
+ }
11303
+ if (kb.matches(data, "tui.select.up")) this.scrollOffset -= 1;
11304
+ else if (kb.matches(data, "tui.select.down")) this.scrollOffset += 1;
11305
+ else if (kb.matches(data, "tui.select.pageUp"))
11306
+ this.scrollOffset -= this.viewportHeight();
11307
+ else if (kb.matches(data, "tui.select.pageDown"))
11308
+ this.scrollOffset += this.viewportHeight();
11309
+ else if (matchesKey(data, Key.home)) this.scrollOffset = 0;
11310
+ else if (matchesKey(data, Key.end))
11311
+ this.scrollOffset = Number.MAX_SAFE_INTEGER;
11312
+ else if (data === "c") {
11313
+ void this.actions.copyToClipboard(open.body).then(() => this.showStatusMessage("Copied report to clipboard")).catch((error) => {
11314
+ const message = `Copy failed: ${formatErrorMessage(error)}`;
11315
+ this.showStatusMessage(message);
11316
+ this.actions.notify(message, "error");
11317
+ });
11318
+ } else if (data === "i") {
11319
+ this.actions.injectReport({
11320
+ title: open.title,
11321
+ body: open.body,
11322
+ item: open.item
11323
+ });
11324
+ this.showStatusMessage("Report added to conversation context");
11325
+ }
11326
+ }
11327
+ viewportHeight() {
11328
+ return Math.max(5, Math.floor(this.tui.terminal.rows * 0.85) - 6);
11329
+ }
11330
+ showStatusMessage(message) {
11331
+ this.statusMessage = message;
11332
+ if (this.statusMessageTimer) clearTimeout(this.statusMessageTimer);
11333
+ this.statusMessageTimer = setTimeout(() => {
11334
+ this.statusMessage = void 0;
11335
+ this.statusMessageTimer = void 0;
11336
+ this.tui.requestRender();
11337
+ }, STATUS_MESSAGE_TTL_MS);
11338
+ }
11339
+ findSnapshot(taskId) {
11340
+ return getWebResearchTaskSnapshots(this.tasks).find(
11341
+ (snapshot) => snapshot.request.id === taskId
11342
+ );
11343
+ }
11344
+ async reloadHistory() {
11345
+ this.history = await this.loadHistory(this.cwd);
11346
+ this.tui.requestRender();
11347
+ }
11348
+ };
11349
+ function computeTableLayout(rows, width) {
11350
+ const providerWidth = clamp2(
11351
+ Math.max(0, ...rows.map((row) => visibleWidth(rowProvider(row)))),
11352
+ 4,
11353
+ 12
11354
+ );
11355
+ const date = 10;
11356
+ const duration = 7;
11357
+ const fixed = 2 + 2 + date + 1 + providerWidth + 1 + duration + 1;
11358
+ return {
11359
+ date,
11360
+ provider: providerWidth,
11361
+ duration,
11362
+ title: Math.max(10, width - fixed)
11363
+ };
11364
+ }
11365
+ function formatResearchTableRow(row, layout, theme, selected, now = Date.now()) {
11366
+ const cursor = selected ? "\u203A " : " ";
11367
+ const glyph = statusGlyph(row, theme);
11368
+ const date = padCell(
11369
+ formatRelativeDate(rowTimestampMs(row), now),
11370
+ layout.date
11371
+ );
11372
+ const provider = padCell(
11373
+ truncateToWidth(rowProvider(row), layout.provider),
11374
+ layout.provider
11375
+ );
11376
+ const duration = padCell(rowDuration(row, now), layout.duration, "right");
11377
+ const title = truncateToWidth(rowTitle(row), layout.title);
11378
+ const dim = (text) => row.kind === "history" ? text : theme.fg("dim", text);
11379
+ return `${cursor}${glyph} ${dim(date)} ${provider} ${theme.fg("muted", duration)} ${title}`;
11380
+ }
11381
+ function statusGlyph(row, theme) {
11382
+ if (row.kind === "running") {
11383
+ const icon = row.snapshot.cancelRequestedAt ? "\u25CC" : getWebResearchProgressIcon(row.snapshot.request);
11384
+ return theme.fg("accent", icon);
11385
+ }
11386
+ switch (row.item.status) {
11387
+ case "completed":
11388
+ return theme.fg("success", "\u2714");
11389
+ case "failed":
11390
+ return theme.fg("error", "\u2718");
11391
+ case "cancelled":
11392
+ return theme.fg("warning", "\u2718");
11393
+ default:
11394
+ return theme.fg("dim", "?");
11395
+ }
11396
+ }
11397
+ function rowProvider(row) {
11398
+ return row.kind === "running" ? providerLabel(row.snapshot.request.provider) : row.item.provider || "?";
11399
+ }
11400
+ function rowTimestampMs(row) {
11401
+ if (row.kind === "running") {
11402
+ return Date.parse(row.snapshot.request.startedAt);
11403
+ }
11404
+ const completed = Date.parse(row.item.completedAt);
11405
+ if (Number.isFinite(completed)) return completed;
11406
+ const started = Date.parse(row.item.startedAt);
11407
+ if (Number.isFinite(started)) return started;
11408
+ return row.item.mtimeMs;
11409
+ }
11410
+ function rowDuration(row, now) {
11411
+ if (row.kind === "running") {
11412
+ return formatCompactElapsed(
11413
+ now - Date.parse(row.snapshot.request.startedAt)
11414
+ );
11415
+ }
11416
+ return row.item.elapsedMs === void 0 ? "\u2014" : formatCompactElapsed(row.item.elapsedMs);
11417
+ }
11418
+ var REDUNDANT_PROGRESS = /* @__PURE__ */ new Set([
11419
+ "in_progress",
11420
+ "running",
11421
+ "starting",
11422
+ "queued",
11423
+ "cancelling"
11424
+ ]);
11425
+ function rowTitle(row) {
11426
+ if (row.kind === "running") {
11427
+ const request = row.snapshot.request;
11428
+ const progress = row.snapshot.cancelRequestedAt ? void 0 : request.progress;
11429
+ const prefix = progress && !REDUNDANT_PROGRESS.has(progress) ? `${progress} \u2014 ` : "";
11430
+ return `${prefix}${cleanSingleLine(request.input)}`;
11431
+ }
11432
+ return row.item.title || cleanSingleLine(row.item.query);
11433
+ }
11434
+ function formatRelativeDate(timestampMs, now) {
11435
+ if (!Number.isFinite(timestampMs)) return "?";
11436
+ const diff = Math.max(0, now - timestampMs);
11437
+ if (diff < 6e4) return "now";
11438
+ if (diff < 36e5) return `${Math.floor(diff / 6e4)}m ago`;
11439
+ if (diff < 864e5) return `${Math.floor(diff / 36e5)}h ago`;
11440
+ if (diff < 7 * 864e5) return `${Math.floor(diff / 864e5)}d ago`;
11441
+ const date = new Date(timestampMs);
11442
+ const month = String(date.getMonth() + 1).padStart(2, "0");
11443
+ const day = String(date.getDate()).padStart(2, "0");
11444
+ if (date.getFullYear() === new Date(now).getFullYear()) {
11445
+ return `${month}-${day}`;
11446
+ }
11447
+ return `${date.getFullYear()}-${month}-${day}`;
11448
+ }
11449
+ function providerLabel(providerId) {
11450
+ return PROVIDERS_BY_ID[providerId]?.label ?? providerId;
11451
+ }
11452
+ function padCell(text, width, align = "left") {
11453
+ const pad = " ".repeat(Math.max(0, width - visibleWidth(text)));
11454
+ return align === "left" ? `${text}${pad}` : `${pad}${text}`;
11455
+ }
11456
+ function clamp2(value, min, max) {
11457
+ return Math.min(Math.max(value, min), Math.max(min, max));
11458
+ }
11459
+ function formatCompactElapsed(ms) {
11460
+ const totalSeconds = Math.max(0, Math.floor(ms / 1e3));
11461
+ const minutes = Math.floor(totalSeconds / 60);
11462
+ const seconds = totalSeconds % 60;
11463
+ if (minutes > 0) {
11464
+ return `${minutes}m${seconds}s`;
11465
+ }
11466
+ return `${totalSeconds}s`;
11467
+ }
11468
+ function cleanSingleLine(text) {
11469
+ return text.replace(/\s+/g, " ").trim();
11470
+ }
11471
+
11472
+ // src/index.ts
11473
+ var DEFAULT_MAX_RESULTS = 5;
11474
+ var MAX_ALLOWED_RESULTS = 20;
11475
+ var MAX_SEARCH_QUERIES = 10;
11476
+ var RESEARCH_HEARTBEAT_MS = 15e3;
11477
+ var WEB_RESEARCH_RESULT_MESSAGE_TYPE = "web-research-result";
11478
+ var WEB_RESEARCH_REPORT_MESSAGE_TYPE = "web-research-report";
11479
+ var WEB_RESEARCH_WIDGET_KEY = "web-research-jobs";
11480
+ var DEFAULT_SUMMARY_SYMBOLS = {
11481
+ success: "\u2714",
11482
+ failure: "\u2718"
11483
+ };
11484
+ function webProvidersExtension(pi) {
11485
+ const activeWebResearchRequests = /* @__PURE__ */ new Map();
11486
+ let latestWidgetContext;
11487
+ let webResearchWidgetTimer;
11488
+ const stopWebResearchWidgetTimer = () => {
11489
+ if (webResearchWidgetTimer) {
11490
+ clearInterval(webResearchWidgetTimer);
11491
+ webResearchWidgetTimer = void 0;
11492
+ }
11493
+ };
11494
+ const ensureWebResearchWidgetTimer = () => {
11495
+ if (webResearchWidgetTimer || activeWebResearchRequests.size === 0) {
11496
+ return;
11497
+ }
11498
+ webResearchWidgetTimer = setInterval(() => {
11499
+ updateWebResearchWidget();
11500
+ }, 1e3);
11501
+ };
11502
+ const updateWebResearchWidget = (ctx) => {
11503
+ const widgetContext = ctx ?? latestWidgetContext;
11504
+ if (!widgetContext) {
11505
+ return;
11506
+ }
11507
+ latestWidgetContext = widgetContext;
11508
+ if (!widgetContext.hasUI) {
11509
+ stopWebResearchWidgetTimer();
11510
+ return;
11511
+ }
11512
+ if (activeWebResearchRequests.size === 0) {
11513
+ stopWebResearchWidgetTimer();
11514
+ widgetContext.ui.setWidget(WEB_RESEARCH_WIDGET_KEY, void 0);
11515
+ return;
11516
+ }
11517
+ ensureWebResearchWidgetTimer();
11518
+ widgetContext.ui.setWidget(
11519
+ WEB_RESEARCH_WIDGET_KEY,
11520
+ buildWebResearchWidgetLines(
11521
+ getActiveWebResearchRequests(activeWebResearchRequests),
11522
+ widgetContext.ui.theme
11523
+ )
11524
+ );
11525
+ };
11526
+ if ("registerMessageRenderer" in pi) {
11527
+ pi.registerMessageRenderer(
11528
+ WEB_RESEARCH_RESULT_MESSAGE_TYPE,
11529
+ (message, state, theme) => renderWebResearchResultMessage(message, state, theme)
11530
+ );
11531
+ pi.registerMessageRenderer(
11532
+ WEB_RESEARCH_REPORT_MESSAGE_TYPE,
11533
+ (message, state, theme) => renderWebResearchReportMessage(message, state, theme)
11534
+ );
11535
+ }
11536
+ pi.registerCommand("web-providers", {
11537
+ description: "Configure web search providers",
9865
11538
  handler: async (_args, ctx) => {
9866
11539
  if (!ctx.hasUI) {
9867
11540
  ctx.ui.notify("web-providers requires interactive mode", "error");
@@ -9874,11 +11547,71 @@ function webProvidersExtension(pi) {
9874
11547
  );
9875
11548
  }
9876
11549
  });
11550
+ pi.registerCommand("web-research", {
11551
+ description: "Browse, inspect, and manage web researches",
11552
+ handler: async (_args, ctx) => {
11553
+ if (!ctx.hasUI) {
11554
+ ctx.ui.notify("web-research requires interactive mode", "error");
11555
+ return;
11556
+ }
11557
+ const actions = {
11558
+ copyToClipboard,
11559
+ injectReport: ({ title, body, item }) => {
11560
+ pi.sendMessage(
11561
+ {
11562
+ customType: WEB_RESEARCH_REPORT_MESSAGE_TYPE,
11563
+ content: formatWebResearchReportMessage(title, body, item),
11564
+ display: true,
11565
+ details: {
11566
+ title,
11567
+ outputPath: item.outputPath,
11568
+ provider: item.provider,
11569
+ query: item.query,
11570
+ status: item.status
11571
+ }
11572
+ },
11573
+ { triggerTurn: false }
11574
+ );
11575
+ },
11576
+ notify: (message, type) => ctx.ui.notify(message, type ?? "info")
11577
+ };
11578
+ let timer;
11579
+ let view;
11580
+ try {
11581
+ await ctx.ui.custom(
11582
+ (tui, theme, _keybindings, done) => {
11583
+ view = new WebResearchManagerView(
11584
+ tui,
11585
+ theme,
11586
+ done,
11587
+ ctx.cwd,
11588
+ activeWebResearchRequests,
11589
+ () => updateWebResearchWidget(ctx),
11590
+ actions
11591
+ );
11592
+ timer = setInterval(() => view?.refresh(), 1e3);
11593
+ return view;
11594
+ },
11595
+ {
11596
+ overlay: true,
11597
+ overlayOptions: () => view?.isReportOpen() ? { anchor: "center", width: "85%", maxHeight: "85%" } : {
11598
+ anchor: "center",
11599
+ width: "75%",
11600
+ maxHeight: "60%",
11601
+ minWidth: 60
11602
+ }
11603
+ }
11604
+ );
11605
+ } finally {
11606
+ if (timer) clearInterval(timer);
11607
+ }
11608
+ }
11609
+ });
9877
11610
  pi.on("session_start", async (_event, ctx) => {
9878
11611
  latestWidgetContext = ctx;
9879
11612
  resetContentStore();
9880
11613
  updateWebResearchWidget(ctx);
9881
- await refreshManagedToolsOnStartup(
11614
+ await refreshManagedToolsOnStartup2(
9882
11615
  pi,
9883
11616
  { activeWebResearchRequests, updateWebResearchWidget },
9884
11617
  ctx.cwd,
@@ -9889,7 +11622,7 @@ function webProvidersExtension(pi) {
9889
11622
  latestWidgetContext = ctx;
9890
11623
  await cleanupContentStore();
9891
11624
  updateWebResearchWidget(ctx);
9892
- await refreshManagedToolsOnStartup(
11625
+ await refreshManagedToolsOnStartup2(
9893
11626
  pi,
9894
11627
  { activeWebResearchRequests, updateWebResearchWidget },
9895
11628
  ctx.cwd,
@@ -10112,7 +11845,7 @@ function registerWebResearchTool(pi, webResearchLifecycle, providerIds) {
10112
11845
  "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."
10113
11846
  ]),
10114
11847
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
10115
- return dispatchWebResearch({
11848
+ return dispatchWebResearch2({
10116
11849
  pi,
10117
11850
  activeWebResearchRequests: webResearchLifecycle.activeWebResearchRequests,
10118
11851
  updateWebResearchWidget: webResearchLifecycle.updateWebResearchWidget,
@@ -10137,109 +11870,38 @@ function registerWebResearchTool(pi, webResearchLifecycle, providerIds) {
10137
11870
  }
10138
11871
  });
10139
11872
  }
10140
- async function runWebProvidersConfig(pi, webResearchLifecycle, ctx) {
10141
- const config = await loadConfig();
10142
- const activeProvider = getInitialProviderSelection(config);
10143
- await ctx.ui.custom(
10144
- (tui, theme, _keybindings, done) => new WebProvidersSettingsView(
10145
- tui,
10146
- theme,
10147
- done,
10148
- ctx,
10149
- config,
10150
- activeProvider
10151
- )
10152
- );
10153
- await refreshManagedTools(pi, webResearchLifecycle, ctx.cwd, {
10154
- addAvailable: true
10155
- });
10156
- }
10157
- function formatStartupConfigError(error) {
10158
- const detail = error instanceof Error ? error.message : String(error);
10159
- return `web-providers config error: ${detail.replace(getConfigPath(), "~/.pi/agent/web-providers.json")}`;
10160
- }
10161
- function getAvailableProviderIdsForCapability(config, cwd, capability) {
10162
- const providerId = getMappedProviderIdForTool(config, capability);
10163
- if (!providerId) {
10164
- return [];
10165
- }
10166
- const provider = PROVIDERS_BY_ID[providerId];
10167
- if (!supportsTool2(provider, capability)) {
10168
- return [];
10169
- }
10170
- const status = getProviderCapabilityStatus(
10171
- config,
10172
- cwd,
10173
- providerId,
10174
- capability,
10175
- {
10176
- resolveSecrets: false
10177
- }
10178
- );
10179
- return isProviderCapabilityExposable(status) ? [providerId] : [];
10180
- }
10181
- function getProviderStatusForTool(config, cwd, providerId, capability) {
10182
- return getProviderCapabilityStatus(config, cwd, providerId, capability);
10183
- }
10184
- function getAvailableManagedToolNames(config, cwd) {
10185
- return Object.keys(CAPABILITY_TOOL_NAMES).filter(
10186
- (capability) => getAvailableProviderIdsForCapability(config, cwd, capability).length > 0
10187
- ).map((capability) => CAPABILITY_TOOL_NAMES[capability]);
10188
- }
10189
- function getSyncedActiveTools(config, cwd, activeToolNames, options) {
10190
- const availableToolNames = new Set(getAvailableManagedToolNames(config, cwd));
10191
- const nextActiveTools = new Set(activeToolNames);
10192
- for (const toolName of MANAGED_TOOL_NAMES) {
10193
- if (availableToolNames.has(toolName)) {
10194
- if (options.addAvailable) {
10195
- nextActiveTools.add(toolName);
10196
- }
10197
- continue;
10198
- }
10199
- nextActiveTools.delete(toolName);
10200
- }
10201
- return nextActiveTools;
10202
- }
10203
- async function refreshManagedTools(pi, webResearchLifecycle, cwd, options) {
11873
+ async function runWebProvidersConfig(pi, webResearchLifecycle, ctx) {
10204
11874
  const config = await loadConfig();
10205
- const nextActiveTools = getSyncedActiveTools(
10206
- config,
10207
- cwd,
10208
- pi.getActiveTools(),
10209
- options
11875
+ const activeProvider = getInitialProviderSelection(config);
11876
+ await ctx.ui.custom(
11877
+ (tui, theme, _keybindings, done) => new WebProvidersSettingsView(
11878
+ tui,
11879
+ theme,
11880
+ done,
11881
+ ctx,
11882
+ config,
11883
+ activeProvider
11884
+ )
10210
11885
  );
10211
- registerManagedTools(pi, webResearchLifecycle, {
10212
- search: getAvailableProviderIdsForCapability(config, cwd, "search"),
10213
- contents: getAvailableProviderIdsForCapability(config, cwd, "contents"),
10214
- answer: getAvailableProviderIdsForCapability(config, cwd, "answer"),
10215
- research: getAvailableProviderIdsForCapability(config, cwd, "research")
11886
+ await refreshManagedTools2(pi, webResearchLifecycle, ctx.cwd, {
11887
+ addAvailable: true
10216
11888
  });
10217
- await syncManagedToolAvailability(pi, nextActiveTools);
10218
11889
  }
10219
- async function refreshManagedToolsOnStartup(pi, webResearchLifecycle, cwd, options) {
10220
- try {
10221
- await refreshManagedTools(pi, webResearchLifecycle, cwd, options);
10222
- } catch (error) {
10223
- const message = formatStartupConfigError(error);
10224
- pi.sendMessage({
10225
- customType: "web-providers-config-error",
10226
- content: message,
10227
- display: true
10228
- });
10229
- await syncManagedToolAvailability(
10230
- pi,
10231
- new Set(
10232
- pi.getActiveTools().filter((toolName) => !MANAGED_TOOL_NAMES.includes(toolName))
10233
- )
10234
- );
10235
- }
11890
+ async function refreshManagedTools2(pi, webResearchLifecycle, cwd, options) {
11891
+ await refreshManagedTools(
11892
+ pi,
11893
+ (providerIdsByCapability) => registerManagedTools(pi, webResearchLifecycle, providerIdsByCapability),
11894
+ cwd,
11895
+ options
11896
+ );
10236
11897
  }
10237
- async function syncManagedToolAvailability(pi, nextActiveTools) {
10238
- const activeTools = pi.getActiveTools();
10239
- const changed = activeTools.length !== nextActiveTools.size || activeTools.some((toolName) => !nextActiveTools.has(toolName));
10240
- if (changed) {
10241
- pi.setActiveTools(Array.from(nextActiveTools));
10242
- }
11898
+ async function refreshManagedToolsOnStartup2(pi, webResearchLifecycle, cwd, options) {
11899
+ await refreshManagedToolsOnStartup(
11900
+ pi,
11901
+ (providerIdsByCapability) => registerManagedTools(pi, webResearchLifecycle, providerIdsByCapability),
11902
+ cwd,
11903
+ options
11904
+ );
10243
11905
  }
10244
11906
  function getSearchMaxResultsLimit(providerId) {
10245
11907
  const capabilities = PROVIDERS_BY_ID[providerId].capabilities;
@@ -10456,12 +12118,12 @@ async function executeRawProviderRequest({
10456
12118
  input
10457
12119
  });
10458
12120
  }
10459
- function buildSearchBatchError(outcomes, providerLabel) {
12121
+ function buildSearchBatchError(outcomes, providerLabel2) {
10460
12122
  const failed = outcomes.filter((outcome) => outcome.error !== void 0);
10461
12123
  if (failed.length === 1) {
10462
12124
  return new Error(
10463
12125
  formatProviderCapabilityFailure(
10464
- providerLabel,
12126
+ providerLabel2,
10465
12127
  "search",
10466
12128
  failed[0]?.error ?? ""
10467
12129
  )
@@ -10471,7 +12133,7 @@ function buildSearchBatchError(outcomes, providerLabel) {
10471
12133
  (outcome, index) => `${index + 1}. ${formatQuotedPreview(outcome.query, 40)} \u2014 ${outcome.error}`
10472
12134
  ).join("; ");
10473
12135
  return new Error(
10474
- `${providerLabel} search failed for ${failed.length} queries: ${summary}`
12136
+ `${providerLabel2} search failed for ${failed.length} queries: ${summary}`
10475
12137
  );
10476
12138
  }
10477
12139
  async function executeSingleSearchQuery({
@@ -10608,12 +12270,12 @@ async function executeAnswerToolInternal({
10608
12270
  })
10609
12271
  };
10610
12272
  }
10611
- function buildAnswerBatchError(outcomes, providerLabel) {
12273
+ function buildAnswerBatchError(outcomes, providerLabel2) {
10612
12274
  const failed = outcomes.filter((outcome) => outcome.error !== void 0);
10613
12275
  if (failed.length === 1) {
10614
12276
  return new Error(
10615
12277
  formatProviderCapabilityFailure(
10616
- providerLabel,
12278
+ providerLabel2,
10617
12279
  "answer",
10618
12280
  failed[0]?.error ?? ""
10619
12281
  )
@@ -10623,7 +12285,7 @@ function buildAnswerBatchError(outcomes, providerLabel) {
10623
12285
  (outcome, index) => `${index + 1}. ${formatQuotedPreview(outcome.query, 40)} \u2014 ${outcome.error}`
10624
12286
  ).join("; ");
10625
12287
  return new Error(
10626
- `${providerLabel} answer failed for ${failed.length} questions: ${summary}`
12288
+ `${providerLabel2} answer failed for ${failed.length} questions: ${summary}`
10627
12289
  );
10628
12290
  }
10629
12291
  function formatAnswerResponses(outcomes) {
@@ -10839,7 +12501,7 @@ async function executeProviderToolInternal({
10839
12501
  })
10840
12502
  };
10841
12503
  }
10842
- async function dispatchWebResearch({
12504
+ async function dispatchWebResearch2({
10843
12505
  pi,
10844
12506
  activeWebResearchRequests,
10845
12507
  updateWebResearchWidget,
@@ -10868,188 +12530,59 @@ async function dispatchWebResearchInternal({
10868
12530
  input,
10869
12531
  executionOverride
10870
12532
  }) {
10871
- await cleanupContentStore();
10872
- const provider = resolveProviderForTool(
12533
+ return dispatchWebResearch({
12534
+ activeWebResearchRequests,
10873
12535
  config,
10874
- ctx.cwd,
10875
- "research",
10876
- explicitProvider
10877
- );
10878
- const request = createWebResearchRequest(ctx.cwd, provider.id, input);
10879
- const providerConfig = getEffectiveProviderConfig(config, provider.id);
10880
- activeWebResearchRequests.set(request.id, request);
10881
- updateWebResearchWidget(ctx);
10882
- trackPendingResearchTask(
10883
- runDispatchedWebResearch({
10884
- pi,
10885
- activeWebResearchRequests,
10886
- updateWebResearchWidget,
10887
- request,
10888
- config,
12536
+ explicitProvider,
12537
+ ctx: { cwd: ctx.cwd },
12538
+ options: providerOptions,
12539
+ input,
12540
+ executionOverride,
12541
+ executeResearch: async ({
12542
+ config: config2,
10889
12543
  provider,
10890
12544
  providerConfig,
10891
- ctx,
10892
- options: providerOptions,
10893
- executionOverride
10894
- })
10895
- );
10896
- return {
10897
- content: [
10898
- {
10899
- type: "text",
10900
- text: `Started web research via ${provider.label}.`
10901
- }
10902
- ],
10903
- details: request,
10904
- display: buildProviderToolDisplay2({
10905
- capability: "research",
10906
- providerId: provider.id,
10907
- details: { tool: "web_research", provider: provider.id },
10908
- text: "started"
10909
- })
10910
- };
10911
- }
10912
- async function runDispatchedWebResearch({
10913
- pi,
10914
- activeWebResearchRequests,
10915
- updateWebResearchWidget,
10916
- request,
10917
- config,
10918
- provider,
10919
- providerConfig,
10920
- ctx,
10921
- options,
10922
- executionOverride
10923
- }) {
10924
- let result;
10925
- let reportText = "";
10926
- try {
10927
- const response = await executeProviderOperation({
12545
+ ctx: ctx2,
12546
+ signal,
12547
+ options,
12548
+ input: input2,
12549
+ onProgress,
12550
+ executionOverride: executionOverride2
12551
+ }) => executeProviderOperation({
10928
12552
  capability: "research",
10929
- config,
12553
+ config: config2,
10930
12554
  provider,
10931
12555
  providerConfig,
10932
- ctx,
10933
- signal: void 0,
12556
+ ctx: ctx2,
12557
+ signal,
10934
12558
  options,
10935
- input: request.input,
10936
- onProgress: (message) => {
10937
- request.progress = summarizeWebResearchProgress(
10938
- message,
10939
- provider.label
10940
- );
10941
- updateWebResearchWidget();
10942
- },
10943
- executionOverride
10944
- });
10945
- const completedAt = (/* @__PURE__ */ new Date()).toISOString();
10946
- result = {
10947
- ...request,
10948
- status: "completed",
10949
- completedAt,
10950
- elapsedMs: Math.max(
10951
- 0,
10952
- Date.parse(completedAt) - Date.parse(request.startedAt)
10953
- ),
10954
- itemCount: response.itemCount
10955
- };
10956
- reportText = response.text;
10957
- } catch (error) {
10958
- const completedAt = (/* @__PURE__ */ new Date()).toISOString();
10959
- result = {
10960
- ...request,
10961
- status: "failed",
10962
- completedAt,
10963
- elapsedMs: Math.max(
10964
- 0,
10965
- Date.parse(completedAt) - Date.parse(request.startedAt)
10966
- ),
10967
- error: formatErrorMessage(error)
10968
- };
10969
- }
10970
- try {
10971
- await writeWebResearchArtifact(result, reportText);
10972
- pi.sendMessage({
10973
- customType: WEB_RESEARCH_RESULT_MESSAGE_TYPE,
10974
- content: formatWebResearchResultMessage(result, reportText),
10975
- display: true,
10976
- details: result
10977
- });
10978
- } finally {
10979
- activeWebResearchRequests.delete(request.id);
10980
- updateWebResearchWidget();
10981
- }
10982
- }
10983
- function createWebResearchRequest(cwd, provider, input) {
10984
- const startedAt = (/* @__PURE__ */ new Date()).toISOString();
10985
- return {
10986
- tool: "web_research",
10987
- id: randomUUID(),
10988
- provider,
10989
- input,
10990
- outputPath: buildWebResearchArtifactPath(cwd, input, startedAt),
10991
- startedAt
10992
- };
10993
- }
10994
- function buildWebResearchArtifactPath(cwd, input, startedAt) {
10995
- const timestamp = startedAt.replaceAll(":", "-").replace(".", "-");
10996
- const slug = slugifyWebResearchInput(input);
10997
- return join2(cwd, RESEARCH_ARTIFACTS_DIR, `${timestamp}-${slug}.md`);
10998
- }
10999
- function slugifyWebResearchInput(input) {
11000
- const slug = input.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60).replace(/-+$/g, "");
11001
- return slug.length > 0 ? slug : "research";
12559
+ input: input2,
12560
+ onProgress,
12561
+ executionOverride: executionOverride2
12562
+ }),
12563
+ deliverResult: (message) => pi.sendMessage(message),
12564
+ onJobsChanged: () => updateWebResearchWidget(ctx),
12565
+ resultMessageType: WEB_RESEARCH_RESULT_MESSAGE_TYPE
12566
+ });
11002
12567
  }
11003
12568
  function buildWebResearchWidgetLines(requests, theme, now = Date.now()) {
11004
- const lines = [theme.fg("accent", "Research jobs:")];
11005
- for (const request of requests.slice().sort((left, right) => left.startedAt.localeCompare(right.startedAt)).slice(0, 3)) {
11006
- const providerLabel = PROVIDERS_BY_ID[request.provider]?.label ?? request.provider;
11007
- const elapsed = formatCompactElapsed(now - Date.parse(request.startedAt));
11008
- const icon = getWebResearchWidgetIcon(request, now);
11009
- lines.push(
11010
- `${icon}${providerLabel} ${theme.fg("muted", `(${elapsed}): `)}${truncateInline(cleanSingleLine(request.input), 70)}`
11011
- );
11012
- }
11013
- if (requests.length > 3) {
11014
- lines.push(theme.fg("muted", `+${requests.length - 3} more`));
11015
- }
11016
- return lines;
11017
- }
11018
- function getWebResearchWidgetIcon(request, _now) {
11019
- if (request.progress === "poll retrying after transient errors") {
11020
- return "\u27F3 ";
11021
- }
11022
- if (request.progress === "queued") {
11023
- return "\u25CC ";
11024
- }
11025
- if (request.progress === "starting") {
11026
- return "\u25D4 ";
11027
- }
11028
- if (request.progress?.startsWith("started:")) {
11029
- return "\u25D1 ";
11030
- }
11031
- return "\u25CF ";
11032
- }
11033
- function summarizeWebResearchProgress(message, providerLabel) {
11034
- const startingMessage = `Starting research via ${providerLabel}`;
11035
- if (message === startingMessage) {
11036
- return "starting";
11037
- }
11038
- const startedPrefix = `${providerLabel} research started: `;
11039
- if (message.startsWith(startedPrefix)) {
11040
- return `started: ${message.slice(startedPrefix.length)}`;
11041
- }
11042
- const statusPrefix = `Research via ${providerLabel}: `;
11043
- if (message.startsWith(statusPrefix)) {
11044
- return message.slice(statusPrefix.length).replace(/\s+\([^)]* elapsed\)$/u, "").trim();
11045
- }
11046
- const retryPrefix = `${providerLabel} research poll is still retrying after transient errors`;
11047
- if (message.startsWith(retryPrefix)) {
11048
- return "poll retrying after transient errors";
12569
+ const sorted = requests.slice().sort((left, right) => left.startedAt.localeCompare(right.startedAt));
12570
+ const jobs = sorted.slice(0, 3).map((request) => {
12571
+ const providerLabel2 = PROVIDERS_BY_ID[request.provider]?.label ?? request.provider;
12572
+ const elapsed = formatCompactElapsed2(now - Date.parse(request.startedAt));
12573
+ const icon = getWebResearchProgressIcon(request);
12574
+ const status = request.progress === "cancelling" ? " cancelling" : "";
12575
+ return `${icon} ${providerLabel2} ${elapsed}${status}`;
12576
+ });
12577
+ if (sorted.length > 3) {
12578
+ jobs.push(`+${sorted.length - 3} more`);
11049
12579
  }
11050
- return message.trim();
12580
+ const count = sorted.length === 1 ? "1 research" : `${sorted.length} researches`;
12581
+ return [
12582
+ `${theme.fg("accent", `${count} running`)}${theme.fg("muted", ` \xB7 ${jobs.join(" \xB7 ")} \xB7 /web-research`)}`
12583
+ ];
11051
12584
  }
11052
- function formatCompactElapsed(ms) {
12585
+ function formatCompactElapsed2(ms) {
11053
12586
  const totalSeconds = Math.max(0, Math.floor(ms / 1e3));
11054
12587
  const minutes = Math.floor(totalSeconds / 60);
11055
12588
  const seconds = totalSeconds % 60;
@@ -11058,68 +12591,6 @@ function formatCompactElapsed(ms) {
11058
12591
  }
11059
12592
  return `${totalSeconds}s`;
11060
12593
  }
11061
- function formatWebResearchResultMessage(result, reportText) {
11062
- const text = reportText.trim();
11063
- if (text.length > 0) {
11064
- return `${text}
11065
- `;
11066
- }
11067
- if (result.error) {
11068
- return `${result.error}
11069
- `;
11070
- }
11071
- return "";
11072
- }
11073
- async function writeWebResearchArtifact(result, reportText) {
11074
- await mkdir2(dirname2(result.outputPath), { recursive: true });
11075
- await writeFile2(
11076
- result.outputPath,
11077
- formatWebResearchArtifact(result, reportText),
11078
- "utf-8"
11079
- );
11080
- }
11081
- function formatWebResearchArtifact(result, reportText) {
11082
- const providerLabel = PROVIDERS_BY_ID[result.provider]?.label ?? result.provider;
11083
- const lines = [
11084
- "# Web research report",
11085
- "",
11086
- "## Query",
11087
- result.input,
11088
- "",
11089
- "## Provider",
11090
- providerLabel,
11091
- "",
11092
- "## Status",
11093
- result.status,
11094
- "",
11095
- "## Started",
11096
- result.startedAt,
11097
- "",
11098
- "## Completed",
11099
- result.completedAt,
11100
- "",
11101
- "## Elapsed",
11102
- formatElapsed(result.elapsedMs)
11103
- ];
11104
- if (typeof result.itemCount === "number") {
11105
- lines.push("", "## Items", String(result.itemCount));
11106
- }
11107
- if (result.error) {
11108
- lines.push("", "## Error", result.error);
11109
- }
11110
- if (reportText) {
11111
- lines.push("", "## Report", reportText);
11112
- }
11113
- return `${lines.join("\n")}
11114
- `;
11115
- }
11116
- function trackPendingResearchTask(task) {
11117
- const tracked = task.catch(() => {
11118
- }).finally(() => {
11119
- pendingResearchTasks.delete(tracked);
11120
- });
11121
- pendingResearchTasks.add(tracked);
11122
- }
11123
12594
  async function executeBatchedContentsTool({
11124
12595
  config,
11125
12596
  provider,
@@ -11281,10 +12752,10 @@ function createToolProgressReporter(capability, providerId, progress) {
11281
12752
  if (Date.now() - lastUpdateAt < RESEARCH_HEARTBEAT_MS) {
11282
12753
  return;
11283
12754
  }
11284
- const providerLabel = PROVIDERS_BY_ID[providerId]?.label ?? providerId;
12755
+ const providerLabel2 = PROVIDERS_BY_ID[providerId]?.label ?? providerId;
11285
12756
  const elapsed = formatElapsed(Date.now() - startedAt);
11286
12757
  emit(
11287
- `Researching via ${providerLabel} (${elapsed} elapsed)`,
12758
+ `Researching via ${providerLabel2} (${elapsed} elapsed)`,
11288
12759
  buildProgressDisplay2(providerId, `Researching ${elapsed}`)
11289
12760
  );
11290
12761
  lastUpdateAt = Date.now();
@@ -11307,38 +12778,38 @@ function renderListCallHeader(toolName, items, theme, suffix, options = {}) {
11307
12778
  invalidate() {
11308
12779
  },
11309
12780
  render(width) {
11310
- const normalizedItems = items.map((item) => cleanSingleLine(item)).filter((item) => item.length > 0);
12781
+ const normalizedItems = items.map((item) => cleanSingleLine2(item)).filter((item) => item.length > 0);
11311
12782
  const toolTitle = theme.fg("toolTitle", theme.bold(toolName));
11312
12783
  const mutedSuffix = suffix ? theme.fg("muted", suffix) : "";
11313
12784
  if (!options.forceMultiline && normalizedItems.length === 1) {
11314
12785
  const singleItem = options.quoteSingleItem ? formatQuotedPreview(normalizedItems[0], 80) : truncateInline(normalizedItems[0], 120);
11315
12786
  const inline = `${toolTitle} ${theme.fg("accent", singleItem)}${mutedSuffix}`;
11316
- const line = truncateToWidth(inline.trimEnd(), width);
11317
- return [line + " ".repeat(Math.max(0, width - visibleWidth(line)))];
12787
+ const line = truncateToWidth2(inline.trimEnd(), width);
12788
+ return [line + " ".repeat(Math.max(0, width - visibleWidth2(line)))];
11318
12789
  }
11319
12790
  let header = toolTitle;
11320
12791
  if (mutedSuffix) {
11321
12792
  header += mutedSuffix;
11322
12793
  }
11323
12794
  const lines = [];
11324
- const headerLine = truncateToWidth(header.trimEnd(), width);
12795
+ const headerLine = truncateToWidth2(header.trimEnd(), width);
11325
12796
  lines.push(
11326
- headerLine + " ".repeat(Math.max(0, width - visibleWidth(headerLine)))
12797
+ headerLine + " ".repeat(Math.max(0, width - visibleWidth2(headerLine)))
11327
12798
  );
11328
12799
  for (const item of normalizedItems) {
11329
12800
  const itemLines = options.forceMultiline ? wrapTextWithAnsi(
11330
12801
  theme.fg("accent", item),
11331
12802
  Math.max(1, width - 2)
11332
12803
  ).map((line) => ` ${line}`) : [
11333
- truncateToWidth(
12804
+ truncateToWidth2(
11334
12805
  ` ${theme.fg("accent", truncateInline(item, 120))}`,
11335
12806
  width
11336
12807
  )
11337
12808
  ];
11338
12809
  for (const itemLine of itemLines) {
11339
- const line = truncateToWidth(itemLine, width);
12810
+ const line = truncateToWidth2(itemLine, width);
11340
12811
  lines.push(
11341
- line + " ".repeat(Math.max(0, width - visibleWidth(line)))
12812
+ line + " ".repeat(Math.max(0, width - visibleWidth2(line)))
11342
12813
  );
11343
12814
  }
11344
12815
  }
@@ -11433,7 +12904,8 @@ function renderWebResearchResultMessage(message, { expanded }, theme, symbols =
11433
12904
  const text = typeof message.content === "string" ? message.content : extractTextContent(message.content);
11434
12905
  const details = isWebResearchResult(message.details) ? message.details : void 0;
11435
12906
  const isSuccess = details?.status === "completed";
11436
- const accent = isSuccess ? "success" : "error";
12907
+ const isCancelled = details?.status === "cancelled";
12908
+ const accent = isSuccess ? "success" : isCancelled ? "warning" : "error";
11437
12909
  const box = new Box(1, 1, (value) => theme.bg("customMessageBg", value));
11438
12910
  if (!expanded) {
11439
12911
  const summary = details ? buildWebResearchResultSummaryLine(details, theme, symbols) : theme.fg(accent, "Web research update");
@@ -11443,10 +12915,42 @@ function renderWebResearchResultMessage(message, { expanded }, theme, symbols =
11443
12915
  return box;
11444
12916
  }
11445
12917
  box.addChild(
11446
- details ? renderMarkdownBlock(renderWebResearchResultMarkdown(details)) : isSuccess ? renderMarkdownBlock(text ?? "") : renderBlockText(text ?? "", theme, "error")
12918
+ details ? renderMarkdownBlock(renderWebResearchResultMarkdown(details)) : isSuccess ? renderMarkdownBlock(text ?? "") : renderBlockText(
12919
+ text ?? "",
12920
+ theme,
12921
+ isCancelled ? "toolOutput" : "error"
12922
+ )
11447
12923
  );
11448
12924
  return box;
11449
12925
  }
12926
+ function formatWebResearchReportMessage(title, body, item) {
12927
+ const provenance = `Saved web research report "${title}" (provider: ${item.provider || "unknown"}, status: ${item.status}, artifact: \`${item.outputPath}\`):`;
12928
+ return `${provenance}
12929
+
12930
+ ${body}
12931
+ `;
12932
+ }
12933
+ function renderWebResearchReportMessage(message, { expanded }, theme) {
12934
+ const text = typeof message.content === "string" ? message.content : extractTextContent(message.content);
12935
+ const details = isWebResearchReportDetails(message.details) ? message.details : void 0;
12936
+ const box = new Box(1, 1, (value) => theme.bg("customMessageBg", value));
12937
+ if (!expanded) {
12938
+ const title = details?.title ?? "saved report";
12939
+ box.addChild(
12940
+ new Text(
12941
+ `${theme.fg("success", `Injected research report: ${title}`)}${theme.fg("muted", ` (${getExpandHint()})`)}`,
12942
+ 0,
12943
+ 0
12944
+ )
12945
+ );
12946
+ return box;
12947
+ }
12948
+ box.addChild(renderMarkdownBlock(text ?? ""));
12949
+ return box;
12950
+ }
12951
+ function isWebResearchReportDetails(details) {
12952
+ return typeof details === "object" && details !== null && "title" in details && "outputPath" in details && !("tool" in details);
12953
+ }
11450
12954
  function renderWebResearchRequestMarkdown(request) {
11451
12955
  return [
11452
12956
  "### Web research",
@@ -11472,7 +12976,7 @@ function renderWebResearchResultMarkdown(result) {
11472
12976
  ].join("\n");
11473
12977
  }
11474
12978
  function buildWebResearchResultSummaryLine(result, theme, symbols) {
11475
- const providerLabel = PROVIDERS_BY_ID[result.provider]?.label ?? result.provider;
12979
+ const providerLabel2 = PROVIDERS_BY_ID[result.provider]?.label ?? result.provider;
11476
12980
  if (result.status === "completed") {
11477
12981
  return renderSuccessSummary(
11478
12982
  `${formatSummaryElapsed(result.elapsedMs)} \xB7 ${basename(result.outputPath)}`,
@@ -11480,8 +12984,8 @@ function buildWebResearchResultSummaryLine(result, theme, symbols) {
11480
12984
  symbols
11481
12985
  );
11482
12986
  }
11483
- const statusText = result.status === "cancelled" ? `${providerLabel} research canceled after ${formatSummaryElapsed(result.elapsedMs)}` : `${providerLabel} research failed after ${formatSummaryElapsed(result.elapsedMs)}`;
11484
- const errorSuffix = result.error ? `: ${normalizeProviderFailureDetail(providerLabel, result.error)}` : "";
12987
+ const statusText = result.status === "cancelled" ? `${providerLabel2} research canceled after ${formatSummaryElapsed(result.elapsedMs)}` : `${providerLabel2} research failed after ${formatSummaryElapsed(result.elapsedMs)}`;
12988
+ const errorSuffix = result.error ? `: ${normalizeProviderFailureDetail(providerLabel2, result.error)}` : "";
11485
12989
  return renderFailureSummary(`${statusText}${errorSuffix}`, theme, symbols);
11486
12990
  }
11487
12991
  function isWebResearchRequest(details) {
@@ -11580,7 +13084,7 @@ function renderEntryList(width, theme, entries, selection) {
11580
13084
  const paddedLabel = entry.label.padEnd(labelWidth, " ");
11581
13085
  const label = selected ? theme.fg("accent", paddedLabel) : paddedLabel;
11582
13086
  const value = selected ? theme.fg("accent", entry.currentValue) : theme.fg("muted", entry.currentValue);
11583
- return truncateToWidth(`${prefix}${label} ${value}`, width);
13087
+ return truncateToWidth2(`${prefix}${label} ${value}`, width);
11584
13088
  });
11585
13089
  }
11586
13090
  function renderSelectedEntryDescription(width, theme, entry) {
@@ -11588,7 +13092,7 @@ function renderSelectedEntryDescription(width, theme, entry) {
11588
13092
  return [];
11589
13093
  }
11590
13094
  return wrapTextWithAnsi(entry.description, Math.max(10, width - 2)).map(
11591
- (line) => truncateToWidth(theme.fg("dim", line), width)
13095
+ (line) => truncateToWidth2(theme.fg("dim", line), width)
11592
13096
  );
11593
13097
  }
11594
13098
  function formatProviderCapabilityChecks(providerId, theme) {
@@ -11769,7 +13273,7 @@ var WebProvidersSettingsView = class {
11769
13273
  }
11770
13274
  lines.push("");
11771
13275
  lines.push(
11772
- truncateToWidth(
13276
+ truncateToWidth2(
11773
13277
  this.theme.fg(
11774
13278
  "dim",
11775
13279
  "\u2191\u2193 move \xB7 Tab/Shift+Tab switch section \xB7 Enter edit/open \xB7 Esc close"
@@ -11788,7 +13292,7 @@ var WebProvidersSettingsView = class {
11788
13292
  this.tui.requestRender();
11789
13293
  return;
11790
13294
  }
11791
- const kb = getKeybindings();
13295
+ const kb = getKeybindings2();
11792
13296
  const entries = this.getActiveSectionEntries();
11793
13297
  if (kb.matches(data, "tui.select.up")) {
11794
13298
  if (entries.length > 0) {
@@ -11798,9 +13302,9 @@ var WebProvidersSettingsView = class {
11798
13302
  if (entries.length > 0) {
11799
13303
  this.moveSelection(1);
11800
13304
  }
11801
- } else if (matchesKey(data, Key.tab)) {
13305
+ } else if (matchesKey2(data, Key2.tab)) {
11802
13306
  this.moveSection(1);
11803
- } else if (matchesKey(data, Key.shift("tab"))) {
13307
+ } else if (matchesKey2(data, Key2.shift("tab"))) {
11804
13308
  this.moveSection(-1);
11805
13309
  } else if (kb.matches(data, "tui.select.confirm") || data === " ") {
11806
13310
  void this.activateCurrentEntry();
@@ -11933,14 +13437,14 @@ var WebProvidersSettingsView = class {
11933
13437
  Math.max(20, Math.floor(width * 0.45))
11934
13438
  );
11935
13439
  const lines = [
11936
- truncateToWidth(
13440
+ truncateToWidth2(
11937
13441
  this.activeSection === section ? this.theme.fg("accent", this.theme.bold(title)) : this.theme.bold(title),
11938
13442
  width
11939
13443
  )
11940
13444
  ];
11941
13445
  if (section === "provider") {
11942
13446
  lines.push(
11943
- truncateToWidth(
13447
+ truncateToWidth2(
11944
13448
  this.theme.fg(
11945
13449
  "dim",
11946
13450
  ` ${"Provider".padEnd(labelWidth, " ")} S C A R Status`
@@ -11955,15 +13459,15 @@ var WebProvidersSettingsView = class {
11955
13459
  const paddedLabel = entry.label.padEnd(labelWidth, " ");
11956
13460
  const label = selected ? this.theme.fg("accent", paddedLabel) : paddedLabel;
11957
13461
  if (entry.currentValue.trim().length === 0) {
11958
- lines.push(truncateToWidth(`${prefix}${label}`, width));
13462
+ lines.push(truncateToWidth2(`${prefix}${label}`, width));
11959
13463
  continue;
11960
13464
  }
11961
13465
  const value = entry.preserveValueStyle ? entry.currentValue : selected ? this.theme.fg("accent", entry.currentValue) : this.theme.fg("muted", entry.currentValue);
11962
- lines.push(truncateToWidth(`${prefix}${label} ${value}`, width));
13466
+ lines.push(truncateToWidth2(`${prefix}${label} ${value}`, width));
11963
13467
  }
11964
13468
  if (section === "provider") {
11965
13469
  lines.push(
11966
- truncateToWidth(
13470
+ truncateToWidth2(
11967
13471
  this.theme.fg("dim", " S=Search C=Contents A=Answer R=Research"),
11968
13472
  width
11969
13473
  )
@@ -12098,7 +13602,7 @@ var ToolSettingsSubmenu = class {
12098
13602
  }
12099
13603
  const entries = this.getEntries();
12100
13604
  const lines = [
12101
- truncateToWidth(
13605
+ truncateToWidth2(
12102
13606
  this.theme.fg("accent", TOOL_INFO[this.toolId].label),
12103
13607
  width
12104
13608
  ),
@@ -12114,7 +13618,7 @@ var ToolSettingsSubmenu = class {
12114
13618
  }
12115
13619
  lines.push("");
12116
13620
  lines.push(
12117
- truncateToWidth(
13621
+ truncateToWidth2(
12118
13622
  this.theme.fg("dim", "\u2191\u2193 move \xB7 Enter edit/toggle \xB7 Esc back"),
12119
13623
  width
12120
13624
  )
@@ -12130,7 +13634,7 @@ var ToolSettingsSubmenu = class {
12130
13634
  this.tui.requestRender();
12131
13635
  return;
12132
13636
  }
12133
- const kb = getKeybindings();
13637
+ const kb = getKeybindings2();
12134
13638
  const entries = this.getEntries();
12135
13639
  if (kb.matches(data, "tui.select.up")) {
12136
13640
  if (this.selection > 0) {
@@ -12333,7 +13837,7 @@ var ProviderSettingsSubmenu = class {
12333
13837
  const providerConfig = this.getProviderConfig();
12334
13838
  const entries = this.getEntries();
12335
13839
  const lines = [
12336
- truncateToWidth(this.theme.fg("accent", provider.label), width),
13840
+ truncateToWidth2(this.theme.fg("accent", provider.label), width),
12337
13841
  "",
12338
13842
  ...renderEntryList(width, this.theme, entries, this.selection)
12339
13843
  ];
@@ -12350,10 +13854,10 @@ var ProviderSettingsSubmenu = class {
12350
13854
  );
12351
13855
  lines.push("");
12352
13856
  lines.push(
12353
- truncateToWidth(this.theme.fg("dim", `Status: ${status}`), width)
13857
+ truncateToWidth2(this.theme.fg("dim", `Status: ${status}`), width)
12354
13858
  );
12355
13859
  lines.push(
12356
- truncateToWidth(
13860
+ truncateToWidth2(
12357
13861
  this.theme.fg("dim", "\u2191\u2193 move \xB7 Enter edit/toggle \xB7 Esc back"),
12358
13862
  width
12359
13863
  )
@@ -12369,7 +13873,7 @@ var ProviderSettingsSubmenu = class {
12369
13873
  this.tui.requestRender();
12370
13874
  return;
12371
13875
  }
12372
- const kb = getKeybindings();
13876
+ const kb = getKeybindings2();
12373
13877
  const entries = this.getEntries();
12374
13878
  if (kb.matches(data, "tui.select.up")) {
12375
13879
  if (this.selection > 0) {
@@ -12504,12 +14008,12 @@ var TextValueSubmenu = class {
12504
14008
  editor;
12505
14009
  render(width) {
12506
14010
  return [
12507
- truncateToWidth(this.theme.fg("accent", this.title), width),
14011
+ truncateToWidth2(this.theme.fg("accent", this.title), width),
12508
14012
  "",
12509
14013
  ...this.editor.render(width),
12510
14014
  "",
12511
- truncateToWidth(this.theme.fg("dim", this.help), width),
12512
- truncateToWidth(
14015
+ truncateToWidth2(this.theme.fg("dim", this.help), width),
14016
+ truncateToWidth2(
12513
14017
  this.theme.fg(
12514
14018
  "dim",
12515
14019
  "Enter to save \xB7 Shift+Enter for newline \xB7 Esc to cancel"
@@ -12522,7 +14026,7 @@ var TextValueSubmenu = class {
12522
14026
  this.editor.invalidate();
12523
14027
  }
12524
14028
  handleInput(data) {
12525
- if (matchesKey(data, Key.escape)) {
14029
+ if (matchesKey2(data, Key2.escape)) {
12526
14030
  this.done(void 0);
12527
14031
  return;
12528
14032
  }
@@ -12646,7 +14150,7 @@ function getSearchQueriesForDisplay(queries) {
12646
14150
  function getAnswerQueriesForDisplay(queries) {
12647
14151
  return getSearchQueriesForDisplay(queries);
12648
14152
  }
12649
- function createBatchCompletionReporter(verb, providerId, providerLabel, total, report) {
14153
+ function createBatchCompletionReporter(verb, providerId, providerLabel2, total, report) {
12650
14154
  if (!report) {
12651
14155
  return {
12652
14156
  start: () => {
@@ -12660,7 +14164,7 @@ function createBatchCompletionReporter(verb, providerId, providerLabel, total, r
12660
14164
  let completedCount = 0;
12661
14165
  let failedCount = 0;
12662
14166
  const emit = () => {
12663
- let message = `${verb} via ${providerLabel}: ${completedCount}/${total} completed`;
14167
+ let message = `${verb} via ${providerLabel2}: ${completedCount}/${total} completed`;
12664
14168
  if (failedCount > 0) {
12665
14169
  message += `, ${failedCount} failed`;
12666
14170
  }
@@ -12712,8 +14216,8 @@ function renderMarkdownBlock(text) {
12712
14216
  if (!text) {
12713
14217
  return new Text("", 0, 0);
12714
14218
  }
12715
- return new Markdown(`
12716
- ${text}`, 0, 0, getMarkdownTheme());
14219
+ return new Markdown2(`
14220
+ ${text}`, 0, 0, getMarkdownTheme2());
12717
14221
  }
12718
14222
  function renderBlockText(text, theme, color) {
12719
14223
  if (!text) {
@@ -12747,12 +14251,12 @@ function prefixWithSymbol(text, symbol) {
12747
14251
  }
12748
14252
  function renderToolProgress(display, fallbackText, theme) {
12749
14253
  const progress = display?.progress;
12750
- const providerLabel = display?.provider?.label;
12751
- if (!progress || !providerLabel) {
14254
+ const providerLabel2 = display?.provider?.label;
14255
+ if (!progress || !providerLabel2) {
12752
14256
  return renderSimpleText(fallbackText ?? "Working\u2026", theme, "warning");
12753
14257
  }
12754
14258
  return new Text(
12755
- `${theme.fg("warning", progress.action)} ${theme.fg("muted", `via ${providerLabel}`)}`,
14259
+ `${theme.fg("warning", progress.action)} ${theme.fg("muted", `via ${providerLabel2}`)}`,
12756
14260
  0,
12757
14261
  0
12758
14262
  );
@@ -12804,15 +14308,15 @@ function buildFailureSummary({
12804
14308
  fallback
12805
14309
  }) {
12806
14310
  const detail = stripTrailingSentencePunctuation(getFirstLine2(text) ?? "");
12807
- const providerLabel = details?.provider !== void 0 ? PROVIDERS_BY_ID[details.provider]?.label ?? details.provider : void 0;
12808
- if (!providerLabel) {
14311
+ const providerLabel2 = details?.provider !== void 0 ? PROVIDERS_BY_ID[details.provider]?.label ?? details.provider : void 0;
14312
+ if (!providerLabel2) {
12809
14313
  return detail || fallback;
12810
14314
  }
12811
- return formatProviderCapabilityFailure(providerLabel, capability, detail);
14315
+ return formatProviderCapabilityFailure(providerLabel2, capability, detail);
12812
14316
  }
12813
- function formatProviderCapabilityFailure(providerLabel, capability, detail) {
14317
+ function formatProviderCapabilityFailure(providerLabel2, capability, detail) {
12814
14318
  const action = getFailureAction(capability);
12815
- const base2 = `${providerLabel} ${action} failed`;
14319
+ const base2 = `${providerLabel2} ${action} failed`;
12816
14320
  if (!detail || detail === base2) {
12817
14321
  return base2;
12818
14322
  }
@@ -12820,14 +14324,14 @@ function formatProviderCapabilityFailure(providerLabel, capability, detail) {
12820
14324
  return detail;
12821
14325
  }
12822
14326
  const normalizedDetail = normalizeProviderFailureDetail(
12823
- providerLabel,
14327
+ providerLabel2,
12824
14328
  detail
12825
14329
  );
12826
14330
  return `${base2}: ${normalizedDetail}`;
12827
14331
  }
12828
- function normalizeProviderFailureDetail(providerLabel, detail) {
14332
+ function normalizeProviderFailureDetail(providerLabel2, detail) {
12829
14333
  const normalized = stripTrailingSentencePunctuation(detail);
12830
- const providerPrefix = `${providerLabel}:`;
14334
+ const providerPrefix = `${providerLabel2}:`;
12831
14335
  return normalized.toLowerCase().startsWith(providerPrefix.toLowerCase()) ? normalized.slice(providerPrefix.length).trim() : normalized;
12832
14336
  }
12833
14337
  function getFailureAction(capability) {
@@ -12876,7 +14380,7 @@ function getFirstLine2(text) {
12876
14380
  }
12877
14381
  function getExpandHint() {
12878
14382
  try {
12879
- const keys = getKeybindings().getKeys("app.tools.expand");
14383
+ const keys = getKeybindings2().getKeys("app.tools.expand");
12880
14384
  if (keys.length > 0) {
12881
14385
  return `${keys.join("/")} to expand`;
12882
14386
  }
@@ -12884,11 +14388,11 @@ function getExpandHint() {
12884
14388
  }
12885
14389
  return "ctrl+o to expand";
12886
14390
  }
12887
- function cleanSingleLine(text) {
14391
+ function cleanSingleLine2(text) {
12888
14392
  return text.replace(/\s+/g, " ").trim();
12889
14393
  }
12890
14394
  function formatQuotedPreview(text, maxLength = 80) {
12891
- return `"${truncateInline(cleanSingleLine(text), maxLength)}"`;
14395
+ return `"${truncateInline(cleanSingleLine2(text), maxLength)}"`;
12892
14396
  }
12893
14397
  function formatSearchResponses(outcomes, prefetch) {
12894
14398
  const body = outcomes.map(
@@ -12914,10 +14418,10 @@ function formatSearchOutcomeSection(outcome, index, total) {
12914
14418
  ${body}`;
12915
14419
  }
12916
14420
  function formatSearchHeading(query2) {
12917
- return `"${escapeMarkdownText(cleanSingleLine(query2))}"`;
14421
+ return `"${escapeMarkdownText(cleanSingleLine2(query2))}"`;
12918
14422
  }
12919
14423
  function formatAnswerHeading(query2) {
12920
- return `"${escapeMarkdownText(cleanSingleLine(query2))}"`;
14424
+ return `"${escapeMarkdownText(cleanSingleLine2(query2))}"`;
12921
14425
  }
12922
14426
  function collectSearchResultUrls(outcomes) {
12923
14427
  return outcomes.flatMap(
@@ -12933,7 +14437,7 @@ function formatSearchResponseMarkdown(response) {
12933
14437
  `${index + 1}. ${formatMarkdownLink(result.title, result.url)}`
12934
14438
  ];
12935
14439
  if (result.snippet) {
12936
- lines.push(` ${escapeMarkdownText(cleanSingleLine(result.snippet))}`);
14440
+ lines.push(` ${escapeMarkdownText(cleanSingleLine2(result.snippet))}`);
12937
14441
  }
12938
14442
  return lines.join("\n");
12939
14443
  }).join("\n\n");
@@ -12942,7 +14446,7 @@ function formatMarkdownLink(label, url2) {
12942
14446
  return `[${escapeMarkdownLinkLabel(label)}](<${url2}>)`;
12943
14447
  }
12944
14448
  function escapeMarkdownLinkLabel(text) {
12945
- return cleanSingleLine(text).replaceAll("\\", "\\\\").replaceAll("]", "\\]");
14449
+ return cleanSingleLine2(text).replaceAll("\\", "\\\\").replaceAll("]", "\\]");
12946
14450
  }
12947
14451
  function escapeMarkdownText(text) {
12948
14452
  return text.replaceAll("\\", "\\\\").replaceAll("*", "\\*").replaceAll("_", "\\_").replaceAll("`", "\\`").replaceAll("#", "\\#").replaceAll("[", "\\[").replaceAll("]", "\\]");
@@ -12963,10 +14467,10 @@ async function truncateAndSaveWithMetadata(text, prefix) {
12963
14467
  truncated: false
12964
14468
  };
12965
14469
  }
12966
- const dir = join2(tmpdir(), `pi-web-providers-${prefix}-${Date.now()}`);
12967
- await mkdir2(dir, { recursive: true });
12968
- const fullPath = join2(dir, "output.txt");
12969
- await writeFile2(fullPath, text, "utf-8");
14470
+ const dir = join3(tmpdir(), `pi-web-providers-${prefix}-${Date.now()}`);
14471
+ await mkdir3(dir, { recursive: true });
14472
+ const fullPath = join3(dir, "output.txt");
14473
+ await writeFile3(fullPath, text, "utf-8");
12970
14474
  return {
12971
14475
  text: truncation.content + `
12972
14476
 
@@ -13073,6 +14577,12 @@ var __test__ = {
13073
14577
  }),
13074
14578
  extractTextContent,
13075
14579
  formatWebResearchResultMessage,
14580
+ getActiveWebResearchRequests,
14581
+ getWebResearchTaskSnapshots,
14582
+ cancelWebResearchTask,
14583
+ loadWebResearchHistory,
14584
+ loadWebResearchPreview,
14585
+ loadWebResearchReport,
13076
14586
  getAvailableManagedToolNames,
13077
14587
  getReadyCompatibleProvidersForTool,
13078
14588
  getEnabledCompatibleProvidersForTool: getReadyCompatibleProvidersForTool,
@@ -13090,9 +14600,10 @@ var __test__ = {
13090
14600
  renderProviderToolResult,
13091
14601
  renderWebResearchDispatchResult,
13092
14602
  renderWebResearchResultMessage,
13093
- waitForPendingResearchTasks: async () => {
13094
- await Promise.all([...pendingResearchTasks]);
13095
- },
14603
+ renderWebResearchReportMessage,
14604
+ formatWebResearchReportMessage,
14605
+ buildWebResearchWidgetLines,
14606
+ waitForPendingResearchTasks,
13096
14607
  formatSearchResponses,
13097
14608
  formatAnswerResponses
13098
14609
  };