pi-web-providers 3.0.0 → 3.2.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 +38 -26
  2. package/dist/index.js +1361 -306
  3. package/package.json +8 -8
package/dist/index.js CHANGED
@@ -2,14 +2,14 @@
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
4
4
  import { tmpdir } from "node:os";
5
- import { dirname as dirname2, join as join2, relative } from "node:path";
5
+ import { basename, dirname as dirname2, join as join2, relative } from "node:path";
6
6
  import {
7
7
  DEFAULT_MAX_BYTES,
8
8
  DEFAULT_MAX_LINES,
9
- formatSize,
9
+ formatSize as formatSize2,
10
10
  getMarkdownTheme,
11
11
  truncateHead
12
- } from "@mariozechner/pi-coding-agent";
12
+ } from "@earendil-works/pi-coding-agent";
13
13
  import {
14
14
  Box,
15
15
  Editor,
@@ -21,13 +21,13 @@ import {
21
21
  truncateToWidth,
22
22
  visibleWidth,
23
23
  wrapTextWithAnsi
24
- } from "@mariozechner/pi-tui";
24
+ } from "@earendil-works/pi-tui";
25
25
  import { Type as Type16 } from "typebox";
26
26
 
27
27
  // src/config.ts
28
28
  import { mkdir, readFile, writeFile } from "node:fs/promises";
29
29
  import { dirname, join } from "node:path";
30
- import { getAgentDir } from "@mariozechner/pi-coding-agent";
30
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
31
31
 
32
32
  // src/config-values.ts
33
33
  import { execSync } from "node:child_process";
@@ -117,7 +117,13 @@ function asJsonObject(value) {
117
117
  function formatJson(value) {
118
118
  return JSON.stringify(value, null, 2);
119
119
  }
120
- function getApiKeyStatus(apiKeyReference) {
120
+ function getApiKeyStatus(apiKeyReference, options = {}) {
121
+ if (!apiKeyReference) {
122
+ return { state: "missing_api_key" };
123
+ }
124
+ if (options.resolveSecrets === false && isSecretReference(apiKeyReference)) {
125
+ return { state: "deferred_secret" };
126
+ }
121
127
  try {
122
128
  return resolveConfigValue(apiKeyReference) ? { state: "ready" } : { state: "missing_api_key" };
123
129
  } catch (error) {
@@ -127,6 +133,9 @@ function getApiKeyStatus(apiKeyReference) {
127
133
  };
128
134
  }
129
135
  }
136
+ function isSecretReference(reference) {
137
+ return reference.startsWith("!") || /^[A-Z][A-Z0-9_]*$/.test(reference);
138
+ }
130
139
  function formatConfigValueError(error) {
131
140
  const message = error instanceof Error ? error.message : String(error);
132
141
  return message.replace(/\s+/g, " ").trim() || "Failed to resolve config value";
@@ -385,8 +394,14 @@ var braveSearchPromptGuidelines = [
385
394
  ];
386
395
  var braveAnswerOptionsSchema = Type.Object(
387
396
  {
397
+ model: Type.Optional(
398
+ Type.Enum({ brave: "brave", bravePro: "brave-pro" }, {
399
+ description: "Brave Answers model. Defaults to 'brave'."
400
+ })
401
+ ),
388
402
  country: Type.Optional(Type.String()),
389
403
  language: Type.Optional(Type.String()),
404
+ safesearch: safesearchOption,
390
405
  enable_citations: Type.Optional(Type.Boolean()),
391
406
  enable_entities: Type.Optional(Type.Boolean()),
392
407
  max_completion_tokens: Type.Optional(Type.Integer({ minimum: 1 }))
@@ -395,8 +410,14 @@ var braveAnswerOptionsSchema = Type.Object(
395
410
  );
396
411
  var braveResearchOptionsSchema = Type.Object(
397
412
  {
413
+ model: Type.Optional(
414
+ Type.Enum({ brave: "brave", bravePro: "brave-pro" }, {
415
+ description: "Brave Answers model. Defaults to 'brave'."
416
+ })
417
+ ),
398
418
  country: Type.Optional(Type.String()),
399
419
  language: Type.Optional(Type.String()),
420
+ safesearch: safesearchOption,
400
421
  enable_entities: Type.Optional(Type.Boolean()),
401
422
  enable_citations: Type.Optional(
402
423
  Type.Boolean({
@@ -448,19 +469,19 @@ var braveImplementation = {
448
469
  options: {}
449
470
  };
450
471
  },
451
- getCapabilityStatus(config, _cwd, tool) {
472
+ getCapabilityStatus(config, _cwd, tool, options) {
452
473
  const key = tool === "answer" || tool === "research" ? config?.credentials?.answers : config?.credentials?.search;
453
- try {
454
- if (tool)
455
- return resolveConfigValue(key) ? { state: "ready" } : { state: "missing_api_key" };
456
- return [
457
- config?.credentials?.search,
458
- config?.credentials?.answers,
459
- config?.credentials?.autosuggest
460
- ].some((v) => resolveConfigValue(v)) ? { state: "ready" } : { state: "missing_api_key" };
461
- } catch (error) {
462
- return { state: "invalid_config", detail: formatConfigValueError(error) };
463
- }
474
+ if (tool) {
475
+ return getApiKeyStatus(key, options);
476
+ }
477
+ const statuses = [
478
+ config?.credentials?.search,
479
+ config?.credentials?.answers,
480
+ config?.credentials?.autosuggest
481
+ ].map((value) => getApiKeyStatus(value, options));
482
+ return statuses.find((status) => status.state === "ready") ?? statuses.find((status) => status.state === "deferred_secret") ?? statuses.find((status) => status.state === "invalid_config") ?? {
483
+ state: "missing_api_key"
484
+ };
464
485
  },
465
486
  async search(query2, maxResults, config, context, options) {
466
487
  const apiKey = requireKey(config.credentials?.search, "Brave search");
@@ -945,25 +966,25 @@ function placeRating(place, details) {
945
966
  return rating === void 0 ? void 0 : `Rating: ${rating}`;
946
967
  }
947
968
  function buildAnswerRequest(raw) {
948
- const answerOptions = pick(raw, [
969
+ const webSearchOptions = pick(raw, [
949
970
  "country",
950
971
  "language",
951
972
  "safesearch",
952
973
  "enable_entities",
953
974
  "enable_citations"
954
975
  ]);
955
- if (answerOptions.enable_citations === void 0) {
956
- answerOptions.enable_citations = true;
976
+ if (webSearchOptions.enable_citations === void 0) {
977
+ webSearchOptions.enable_citations = true;
957
978
  }
958
- const stream = answerOptions.enable_citations === true || answerOptions.enable_entities === true;
979
+ const stream = webSearchOptions.enable_citations === true || webSearchOptions.enable_entities === true;
959
980
  return {
960
981
  stream,
961
- ...pick(raw, ["max_completion_tokens", "metadata", "seed"]),
962
- ...answerOptions
982
+ ...pick(raw, ["model", "max_completion_tokens", "metadata", "seed"]),
983
+ web_search_options: webSearchOptions
963
984
  };
964
985
  }
965
986
  function buildResearchRequest(raw) {
966
- const researchOptions = pick(raw, [
987
+ const webSearchOptions = pick(raw, [
967
988
  "country",
968
989
  "language",
969
990
  "safesearch",
@@ -975,12 +996,12 @@ function buildResearchRequest(raw) {
975
996
  "research_maximum_number_of_seconds",
976
997
  "research_maximum_number_of_results_per_query"
977
998
  ]);
999
+ webSearchOptions.enable_research = true;
1000
+ webSearchOptions.enable_citations = false;
978
1001
  return {
979
1002
  stream: true,
980
- ...pick(raw, ["max_completion_tokens", "metadata", "seed"]),
981
- ...researchOptions,
982
- enable_research: true,
983
- enable_citations: false
1003
+ ...pick(raw, ["model", "max_completion_tokens", "metadata", "seed"]),
1004
+ web_search_options: webSearchOptions
984
1005
  };
985
1006
  }
986
1007
  async function completion(input, config, context, request) {
@@ -1197,10 +1218,11 @@ var braveProvider = defineProvider({
1197
1218
  },
1198
1219
  optionCapabilities: ["search", "answer", "research"]
1199
1220
  },
1200
- getCapabilityStatus: (config, cwd, tool) => braveImplementation.getCapabilityStatus(
1221
+ getCapabilityStatus: (config, cwd, tool, options) => braveImplementation.getCapabilityStatus(
1201
1222
  config,
1202
1223
  cwd,
1203
- tool
1224
+ tool,
1225
+ options
1204
1226
  ),
1205
1227
  capabilities: {
1206
1228
  search: defineCapability({
@@ -1666,10 +1688,12 @@ var cloudflareImplementation = {
1666
1688
  }
1667
1689
  };
1668
1690
  },
1669
- getCapabilityStatus(config) {
1670
- const apiTokenStatus = getApiKeyStatus(config?.credentials?.api);
1691
+ getCapabilityStatus(config, _cwd, _tool, options) {
1692
+ const apiTokenStatus = getApiKeyStatus(config?.credentials?.api, options);
1671
1693
  if (apiTokenStatus.state !== "ready") {
1672
- return apiTokenStatus;
1694
+ if (apiTokenStatus.state !== "deferred_secret") {
1695
+ return apiTokenStatus;
1696
+ }
1673
1697
  }
1674
1698
  try {
1675
1699
  if (!resolveConfigValue(config?.accountId)) {
@@ -1681,7 +1705,7 @@ var cloudflareImplementation = {
1681
1705
  detail: formatConfigValueError(error)
1682
1706
  };
1683
1707
  }
1684
- return { state: "ready" };
1708
+ return apiTokenStatus.state === "deferred_secret" ? apiTokenStatus : { state: "ready" };
1685
1709
  },
1686
1710
  async contents(urls, config, context, options) {
1687
1711
  const client = createClient(config);
@@ -1740,10 +1764,11 @@ var cloudflareProvider = defineProvider({
1740
1764
  createTemplate: () => cloudflareImplementation.createTemplate(),
1741
1765
  fields: ["credentials", "accountId", "options", "settings"]
1742
1766
  },
1743
- getCapabilityStatus: (config, cwd, tool) => cloudflareImplementation.getCapabilityStatus(
1767
+ getCapabilityStatus: (config, cwd, tool, options) => cloudflareImplementation.getCapabilityStatus(
1744
1768
  config,
1745
1769
  cwd,
1746
- tool
1770
+ tool,
1771
+ options
1747
1772
  ),
1748
1773
  capabilities: {
1749
1774
  contents: defineCapability({
@@ -2876,8 +2901,8 @@ var exaImplementation = {
2876
2901
  }
2877
2902
  };
2878
2903
  },
2879
- getCapabilityStatus(config) {
2880
- return getApiKeyStatus(config?.credentials?.api);
2904
+ getCapabilityStatus(config, _cwd, _tool, options) {
2905
+ return getApiKeyStatus(config?.credentials?.api, options);
2881
2906
  },
2882
2907
  async search(query2, maxResults, config, _context, searchOptions) {
2883
2908
  const client = createClient2(config);
@@ -3012,10 +3037,11 @@ var exaProvider = defineProvider({
3012
3037
  fields: ["credentials", "baseUrl", "options", "settings"],
3013
3038
  optionCapabilities: ["search"]
3014
3039
  },
3015
- getCapabilityStatus: (config, cwd, tool) => exaImplementation.getCapabilityStatus(
3040
+ getCapabilityStatus: (config, cwd, tool, options) => exaImplementation.getCapabilityStatus(
3016
3041
  config,
3017
3042
  cwd,
3018
- tool
3043
+ tool,
3044
+ options
3019
3045
  ),
3020
3046
  capabilities: {
3021
3047
  search: defineCapability({
@@ -3070,6 +3096,7 @@ var exaProvider = defineProvider({
3070
3096
  // src/providers/firecrawl.ts
3071
3097
  import FirecrawlClient from "@mendable/firecrawl-js";
3072
3098
  import { Type as Type7 } from "typebox";
3099
+ var FIRECRAWL_CLOUD_HOST = "api.firecrawl.dev";
3073
3100
  var firecrawlSearchOptionsSchema = Type7.Object(
3074
3101
  {
3075
3102
  lang: Type7.Optional(
@@ -3201,8 +3228,8 @@ var firecrawlImplementation = {
3201
3228
  }
3202
3229
  };
3203
3230
  },
3204
- getCapabilityStatus(config) {
3205
- return getApiKeyStatus(config?.credentials?.api);
3231
+ getCapabilityStatus(config, _cwd, _tool, options) {
3232
+ return getFirecrawlCapabilityStatus(config, options);
3206
3233
  },
3207
3234
  async search(query2, maxResults, config, _context, options) {
3208
3235
  const client = createClient3(config);
@@ -3255,15 +3282,26 @@ var firecrawlImplementation = {
3255
3282
  }
3256
3283
  };
3257
3284
  function createClient3(config) {
3285
+ const apiUrl = resolveConfigValue(config.baseUrl);
3258
3286
  const apiKey = resolveConfigValue(config.credentials?.api);
3259
- if (!apiKey) {
3287
+ if (isFirecrawlCloudApiUrl(apiUrl) && !apiKey) {
3260
3288
  throw new Error("is missing an API key");
3261
3289
  }
3262
3290
  return new FirecrawlClient({
3263
3291
  apiKey,
3264
- apiUrl: resolveConfigValue(config.baseUrl)
3292
+ apiUrl
3265
3293
  });
3266
3294
  }
3295
+ function getFirecrawlCapabilityStatus(config, options) {
3296
+ if (!config?.baseUrl || isFirecrawlCloudApiUrl(config.baseUrl)) {
3297
+ return getApiKeyStatus(config?.credentials?.api, options);
3298
+ }
3299
+ const apiKeyStatus = getApiKeyStatus(config.credentials?.api, options);
3300
+ return apiKeyStatus.state === "missing_api_key" ? { state: "ready" } : apiKeyStatus;
3301
+ }
3302
+ function isFirecrawlCloudApiUrl(apiUrl) {
3303
+ return !apiUrl || apiUrl.includes(FIRECRAWL_CLOUD_HOST);
3304
+ }
3267
3305
  function flattenSearchResults(response) {
3268
3306
  return ["web", "news", "images"].flatMap(
3269
3307
  (source) => (response[source] ?? []).map((entry) => toSearchResult(source, entry)).filter((entry) => entry !== null)
@@ -3321,10 +3359,11 @@ var firecrawlProvider = defineProvider({
3321
3359
  createTemplate: () => firecrawlImplementation.createTemplate(),
3322
3360
  fields: ["credentials", "baseUrl", "options", "settings"]
3323
3361
  },
3324
- getCapabilityStatus: (config, cwd, tool) => firecrawlImplementation.getCapabilityStatus(
3362
+ getCapabilityStatus: (config, cwd, tool, options) => firecrawlImplementation.getCapabilityStatus(
3325
3363
  config,
3326
3364
  cwd,
3327
- tool
3365
+ tool,
3366
+ options
3328
3367
  ),
3329
3368
  capabilities: {
3330
3369
  search: defineCapability({
@@ -3479,8 +3518,8 @@ var geminiImplementation = {
3479
3518
  }
3480
3519
  };
3481
3520
  },
3482
- getCapabilityStatus(config) {
3483
- return getApiKeyStatus(config?.credentials?.api);
3521
+ getCapabilityStatus(config, _cwd, _tool, options) {
3522
+ return getApiKeyStatus(config?.credentials?.api, options);
3484
3523
  },
3485
3524
  async search(query2, maxResults, config, context, options) {
3486
3525
  const ai = this.createClient(config);
@@ -4109,10 +4148,11 @@ var geminiProvider = defineProvider({
4109
4148
  createTemplate: () => geminiImplementation.createTemplate(),
4110
4149
  fields: ["credentials", "options", "settings"]
4111
4150
  },
4112
- getCapabilityStatus: (config, cwd, tool) => geminiImplementation.getCapabilityStatus(
4151
+ getCapabilityStatus: (config, cwd, tool, options) => geminiImplementation.getCapabilityStatus(
4113
4152
  config,
4114
4153
  cwd,
4115
- tool
4154
+ tool,
4155
+ options
4116
4156
  ),
4117
4157
  capabilities: {
4118
4158
  search: defineCapability({
@@ -4201,6 +4241,45 @@ var linkupContentsOptionsSchema = Type9.Object(
4201
4241
  },
4202
4242
  { description: "Linkup fetch options." }
4203
4243
  );
4244
+ var linkupResearchOptionsSchema = Type9.Object(
4245
+ {
4246
+ outputType: Type9.Optional(
4247
+ literalUnion(["sourcedAnswer", "structured"], {
4248
+ description: "Research output type. Defaults to 'sourcedAnswer' unless structuredOutputSchema is provided."
4249
+ })
4250
+ ),
4251
+ mode: Type9.Optional(
4252
+ literalUnion(["answer", "auto", "investigate", "research"], {
4253
+ description: "Research mode. Use 'answer' for precise verified answers, 'investigate' for focused deep dives, 'research' for broad reports, or omit/auto to let Linkup classify the task."
4254
+ })
4255
+ ),
4256
+ reasoningDepth: Type9.Optional(
4257
+ literalUnion(["S", "M", "L", "XL"], {
4258
+ description: "Reasoning depth. Higher values trade latency for more thorough investigation."
4259
+ })
4260
+ ),
4261
+ includeDomains: Type9.Optional(
4262
+ Type9.Array(Type9.String(), {
4263
+ description: "Restrict research to these domains."
4264
+ })
4265
+ ),
4266
+ excludeDomains: Type9.Optional(
4267
+ Type9.Array(Type9.String(), { description: "Exclude these domains." })
4268
+ ),
4269
+ fromDate: Type9.Optional(
4270
+ Type9.String({ description: "ISO date string for earliest result date." })
4271
+ ),
4272
+ toDate: Type9.Optional(
4273
+ Type9.String({ description: "ISO date string for latest result date." })
4274
+ ),
4275
+ structuredOutputSchema: Type9.Optional(
4276
+ Type9.Record(Type9.String(), Type9.Any(), {
4277
+ description: "JSON schema object required when outputType is 'structured'."
4278
+ })
4279
+ )
4280
+ },
4281
+ { description: "Linkup research options." }
4282
+ );
4204
4283
  var linkupImplementation = {
4205
4284
  id: "linkup",
4206
4285
  label: "Linkup",
@@ -4211,6 +4290,8 @@ var linkupImplementation = {
4211
4290
  return linkupSearchOptionsSchema;
4212
4291
  case "contents":
4213
4292
  return linkupContentsOptionsSchema;
4293
+ case "research":
4294
+ return linkupResearchOptionsSchema;
4214
4295
  default:
4215
4296
  return void 0;
4216
4297
  }
@@ -4220,8 +4301,8 @@ var linkupImplementation = {
4220
4301
  credentials: { api: "LINKUP_API_KEY" }
4221
4302
  };
4222
4303
  },
4223
- getCapabilityStatus(config) {
4224
- return getApiKeyStatus(config?.credentials?.api);
4304
+ getCapabilityStatus(config, _cwd, _tool, options) {
4305
+ return getApiKeyStatus(config?.credentials?.api, options);
4225
4306
  },
4226
4307
  async search(query2, maxResults, config, _context, options) {
4227
4308
  const client = createClient4(config);
@@ -4267,6 +4348,51 @@ var linkupImplementation = {
4267
4348
  })
4268
4349
  )
4269
4350
  };
4351
+ },
4352
+ async research(input, config, context, options) {
4353
+ return await executeAsyncResearch({
4354
+ providerLabel: linkupImplementation.label,
4355
+ providerId: linkupImplementation.id,
4356
+ context,
4357
+ start: (researchContext) => linkupImplementation.startResearch(
4358
+ input,
4359
+ config,
4360
+ researchContext,
4361
+ options
4362
+ ),
4363
+ poll: (id, researchContext) => linkupImplementation.pollResearch(id, config, researchContext)
4364
+ });
4365
+ },
4366
+ async startResearch(input, config, _context, options) {
4367
+ const client = createClient4(config);
4368
+ const defaults = asJsonObject(config.options?.research) ?? {};
4369
+ const task = await client.research(
4370
+ buildResearchParams(input, {
4371
+ ...defaults,
4372
+ ...options ?? {}
4373
+ })
4374
+ );
4375
+ return { id: task.id };
4376
+ },
4377
+ async pollResearch(id, config, _context) {
4378
+ const client = createClient4(config);
4379
+ const task = await client.getResearch(id);
4380
+ if (task.status === "completed") {
4381
+ return {
4382
+ status: "completed",
4383
+ output: formatResearchTaskOutput(task)
4384
+ };
4385
+ }
4386
+ if (task.status === "failed") {
4387
+ return {
4388
+ status: "failed",
4389
+ error: task.error ?? "research failed"
4390
+ };
4391
+ }
4392
+ return {
4393
+ status: "in_progress",
4394
+ statusText: task.status
4395
+ };
4270
4396
  }
4271
4397
  };
4272
4398
  function buildSearchParams(query2, maxResults, options) {
@@ -4311,6 +4437,45 @@ function buildFetchParams(url2, options) {
4311
4437
  ...fetchOptions.extractImages !== void 0 ? { extractImages: fetchOptions.extractImages } : {}
4312
4438
  };
4313
4439
  }
4440
+ function buildResearchParams(input, options) {
4441
+ const researchOptions = options;
4442
+ if (researchOptions.q !== void 0 || researchOptions.query !== void 0 || researchOptions.input !== void 0) {
4443
+ throw new Error(
4444
+ "Linkup research options cannot override the managed input."
4445
+ );
4446
+ }
4447
+ const outputType = researchOptions.outputType ?? (researchOptions.structuredOutputSchema !== void 0 ? "structured" : "sourcedAnswer");
4448
+ if (outputType === "structured" && researchOptions.structuredOutputSchema === void 0) {
4449
+ throw new Error(
4450
+ "Linkup research outputType 'structured' requires structuredOutputSchema."
4451
+ );
4452
+ }
4453
+ if (outputType === "sourcedAnswer" && researchOptions.structuredOutputSchema !== void 0) {
4454
+ throw new Error(
4455
+ "Linkup research structuredOutputSchema requires outputType 'structured'."
4456
+ );
4457
+ }
4458
+ const commonParams = {
4459
+ query: input,
4460
+ ...researchOptions.includeDomains !== void 0 ? { includeDomains: researchOptions.includeDomains } : {},
4461
+ ...researchOptions.excludeDomains !== void 0 ? { excludeDomains: researchOptions.excludeDomains } : {},
4462
+ ...researchOptions.fromDate !== void 0 ? { fromDate: toDate(researchOptions.fromDate, "fromDate") } : {},
4463
+ ...researchOptions.toDate !== void 0 ? { toDate: toDate(researchOptions.toDate, "toDate") } : {},
4464
+ ...researchOptions.mode !== void 0 ? { mode: researchOptions.mode } : {},
4465
+ ...researchOptions.reasoningDepth !== void 0 ? { reasoningDepth: researchOptions.reasoningDepth } : {}
4466
+ };
4467
+ if (outputType === "structured") {
4468
+ return {
4469
+ ...commonParams,
4470
+ outputType,
4471
+ structuredOutputSchema: researchOptions.structuredOutputSchema
4472
+ };
4473
+ }
4474
+ return {
4475
+ ...commonParams,
4476
+ outputType
4477
+ };
4478
+ }
4314
4479
  function createClient4(config) {
4315
4480
  const apiKey = resolveConfigValue(config.credentials?.api);
4316
4481
  if (!apiKey) {
@@ -4321,6 +4486,40 @@ function createClient4(config) {
4321
4486
  baseUrl: resolveConfigValue(config.baseUrl)
4322
4487
  });
4323
4488
  }
4489
+ function formatResearchTaskOutput(task) {
4490
+ const output = task.output;
4491
+ if (!output) {
4492
+ return {
4493
+ provider: linkupImplementation.id,
4494
+ text: "Linkup research completed without textual output."
4495
+ };
4496
+ }
4497
+ const outputRecord = asRecord2(output);
4498
+ const inputRecord = asRecord2(task.input);
4499
+ const outputType = inputRecord ? readString3(inputRecord.outputType) : void 0;
4500
+ const answer = outputRecord ? readString3(outputRecord.answer) : void 0;
4501
+ const sources = outputRecord ? readSources(outputRecord.sources) : [];
4502
+ if (outputType !== "structured" && answer !== void 0) {
4503
+ const lines = [answer];
4504
+ if (sources.length > 0) {
4505
+ lines.push("");
4506
+ lines.push("Sources:");
4507
+ for (const [index, source] of sources.entries()) {
4508
+ lines.push(`${index + 1}. ${source.title}`);
4509
+ lines.push(` ${source.url}`);
4510
+ }
4511
+ }
4512
+ return {
4513
+ provider: linkupImplementation.id,
4514
+ text: lines.join("\n").trimEnd(),
4515
+ itemCount: sources.length
4516
+ };
4517
+ }
4518
+ return {
4519
+ provider: linkupImplementation.id,
4520
+ text: formatJson(output)
4521
+ };
4522
+ }
4324
4523
  function toSearchResult2(value) {
4325
4524
  const entry = asRecord2(value);
4326
4525
  if (!entry) {
@@ -4342,6 +4541,27 @@ function toSearchResult2(value) {
4342
4541
  metadata: Object.keys(metadata).length > 0 ? metadata : void 0
4343
4542
  };
4344
4543
  }
4544
+ function readSources(value) {
4545
+ if (!Array.isArray(value)) {
4546
+ return [];
4547
+ }
4548
+ return value.flatMap((entry) => {
4549
+ const source = asRecord2(entry);
4550
+ if (!source) {
4551
+ return [];
4552
+ }
4553
+ const url2 = readString3(source.url);
4554
+ if (!url2) {
4555
+ return [];
4556
+ }
4557
+ return [
4558
+ {
4559
+ title: readString3(source.name) ?? url2,
4560
+ url: url2
4561
+ }
4562
+ ];
4563
+ });
4564
+ }
4345
4565
  function asRecord2(value) {
4346
4566
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
4347
4567
  }
@@ -4365,10 +4585,11 @@ var linkupProvider = defineProvider({
4365
4585
  createTemplate: () => linkupImplementation.createTemplate(),
4366
4586
  fields: ["credentials", "baseUrl", "options", "settings"]
4367
4587
  },
4368
- getCapabilityStatus: (config, cwd, tool) => linkupImplementation.getCapabilityStatus(
4588
+ getCapabilityStatus: (config, cwd, tool, options) => linkupImplementation.getCapabilityStatus(
4369
4589
  config,
4370
4590
  cwd,
4371
- tool
4591
+ tool,
4592
+ options
4372
4593
  ),
4373
4594
  capabilities: {
4374
4595
  search: defineCapability({
@@ -4394,6 +4615,17 @@ var linkupProvider = defineProvider({
4394
4615
  input.options
4395
4616
  );
4396
4617
  }
4618
+ }),
4619
+ research: defineCapability({
4620
+ options: linkupImplementation.getToolOptionsSchema?.("research"),
4621
+ async execute(input, ctx) {
4622
+ return await linkupImplementation.research(
4623
+ input.input,
4624
+ ctx.config,
4625
+ ctx,
4626
+ input.options
4627
+ );
4628
+ }
4397
4629
  })
4398
4630
  }
4399
4631
  });
@@ -4414,8 +4646,8 @@ var ollamaProvider = defineProvider({
4414
4646
  },
4415
4647
  fields: ["credentials", "baseUrl", "settings"]
4416
4648
  },
4417
- getCapabilityStatus(config) {
4418
- return getApiKeyStatus(config?.credentials?.api);
4649
+ getCapabilityStatus(config, _cwd, _tool, options) {
4650
+ return getApiKeyStatus(config?.credentials?.api, options);
4419
4651
  },
4420
4652
  capabilities: {
4421
4653
  search: defineCapability({
@@ -4679,8 +4911,8 @@ var openaiImplementation = {
4679
4911
  }
4680
4912
  };
4681
4913
  },
4682
- getCapabilityStatus(config) {
4683
- return getApiKeyStatus(config?.credentials?.api);
4914
+ getCapabilityStatus(config, _cwd, _tool, options) {
4915
+ return getApiKeyStatus(config?.credentials?.api, options);
4684
4916
  },
4685
4917
  async search(query2, maxResults, config, context, options) {
4686
4918
  const client = createClient5(config);
@@ -5036,10 +5268,11 @@ var openaiProvider = defineProvider({
5036
5268
  fields: ["credentials", "baseUrl", "options", "settings"],
5037
5269
  optionCapabilities: ["search", "answer", "research"]
5038
5270
  },
5039
- getCapabilityStatus: (config, cwd, tool) => openaiImplementation.getCapabilityStatus(
5271
+ getCapabilityStatus: (config, cwd, tool, options) => openaiImplementation.getCapabilityStatus(
5040
5272
  config,
5041
5273
  cwd,
5042
- tool
5274
+ tool,
5275
+ options
5043
5276
  ),
5044
5277
  capabilities: {
5045
5278
  search: defineCapability({
@@ -5134,8 +5367,8 @@ var parallelImplementation = {
5134
5367
  }
5135
5368
  };
5136
5369
  },
5137
- getCapabilityStatus(config) {
5138
- return getApiKeyStatus(config?.credentials?.api);
5370
+ getCapabilityStatus(config, _cwd, _tool, options) {
5371
+ return getApiKeyStatus(config?.credentials?.api, options);
5139
5372
  },
5140
5373
  async search(query2, maxResults, config, context, options) {
5141
5374
  const client = createClient6(config);
@@ -5219,10 +5452,11 @@ var parallelProvider = defineProvider({
5219
5452
  createTemplate: () => parallelImplementation.createTemplate(),
5220
5453
  fields: ["credentials", "baseUrl", "options", "settings"]
5221
5454
  },
5222
- getCapabilityStatus: (config, cwd, tool) => parallelImplementation.getCapabilityStatus(
5455
+ getCapabilityStatus: (config, cwd, tool, options) => parallelImplementation.getCapabilityStatus(
5223
5456
  config,
5224
5457
  cwd,
5225
- tool
5458
+ tool,
5459
+ options
5226
5460
  ),
5227
5461
  capabilities: {
5228
5462
  search: defineCapability({
@@ -5327,8 +5561,8 @@ var perplexityImplementation = {
5327
5561
  }
5328
5562
  };
5329
5563
  },
5330
- getCapabilityStatus(config) {
5331
- return getApiKeyStatus(config?.credentials?.api);
5564
+ getCapabilityStatus(config, _cwd, _tool, options) {
5565
+ return getApiKeyStatus(config?.credentials?.api, options);
5332
5566
  },
5333
5567
  async search(query2, maxResults, config, context, options) {
5334
5568
  const client = createClient7(config);
@@ -5538,10 +5772,11 @@ var perplexityProvider = defineProvider({
5538
5772
  createTemplate: () => perplexityImplementation.createTemplate(),
5539
5773
  fields: ["credentials", "baseUrl", "options", "settings"]
5540
5774
  },
5541
- getCapabilityStatus: (config, cwd, tool) => perplexityImplementation.getCapabilityStatus(
5775
+ getCapabilityStatus: (config, cwd, tool, options) => perplexityImplementation.getCapabilityStatus(
5542
5776
  config,
5543
5777
  cwd,
5544
- tool
5778
+ tool,
5779
+ options
5545
5780
  ),
5546
5781
  capabilities: {
5547
5782
  search: defineCapability({
@@ -5584,9 +5819,103 @@ var perplexityProvider = defineProvider({
5584
5819
 
5585
5820
  // src/providers/serper.ts
5586
5821
  import { Type as Type13 } from "typebox";
5822
+
5823
+ // src/types.ts
5824
+ var TOOLS = ["search", "contents", "answer", "research"];
5825
+ var SERPER_SEARCH_MODE_VALUES = {
5826
+ search: "search",
5827
+ images: "images",
5828
+ videos: "videos",
5829
+ places: "places",
5830
+ maps: "maps",
5831
+ reviews: "reviews",
5832
+ news: "news",
5833
+ shopping: "shopping",
5834
+ productReviews: "product-reviews",
5835
+ lens: "lens",
5836
+ scholar: "scholar",
5837
+ patents: "patents",
5838
+ autocomplete: "autocomplete",
5839
+ webpage: "webpage"
5840
+ };
5841
+
5842
+ // src/providers/serper.ts
5587
5843
  var DEFAULT_BASE_URL3 = "https://google.serper.dev";
5844
+ var DEFAULT_SCRAPE_URL = "https://scrape.serper.dev";
5845
+ var SERPER_SEARCH_MODES = Object.values(SERPER_SEARCH_MODE_VALUES);
5846
+ var SERPER_SEARCH_MODE_SET = new Set(SERPER_SEARCH_MODES);
5847
+ var RESERVED_REQUEST_OPTION_KEYS = [
5848
+ "q",
5849
+ "num",
5850
+ "mode",
5851
+ "url",
5852
+ "productId",
5853
+ "nextPageToken",
5854
+ "ll",
5855
+ "placeId",
5856
+ "cid",
5857
+ "fid",
5858
+ "sortBy",
5859
+ "topicId",
5860
+ "includeMarkdown",
5861
+ "includeImages",
5862
+ "includeLinks",
5863
+ "includeVideos",
5864
+ "location",
5865
+ "gl",
5866
+ "hl",
5867
+ "tbs",
5868
+ "page",
5869
+ "autocorrect"
5870
+ ];
5871
+ var PRIMARY_RESULT_FIELDS_BY_MODE = {
5872
+ search: ["organic"],
5873
+ images: ["images"],
5874
+ videos: ["videos"],
5875
+ places: ["places"],
5876
+ maps: ["maps", "places"],
5877
+ reviews: ["reviews"],
5878
+ news: ["news"],
5879
+ shopping: ["shopping"],
5880
+ "product-reviews": ["reviews", "productReviews"],
5881
+ lens: ["visualMatches", "organic", "images"],
5882
+ scholar: ["organic"],
5883
+ patents: ["organic"],
5884
+ autocomplete: ["suggestions"],
5885
+ webpage: []
5886
+ };
5887
+ var CONTEXT_ARRAY_FIELDS = [
5888
+ "peopleAlsoAsk",
5889
+ "relatedSearches",
5890
+ "topStories",
5891
+ "news",
5892
+ "images",
5893
+ "videos",
5894
+ "places",
5895
+ "maps",
5896
+ "shopping",
5897
+ "reviews",
5898
+ "productReviews",
5899
+ "visualMatches",
5900
+ "suggestions"
5901
+ ];
5902
+ var serperSearchPromptGuidelines = [
5903
+ "Use Serper news mode for recent journalism, current events, announcements, or time-sensitive reporting.",
5904
+ "Use Serper images or videos mode when the user asks for visual references, screenshots, diagrams, clips, tutorials, or media results.",
5905
+ "Use Serper places or maps mode for local businesses, venues, addresses, ratings, phone numbers, opening details, or nearby/in-location searches.",
5906
+ "Use Serper reviews mode when the task needs Google business reviews. Prefer cid, fid, or placeId from a maps or places result when available; otherwise use the search query as the place identifier.",
5907
+ "Use Serper shopping mode for product listings, prices, merchants, offers, or purchase comparisons, and use product-reviews mode when the task needs reviews for a known product ID.",
5908
+ "Use Serper scholar mode for academic papers and patents mode for patent searches.",
5909
+ "Use Serper autocomplete mode when the task is to discover search suggestions or query completions rather than source pages.",
5910
+ "Use Serper lens mode for reverse image search with an image URL, and use webpage mode to scrape a specific URL. Webpage mode includes Markdown by default."
5911
+ ];
5588
5912
  var serperSearchOptionsSchema = Type13.Object(
5589
5913
  {
5914
+ mode: Type13.Optional(
5915
+ Type13.Enum(SERPER_SEARCH_MODE_VALUES, {
5916
+ description: "Serper search type. Use 'search' for web results, 'news' for recent journalism/current events, 'images' for visual references, 'videos' for clips/tutorials, 'places' or 'maps' for local businesses/venues, 'reviews' for Google business reviews by place ID/CID/FID or query, 'shopping' for products, 'product-reviews' for product reviews, 'lens' for reverse image search, 'scholar' for scholarly articles, 'patents' for patents, 'autocomplete' for suggestions, and 'webpage' to scrape a URL."
5917
+ })
5918
+ ),
5590
5919
  gl: Type13.Optional(
5591
5920
  Type13.String({
5592
5921
  description: "Country code hint for Google results (for example 'us')."
@@ -5608,10 +5937,63 @@ var serperSearchOptionsSchema = Type13.Object(
5608
5937
  description: "1-based results page to request from Serper."
5609
5938
  })
5610
5939
  ),
5940
+ tbs: Type13.Optional(
5941
+ Type13.String({
5942
+ description: "Google time/date or vertical-specific filter string passed through to Serper, for example 'qdr:d' for past day."
5943
+ })
5944
+ ),
5611
5945
  autocorrect: Type13.Optional(
5612
5946
  Type13.Boolean({
5613
5947
  description: "Enable or disable Serper query autocorrection."
5614
5948
  })
5949
+ ),
5950
+ url: Type13.Optional(
5951
+ Type13.String({
5952
+ description: "URL for modes that need one: image URL for 'lens', or page URL for 'webpage'. Defaults to the query string when omitted."
5953
+ })
5954
+ ),
5955
+ ll: Type13.Optional(
5956
+ Type13.String({
5957
+ description: "Google Maps latitude/longitude/zoom hint, for example '@40.6973709,-74.1444871,11z'."
5958
+ })
5959
+ ),
5960
+ placeId: Type13.Optional(
5961
+ Type13.String({ description: "Google place ID for maps or reviews." })
5962
+ ),
5963
+ cid: Type13.Optional(
5964
+ Type13.String({ description: "Google CID for maps or reviews." })
5965
+ ),
5966
+ fid: Type13.Optional(Type13.String({ description: "Google FID for reviews." })),
5967
+ sortBy: Type13.Optional(
5968
+ Type13.String({ description: "Review sort order for reviews mode." })
5969
+ ),
5970
+ topicId: Type13.Optional(
5971
+ Type13.String({ description: "Review topic ID for reviews mode." })
5972
+ ),
5973
+ productId: Type13.Optional(
5974
+ Type13.String({
5975
+ description: "Google product ID for product-reviews mode. Defaults to the query string when omitted."
5976
+ })
5977
+ ),
5978
+ nextPageToken: Type13.Optional(
5979
+ Type13.String({
5980
+ description: "Pagination token for reviews or product-reviews modes."
5981
+ })
5982
+ ),
5983
+ includeMarkdown: Type13.Optional(
5984
+ Type13.Boolean({
5985
+ default: true,
5986
+ description: "Include Markdown content in webpage mode. Defaults to true."
5987
+ })
5988
+ ),
5989
+ includeImages: Type13.Optional(
5990
+ Type13.Boolean({ description: "Include image metadata in webpage mode." })
5991
+ ),
5992
+ includeLinks: Type13.Optional(
5993
+ Type13.Boolean({ description: "Include link metadata in webpage mode." })
5994
+ ),
5995
+ includeVideos: Type13.Optional(
5996
+ Type13.Boolean({ description: "Include video metadata in webpage mode." })
5615
5997
  )
5616
5998
  },
5617
5999
  { description: "Serper search options." }
@@ -5631,58 +6013,218 @@ var serperImplementation = {
5631
6013
  createTemplate() {
5632
6014
  return {
5633
6015
  credentials: { api: "SERPER_API_KEY" },
5634
- options: {}
6016
+ options: {
6017
+ search: {
6018
+ includeMarkdown: true
6019
+ }
6020
+ }
5635
6021
  };
5636
6022
  },
5637
- getCapabilityStatus(config) {
5638
- return getApiKeyStatus(config?.credentials?.api);
6023
+ getCapabilityStatus(config, _cwd, _tool, options) {
6024
+ return getApiKeyStatus(config?.credentials?.api, options);
5639
6025
  },
5640
6026
  async search(query2, maxResults, config, context, options) {
5641
6027
  const apiKey = resolveConfigValue(config.credentials?.api);
5642
6028
  if (!apiKey) {
5643
6029
  throw new Error("is missing an API key");
5644
6030
  }
5645
- const defaults = asJsonObject(config.options?.search) ?? {};
6031
+ const defaults = asJsonObject(config.options?.search);
5646
6032
  const callOptions = asJsonObject(options);
5647
- const {
5648
- q: _ignoredQuery,
5649
- num: _ignoredNum,
5650
- ...providerOptions
5651
- } = {
6033
+ const requestOptions = readRequestOptions({
5652
6034
  ...defaults,
5653
- ...callOptions ?? {}
5654
- };
5655
- const response = await fetch(joinUrl(resolveConfigValue(config.baseUrl)), {
5656
- method: "POST",
5657
- headers: {
5658
- "content-type": "application/json",
5659
- "x-api-key": apiKey
5660
- },
5661
- body: JSON.stringify({
5662
- q: query2,
5663
- num: clampMaxResults2(maxResults),
5664
- ...providerOptions
5665
- }),
5666
- signal: context.signal
6035
+ ...callOptions
5667
6036
  });
6037
+ const requestBody = buildRequestBody(
6038
+ query2,
6039
+ clampMaxResults2(maxResults),
6040
+ requestOptions
6041
+ );
6042
+ const response = await fetch(
6043
+ joinUrl(resolveConfigValue(config.baseUrl), requestOptions.mode),
6044
+ {
6045
+ method: "POST",
6046
+ headers: {
6047
+ "content-type": "application/json",
6048
+ "x-api-key": apiKey
6049
+ },
6050
+ body: JSON.stringify(requestBody),
6051
+ signal: context.signal
6052
+ }
6053
+ );
5668
6054
  if (!response.ok) {
5669
6055
  throw new Error(await buildHttpError2(response));
5670
6056
  }
5671
6057
  const payload = await response.json();
5672
- const responseRecord = asRecord3(payload) ?? {};
5673
- const organic = asArray(responseRecord.organic) ?? [];
5674
- const searchContext = buildSearchContext(responseRecord);
6058
+ const responseRecord = enrichResponseRecord(
6059
+ asRecord3(payload) ?? {},
6060
+ requestOptions.mode,
6061
+ requestBody
6062
+ );
6063
+ const results = readPrimaryResults(responseRecord, requestOptions.mode);
6064
+ const searchContext = buildSearchContext(
6065
+ responseRecord,
6066
+ requestOptions.mode
6067
+ );
5675
6068
  return {
5676
6069
  provider: serperImplementation.id,
5677
- results: organic.map((entry) => toSearchResult3(entry, searchContext)).filter(
6070
+ results: results.map(
6071
+ (entry) => toSearchResult3(entry, searchContext, requestOptions.mode)
6072
+ ).filter(
5678
6073
  (result) => result !== null
5679
6074
  ).slice(0, clampMaxResults2(maxResults))
5680
6075
  };
5681
6076
  }
5682
6077
  };
5683
- function joinUrl(baseUrl) {
6078
+ function joinUrl(baseUrl, mode = "search") {
5684
6079
  const base2 = (baseUrl ?? DEFAULT_BASE_URL3).replace(/\/+$/, "");
5685
- return `${base2}/search`;
6080
+ if (mode === "webpage" && base2 === DEFAULT_BASE_URL3) {
6081
+ return DEFAULT_SCRAPE_URL;
6082
+ }
6083
+ return `${base2}/${mode}`;
6084
+ }
6085
+ function readRequestOptions(options) {
6086
+ const result = {
6087
+ mode: readSearchMode(options.mode),
6088
+ extra: extractExtraMetadata(options, RESERVED_REQUEST_OPTION_KEYS)
6089
+ };
6090
+ copyStringOption(result, "gl", options.gl);
6091
+ copyStringOption(result, "hl", options.hl);
6092
+ copyStringOption(result, "location", options.location);
6093
+ copyStringOption(result, "tbs", options.tbs);
6094
+ copyStringOption(result, "url", options.url);
6095
+ copyStringOption(result, "ll", options.ll);
6096
+ copyStringOption(result, "placeId", options.placeId);
6097
+ copyStringOption(result, "cid", options.cid);
6098
+ copyStringOption(result, "fid", options.fid);
6099
+ copyStringOption(result, "sortBy", options.sortBy);
6100
+ copyStringOption(result, "topicId", options.topicId);
6101
+ copyStringOption(result, "productId", options.productId);
6102
+ copyStringOption(result, "nextPageToken", options.nextPageToken);
6103
+ copyBooleanOption(result, "autocorrect", options.autocorrect);
6104
+ copyBooleanOption(result, "includeMarkdown", options.includeMarkdown);
6105
+ copyBooleanOption(result, "includeImages", options.includeImages);
6106
+ copyBooleanOption(result, "includeLinks", options.includeLinks);
6107
+ copyBooleanOption(result, "includeVideos", options.includeVideos);
6108
+ const page = readInteger2(options.page);
6109
+ if (page !== void 0) {
6110
+ result.page = Math.max(1, page);
6111
+ }
6112
+ return result;
6113
+ }
6114
+ function buildRequestBody(query2, maxResults, options) {
6115
+ const common = omitUndefined({
6116
+ location: options.location,
6117
+ gl: options.gl,
6118
+ hl: options.hl
6119
+ });
6120
+ const withExtra = (body) => ({
6121
+ ...body,
6122
+ ...options.extra
6123
+ });
6124
+ switch (options.mode) {
6125
+ case "webpage":
6126
+ return withExtra(
6127
+ omitUndefined({
6128
+ url: options.url ?? query2,
6129
+ includeMarkdown: options.includeMarkdown ?? true,
6130
+ includeImages: options.includeImages,
6131
+ includeLinks: options.includeLinks,
6132
+ includeVideos: options.includeVideos
6133
+ })
6134
+ );
6135
+ case "product-reviews":
6136
+ return withExtra(
6137
+ omitUndefined({
6138
+ productId: options.productId ?? query2,
6139
+ nextPageToken: options.nextPageToken,
6140
+ ...common,
6141
+ num: maxResults
6142
+ })
6143
+ );
6144
+ case "autocomplete":
6145
+ return withExtra({ q: query2, ...common });
6146
+ case "maps":
6147
+ return withExtra(
6148
+ omitUndefined({
6149
+ q: query2,
6150
+ num: maxResults,
6151
+ ...common,
6152
+ ll: options.ll,
6153
+ placeId: options.placeId,
6154
+ cid: options.cid,
6155
+ page: options.page
6156
+ })
6157
+ );
6158
+ case "reviews": {
6159
+ const hasExplicitPlaceIdentifier = firstNonEmptyString(options.cid, options.fid, options.placeId) !== void 0;
6160
+ return withExtra(
6161
+ omitUndefined({
6162
+ q: hasExplicitPlaceIdentifier ? void 0 : query2,
6163
+ cid: options.cid,
6164
+ fid: options.fid,
6165
+ placeId: options.placeId,
6166
+ gl: options.gl,
6167
+ hl: options.hl,
6168
+ sortBy: options.sortBy,
6169
+ topicId: options.topicId,
6170
+ nextPageToken: options.nextPageToken
6171
+ })
6172
+ );
6173
+ }
6174
+ case "lens":
6175
+ return withExtra(
6176
+ omitUndefined({
6177
+ url: options.url ?? query2,
6178
+ ...common,
6179
+ tbs: options.tbs
6180
+ })
6181
+ );
6182
+ case "scholar":
6183
+ return withExtra(
6184
+ omitUndefined({
6185
+ q: query2,
6186
+ ...common,
6187
+ autocorrect: options.autocorrect,
6188
+ tbs: options.tbs,
6189
+ page: options.page
6190
+ })
6191
+ );
6192
+ default:
6193
+ return withExtra(
6194
+ omitUndefined({
6195
+ q: query2,
6196
+ num: maxResults,
6197
+ ...common,
6198
+ autocorrect: options.autocorrect,
6199
+ tbs: options.tbs,
6200
+ page: options.page
6201
+ })
6202
+ );
6203
+ }
6204
+ }
6205
+ function enrichResponseRecord(response, mode, requestBody) {
6206
+ if (mode !== "webpage") {
6207
+ return response;
6208
+ }
6209
+ return omitUndefined({
6210
+ ...response,
6211
+ url: readString4(response.url) ?? readString4(requestBody.url)
6212
+ });
6213
+ }
6214
+ function readSearchMode(value) {
6215
+ return typeof value === "string" && SERPER_SEARCH_MODE_SET.has(value) ? value : "search";
6216
+ }
6217
+ function readPrimaryResults(response, mode) {
6218
+ if (mode === "webpage") {
6219
+ return [response];
6220
+ }
6221
+ for (const field of PRIMARY_RESULT_FIELDS_BY_MODE[mode]) {
6222
+ const values = asArray(response[field]);
6223
+ if (values) {
6224
+ return values;
6225
+ }
6226
+ }
6227
+ return [];
5686
6228
  }
5687
6229
  function clampMaxResults2(value) {
5688
6230
  return Math.max(1, Math.min(20, Math.trunc(value || 0)));
@@ -5709,18 +6251,52 @@ async function readErrorDetail2(response) {
5709
6251
  return text;
5710
6252
  }
5711
6253
  }
5712
- function toSearchResult3(entry, searchContext) {
6254
+ function toSearchResult3(entry, searchContext, mode) {
6255
+ if (typeof entry === "string") {
6256
+ return {
6257
+ title: entry,
6258
+ url: mode === "autocomplete" ? toGoogleSearchUrl(entry) : "",
6259
+ snippet: entry,
6260
+ metadata: {
6261
+ source: mode,
6262
+ ...searchContext ? { searchContext } : {}
6263
+ }
6264
+ };
6265
+ }
5713
6266
  const record = asRecord3(entry);
5714
6267
  if (!record) {
5715
6268
  return null;
5716
6269
  }
5717
- const url2 = readString4(record.link) ?? "";
5718
- const title = readString4(record.title) || url2 || "Untitled";
6270
+ const responseMetadata = asRecord3(record.metadata);
6271
+ const user = asRecord3(record.user);
6272
+ const resultUrl = firstString(record.link, record.website, record.url, record.imageUrl) ?? "";
6273
+ const title = firstNonEmptyString(
6274
+ record.title,
6275
+ responseMetadata?.title,
6276
+ record.name,
6277
+ record.query,
6278
+ record.value,
6279
+ user?.name,
6280
+ formatReviewTitle(record, user),
6281
+ resultUrl
6282
+ ) ?? "Untitled";
6283
+ const url2 = resultUrl || (mode === "autocomplete" ? toGoogleSearchUrl(title) : "");
5719
6284
  const snippet = trimSnippet(
5720
- readString4(record.snippet) ?? readString4(record.richSnippet) ?? readString4(record.date) ?? ""
6285
+ firstNonEmptyString(
6286
+ record.snippet,
6287
+ record.richSnippet,
6288
+ record.markdown,
6289
+ record.text,
6290
+ record.address,
6291
+ record.price,
6292
+ record.date,
6293
+ record.name,
6294
+ record.value,
6295
+ record.url
6296
+ ) ?? ""
5721
6297
  );
5722
6298
  const metadata = omitUndefined({
5723
- source: "organic",
6299
+ source: readString4(record.source) ?? (mode === "search" ? "organic" : mode),
5724
6300
  position: readNumber(record.position),
5725
6301
  date: readString4(record.date),
5726
6302
  attributes: asRecord3(record.attributes),
@@ -5728,7 +6304,16 @@ function toSearchResult3(entry, searchContext) {
5728
6304
  rating: readNumber(record.rating),
5729
6305
  ratingCount: readNumber(record.ratingCount),
5730
6306
  cid: readString4(record.cid),
5731
- ...extractExtraMetadata(record, ["title", "link", "snippet"]),
6307
+ ...extractExtraMetadata(record, [
6308
+ "title",
6309
+ "name",
6310
+ "query",
6311
+ "value",
6312
+ "link",
6313
+ "website",
6314
+ "url",
6315
+ "snippet"
6316
+ ]),
5732
6317
  ...searchContext ? { searchContext } : {}
5733
6318
  });
5734
6319
  return {
@@ -5738,29 +6323,78 @@ function toSearchResult3(entry, searchContext) {
5738
6323
  ...Object.keys(metadata).length > 0 ? { metadata } : {}
5739
6324
  };
5740
6325
  }
5741
- function buildSearchContext(response) {
6326
+ function buildSearchContext(response, mode) {
5742
6327
  const context = omitUndefined({
5743
6328
  searchParameters: asRecord3(response.searchParameters),
5744
6329
  searchInformation: asRecord3(response.searchInformation),
5745
6330
  credits: readNumber(response.credits),
5746
6331
  answerBox: asRecord3(response.answerBox),
5747
- knowledgeGraph: asRecord3(response.knowledgeGraph),
5748
- peopleAlsoAsk: asArray(response.peopleAlsoAsk),
5749
- relatedSearches: asArray(response.relatedSearches),
5750
- topStories: asArray(response.topStories),
5751
- news: asArray(response.news),
5752
- images: asArray(response.images),
5753
- videos: asArray(response.videos),
5754
- places: asArray(response.places)
6332
+ knowledgeGraph: asRecord3(response.knowledgeGraph)
5755
6333
  });
6334
+ const primaryResultFields = new Set(
6335
+ PRIMARY_RESULT_FIELDS_BY_MODE[mode]
6336
+ );
6337
+ for (const field of CONTEXT_ARRAY_FIELDS) {
6338
+ if (primaryResultFields.has(field)) {
6339
+ continue;
6340
+ }
6341
+ const value = asArray(response[field]);
6342
+ if (value) {
6343
+ context[field] = value;
6344
+ }
6345
+ }
5756
6346
  return Object.keys(context).length > 0 ? context : void 0;
5757
6347
  }
5758
- function extractExtraMetadata(record, ignoredKeys) {
5759
- return Object.fromEntries(
5760
- Object.entries(record).filter(
5761
- ([key, value]) => !ignoredKeys.includes(key) && value !== void 0
5762
- )
5763
- );
6348
+ function copyStringOption(target, key, value) {
6349
+ const text = readString4(value);
6350
+ if (text !== void 0) {
6351
+ target[key] = text;
6352
+ }
6353
+ }
6354
+ function copyBooleanOption(target, key, value) {
6355
+ const flag = readBoolean(value);
6356
+ if (flag !== void 0) {
6357
+ target[key] = flag;
6358
+ }
6359
+ }
6360
+ function firstString(...values) {
6361
+ return values.find((value) => typeof value === "string");
6362
+ }
6363
+ function toGoogleSearchUrl(query2) {
6364
+ return `https://www.google.com/search?q=${encodeURIComponent(query2)}`;
6365
+ }
6366
+ function formatReviewTitle(record, user) {
6367
+ const userName = readString4(user?.name);
6368
+ const rating = readNumber(record.rating);
6369
+ const date = readString4(record.date) ?? readString4(record.isoDate);
6370
+ if (userName && rating !== void 0) {
6371
+ return `${userName} (${rating}-star review)`;
6372
+ }
6373
+ if (userName) {
6374
+ return `${userName}'s review`;
6375
+ }
6376
+ if (rating !== void 0 && date) {
6377
+ return `${rating}-star review from ${date}`;
6378
+ }
6379
+ if (rating !== void 0) {
6380
+ return `${rating}-star review`;
6381
+ }
6382
+ if (date) {
6383
+ return `Review from ${date}`;
6384
+ }
6385
+ return void 0;
6386
+ }
6387
+ function firstNonEmptyString(...values) {
6388
+ return values.find(
6389
+ (value) => typeof value === "string" && value.length > 0
6390
+ );
6391
+ }
6392
+ function extractExtraMetadata(record, ignoredKeys) {
6393
+ return Object.fromEntries(
6394
+ Object.entries(record).filter(
6395
+ ([key, value]) => !ignoredKeys.includes(key) && value !== void 0
6396
+ )
6397
+ );
5764
6398
  }
5765
6399
  function omitUndefined(value) {
5766
6400
  return Object.fromEntries(
@@ -5779,6 +6413,12 @@ function readString4(value) {
5779
6413
  function readNumber(value) {
5780
6414
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
5781
6415
  }
6416
+ function readInteger2(value) {
6417
+ return typeof value === "number" && Number.isInteger(value) ? value : void 0;
6418
+ }
6419
+ function readBoolean(value) {
6420
+ return typeof value === "boolean" ? value : void 0;
6421
+ }
5782
6422
  var serperProvider = defineProvider({
5783
6423
  id: "serper",
5784
6424
  label: serperImplementation.label,
@@ -5788,14 +6428,16 @@ var serperProvider = defineProvider({
5788
6428
  fields: ["credentials", "baseUrl", "options", "settings"],
5789
6429
  optionCapabilities: ["search"]
5790
6430
  },
5791
- getCapabilityStatus: (config, cwd, tool) => serperImplementation.getCapabilityStatus(
6431
+ getCapabilityStatus: (config, cwd, tool, options) => serperImplementation.getCapabilityStatus(
5792
6432
  config,
5793
6433
  cwd,
5794
- tool
6434
+ tool,
6435
+ options
5795
6436
  ),
5796
6437
  capabilities: {
5797
6438
  search: defineCapability({
5798
6439
  options: serperImplementation.getToolOptionsSchema?.("search"),
6440
+ promptGuidelines: serperSearchPromptGuidelines,
5799
6441
  async execute(input, ctx) {
5800
6442
  const { query: query2, maxResults, options } = input;
5801
6443
  return await serperImplementation.search(
@@ -5920,8 +6562,8 @@ var tavilyImplementation = {
5920
6562
  }
5921
6563
  };
5922
6564
  },
5923
- getCapabilityStatus(config) {
5924
- return getApiKeyStatus(config?.credentials?.api);
6565
+ getCapabilityStatus(config, _cwd, _tool, options) {
6566
+ return getApiKeyStatus(config?.credentials?.api, options);
5925
6567
  },
5926
6568
  async search(query2, maxResults, config, _context, options) {
5927
6569
  const client = createClient8(config);
@@ -6019,10 +6661,11 @@ var tavilyProvider = defineProvider({
6019
6661
  createTemplate: () => tavilyImplementation.createTemplate(),
6020
6662
  fields: ["credentials", "baseUrl", "options", "settings"]
6021
6663
  },
6022
- getCapabilityStatus: (config, cwd, tool) => tavilyImplementation.getCapabilityStatus(
6664
+ getCapabilityStatus: (config, cwd, tool, options) => tavilyImplementation.getCapabilityStatus(
6023
6665
  config,
6024
6666
  cwd,
6025
- tool
6667
+ tool,
6668
+ options
6026
6669
  ),
6027
6670
  capabilities: {
6028
6671
  search: defineCapability({
@@ -6126,8 +6769,8 @@ var valyuImplementation = {
6126
6769
  }
6127
6770
  };
6128
6771
  },
6129
- getCapabilityStatus(config) {
6130
- return getApiKeyStatus(config?.credentials?.api);
6772
+ getCapabilityStatus(config, _cwd, _tool, options) {
6773
+ return getApiKeyStatus(config?.credentials?.api, options);
6131
6774
  },
6132
6775
  async search(query2, maxResults, config, _context, searchOptions) {
6133
6776
  const client = createClient9(config);
@@ -6302,10 +6945,11 @@ var valyuProvider = defineProvider({
6302
6945
  fields: ["credentials", "baseUrl", "options", "settings"],
6303
6946
  optionCapabilities: ["search", "answer", "research"]
6304
6947
  },
6305
- getCapabilityStatus: (config, cwd, tool) => valyuImplementation.getCapabilityStatus(
6948
+ getCapabilityStatus: (config, cwd, tool, options) => valyuImplementation.getCapabilityStatus(
6306
6949
  config,
6307
6950
  cwd,
6308
- tool
6951
+ tool,
6952
+ options
6309
6953
  ),
6310
6954
  capabilities: {
6311
6955
  search: defineCapability({
@@ -6380,9 +7024,6 @@ var PROVIDERS_BY_ID = PROVIDERS;
6380
7024
  var PROVIDER_LIST = Object.values(PROVIDERS);
6381
7025
  var PROVIDER_IDS = Object.keys(PROVIDERS);
6382
7026
 
6383
- // src/types.ts
6384
- var TOOLS = ["search", "contents", "answer", "research"];
6385
-
6386
7027
  // src/provider-tools.ts
6387
7028
  var TOOL_INFO = {
6388
7029
  search: {
@@ -7138,17 +7779,21 @@ function isPlainObject4(value) {
7138
7779
  function getMappedProviderIdForTool(config, tool) {
7139
7780
  return getMappedProviderForTool(config, tool);
7140
7781
  }
7141
- function getProviderCapabilityStatus(config, cwd, providerId, tool) {
7782
+ function getProviderCapabilityStatus(config, cwd, providerId, tool, options) {
7142
7783
  const provider = PROVIDERS[providerId];
7143
7784
  return provider.getCapabilityStatus(
7144
7785
  getEffectiveProviderConfig(config, providerId),
7145
7786
  cwd,
7146
- tool
7787
+ tool,
7788
+ options
7147
7789
  );
7148
7790
  }
7149
7791
  function isProviderCapabilityReady(status) {
7150
7792
  return status.state === "ready";
7151
7793
  }
7794
+ function isProviderCapabilityExposable(status) {
7795
+ return status.state === "ready" || status.state === "deferred_secret";
7796
+ }
7152
7797
  function getProviderSetupState(config, providerId) {
7153
7798
  if (providerId === "claude" || providerId === "codex") {
7154
7799
  return "builtin";
@@ -7163,12 +7808,17 @@ function getProviderSetupState(config, providerId) {
7163
7808
  if (providerId === "cloudflare") {
7164
7809
  return providerConfig.credentials !== void 0 || providerConfig.accountId !== void 0 ? "configured" : "none";
7165
7810
  }
7811
+ if (providerId === "firecrawl") {
7812
+ return providerConfig.credentials !== void 0 || providerConfig.baseUrl !== void 0 ? "configured" : "none";
7813
+ }
7166
7814
  return providerConfig.credentials !== void 0 ? "configured" : "none";
7167
7815
  }
7168
7816
  function formatProviderCapabilityStatus(status, providerId, tool) {
7169
7817
  switch (status.state) {
7170
7818
  case "ready":
7171
7819
  return "Ready";
7820
+ case "deferred_secret":
7821
+ return "Secret resolved on first use";
7172
7822
  case "missing_api_key":
7173
7823
  return "Missing API key";
7174
7824
  case "missing_executable":
@@ -8812,6 +9462,158 @@ function cleanupCapabilityOptions(config, keys) {
8812
9462
  cleanupEmpty(config, "options");
8813
9463
  }
8814
9464
 
9465
+ // src/tool-display.ts
9466
+ import { formatSize } from "@earendil-works/pi-coding-agent";
9467
+ var ANSWER_EXCERPT_MAX_LENGTH = 100;
9468
+ function buildSearchToolDisplay(details) {
9469
+ return buildToolDisplay(details.provider, buildSearchSummaryParts(details));
9470
+ }
9471
+ function buildProgressDisplay(providerId, action) {
9472
+ return {
9473
+ provider: getProviderDisplay(providerId),
9474
+ progress: { action }
9475
+ };
9476
+ }
9477
+ function buildProviderToolDisplay({
9478
+ capability,
9479
+ providerId,
9480
+ details,
9481
+ text,
9482
+ outputBytes,
9483
+ outputTruncated,
9484
+ failedItemCount
9485
+ }) {
9486
+ const summary = capability === "contents" && details.tool === "web_contents" ? buildContentsDisplaySummary(details, text, {
9487
+ outputBytes,
9488
+ outputTruncated,
9489
+ failedItemCount
9490
+ }) : capability === "research" && text ? { success: text } : buildCollapsedProviderToolSummaryParts(details, text);
9491
+ return buildToolDisplay(providerId, summary);
9492
+ }
9493
+ function buildCollapsedProviderToolSummary(details, text) {
9494
+ const summary = buildCollapsedProviderToolSummaryParts(details, text);
9495
+ return summary.failure ? `${summary.success}, ${summary.failure}` : summary.success;
9496
+ }
9497
+ function buildCollapsedProviderToolSummaryParts(details, text) {
9498
+ if (details?.tool === "web_answer") {
9499
+ return buildAnswerCollapsedSummary(details, text);
9500
+ }
9501
+ if (details?.tool === "web_contents") {
9502
+ return buildContentsSummary(details, text);
9503
+ }
9504
+ const baseSummary = getCompactProviderToolSummary(details) ?? getFirstLine(text) ?? `${details?.tool ?? "tool"} output available`;
9505
+ return { success: baseSummary };
9506
+ }
9507
+ function buildSearchSummaryParts({
9508
+ queryCount,
9509
+ resultCount,
9510
+ failedQueryCount
9511
+ }) {
9512
+ const success = typeof resultCount === "number" ? `${resultCount} result${resultCount === 1 ? "" : "s"}` : "Search output available";
9513
+ if (failedQueryCount && failedQueryCount > 0 && queryCount) {
9514
+ return {
9515
+ success,
9516
+ failure: `${failedQueryCount} of ${queryCount} ${queryCount === 1 ? "query" : "queries"} failed`
9517
+ };
9518
+ }
9519
+ return { success };
9520
+ }
9521
+ function buildToolDisplay(providerId, outcome) {
9522
+ return {
9523
+ provider: getProviderDisplay(providerId),
9524
+ outcome
9525
+ };
9526
+ }
9527
+ function getProviderDisplay(providerId) {
9528
+ const provider = PROVIDERS_BY_ID[providerId];
9529
+ return { id: providerId, label: provider?.label ?? providerId };
9530
+ }
9531
+ function buildContentsDisplaySummary(details, text, metadata) {
9532
+ const totalCount = details.itemCount ?? inferContentsPageCount(text);
9533
+ const failedCount = metadata.failedItemCount ?? inferContentsFailureCount(text);
9534
+ const successCount = totalCount === void 0 ? void 0 : Math.max(0, totalCount - (failedCount ?? 0));
9535
+ const sizeSummary = typeof metadata.outputBytes === "number" ? `${formatSize(metadata.outputBytes)}${metadata.outputTruncated ? " (truncated)" : ""}` : void 0;
9536
+ const success = successCount === void 0 ? sizeSummary ?? "Contents output available" : successCount === 1 && sizeSummary ? sizeSummary : `${successCount} page${successCount === 1 ? "" : "s"}${sizeSummary ? `, ${sizeSummary}` : ""}`;
9537
+ if (failedCount && failedCount > 0 && totalCount) {
9538
+ return {
9539
+ success,
9540
+ failure: `${failedCount} of ${totalCount} ${totalCount === 1 ? "page" : "pages"} failed`
9541
+ };
9542
+ }
9543
+ return { success };
9544
+ }
9545
+ function buildAnswerCollapsedSummary(details, text) {
9546
+ if (typeof details.queryCount === "number" && (details.queryCount > 1 || (details.failedQueryCount ?? 0) > 0)) {
9547
+ return buildAnswerSummary(details);
9548
+ }
9549
+ return { success: buildAnswerExcerpt(text) ?? "Answer output available" };
9550
+ }
9551
+ function buildAnswerSummary(details) {
9552
+ const queryCount = details.queryCount ?? 0;
9553
+ const failedQueryCount = details.failedQueryCount ?? 0;
9554
+ const answerCount = Math.max(0, queryCount - failedQueryCount);
9555
+ const success = `${answerCount} answer${answerCount === 1 ? "" : "s"}`;
9556
+ if (failedQueryCount > 0) {
9557
+ return {
9558
+ success,
9559
+ failure: `${failedQueryCount} of ${queryCount} ${queryCount === 1 ? "question" : "questions"} failed`
9560
+ };
9561
+ }
9562
+ return { success };
9563
+ }
9564
+ function buildAnswerExcerpt(text) {
9565
+ const excerpt = getFirstLine(text);
9566
+ if (!excerpt) {
9567
+ return void 0;
9568
+ }
9569
+ if (excerpt.length <= ANSWER_EXCERPT_MAX_LENGTH) {
9570
+ return excerpt;
9571
+ }
9572
+ return `${excerpt.slice(0, ANSWER_EXCERPT_MAX_LENGTH - 1).trimEnd()}\u2026`;
9573
+ }
9574
+ function buildContentsSummary(details, text) {
9575
+ const totalCount = details.itemCount ?? inferContentsPageCount(text);
9576
+ const failedCount = inferContentsFailureCount(text);
9577
+ const successCount = totalCount === void 0 ? void 0 : Math.max(0, totalCount - (failedCount ?? 0));
9578
+ const success = successCount === void 0 ? "Contents output available" : `${successCount} page${successCount === 1 ? "" : "s"}`;
9579
+ if (failedCount && failedCount > 0 && totalCount) {
9580
+ return {
9581
+ success,
9582
+ failure: `${failedCount} of ${totalCount} ${totalCount === 1 ? "page" : "pages"} failed`
9583
+ };
9584
+ }
9585
+ return { success };
9586
+ }
9587
+ function getCompactProviderToolSummary(details) {
9588
+ if (!details) {
9589
+ return void 0;
9590
+ }
9591
+ if (details.tool === "web_contents" && typeof details.itemCount === "number") {
9592
+ return `${details.itemCount} page${details.itemCount === 1 ? "" : "s"}`;
9593
+ }
9594
+ if (details.tool === "web_research") {
9595
+ return "Research";
9596
+ }
9597
+ return void 0;
9598
+ }
9599
+ function inferContentsPageCount(text) {
9600
+ if (!text) {
9601
+ return void 0;
9602
+ }
9603
+ const pageMatches = text.match(/^##\s+/gm);
9604
+ return pageMatches?.length;
9605
+ }
9606
+ function inferContentsFailureCount(text) {
9607
+ if (!text) {
9608
+ return void 0;
9609
+ }
9610
+ const failureMatches = text.match(/^##\s+(?:\d+\.\s+)?Error:/gm);
9611
+ return failureMatches?.length;
9612
+ }
9613
+ function getFirstLine(text) {
9614
+ return text?.split("\n").map((line) => line.trim()).find((line) => line.length > 0);
9615
+ }
9616
+
8815
9617
  // src/index.ts
8816
9618
  var DEFAULT_MAX_RESULTS = 5;
8817
9619
  var MAX_ALLOWED_RESULTS = 20;
@@ -8828,6 +9630,10 @@ var CAPABILITY_TOOL_NAMES = {
8828
9630
  research: "web_research"
8829
9631
  };
8830
9632
  var MANAGED_TOOL_NAMES = Object.values(CAPABILITY_TOOL_NAMES);
9633
+ var DEFAULT_SUMMARY_SYMBOLS = {
9634
+ success: "\u2714",
9635
+ failure: "\u2718"
9636
+ };
8831
9637
  function webProvidersExtension(pi) {
8832
9638
  const activeWebResearchRequests = /* @__PURE__ */ new Map();
8833
9639
  let latestWidgetContext;
@@ -8873,7 +9679,7 @@ function webProvidersExtension(pi) {
8873
9679
  if ("registerMessageRenderer" in pi) {
8874
9680
  pi.registerMessageRenderer(
8875
9681
  WEB_RESEARCH_RESULT_MESSAGE_TYPE,
8876
- renderWebResearchResultMessage
9682
+ (message, state, theme) => renderWebResearchResultMessage(message, state, theme)
8877
9683
  );
8878
9684
  }
8879
9685
  pi.registerCommand("web-providers", {
@@ -8934,7 +9740,7 @@ function registerWebSearchTool(pi, providerIds) {
8934
9740
  pi.registerTool({
8935
9741
  name: "web_search",
8936
9742
  label: "Web Search",
8937
- description: `Find likely sources on the public web for up to ${MAX_SEARCH_QUERIES} queries in a single call and return titles, URLs, and snippets grouped by query. Output is truncated to ${DEFAULT_MAX_LINES} lines or ${formatSize(DEFAULT_MAX_BYTES)} when needed.`,
9743
+ description: `Find likely sources on the public web for up to ${MAX_SEARCH_QUERIES} queries in a single call and return titles, URLs, and snippets grouped by query. Output is truncated to ${DEFAULT_MAX_LINES} lines or ${formatSize2(DEFAULT_MAX_BYTES)} when needed.`,
8938
9744
  promptGuidelines: buildPromptGuidelines("search", selectedProviderId, [
8939
9745
  "Batch related searches when grouped comparison matters; use separate sibling web_search calls when independent results should surface as soon as they are ready."
8940
9746
  ]),
@@ -9179,12 +9985,20 @@ function getAvailableProviderIdsForCapability(config, cwd, capability) {
9179
9985
  if (!providerId) {
9180
9986
  return [];
9181
9987
  }
9182
- try {
9183
- resolveProviderForTool(config, cwd, capability);
9184
- return [providerId];
9185
- } catch {
9988
+ const provider = PROVIDERS_BY_ID[providerId];
9989
+ if (!supportsTool2(provider, capability)) {
9186
9990
  return [];
9187
9991
  }
9992
+ const status = getProviderCapabilityStatus(
9993
+ config,
9994
+ cwd,
9995
+ providerId,
9996
+ capability,
9997
+ {
9998
+ resolveSecrets: false
9999
+ }
10000
+ );
10001
+ return isProviderCapabilityExposable(status) ? [providerId] : [];
9188
10002
  }
9189
10003
  function getProviderStatusForTool(config, cwd, providerId, capability) {
9190
10004
  return getProviderCapabilityStatus(config, cwd, providerId, capability);
@@ -9324,6 +10138,7 @@ async function executeSearchToolInternal({
9324
10138
  );
9325
10139
  const batchProgress = searchQueries.length > 1 ? createBatchCompletionReporter(
9326
10140
  "Searching",
10141
+ provider.id,
9327
10142
  provider.label,
9328
10143
  searchQueries.length,
9329
10144
  progressReporter.report
@@ -9372,7 +10187,7 @@ async function executeSearchToolInternal({
9372
10187
  progressReporter.stop();
9373
10188
  }
9374
10189
  if (outcomes.every((outcome) => outcome.error !== void 0)) {
9375
- throw buildSearchBatchError(outcomes);
10190
+ throw buildSearchBatchError(outcomes, provider.label);
9376
10191
  }
9377
10192
  const prefetch = prefetchOptions !== void 0 && executionOverrides === void 0 ? await startContentsPrefetch({
9378
10193
  config,
@@ -9384,9 +10199,11 @@ async function executeSearchToolInternal({
9384
10199
  formatSearchResponses(outcomes, prefetch),
9385
10200
  "web-search"
9386
10201
  );
10202
+ const details = buildWebSearchDetails(provider.id, outcomes);
9387
10203
  return {
9388
10204
  content: [{ type: "text", text: rendered }],
9389
- details: buildWebSearchDetails(provider.id, outcomes)
10205
+ details,
10206
+ display: buildSearchToolDisplay2(details)
9390
10207
  };
9391
10208
  }
9392
10209
  async function executeRawProviderRequest({
@@ -9461,16 +10278,22 @@ async function executeRawProviderRequest({
9461
10278
  input
9462
10279
  });
9463
10280
  }
9464
- function buildSearchBatchError(outcomes) {
10281
+ function buildSearchBatchError(outcomes, providerLabel) {
9465
10282
  const failed = outcomes.filter((outcome) => outcome.error !== void 0);
9466
10283
  if (failed.length === 1) {
9467
- return new Error(failed[0]?.error ?? "web_search failed.");
10284
+ return new Error(
10285
+ formatProviderCapabilityFailure(
10286
+ providerLabel,
10287
+ "search",
10288
+ failed[0]?.error ?? ""
10289
+ )
10290
+ );
9468
10291
  }
9469
10292
  const summary = failed.map(
9470
10293
  (outcome, index) => `${index + 1}. ${formatQuotedPreview(outcome.query, 40)} \u2014 ${outcome.error}`
9471
10294
  ).join("; ");
9472
10295
  return new Error(
9473
- `All ${failed.length} web_search queries failed: ${summary}`
10296
+ `${providerLabel} search failed for ${failed.length} queries: ${summary}`
9474
10297
  );
9475
10298
  }
9476
10299
  async function executeSingleSearchQuery({
@@ -9546,6 +10369,7 @@ async function executeAnswerToolInternal({
9546
10369
  );
9547
10370
  const batchProgress = answerQueries.length > 1 ? createBatchCompletionReporter(
9548
10371
  "Answering",
10372
+ provider.id,
9549
10373
  provider.label,
9550
10374
  answerQueries.length,
9551
10375
  progressReporter.report
@@ -9588,7 +10412,7 @@ async function executeAnswerToolInternal({
9588
10412
  progressReporter.stop();
9589
10413
  }
9590
10414
  if (outcomes.every((outcome) => outcome.error !== void 0)) {
9591
- throw buildAnswerBatchError(outcomes);
10415
+ throw buildAnswerBatchError(outcomes, provider.label);
9592
10416
  }
9593
10417
  const text = await truncateAndSave(
9594
10418
  formatAnswerResponses(outcomes),
@@ -9597,19 +10421,31 @@ async function executeAnswerToolInternal({
9597
10421
  const details = buildWebAnswerDetails(provider.id, outcomes);
9598
10422
  return {
9599
10423
  content: [{ type: "text", text }],
9600
- details
10424
+ details,
10425
+ display: buildProviderToolDisplay2({
10426
+ capability: "answer",
10427
+ providerId: provider.id,
10428
+ details,
10429
+ text
10430
+ })
9601
10431
  };
9602
10432
  }
9603
- function buildAnswerBatchError(outcomes) {
10433
+ function buildAnswerBatchError(outcomes, providerLabel) {
9604
10434
  const failed = outcomes.filter((outcome) => outcome.error !== void 0);
9605
10435
  if (failed.length === 1) {
9606
- return new Error(failed[0]?.error ?? "web_answer failed.");
10436
+ return new Error(
10437
+ formatProviderCapabilityFailure(
10438
+ providerLabel,
10439
+ "answer",
10440
+ failed[0]?.error ?? ""
10441
+ )
10442
+ );
9607
10443
  }
9608
10444
  const summary = failed.map(
9609
10445
  (outcome, index) => `${index + 1}. ${formatQuotedPreview(outcome.query, 40)} \u2014 ${outcome.error}`
9610
10446
  ).join("; ");
9611
10447
  return new Error(
9612
- `All ${failed.length} web_answer queries failed: ${summary}`
10448
+ `${providerLabel} answer failed for ${failed.length} questions: ${summary}`
9613
10449
  );
9614
10450
  }
9615
10451
  function formatAnswerResponses(outcomes) {
@@ -9671,13 +10507,24 @@ async function executeProviderOperation({
9671
10507
  });
9672
10508
  }
9673
10509
  if (capability === "contents") {
10510
+ const urlCount = (urls ?? []).length;
9674
10511
  onProgress?.(
9675
- `Fetching contents via ${provider.label} for ${(urls ?? []).length} URL(s)`
10512
+ `Fetching contents via ${provider.label} for ${urlCount} URL(s)`,
10513
+ buildProgressDisplay2(
10514
+ provider.id,
10515
+ urlCount === 1 ? "Fetching page" : `Fetching ${urlCount} pages`
10516
+ )
9676
10517
  );
9677
10518
  } else if (capability === "answer") {
9678
- onProgress?.(`Answering via ${provider.label}`);
10519
+ onProgress?.(
10520
+ `Answering via ${provider.label}`,
10521
+ buildProgressDisplay2(provider.id, "Answering")
10522
+ );
9679
10523
  } else if (capability === "research") {
9680
- onProgress?.(`Researching via ${provider.label}`);
10524
+ onProgress?.(
10525
+ `Researching via ${provider.label}`,
10526
+ buildProgressDisplay2(provider.id, "Researching")
10527
+ );
9681
10528
  }
9682
10529
  const result = executionOverride ? await executeProviderExecution(executionOverride, {
9683
10530
  cwd: ctx.cwd,
@@ -9782,18 +10629,36 @@ async function executeProviderToolInternal({
9782
10629
  } finally {
9783
10630
  progressReporter.stop();
9784
10631
  }
9785
- const details = {
9786
- tool: `web_${capability}`,
9787
- provider: response.provider,
9788
- itemCount: isContentsResponse2(response) ? response.answers.length : response.itemCount
9789
- };
9790
- const text = await truncateAndSave(
10632
+ const rendered = await truncateAndSaveWithMetadata(
9791
10633
  isContentsResponse2(response) ? formatContentsResponse(response) : response.text,
9792
10634
  capability
9793
10635
  );
10636
+ const details = isContentsResponse2(response) ? {
10637
+ tool: "web_contents",
10638
+ provider: response.provider,
10639
+ itemCount: response.answers.length
10640
+ } : capability === "answer" ? {
10641
+ tool: "web_answer",
10642
+ provider: response.provider,
10643
+ itemCount: response.itemCount,
10644
+ queryCount: 1,
10645
+ failedQueryCount: 0
10646
+ } : {
10647
+ tool: "web_research",
10648
+ provider: response.provider
10649
+ };
9794
10650
  return {
9795
- content: [{ type: "text", text }],
9796
- details
10651
+ content: [{ type: "text", text: rendered.text }],
10652
+ details,
10653
+ display: buildProviderToolDisplay2({
10654
+ capability,
10655
+ providerId: response.provider,
10656
+ details,
10657
+ text: rendered.text,
10658
+ outputBytes: capability === "contents" ? rendered.totalBytes : void 0,
10659
+ outputTruncated: capability === "contents" ? rendered.truncated : void 0,
10660
+ failedItemCount: isContentsResponse2(response) ? response.answers.filter((answer) => answer.error !== void 0).length : void 0
10661
+ })
9797
10662
  };
9798
10663
  }
9799
10664
  async function dispatchWebResearch({
@@ -9857,7 +10722,13 @@ async function dispatchWebResearchInternal({
9857
10722
  text: `Started web research via ${provider.label}.`
9858
10723
  }
9859
10724
  ],
9860
- details: request
10725
+ details: request,
10726
+ display: buildProviderToolDisplay2({
10727
+ capability: "research",
10728
+ providerId: provider.id,
10729
+ details: { tool: "web_research", provider: provider.id },
10730
+ text: "started"
10731
+ })
9861
10732
  };
9862
10733
  }
9863
10734
  async function runDispatchedWebResearch({
@@ -10089,6 +10960,7 @@ async function executeBatchedContentsTool({
10089
10960
  }
10090
10961
  const batchProgress = createBatchCompletionReporter(
10091
10962
  "Fetching contents",
10963
+ provider.id,
10092
10964
  provider.label,
10093
10965
  urls.length,
10094
10966
  progressReport
@@ -10140,7 +11012,11 @@ async function executeBatchedContentsTool({
10140
11012
  );
10141
11013
  if (successful.length === 0 && failures.length > 0) {
10142
11014
  throw new Error(
10143
- failures.length === 1 ? failures[0]?.error ?? "web_contents failed." : `web_contents failed for all ${failures.length} URL(s): ${failures.map(
11015
+ failures.length === 1 ? formatProviderCapabilityFailure(
11016
+ provider.label,
11017
+ "contents",
11018
+ failures[0]?.error ?? ""
11019
+ ) : `${provider.label} fetch failed for ${failures.length} pages: ${failures.map(
10144
11020
  (failure, index) => `${index + 1}. ${failure.url} \u2014 ${failure.error}`
10145
11021
  ).join("; ")}`
10146
11022
  );
@@ -10205,10 +11081,11 @@ function createProgressEmitter(onUpdate) {
10205
11081
  if (!onUpdate) {
10206
11082
  return void 0;
10207
11083
  }
10208
- return (message) => {
11084
+ return (message, display) => {
10209
11085
  onUpdate({
10210
11086
  content: [{ type: "text", text: message }],
10211
- details: {}
11087
+ details: {},
11088
+ display
10212
11089
  });
10213
11090
  };
10214
11091
  }
@@ -10217,7 +11094,7 @@ function createToolProgressReporter(capability, providerId, progress) {
10217
11094
  return { report: void 0, stop: () => {
10218
11095
  } };
10219
11096
  }
10220
- const emit = (message) => progress(message);
11097
+ const emit = (message, display) => progress(message, display);
10221
11098
  const startedAt = Date.now();
10222
11099
  let lastUpdateAt = startedAt;
10223
11100
  let timer;
@@ -10228,14 +11105,17 @@ function createToolProgressReporter(capability, providerId, progress) {
10228
11105
  }
10229
11106
  const providerLabel = PROVIDERS_BY_ID[providerId]?.label ?? providerId;
10230
11107
  const elapsed = formatElapsed(Date.now() - startedAt);
10231
- emit(`Researching via ${providerLabel} (${elapsed} elapsed)`);
11108
+ emit(
11109
+ `Researching via ${providerLabel} (${elapsed} elapsed)`,
11110
+ buildProgressDisplay2(providerId, `Researching ${elapsed}`)
11111
+ );
10232
11112
  lastUpdateAt = Date.now();
10233
11113
  }, RESEARCH_HEARTBEAT_MS);
10234
11114
  }
10235
11115
  return {
10236
- report: (message) => {
11116
+ report: (message, display) => {
10237
11117
  lastUpdateAt = Date.now();
10238
- emit(message);
11118
+ emit(message, display);
10239
11119
  },
10240
11120
  stop: () => {
10241
11121
  if (timer) {
@@ -10306,77 +11186,125 @@ function renderQuestionCallHeader(params, theme) {
10306
11186
  );
10307
11187
  }
10308
11188
  function renderResearchCallHeader(params, theme) {
10309
- return renderListCallHeader("web_research", [params.input], theme);
11189
+ return renderListCallHeader(
11190
+ "web_research",
11191
+ [params.input],
11192
+ theme,
11193
+ void 0,
11194
+ { quoteSingleItem: true }
11195
+ );
10310
11196
  }
10311
- function renderSearchToolResult(result, expanded, isPartial, theme) {
11197
+ function renderWebToolResult(result, state, theme, config, symbols = DEFAULT_SUMMARY_SYMBOLS) {
10312
11198
  const text = extractTextContent(result.content);
10313
- const isError = Boolean(result.isError);
10314
- if (isPartial) {
10315
- return renderSimpleText(text ?? "Working\u2026", theme, "warning");
11199
+ if (state.isPartial) {
11200
+ return renderToolProgress(result.display, text, theme);
10316
11201
  }
10317
- if (isError) {
10318
- return renderBlockText(text ?? "web_search failed", theme, "error");
11202
+ if (result.isError) {
11203
+ return renderFailureText(
11204
+ buildFailureSummary({
11205
+ text,
11206
+ details: result.details,
11207
+ capability: config.capability,
11208
+ fallback: config.failureText
11209
+ }),
11210
+ theme,
11211
+ symbols
11212
+ );
10319
11213
  }
10320
- const details = result.details;
10321
- if (!details || expanded) {
10322
- return renderMarkdownBlock(text ?? "");
11214
+ const details = config.getDetails(result.details);
11215
+ if (state.expanded) {
11216
+ return config.renderExpanded(details, text);
10323
11217
  }
10324
- return renderCollapsedSearchSummary(details, text, theme);
11218
+ const summary = config.preferDisplaySummary === false ? config.getCollapsedSummary(details, text) : getDisplaySummaryParts(result.display) ?? config.getCollapsedSummary(details, text);
11219
+ return renderCollapsedSummary(summary, theme, symbols);
10325
11220
  }
10326
- function renderWebResearchDispatchResult(result, expanded, theme) {
10327
- const text = extractTextContent(result.content) ?? "Started web research.";
10328
- const details = isWebResearchRequest(result.details) ? result.details : void 0;
10329
- if (expanded) {
10330
- const expandedText = details ? [
10331
- text,
10332
- "",
10333
- "## Research brief",
10334
- "",
10335
- details.input,
10336
- "",
10337
- "## Report path",
10338
- "",
10339
- `\`${details.outputPath}\``
10340
- ].join("\n") : text;
10341
- return renderMarkdownBlock(expandedText);
10342
- }
10343
- const summary = details ? `Started web research via ${PROVIDERS_BY_ID[details.provider]?.label ?? details.provider}` : text;
10344
- let summaryText = theme.fg("success", summary);
10345
- summaryText += theme.fg("muted", ` (${getExpandHint()})`);
10346
- return new Text(summaryText, 0, 0);
11221
+ function renderSearchToolResult(result, expanded, isPartial, theme, symbols = DEFAULT_SUMMARY_SYMBOLS) {
11222
+ return renderWebToolResult(
11223
+ result,
11224
+ { expanded, isPartial },
11225
+ theme,
11226
+ {
11227
+ capability: "search",
11228
+ failureText: "web_search failed",
11229
+ getDetails: (details) => details,
11230
+ getCollapsedSummary: (details, text) => details ? buildSearchSummaryParts2(details) : { success: getFirstLine2(text) ?? "web_search output available" },
11231
+ renderExpanded: (_details, text) => renderMarkdownBlock(text ?? "")
11232
+ },
11233
+ symbols
11234
+ );
10347
11235
  }
10348
- function renderWebResearchResultMessage(message, { expanded }, theme) {
11236
+ function renderWebResearchDispatchResult(result, expanded, theme, symbols = DEFAULT_SUMMARY_SYMBOLS) {
11237
+ return renderWebToolResult(
11238
+ result,
11239
+ { expanded },
11240
+ theme,
11241
+ {
11242
+ capability: "research",
11243
+ failureText: "web_research failed",
11244
+ getDetails: (details) => isWebResearchRequest(details) ? details : void 0,
11245
+ getCollapsedSummary: () => ({ success: "started" }),
11246
+ renderExpanded: (details, text) => renderMarkdownBlock(
11247
+ details ? renderWebResearchRequestMarkdown(details) : text ?? "Started web research."
11248
+ ),
11249
+ preferDisplaySummary: false
11250
+ },
11251
+ symbols
11252
+ );
11253
+ }
11254
+ function renderWebResearchResultMessage(message, { expanded }, theme, symbols = DEFAULT_SUMMARY_SYMBOLS) {
10349
11255
  const text = typeof message.content === "string" ? message.content : extractTextContent(message.content);
10350
11256
  const details = isWebResearchResult(message.details) ? message.details : void 0;
10351
11257
  const isSuccess = details?.status === "completed";
10352
11258
  const accent = isSuccess ? "success" : "error";
10353
11259
  const box = new Box(1, 1, (value) => theme.bg("customMessageBg", value));
10354
11260
  if (!expanded) {
10355
- const lines = details ? buildWebResearchResultSummaryLines(details, theme) : [theme.fg(accent, "Web research update")];
10356
- lines.push(theme.fg("muted", `(${getExpandHint()})`));
10357
- box.addChild(new Text(lines.join("\n"), 0, 0));
11261
+ const summary = details ? buildWebResearchResultSummaryLine(details, theme, symbols) : theme.fg(accent, "Web research update");
11262
+ box.addChild(
11263
+ new Text(`${summary}${theme.fg("muted", ` (${getExpandHint()})`)}`, 0, 0)
11264
+ );
10358
11265
  return box;
10359
11266
  }
10360
11267
  box.addChild(
10361
- isSuccess ? renderMarkdownBlock(text ?? "") : renderBlockText(text ?? "", theme, "error")
11268
+ details ? renderMarkdownBlock(renderWebResearchResultMarkdown(details)) : isSuccess ? renderMarkdownBlock(text ?? "") : renderBlockText(text ?? "", theme, "error")
10362
11269
  );
10363
11270
  return box;
10364
11271
  }
10365
- function buildWebResearchResultSummaryLines(result, theme) {
11272
+ function renderWebResearchRequestMarkdown(request) {
11273
+ return [
11274
+ "### Web research",
11275
+ "",
11276
+ `**Brief:** ${request.input}`,
11277
+ "",
11278
+ "**Status:** running ",
11279
+ `**Elapsed:** ${formatSummaryElapsed(Date.now() - Date.parse(request.startedAt))} `,
11280
+ `**Artifact:** \`${request.outputPath}\``
11281
+ ].join("\n");
11282
+ }
11283
+ function renderWebResearchResultMarkdown(result) {
11284
+ const status = result.status === "completed" ? "completed" : result.status;
11285
+ return [
11286
+ "### Web research",
11287
+ "",
11288
+ `**Brief:** ${result.input}`,
11289
+ "",
11290
+ `**Status:** ${status} `,
11291
+ `**Duration:** ${formatSummaryElapsed(result.elapsedMs)} `,
11292
+ `**Artifact:** \`${result.outputPath}\``,
11293
+ ...result.error ? ["", `**Error:** ${result.error}`] : []
11294
+ ].join("\n");
11295
+ }
11296
+ function buildWebResearchResultSummaryLine(result, theme, symbols) {
10366
11297
  const providerLabel = PROVIDERS_BY_ID[result.provider]?.label ?? result.provider;
10367
- const statusLine = result.status === "completed" ? `Web research completed via ${providerLabel}` : `Web research failed via ${providerLabel}`;
10368
- const lines = [
10369
- theme.fg(result.status === "completed" ? "success" : "error", statusLine)
10370
- ];
10371
- lines.push(
10372
- theme.fg("muted", `\u25CB start: ${result.startedAt}`),
10373
- theme.fg("muted", `\u25F4 duration: ${formatElapsed(result.elapsedMs)}`),
10374
- theme.fg("muted", `\u21B3 file: ${result.outputPath}`)
10375
- );
10376
- if (result.error) {
10377
- lines.push(theme.fg("muted", `\u2715 error: ${result.error}`));
11298
+ if (result.status === "completed") {
11299
+ return renderSuccessSummary(
11300
+ `${formatSummaryElapsed(result.elapsedMs)} \xB7 ${basename(result.outputPath)}`,
11301
+ theme,
11302
+ symbols
11303
+ );
10378
11304
  }
10379
- return lines;
11305
+ const statusText = result.status === "cancelled" ? `${providerLabel} research canceled after ${formatSummaryElapsed(result.elapsedMs)}` : `${providerLabel} research failed after ${formatSummaryElapsed(result.elapsedMs)}`;
11306
+ const errorSuffix = result.error ? `: ${normalizeProviderFailureDetail(providerLabel, result.error)}` : "";
11307
+ return renderFailureSummary(`${statusText}${errorSuffix}`, theme, symbols);
10380
11308
  }
10381
11309
  function isWebResearchRequest(details) {
10382
11310
  return typeof details === "object" && details !== null && "tool" in details && details.tool === "web_research" && "startedAt" in details && "outputPath" in details && !("status" in details);
@@ -10385,48 +11313,58 @@ function isWebResearchResult(details) {
10385
11313
  return typeof details === "object" && details !== null && "tool" in details && details.tool === "web_research" && "status" in details && "completedAt" in details;
10386
11314
  }
10387
11315
  function renderProviderToolResult(result, expanded, isPartial, failureText, theme, options = {}) {
10388
- const text = extractTextContent(result.content);
10389
- if (isPartial) {
10390
- return renderSimpleText(text ?? "Working\u2026", theme, "warning");
10391
- }
10392
- if (result.isError) {
10393
- return renderBlockText(text ?? failureText, theme, "error");
10394
- }
10395
- if (expanded) {
10396
- return options.markdownWhenExpanded ? renderMarkdownBlock(text ?? "") : renderBlockText(text ?? "", theme, "toolOutput");
10397
- }
10398
- const details = result.details;
10399
- const summary = renderCollapsedProviderToolSummary(details, text);
10400
- let summaryText = theme.fg("success", summary);
10401
- summaryText += theme.fg("muted", ` (${getExpandHint()})`);
10402
- return new Text(summaryText, 0, 0);
11316
+ return renderWebToolResult(
11317
+ result,
11318
+ { expanded, isPartial },
11319
+ theme,
11320
+ {
11321
+ capability: toolFromFailureText(failureText),
11322
+ failureText,
11323
+ getDetails: (details) => details,
11324
+ getCollapsedSummary: buildCollapsedProviderToolSummary2,
11325
+ renderExpanded: (_details, text) => options.markdownWhenExpanded ? renderMarkdownBlock(text ?? "") : renderBlockText(text ?? "", theme, "toolOutput")
11326
+ },
11327
+ options.symbols
11328
+ );
10403
11329
  }
10404
11330
  function renderCollapsedProviderToolSummary(details, text) {
10405
- if (details?.tool === "web_answer" && typeof details.queryCount === "number" && details.queryCount > 1) {
10406
- const providerLabel = PROVIDERS_BY_ID[details.provider]?.label ?? details.provider;
10407
- const failureSuffix = details.failedQueryCount && details.failedQueryCount > 0 ? `, ${details.failedQueryCount} failed` : "";
10408
- return `${details.queryCount} questions via ${providerLabel}${failureSuffix}`;
10409
- }
10410
- const baseSummary = getCompactProviderToolSummary(details) ?? getFirstLine(text) ?? `${details?.tool ?? "tool"} output available`;
10411
- if (!details?.provider) {
10412
- return baseSummary;
10413
- }
10414
- return appendProviderSummary(baseSummary, details.provider);
11331
+ return buildCollapsedProviderToolSummary(details, text);
10415
11332
  }
10416
- function getCompactProviderToolSummary(details) {
10417
- if (!details) {
10418
- return void 0;
10419
- }
10420
- if (details.tool === "web_contents" && typeof details.itemCount === "number") {
10421
- return `${details.itemCount} page${details.itemCount === 1 ? "" : "s"}`;
10422
- }
10423
- if (details.tool === "web_answer") {
10424
- return "Answer";
10425
- }
10426
- if (details.tool === "web_research") {
10427
- return "Research";
10428
- }
10429
- return void 0;
11333
+ function buildCollapsedProviderToolSummary2(details, text) {
11334
+ return buildCollapsedProviderToolSummaryParts(details, text);
11335
+ }
11336
+ function renderCollapsedSummary(summary, theme, symbols = DEFAULT_SUMMARY_SYMBOLS) {
11337
+ let rendered = renderSummary(summary, theme, symbols);
11338
+ rendered += theme.fg("muted", ` (${getExpandHint()})`);
11339
+ return new Text(rendered, 0, 0);
11340
+ }
11341
+ function getDisplaySummaryParts(display) {
11342
+ return display?.outcome;
11343
+ }
11344
+ function buildSearchToolDisplay2(details) {
11345
+ return buildSearchToolDisplay(details);
11346
+ }
11347
+ function buildProgressDisplay2(providerId, action) {
11348
+ return buildProgressDisplay(providerId, action);
11349
+ }
11350
+ function buildProviderToolDisplay2({
11351
+ capability,
11352
+ providerId,
11353
+ details,
11354
+ text,
11355
+ outputBytes,
11356
+ outputTruncated,
11357
+ failedItemCount
11358
+ }) {
11359
+ return buildProviderToolDisplay({
11360
+ capability,
11361
+ providerId,
11362
+ details,
11363
+ text,
11364
+ outputBytes,
11365
+ outputTruncated,
11366
+ failedItemCount
11367
+ });
10430
11368
  }
10431
11369
  function getProviderSettings(providerId) {
10432
11370
  return getProviderConfigManifest(providerId).settings;
@@ -10486,12 +11424,19 @@ function resolveProviderSelectionValue(providerIds, value) {
10486
11424
  );
10487
11425
  }
10488
11426
  function getReadyCompatibleProvidersForTool(config, cwd, toolId) {
11427
+ const mappedProviderId = getMappedProviderIdForTool(config, toolId);
10489
11428
  return sortProviderIdsForSettings(
10490
- getCompatibleProviders(toolId).filter(
10491
- (providerId) => isProviderCapabilityReady(
10492
- getProviderCapabilityStatus(config, cwd, providerId, toolId)
10493
- )
10494
- )
11429
+ getCompatibleProviders(toolId).filter((providerId) => {
11430
+ const setupState = getProviderSetupState(config, providerId);
11431
+ if (setupState === "none" && providerId !== mappedProviderId) {
11432
+ return false;
11433
+ }
11434
+ return isProviderCapabilityExposable(
11435
+ getProviderCapabilityStatus(config, cwd, providerId, toolId, {
11436
+ resolveSecrets: false
11437
+ })
11438
+ );
11439
+ })
10495
11440
  );
10496
11441
  }
10497
11442
  function sortProviderIdsForSettings(providerIds) {
@@ -10571,6 +11516,9 @@ function ensureSettings(config) {
10571
11516
  return config.settings;
10572
11517
  }
10573
11518
  function cleanupSettings(config) {
11519
+ if (config.settings?.search && Object.keys(config.settings.search).length === 0) {
11520
+ delete config.settings.search;
11521
+ }
10574
11522
  if (config.settings && Object.keys(config.settings).length === 0) {
10575
11523
  delete config.settings;
10576
11524
  }
@@ -10718,19 +11666,21 @@ var WebProvidersSettingsView = class {
10718
11666
  id: `tool:${toolId}`,
10719
11667
  label: TOOL_INFO[toolId].label,
10720
11668
  currentValue,
10721
- description: `Press Enter to configure web_${toolId}. ${TOOL_INFO[toolId].help} Route web_${toolId} to one compatible provider or turn it off.` + (compatibleLabels.length > 0 ? ` Ready compatible providers: ${compatibleLabels.join(", ")}.` : ""),
11669
+ description: `Press Enter to configure web_${toolId}. ${TOOL_INFO[toolId].help} Route web_${toolId} to one compatible provider or turn it off.` + (compatibleLabels.length > 0 ? ` Compatible providers: ${compatibleLabels.join(", ")}.` : ""),
10722
11670
  kind: "action"
10723
11671
  };
10724
11672
  });
10725
11673
  }
10726
11674
  buildSettingsSectionItems() {
10727
- return SETTING_IDS.map((id) => ({
10728
- id: `settings:${id}`,
10729
- label: SETTING_META[id].label,
10730
- currentValue: getSharedSettingDisplayValue(this.config, id),
10731
- description: SETTING_META[id].help,
10732
- kind: "text"
10733
- }));
11675
+ return [
11676
+ ...SETTING_IDS.map((id) => ({
11677
+ id: `settings:${id}`,
11678
+ label: SETTING_META[id].label,
11679
+ currentValue: getSharedSettingDisplayValue(this.config, id),
11680
+ description: SETTING_META[id].help,
11681
+ kind: "text"
11682
+ }))
11683
+ ];
10734
11684
  }
10735
11685
  getSectionEntries(section) {
10736
11686
  if (section === "provider") return this.buildProviderSectionItems();
@@ -11040,7 +11990,7 @@ var ToolSettingsSubmenu = class {
11040
11990
  id: "provider",
11041
11991
  label: "Provider",
11042
11992
  currentValue: currentProviderValue,
11043
- description: `Route web_${this.toolId} to one compatible ready provider or turn it off.`,
11993
+ description: `Route web_${this.toolId} to one compatible provider or turn it off.`,
11044
11994
  kind: "cycle",
11045
11995
  values: providerValues
11046
11996
  }
@@ -11452,17 +12402,24 @@ function formatProviderSetupState(state) {
11452
12402
  function getProviderReadinessSummary(config, cwd, providerId) {
11453
12403
  const tools = getProviderTools(providerId);
11454
12404
  const statuses = tools.map(
11455
- (tool) => getProviderCapabilityStatus(config, cwd, providerId, tool)
12405
+ (tool) => getProviderCapabilityStatus(config, cwd, providerId, tool, {
12406
+ resolveSecrets: false
12407
+ })
11456
12408
  );
11457
12409
  if (statuses.some((status) => status.state === "ready")) {
11458
12410
  return "Ready";
11459
12411
  }
12412
+ if (statuses.some((status) => status.state === "deferred_secret")) {
12413
+ return "Secrets resolved on first use";
12414
+ }
11460
12415
  return formatProviderCapabilityStatus(statuses[0], providerId, tools[0]);
11461
12416
  }
11462
12417
  function getProviderReadinessSummaryForProviderConfig(providerId, providerConfig) {
11463
12418
  const status = PROVIDERS_BY_ID[providerId].getCapabilityStatus(
11464
12419
  providerConfig ?? PROVIDERS_BY_ID[providerId].config.createTemplate(),
11465
- ""
12420
+ "",
12421
+ void 0,
12422
+ { resolveSecrets: false }
11466
12423
  );
11467
12424
  return formatProviderCapabilityStatus(status, providerId);
11468
12425
  }
@@ -11511,7 +12468,7 @@ function getSearchQueriesForDisplay(queries) {
11511
12468
  function getAnswerQueriesForDisplay(queries) {
11512
12469
  return getSearchQueriesForDisplay(queries);
11513
12470
  }
11514
- function createBatchCompletionReporter(verb, providerLabel, total, report) {
12471
+ function createBatchCompletionReporter(verb, providerId, providerLabel, total, report) {
11515
12472
  if (!report) {
11516
12473
  return {
11517
12474
  start: () => {
@@ -11529,7 +12486,8 @@ function createBatchCompletionReporter(verb, providerLabel, total, report) {
11529
12486
  if (failedCount > 0) {
11530
12487
  message += `, ${failedCount} failed`;
11531
12488
  }
11532
- report(message);
12489
+ const action = verb === "Fetching contents" ? `Fetching ${completedCount}/${total} pages` : `${verb} ${completedCount}/${total}`;
12490
+ report(message, buildProgressDisplay2(providerId, action));
11533
12491
  };
11534
12492
  return {
11535
12493
  start: emit,
@@ -11590,34 +12548,52 @@ ${rendered}`, 0, 0);
11590
12548
  function renderSimpleText(text, theme, color) {
11591
12549
  return new Text(theme.fg(color, text), 0, 0);
11592
12550
  }
11593
- function renderCollapsedSearchSummary(details, text, theme) {
12551
+ function renderSummary(summary, theme, symbols) {
12552
+ let rendered = renderSuccessSummary(summary.success, theme, symbols);
12553
+ if (summary.failure) {
12554
+ rendered += `, ${renderFailureSummary(summary.failure, theme, symbols)}`;
12555
+ }
12556
+ return rendered;
12557
+ }
12558
+ function renderSuccessSummary(text, theme, symbols) {
12559
+ return theme.fg("success", prefixWithSymbol(text, symbols.success));
12560
+ }
12561
+ function renderFailureSummary(text, theme, symbols) {
12562
+ return theme.fg("error", prefixWithSymbol(text, symbols.failure));
12563
+ }
12564
+ function renderFailureText(text, theme, symbols) {
12565
+ return new Text(renderFailureSummary(text, theme, symbols), 0, 0);
12566
+ }
12567
+ function prefixWithSymbol(text, symbol) {
12568
+ return symbol ? `${symbol} ${text}` : text;
12569
+ }
12570
+ function renderToolProgress(display, fallbackText, theme) {
12571
+ const progress = display?.progress;
12572
+ const providerLabel = display?.provider?.label;
12573
+ if (!progress || !providerLabel) {
12574
+ return renderSimpleText(fallbackText ?? "Working\u2026", theme, "warning");
12575
+ }
12576
+ return new Text(
12577
+ `${theme.fg("warning", progress.action)} ${theme.fg("muted", `via ${providerLabel}`)}`,
12578
+ 0,
12579
+ 0
12580
+ );
12581
+ }
12582
+ function renderCollapsedSearchSummary(details, text, theme, symbols = DEFAULT_SUMMARY_SYMBOLS) {
11594
12583
  const queryCount = typeof details?.queryCount === "number" ? details.queryCount : inferSearchQueryCount(text);
11595
12584
  const resultCount = typeof details?.resultCount === "number" ? details.resultCount : inferSearchResultCount(text);
11596
12585
  const failedQueryCount = typeof details?.failedQueryCount === "number" ? details.failedQueryCount : inferSearchFailureCount(text);
11597
- const providerLabel = typeof details?.provider === "string" ? PROVIDERS_BY_ID[details.provider]?.label ?? details.provider : void 0;
11598
- let base2 = buildSearchSummaryText({
12586
+ const summary = buildSearchSummaryParts2({
11599
12587
  queryCount,
11600
- resultCount
12588
+ resultCount,
12589
+ failedQueryCount
11601
12590
  });
11602
- if (providerLabel) {
11603
- base2 = `${base2} via ${providerLabel}`;
11604
- }
11605
- if (failedQueryCount && failedQueryCount > 0) {
11606
- base2 += `, ${failedQueryCount} failed`;
11607
- }
11608
- let summary = theme.fg("success", base2);
11609
- summary += theme.fg("muted", ` (${getExpandHint()})`);
11610
- return new Text(summary, 0, 0);
12591
+ let rendered = renderSummary(summary, theme, symbols);
12592
+ rendered += theme.fg("muted", ` (${getExpandHint()})`);
12593
+ return new Text(rendered, 0, 0);
11611
12594
  }
11612
- function buildSearchSummaryText({
11613
- queryCount,
11614
- resultCount
11615
- }) {
11616
- const countSummary = typeof resultCount === "number" ? `${resultCount} result${resultCount === 1 ? "" : "s"}` : "Search output available";
11617
- if (queryCount && queryCount > 1) {
11618
- return `${queryCount} queries, ${countSummary}`;
11619
- }
11620
- return countSummary;
12595
+ function buildSearchSummaryParts2(options) {
12596
+ return buildSearchSummaryParts(options);
11621
12597
  }
11622
12598
  function inferSearchQueryCount(text) {
11623
12599
  if (!text) {
@@ -11643,12 +12619,77 @@ function inferSearchFailureCount(text) {
11643
12619
  const failureMatches = text.match(/^Search failed:/gm);
11644
12620
  return failureMatches?.length;
11645
12621
  }
11646
- function appendProviderSummary(summary, provider) {
11647
- const providerLabel = PROVIDERS_BY_ID[provider]?.label ?? provider;
11648
- const providerSuffix = `via ${providerLabel}`;
11649
- return summary.toLowerCase().includes(providerSuffix.toLowerCase()) ? summary : `${summary} ${providerSuffix}`;
12622
+ function buildFailureSummary({
12623
+ text,
12624
+ details,
12625
+ capability,
12626
+ fallback
12627
+ }) {
12628
+ const detail = stripTrailingSentencePunctuation(getFirstLine2(text) ?? "");
12629
+ const providerLabel = details?.provider !== void 0 ? PROVIDERS_BY_ID[details.provider]?.label ?? details.provider : void 0;
12630
+ if (!providerLabel) {
12631
+ return detail || fallback;
12632
+ }
12633
+ return formatProviderCapabilityFailure(providerLabel, capability, detail);
12634
+ }
12635
+ function formatProviderCapabilityFailure(providerLabel, capability, detail) {
12636
+ const action = getFailureAction(capability);
12637
+ const base2 = `${providerLabel} ${action} failed`;
12638
+ if (!detail || detail === base2) {
12639
+ return base2;
12640
+ }
12641
+ if (detail.toLowerCase().startsWith(base2.toLowerCase())) {
12642
+ return detail;
12643
+ }
12644
+ const normalizedDetail = normalizeProviderFailureDetail(
12645
+ providerLabel,
12646
+ detail
12647
+ );
12648
+ return `${base2}: ${normalizedDetail}`;
12649
+ }
12650
+ function normalizeProviderFailureDetail(providerLabel, detail) {
12651
+ const normalized = stripTrailingSentencePunctuation(detail);
12652
+ const providerPrefix = `${providerLabel}:`;
12653
+ return normalized.toLowerCase().startsWith(providerPrefix.toLowerCase()) ? normalized.slice(providerPrefix.length).trim() : normalized;
11650
12654
  }
11651
- function getFirstLine(text) {
12655
+ function getFailureAction(capability) {
12656
+ switch (capability) {
12657
+ case "contents":
12658
+ return "fetch";
12659
+ case "search":
12660
+ case "answer":
12661
+ case "research":
12662
+ return capability;
12663
+ }
12664
+ }
12665
+ function toolFromFailureText(text) {
12666
+ if (text.startsWith("web_contents")) {
12667
+ return "contents";
12668
+ }
12669
+ if (text.startsWith("web_answer")) {
12670
+ return "answer";
12671
+ }
12672
+ if (text.startsWith("web_research")) {
12673
+ return "research";
12674
+ }
12675
+ return "search";
12676
+ }
12677
+ function stripTrailingSentencePunctuation(text) {
12678
+ return text.trim().replace(/[.\s]+$/u, "");
12679
+ }
12680
+ function formatSummaryElapsed(ms) {
12681
+ const totalSeconds = Math.max(0, Math.floor(ms / 1e3));
12682
+ const minutes = Math.floor(totalSeconds / 60);
12683
+ const seconds = totalSeconds % 60;
12684
+ if (minutes > 0 && seconds === 0) {
12685
+ return `${minutes}m`;
12686
+ }
12687
+ if (minutes > 0) {
12688
+ return `${minutes}m ${seconds}s`;
12689
+ }
12690
+ return `${seconds}s`;
12691
+ }
12692
+ function getFirstLine2(text) {
11652
12693
  if (!text) {
11653
12694
  return void 0;
11654
12695
  }
@@ -11729,18 +12770,32 @@ function escapeMarkdownText(text) {
11729
12770
  return text.replaceAll("\\", "\\\\").replaceAll("*", "\\*").replaceAll("_", "\\_").replaceAll("`", "\\`").replaceAll("#", "\\#").replaceAll("[", "\\[").replaceAll("]", "\\]");
11730
12771
  }
11731
12772
  async function truncateAndSave(text, prefix) {
12773
+ return (await truncateAndSaveWithMetadata(text, prefix)).text;
12774
+ }
12775
+ async function truncateAndSaveWithMetadata(text, prefix) {
12776
+ const totalBytes = Buffer.byteLength(text, "utf-8");
11732
12777
  const truncation = truncateHead(text, {
11733
12778
  maxLines: DEFAULT_MAX_LINES,
11734
12779
  maxBytes: DEFAULT_MAX_BYTES
11735
12780
  });
11736
- if (!truncation.truncated) return truncation.content;
12781
+ if (!truncation.truncated) {
12782
+ return {
12783
+ text: truncation.content,
12784
+ totalBytes,
12785
+ truncated: false
12786
+ };
12787
+ }
11737
12788
  const dir = join2(tmpdir(), `pi-web-providers-${prefix}-${Date.now()}`);
11738
12789
  await mkdir2(dir, { recursive: true });
11739
12790
  const fullPath = join2(dir, "output.txt");
11740
12791
  await writeFile2(fullPath, text, "utf-8");
11741
- return truncation.content + `
12792
+ return {
12793
+ text: truncation.content + `
11742
12794
 
11743
- [Output truncated: ${truncation.outputLines} of ${truncation.totalLines} lines (${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)}). Full output saved to: ${fullPath}]`;
12795
+ [Output truncated: ${truncation.outputLines} of ${truncation.totalLines} lines (${formatSize2(truncation.outputBytes)} of ${formatSize2(truncation.totalBytes)}). Full output saved to: ${fullPath}]`,
12796
+ totalBytes,
12797
+ truncated: true
12798
+ };
11744
12799
  }
11745
12800
  function truncateInline(text, maxLength) {
11746
12801
  if (text.length <= maxLength) return text;