pi-web-providers 3.3.0 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +16 -1
  2. package/dist/index.js +1439 -546
  3. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -1,25 +1,25 @@
1
1
  // src/index.ts
2
- import { randomUUID } from "node:crypto";
3
- import { mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
2
+ import { mkdir as mkdir3, writeFile as writeFile3 } from "node:fs/promises";
4
3
  import { tmpdir } from "node:os";
5
- import { basename, dirname as dirname2, join as join2, relative } from "node:path";
4
+ import { basename, join as join3 } from "node:path";
6
5
  import {
6
+ copyToClipboard,
7
7
  DEFAULT_MAX_BYTES,
8
8
  DEFAULT_MAX_LINES,
9
9
  formatSize as formatSize2,
10
- getMarkdownTheme,
10
+ getMarkdownTheme as getMarkdownTheme2,
11
11
  truncateHead
12
12
  } from "@earendil-works/pi-coding-agent";
13
13
  import {
14
14
  Box,
15
15
  Editor,
16
- getKeybindings,
17
- Key,
18
- Markdown,
19
- matchesKey,
16
+ getKeybindings as getKeybindings2,
17
+ Key as Key2,
18
+ Markdown as Markdown2,
19
+ matchesKey as matchesKey2,
20
20
  Text,
21
- truncateToWidth,
22
- visibleWidth,
21
+ truncateToWidth as truncateToWidth2,
22
+ visibleWidth as visibleWidth2,
23
23
  wrapTextWithAnsi
24
24
  } from "@earendil-works/pi-tui";
25
25
  import { Type as Type16 } from "typebox";
@@ -2454,39 +2454,39 @@ function createDefaultExecutionSettings(overrides = {}) {
2454
2454
  function normalizeDiagnosticDetail(detail) {
2455
2455
  return detail.trim().replace(/[.\s]+$/u, "");
2456
2456
  }
2457
- function startsWithProviderLabel(providerLabel, detail) {
2458
- return detail.toLowerCase().startsWith(providerLabel.toLowerCase());
2457
+ function startsWithProviderLabel(providerLabel2, detail) {
2458
+ return detail.toLowerCase().startsWith(providerLabel2.toLowerCase());
2459
2459
  }
2460
2460
  function readsLikeProviderClause(detail) {
2461
2461
  return /^(is|has|was|returned|did|does|could|cannot|must|should|search\b|contents\b|answer\b|research\b|output\b|response\b|result\b|query\b|no\b|missing\b|deep research\b)/iu.test(
2462
2462
  detail
2463
2463
  );
2464
2464
  }
2465
- function formatProviderDiagnostic(providerLabel, detail) {
2465
+ function formatProviderDiagnostic(providerLabel2, detail) {
2466
2466
  const normalized = normalizeDiagnosticDetail(detail);
2467
2467
  if (!normalized) {
2468
- return `${providerLabel} failed.`;
2468
+ return `${providerLabel2} failed.`;
2469
2469
  }
2470
- if (startsWithProviderLabel(providerLabel, normalized)) {
2470
+ if (startsWithProviderLabel(providerLabel2, normalized)) {
2471
2471
  return `${normalized}.`;
2472
2472
  }
2473
2473
  if (readsLikeProviderClause(normalized)) {
2474
- return `${providerLabel} ${normalized}.`;
2474
+ return `${providerLabel2} ${normalized}.`;
2475
2475
  }
2476
- return `${providerLabel}: ${normalized}.`;
2476
+ return `${providerLabel2}: ${normalized}.`;
2477
2477
  }
2478
- function formatResearchTerminalDiagnostic(providerLabel, status, detail) {
2478
+ function formatResearchTerminalDiagnostic(providerLabel2, status, detail) {
2479
2479
  const normalized = detail ? normalizeDiagnosticDetail(detail) : "";
2480
2480
  if (!normalized) {
2481
- return status === "cancelled" ? `${providerLabel} research was canceled.` : `${providerLabel} research failed.`;
2481
+ return status === "cancelled" ? `${providerLabel2} research was canceled.` : `${providerLabel2} research failed.`;
2482
2482
  }
2483
- if (startsWithProviderLabel(providerLabel, normalized)) {
2483
+ if (startsWithProviderLabel(providerLabel2, normalized)) {
2484
2484
  return `${normalized}.`;
2485
2485
  }
2486
2486
  if (/^research\b/iu.test(normalized)) {
2487
- return `${providerLabel} ${normalized}.`;
2487
+ return `${providerLabel2} ${normalized}.`;
2488
2488
  }
2489
- return status === "cancelled" ? `${providerLabel} research was canceled: ${normalized}.` : `${providerLabel} research failed: ${normalized}.`;
2489
+ return status === "cancelled" ? `${providerLabel2} research was canceled: ${normalized}.` : `${providerLabel2} research failed: ${normalized}.`;
2490
2490
  }
2491
2491
 
2492
2492
  // src/execution-policy.ts
@@ -2532,7 +2532,7 @@ async function runWithExecutionPolicy(label, operation, settings, context) {
2532
2532
  throw new Error(`${label} failed.`);
2533
2533
  }
2534
2534
  async function executeAsyncResearch({
2535
- providerLabel,
2535
+ providerLabel: providerLabel2,
2536
2536
  providerId,
2537
2537
  context,
2538
2538
  pollIntervalMs = DEFAULT_RESEARCH_POLL_INTERVAL_MS,
@@ -2541,7 +2541,7 @@ async function executeAsyncResearch({
2541
2541
  start,
2542
2542
  poll
2543
2543
  }) {
2544
- const timeoutMessage = `${providerLabel} research exceeded ${formatDuration(timeoutMs)}.`;
2544
+ const timeoutMessage = `${providerLabel2} research exceeded ${formatDuration(timeoutMs)}.`;
2545
2545
  const deadline = createDeadlineSignal(
2546
2546
  context.signal,
2547
2547
  timeoutMs,
@@ -2555,7 +2555,7 @@ async function executeAsyncResearch({
2555
2555
  let lastProgressStatus;
2556
2556
  const startedAt = Date.now();
2557
2557
  try {
2558
- researchContext.onProgress?.(`Starting research via ${providerLabel}`);
2558
+ researchContext.onProgress?.(`Starting research via ${providerLabel2}`);
2559
2559
  const job = await withAbortAndOptionalTimeout(
2560
2560
  start(researchContext),
2561
2561
  void 0,
@@ -2564,14 +2564,14 @@ async function executeAsyncResearch({
2564
2564
  );
2565
2565
  const jobId = job.id;
2566
2566
  if (!jobId) {
2567
- throw new Error(`${providerLabel} research did not return a job id.`);
2567
+ throw new Error(`${providerLabel2} research did not return a job id.`);
2568
2568
  }
2569
- researchContext.onProgress?.(`${providerLabel} research started: ${jobId}`);
2569
+ researchContext.onProgress?.(`${providerLabel2} research started: ${jobId}`);
2570
2570
  let consecutivePollErrors = 0;
2571
2571
  while (true) {
2572
2572
  throwIfAborted(
2573
2573
  researchContext.signal,
2574
- `${providerLabel} research aborted.`
2574
+ `${providerLabel2} research aborted.`
2575
2575
  );
2576
2576
  try {
2577
2577
  const result = await withAbortAndOptionalTimeout(
@@ -2584,7 +2584,7 @@ async function executeAsyncResearch({
2584
2584
  const progressStatus = result.statusText ?? result.status;
2585
2585
  if (result.status !== lastStatus || progressStatus !== lastProgressStatus) {
2586
2586
  researchContext.onProgress?.(
2587
- `Research via ${providerLabel}: ${progressStatus} (${formatElapsed(Date.now() - startedAt)} elapsed)`
2587
+ `Research via ${providerLabel2}: ${progressStatus} (${formatElapsed(Date.now() - startedAt)} elapsed)`
2588
2588
  );
2589
2589
  lastStatus = result.status;
2590
2590
  lastProgressStatus = progressStatus;
@@ -2592,13 +2592,13 @@ async function executeAsyncResearch({
2592
2592
  if (result.status === "completed") {
2593
2593
  return result.output ?? {
2594
2594
  provider: providerId,
2595
- text: `${providerLabel} research completed without textual output.`
2595
+ text: `${providerLabel2} research completed without textual output.`
2596
2596
  };
2597
2597
  }
2598
2598
  if (result.status === "failed" || result.status === "cancelled") {
2599
2599
  throw new Error(
2600
2600
  formatResearchTerminalDiagnostic(
2601
- providerLabel,
2601
+ providerLabel2,
2602
2602
  result.status,
2603
2603
  result.error
2604
2604
  )
@@ -2614,11 +2614,11 @@ async function executeAsyncResearch({
2614
2614
  consecutivePollErrors += 1;
2615
2615
  if (consecutivePollErrors >= maxConsecutivePollErrors) {
2616
2616
  throw new Error(
2617
- `${providerLabel} research polling failed too many times in a row: ${formatErrorMessage(error)}`
2617
+ `${providerLabel2} research polling failed too many times in a row: ${formatErrorMessage(error)}`
2618
2618
  );
2619
2619
  }
2620
2620
  researchContext.onProgress?.(
2621
- `${providerLabel} research poll is still retrying after transient errors (${consecutivePollErrors}/${maxConsecutivePollErrors} consecutive poll failures). Background job id: ${jobId}`
2621
+ `${providerLabel2} research poll is still retrying after transient errors (${consecutivePollErrors}/${maxConsecutivePollErrors} consecutive poll failures). Background job id: ${jobId}`
2622
2622
  );
2623
2623
  }
2624
2624
  await sleep(pollIntervalMs, researchContext.signal);
@@ -2626,11 +2626,11 @@ async function executeAsyncResearch({
2626
2626
  } catch (error) {
2627
2627
  if (isAbortErrorFromSignal(researchContext.signal, error)) {
2628
2628
  throw new Error(
2629
- formatProviderDiagnostic(providerLabel, formatErrorMessage(error))
2629
+ formatProviderDiagnostic(providerLabel2, formatErrorMessage(error))
2630
2630
  );
2631
2631
  }
2632
2632
  throw new Error(
2633
- formatProviderDiagnostic(providerLabel, formatErrorMessage(error))
2633
+ formatProviderDiagnostic(providerLabel2, formatErrorMessage(error))
2634
2634
  );
2635
2635
  } finally {
2636
2636
  deadline.cleanup();
@@ -3713,7 +3713,7 @@ var geminiImplementation = {
3713
3713
  context.signal
3714
3714
  );
3715
3715
  const results = await Promise.all(
3716
- extractGoogleSearchResults(interaction.outputs).slice(0, maxResults).map(async (result) => {
3716
+ extractGoogleSearchResults(readInteractionSteps(interaction)).slice(0, maxResults).map(async (result) => {
3717
3717
  const resolvedUrl = await resolveGoogleSearchUrl(
3718
3718
  result.url,
3719
3719
  context.signal
@@ -3800,7 +3800,7 @@ var geminiImplementation = {
3800
3800
  );
3801
3801
  const status = readNonEmptyString3(interaction.status) ?? "unknown";
3802
3802
  if (status === "completed") {
3803
- const text = formatInteractionOutputs(interaction.outputs);
3803
+ const text = formatInteractionSteps(readInteractionSteps(interaction));
3804
3804
  return {
3805
3805
  status: "completed",
3806
3806
  output: {
@@ -3830,7 +3830,7 @@ var geminiImplementation = {
3830
3830
  if (status === "requires_action") {
3831
3831
  return {
3832
3832
  status: "failed",
3833
- error: describeGeminiRequiredAction(interaction.outputs)
3833
+ error: describeGeminiRequiredAction(readInteractionSteps(interaction))
3834
3834
  };
3835
3835
  }
3836
3836
  return status === "in_progress" ? { status: "in_progress" } : { status: "in_progress", statusText: status };
@@ -3864,17 +3864,20 @@ function addAbortSignalToGeminiConfig(config, signal) {
3864
3864
  abortSignal: signal
3865
3865
  };
3866
3866
  }
3867
- function extractGoogleSearchResults(outputs) {
3867
+ function readInteractionSteps(interaction) {
3868
+ return typeof interaction === "object" && interaction !== null ? interaction.steps : void 0;
3869
+ }
3870
+ function extractGoogleSearchResults(steps) {
3868
3871
  const seen = /* @__PURE__ */ new Set();
3869
3872
  const results = [];
3870
- if (!Array.isArray(outputs)) {
3873
+ if (!Array.isArray(steps)) {
3871
3874
  return results;
3872
3875
  }
3873
- for (const output of outputs) {
3874
- if (typeof output !== "object" || output === null) {
3876
+ for (const step of steps) {
3877
+ if (typeof step !== "object" || step === null) {
3875
3878
  continue;
3876
3879
  }
3877
- const content = output;
3880
+ const content = step;
3878
3881
  if (content.type !== "google_search_result") {
3879
3882
  continue;
3880
3883
  }
@@ -4040,16 +4043,21 @@ function extractGroundingSources(chunks) {
4040
4043
  }
4041
4044
  return sources;
4042
4045
  }
4043
- function formatInteractionOutputs(outputs) {
4046
+ function formatInteractionSteps(steps) {
4044
4047
  const lines = [];
4045
- if (!Array.isArray(outputs)) {
4048
+ if (!Array.isArray(steps)) {
4046
4049
  return "";
4047
4050
  }
4048
- for (const output of outputs) {
4049
- if (typeof output === "object" && output !== null && "type" in output && output.type === "text" && "text" in output && typeof output.text === "string") {
4050
- const text = output.text.trim();
4051
- if (text) {
4052
- lines.push(text);
4051
+ for (const step of steps) {
4052
+ if (typeof step !== "object" || step === null || !("type" in step) || step.type !== "model_output" || !("content" in step) || !Array.isArray(step.content)) {
4053
+ continue;
4054
+ }
4055
+ for (const part of step.content) {
4056
+ if (typeof part === "object" && part !== null && "type" in part && part.type === "text" && "text" in part && typeof part.text === "string") {
4057
+ const text = part.text.trim();
4058
+ if (text) {
4059
+ lines.push(text);
4060
+ }
4053
4061
  }
4054
4062
  }
4055
4063
  }
@@ -4244,14 +4252,12 @@ function buildGeminiGenerateContentRequest({
4244
4252
  }
4245
4253
  };
4246
4254
  }
4247
- function describeGeminiRequiredAction(outputs) {
4248
- if (!Array.isArray(outputs) || outputs.length === 0) {
4255
+ function describeGeminiRequiredAction(steps) {
4256
+ if (!Array.isArray(steps) || steps.length === 0) {
4249
4257
  return "research requires additional action";
4250
4258
  }
4251
- const firstOutput = outputs.find(
4252
- (value) => typeof value === "object" && value !== null
4253
- );
4254
- const type = readNonEmptyString3(firstOutput?.type);
4259
+ const lastStep = [...steps].reverse().find((value) => typeof value === "object" && value !== null);
4260
+ const type = readNonEmptyString3(lastStep?.type);
4255
4261
  if (!type) {
4256
4262
  return "research requires additional action";
4257
4263
  }
@@ -7849,45 +7855,6 @@ ${JSON.stringify(value, null, 2).trim()}
7849
7855
  \`\`\``;
7850
7856
  }
7851
7857
 
7852
- // src/options.ts
7853
- function buildToolOptionsSchema(_capability, providerSchema) {
7854
- if (!providerSchema || Object.keys(providerSchema.properties).length === 0) {
7855
- return void 0;
7856
- }
7857
- return closeObjectSchemas(providerSchema);
7858
- }
7859
- function closeObjectSchemas(schema) {
7860
- if (!isSchemaRecord(schema)) {
7861
- return schema;
7862
- }
7863
- const properties = isSchemaRecord(schema.properties) ? Object.fromEntries(
7864
- Object.entries(schema.properties).map(([key, value]) => [
7865
- key,
7866
- closeObjectSchemas(value)
7867
- ])
7868
- ) : schema.properties;
7869
- const items = isSchemaRecord(schema.items) ? closeObjectSchemas(schema.items) : Array.isArray(schema.items) ? schema.items.map((item) => closeObjectSchemas(item)) : schema.items;
7870
- return {
7871
- ...schema,
7872
- ...properties ? { properties } : {},
7873
- ...items ? { items } : {},
7874
- ...mapSchemaArray(schema, "anyOf"),
7875
- ...mapSchemaArray(schema, "oneOf"),
7876
- ...mapSchemaArray(schema, "allOf"),
7877
- ...schema.type === "object" && isSchemaRecord(schema.properties) ? { additionalProperties: false } : {}
7878
- };
7879
- }
7880
- function mapSchemaArray(schema, key) {
7881
- const value = schema[key];
7882
- return Array.isArray(value) ? { [key]: value.map((entry) => closeObjectSchemas(entry)) } : {};
7883
- }
7884
- function isSchemaRecord(value) {
7885
- return typeof value === "object" && value !== null && !Array.isArray(value);
7886
- }
7887
-
7888
- // src/prefetch-manager.ts
7889
- import { createHash } from "node:crypto";
7890
-
7891
7858
  // src/provider-resolution.ts
7892
7859
  function supportsTool2(provider, tool) {
7893
7860
  return provider.capabilities[tool] !== void 0;
@@ -8029,6 +7996,140 @@ function resolveProviderForTool(config, cwd, tool, explicit) {
8029
7996
  return provider;
8030
7997
  }
8031
7998
 
7999
+ // src/managed-tools.ts
8000
+ var CAPABILITY_TOOL_NAMES = {
8001
+ search: "web_search",
8002
+ contents: "web_contents",
8003
+ answer: "web_answer",
8004
+ research: "web_research"
8005
+ };
8006
+ var MANAGED_TOOL_NAMES = Object.values(CAPABILITY_TOOL_NAMES);
8007
+ function getAvailableProviderIdsForCapability(config, cwd, capability) {
8008
+ const providerId = getMappedProviderIdForTool(config, capability);
8009
+ if (!providerId) {
8010
+ return [];
8011
+ }
8012
+ const provider = PROVIDERS_BY_ID[providerId];
8013
+ if (!supportsTool2(provider, capability)) {
8014
+ return [];
8015
+ }
8016
+ const status = getProviderCapabilityStatus(
8017
+ config,
8018
+ cwd,
8019
+ providerId,
8020
+ capability,
8021
+ {
8022
+ resolveSecrets: false
8023
+ }
8024
+ );
8025
+ return isProviderCapabilityExposable(status) ? [providerId] : [];
8026
+ }
8027
+ function getProviderStatusForTool(config, cwd, providerId, capability) {
8028
+ return getProviderCapabilityStatus(config, cwd, providerId, capability);
8029
+ }
8030
+ function getAvailableManagedToolNames(config, cwd) {
8031
+ return Object.keys(CAPABILITY_TOOL_NAMES).filter(
8032
+ (capability) => getAvailableProviderIdsForCapability(config, cwd, capability).length > 0
8033
+ ).map((capability) => CAPABILITY_TOOL_NAMES[capability]);
8034
+ }
8035
+ function getSyncedActiveTools(config, cwd, activeToolNames, options) {
8036
+ const availableToolNames = new Set(getAvailableManagedToolNames(config, cwd));
8037
+ const nextActiveTools = new Set(activeToolNames);
8038
+ for (const toolName of MANAGED_TOOL_NAMES) {
8039
+ if (availableToolNames.has(toolName)) {
8040
+ if (options.addAvailable) {
8041
+ nextActiveTools.add(toolName);
8042
+ }
8043
+ continue;
8044
+ }
8045
+ nextActiveTools.delete(toolName);
8046
+ }
8047
+ return nextActiveTools;
8048
+ }
8049
+ async function refreshManagedTools(pi, registerManagedTools2, cwd, options) {
8050
+ const config = await loadConfig();
8051
+ const nextActiveTools = getSyncedActiveTools(
8052
+ config,
8053
+ cwd,
8054
+ pi.getActiveTools(),
8055
+ options
8056
+ );
8057
+ registerManagedTools2({
8058
+ search: getAvailableProviderIdsForCapability(config, cwd, "search"),
8059
+ contents: getAvailableProviderIdsForCapability(config, cwd, "contents"),
8060
+ answer: getAvailableProviderIdsForCapability(config, cwd, "answer"),
8061
+ research: getAvailableProviderIdsForCapability(config, cwd, "research")
8062
+ });
8063
+ await syncManagedToolAvailability(pi, nextActiveTools);
8064
+ }
8065
+ async function refreshManagedToolsOnStartup(pi, registerManagedTools2, cwd, options) {
8066
+ try {
8067
+ await refreshManagedTools(pi, registerManagedTools2, cwd, options);
8068
+ } catch (error) {
8069
+ pi.sendMessage({
8070
+ customType: "web-providers-config-error",
8071
+ content: formatStartupConfigError(error),
8072
+ display: true
8073
+ });
8074
+ await syncManagedToolAvailability(
8075
+ pi,
8076
+ new Set(
8077
+ pi.getActiveTools().filter((toolName) => !MANAGED_TOOL_NAMES.includes(toolName))
8078
+ )
8079
+ );
8080
+ }
8081
+ }
8082
+ function formatStartupConfigError(error) {
8083
+ const detail = error instanceof Error ? error.message : String(error);
8084
+ return `web-providers config error: ${detail.replace(getConfigPath(), "~/.pi/agent/web-providers.json")}`;
8085
+ }
8086
+ async function syncManagedToolAvailability(pi, nextActiveTools) {
8087
+ const activeTools = pi.getActiveTools();
8088
+ const changed = activeTools.length !== nextActiveTools.size || activeTools.some((toolName) => !nextActiveTools.has(toolName));
8089
+ if (changed) {
8090
+ pi.setActiveTools(Array.from(nextActiveTools));
8091
+ }
8092
+ }
8093
+
8094
+ // src/options.ts
8095
+ function buildToolOptionsSchema(_capability, providerSchema) {
8096
+ if (!providerSchema || Object.keys(providerSchema.properties).length === 0) {
8097
+ return void 0;
8098
+ }
8099
+ return closeObjectSchemas(providerSchema);
8100
+ }
8101
+ function closeObjectSchemas(schema) {
8102
+ if (!isSchemaRecord(schema)) {
8103
+ return schema;
8104
+ }
8105
+ const properties = isSchemaRecord(schema.properties) ? Object.fromEntries(
8106
+ Object.entries(schema.properties).map(([key, value]) => [
8107
+ key,
8108
+ closeObjectSchemas(value)
8109
+ ])
8110
+ ) : schema.properties;
8111
+ const items = isSchemaRecord(schema.items) ? closeObjectSchemas(schema.items) : Array.isArray(schema.items) ? schema.items.map((item) => closeObjectSchemas(item)) : schema.items;
8112
+ return {
8113
+ ...schema,
8114
+ ...properties ? { properties } : {},
8115
+ ...items ? { items } : {},
8116
+ ...mapSchemaArray(schema, "anyOf"),
8117
+ ...mapSchemaArray(schema, "oneOf"),
8118
+ ...mapSchemaArray(schema, "allOf"),
8119
+ ...schema.type === "object" && isSchemaRecord(schema.properties) ? { additionalProperties: false } : {}
8120
+ };
8121
+ }
8122
+ function mapSchemaArray(schema, key) {
8123
+ const value = schema[key];
8124
+ return Array.isArray(value) ? { [key]: value.map((entry) => closeObjectSchemas(entry)) } : {};
8125
+ }
8126
+ function isSchemaRecord(value) {
8127
+ return typeof value === "object" && value !== null && !Array.isArray(value);
8128
+ }
8129
+
8130
+ // src/prefetch-manager.ts
8131
+ import { createHash } from "node:crypto";
8132
+
8032
8133
  // src/provider-runtime.ts
8033
8134
  async function executeProviderRequest(provider, config, request, context) {
8034
8135
  return await executeProviderExecution(
@@ -8122,7 +8223,7 @@ function resolveExecutionPolicy(defaults) {
8122
8223
  retryDelayMs: defaults?.retryDelayMs ?? 2e3
8123
8224
  };
8124
8225
  }
8125
- function createResearchDeadlineSignal(signal, providerLabel, timeoutMs) {
8226
+ function createResearchDeadlineSignal(signal, providerLabel2, timeoutMs) {
8126
8227
  if (timeoutMs === void 0) {
8127
8228
  return void 0;
8128
8229
  }
@@ -8137,7 +8238,7 @@ function createResearchDeadlineSignal(signal, providerLabel, timeoutMs) {
8137
8238
  const timer = setTimeout(() => {
8138
8239
  controller.abort(
8139
8240
  new Error(
8140
- `${providerLabel} research exceeded ${formatDuration(timeoutMs)}.`
8241
+ `${providerLabel2} research exceeded ${formatDuration(timeoutMs)}.`
8141
8242
  )
8142
8243
  );
8143
8244
  }, timeoutMs);
@@ -9792,51 +9893,1001 @@ function getFirstLine(text) {
9792
9893
  return text?.split("\n").map((line) => line.trim()).find((line) => line.length > 0);
9793
9894
  }
9794
9895
 
9795
- // src/index.ts
9796
- var DEFAULT_MAX_RESULTS = 5;
9797
- var MAX_ALLOWED_RESULTS = 20;
9798
- var MAX_SEARCH_QUERIES = 10;
9799
- var RESEARCH_HEARTBEAT_MS = 15e3;
9800
- var WEB_RESEARCH_RESULT_MESSAGE_TYPE = "web-research-result";
9801
- var WEB_RESEARCH_WIDGET_KEY = "web-research-jobs";
9896
+ // src/web-research-lifecycle.ts
9897
+ import { randomUUID } from "node:crypto";
9898
+ import { mkdir as mkdir2, readdir, readFile as readFile2, stat, writeFile as writeFile2 } from "node:fs/promises";
9899
+ import { dirname as dirname2, join as join2 } from "node:path";
9802
9900
  var RESEARCH_ARTIFACTS_DIR = join2(".pi", "artifacts", "research");
9901
+ var MAX_RESEARCH_HISTORY_ITEMS = 20;
9902
+ var RESEARCH_PREVIEW_MAX_BYTES = 5e4;
9903
+ var RESEARCH_REPORT_MAX_BYTES = 2e5;
9803
9904
  var pendingResearchTasks = /* @__PURE__ */ new Set();
9804
- var CAPABILITY_TOOL_NAMES = {
9805
- search: "web_search",
9806
- contents: "web_contents",
9807
- answer: "web_answer",
9808
- research: "web_research"
9809
- };
9810
- var MANAGED_TOOL_NAMES = Object.values(CAPABILITY_TOOL_NAMES);
9811
- var DEFAULT_SUMMARY_SYMBOLS = {
9812
- success: "\u2714",
9813
- failure: "\u2718"
9814
- };
9815
- function webProvidersExtension(pi) {
9816
- const activeWebResearchRequests = /* @__PURE__ */ new Map();
9817
- let latestWidgetContext;
9818
- let webResearchWidgetTimer;
9819
- const stopWebResearchWidgetTimer = () => {
9820
- if (webResearchWidgetTimer) {
9821
- clearInterval(webResearchWidgetTimer);
9822
- webResearchWidgetTimer = void 0;
9823
- }
9824
- };
9825
- const ensureWebResearchWidgetTimer = () => {
9826
- if (webResearchWidgetTimer || activeWebResearchRequests.size === 0) {
9827
- return;
9905
+ async function dispatchWebResearch({
9906
+ activeWebResearchRequests,
9907
+ config,
9908
+ explicitProvider,
9909
+ ctx,
9910
+ options,
9911
+ input,
9912
+ executionOverride,
9913
+ executeResearch,
9914
+ deliverResult,
9915
+ onJobsChanged,
9916
+ resultMessageType
9917
+ }) {
9918
+ await cleanupContentStore();
9919
+ const provider = resolveProviderForTool(
9920
+ config,
9921
+ ctx.cwd,
9922
+ "research",
9923
+ explicitProvider
9924
+ );
9925
+ const request = createWebResearchRequest(ctx.cwd, provider.id, input);
9926
+ const abortController = new AbortController();
9927
+ const task = { request, abortController };
9928
+ const providerConfig = getEffectiveProviderConfig(config, provider.id);
9929
+ activeWebResearchRequests.set(request.id, task);
9930
+ onJobsChanged();
9931
+ trackPendingResearchTask(
9932
+ runDispatchedWebResearch({
9933
+ activeWebResearchRequests,
9934
+ task,
9935
+ config,
9936
+ provider,
9937
+ providerConfig,
9938
+ ctx,
9939
+ options,
9940
+ executionOverride,
9941
+ executeResearch,
9942
+ deliverResult,
9943
+ onJobsChanged,
9944
+ resultMessageType
9945
+ })
9946
+ );
9947
+ return {
9948
+ content: [
9949
+ {
9950
+ type: "text",
9951
+ text: `Started web research via ${provider.label}.`
9952
+ }
9953
+ ],
9954
+ details: request,
9955
+ display: {
9956
+ provider: { id: provider.id, label: provider.label },
9957
+ outcome: { success: "started" }
9828
9958
  }
9829
- webResearchWidgetTimer = setInterval(() => {
9830
- updateWebResearchWidget();
9831
- }, 1e3);
9832
9959
  };
9833
- const updateWebResearchWidget = (ctx) => {
9834
- const widgetContext = ctx ?? latestWidgetContext;
9835
- if (!widgetContext) {
9836
- return;
9837
- }
9838
- latestWidgetContext = widgetContext;
9839
- if (!widgetContext.hasUI) {
9960
+ }
9961
+ async function runDispatchedWebResearch({
9962
+ activeWebResearchRequests,
9963
+ task,
9964
+ config,
9965
+ provider,
9966
+ providerConfig,
9967
+ ctx,
9968
+ options,
9969
+ executionOverride,
9970
+ executeResearch,
9971
+ deliverResult,
9972
+ onJobsChanged,
9973
+ resultMessageType
9974
+ }) {
9975
+ const { request, abortController } = task;
9976
+ let result;
9977
+ let reportText = "";
9978
+ try {
9979
+ const response = await executeResearch({
9980
+ config,
9981
+ provider,
9982
+ providerConfig,
9983
+ ctx,
9984
+ signal: abortController.signal,
9985
+ options,
9986
+ input: request.input,
9987
+ onProgress: (message) => {
9988
+ request.progress = summarizeWebResearchProgress(
9989
+ message,
9990
+ provider.label
9991
+ );
9992
+ onJobsChanged();
9993
+ },
9994
+ executionOverride
9995
+ });
9996
+ result = buildWebResearchResult(request, abortController, task, response);
9997
+ if (result.status === "completed") {
9998
+ reportText = response.text;
9999
+ }
10000
+ } catch (error) {
10001
+ result = buildFailedWebResearchResult(
10002
+ request,
10003
+ abortController,
10004
+ task,
10005
+ error
10006
+ );
10007
+ }
10008
+ try {
10009
+ await writeWebResearchArtifact(result, reportText);
10010
+ deliverResult({
10011
+ customType: resultMessageType,
10012
+ content: formatWebResearchResultMessage(result, reportText),
10013
+ display: true,
10014
+ details: result
10015
+ });
10016
+ } finally {
10017
+ activeWebResearchRequests.delete(request.id);
10018
+ onJobsChanged();
10019
+ }
10020
+ }
10021
+ function buildWebResearchResult(request, abortController, task, response) {
10022
+ const completedAt = (/* @__PURE__ */ new Date()).toISOString();
10023
+ if (abortController.signal.aborted && task.cancelRequestedAt !== void 0) {
10024
+ return {
10025
+ ...request,
10026
+ status: "cancelled",
10027
+ completedAt,
10028
+ elapsedMs: elapsedMs(request.startedAt, completedAt),
10029
+ error: "web research was cancelled by the user."
10030
+ };
10031
+ }
10032
+ return {
10033
+ ...request,
10034
+ status: "completed",
10035
+ completedAt,
10036
+ elapsedMs: elapsedMs(request.startedAt, completedAt),
10037
+ itemCount: response.itemCount
10038
+ };
10039
+ }
10040
+ function buildFailedWebResearchResult(request, abortController, task, error) {
10041
+ const completedAt = (/* @__PURE__ */ new Date()).toISOString();
10042
+ const cancelled = abortController.signal.aborted && task.cancelRequestedAt !== void 0;
10043
+ return {
10044
+ ...request,
10045
+ status: cancelled ? "cancelled" : "failed",
10046
+ completedAt,
10047
+ elapsedMs: elapsedMs(request.startedAt, completedAt),
10048
+ error: cancelled ? "web research was cancelled by the user." : formatErrorMessage(error)
10049
+ };
10050
+ }
10051
+ function elapsedMs(startedAt, completedAt) {
10052
+ return Math.max(0, Date.parse(completedAt) - Date.parse(startedAt));
10053
+ }
10054
+ function getActiveWebResearchRequests(tasks) {
10055
+ return [...tasks.values()].map((task) => task.request);
10056
+ }
10057
+ function getWebResearchTaskSnapshots(tasks) {
10058
+ return [...tasks.values()].map((task) => ({
10059
+ request: task.request,
10060
+ cancelRequestedAt: task.cancelRequestedAt
10061
+ }));
10062
+ }
10063
+ function cancelWebResearchTask(tasks, id) {
10064
+ const task = tasks.get(id);
10065
+ if (!task || task.abortController.signal.aborted) return false;
10066
+ task.cancelRequestedAt = (/* @__PURE__ */ new Date()).toISOString();
10067
+ task.request.progress = "cancelling";
10068
+ task.abortController.abort(
10069
+ new Error("web research was cancelled by the user.")
10070
+ );
10071
+ return true;
10072
+ }
10073
+ function createWebResearchRequest(cwd, provider, input) {
10074
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
10075
+ return {
10076
+ tool: "web_research",
10077
+ id: randomUUID(),
10078
+ provider,
10079
+ input,
10080
+ outputPath: buildWebResearchArtifactPath(cwd, input, startedAt),
10081
+ startedAt
10082
+ };
10083
+ }
10084
+ function buildWebResearchArtifactPath(cwd, input, startedAt) {
10085
+ const timestamp = startedAt.replaceAll(":", "-").replace(".", "-");
10086
+ const slug = slugifyWebResearchInput(input);
10087
+ return join2(cwd, RESEARCH_ARTIFACTS_DIR, `${timestamp}-${slug}.md`);
10088
+ }
10089
+ function slugifyWebResearchInput(input) {
10090
+ const slug = input.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60).replace(/-+$/g, "");
10091
+ return slug.length > 0 ? slug : "research";
10092
+ }
10093
+ function getWebResearchProgressIcon(request) {
10094
+ if (request.progress === "poll retrying after transient errors") {
10095
+ return "\u27F3";
10096
+ }
10097
+ if (request.progress === "queued" || request.progress === "cancelling") {
10098
+ return "\u25CC";
10099
+ }
10100
+ if (request.progress === "starting") {
10101
+ return "\u25D4";
10102
+ }
10103
+ if (request.progress?.startsWith("started:")) {
10104
+ return "\u25D1";
10105
+ }
10106
+ return "\u25CF";
10107
+ }
10108
+ function summarizeWebResearchProgress(message, providerLabel2) {
10109
+ const startingMessage = `Starting research via ${providerLabel2}`;
10110
+ if (message === startingMessage) {
10111
+ return "starting";
10112
+ }
10113
+ const startedPrefix = `${providerLabel2} research started: `;
10114
+ if (message.startsWith(startedPrefix)) {
10115
+ return `started: ${message.slice(startedPrefix.length)}`;
10116
+ }
10117
+ const statusPrefix = `Research via ${providerLabel2}: `;
10118
+ if (message.startsWith(statusPrefix)) {
10119
+ return message.slice(statusPrefix.length).replace(/\s+\([^)]* elapsed\)$/u, "").trim();
10120
+ }
10121
+ const retryPrefix = `${providerLabel2} research poll is still retrying after transient errors`;
10122
+ if (message.startsWith(retryPrefix)) {
10123
+ return "poll retrying after transient errors";
10124
+ }
10125
+ return message.trim();
10126
+ }
10127
+ function formatWebResearchResultMessage(result, reportText) {
10128
+ const text = reportText.trim();
10129
+ if (text.length > 0) {
10130
+ return `${text}
10131
+ `;
10132
+ }
10133
+ if (result.error) {
10134
+ return `${result.error}
10135
+ `;
10136
+ }
10137
+ return "";
10138
+ }
10139
+ async function writeWebResearchArtifact(result, reportText) {
10140
+ await mkdir2(dirname2(result.outputPath), { recursive: true });
10141
+ await writeFile2(
10142
+ result.outputPath,
10143
+ formatWebResearchArtifact(result, reportText),
10144
+ "utf-8"
10145
+ );
10146
+ }
10147
+ function formatWebResearchArtifact(result, reportText) {
10148
+ const providerLabel2 = PROVIDERS_BY_ID[result.provider]?.label ?? result.provider;
10149
+ const metadata = {
10150
+ query: result.input,
10151
+ provider: providerLabel2,
10152
+ providerId: result.provider,
10153
+ status: result.status,
10154
+ startedAt: result.startedAt,
10155
+ completedAt: result.completedAt,
10156
+ elapsedMs: result.elapsedMs,
10157
+ itemCount: result.itemCount,
10158
+ error: result.error
10159
+ };
10160
+ const lines = [
10161
+ "---",
10162
+ ...Object.entries(metadata).flatMap(
10163
+ ([key, value]) => value === void 0 ? [] : [`${key}: ${formatYamlScalar(value)}`]
10164
+ ),
10165
+ "---",
10166
+ "",
10167
+ "# Web research report"
10168
+ ];
10169
+ if (reportText) {
10170
+ lines.push("", reportText);
10171
+ }
10172
+ return `${lines.join("\n")}
10173
+ `;
10174
+ }
10175
+ function formatYamlScalar(value) {
10176
+ if (typeof value === "number") {
10177
+ return String(value);
10178
+ }
10179
+ return JSON.stringify(value);
10180
+ }
10181
+ async function loadWebResearchHistory(cwd, maxItems = MAX_RESEARCH_HISTORY_ITEMS) {
10182
+ const dir = join2(cwd, RESEARCH_ARTIFACTS_DIR);
10183
+ let entries;
10184
+ try {
10185
+ entries = await readdir(dir);
10186
+ } catch {
10187
+ return [];
10188
+ }
10189
+ const markdown = entries.filter((name) => name.endsWith(".md"));
10190
+ const withStats = await Promise.all(
10191
+ markdown.map(async (fileName) => {
10192
+ const outputPath = join2(dir, fileName);
10193
+ try {
10194
+ return { fileName, outputPath, stat: await stat(outputPath) };
10195
+ } catch {
10196
+ return void 0;
10197
+ }
10198
+ })
10199
+ );
10200
+ const newest = withStats.filter((item) => item !== void 0).sort((left, right) => right.stat.mtimeMs - left.stat.mtimeMs).slice(0, maxItems);
10201
+ return Promise.all(
10202
+ newest.map(async ({ fileName, outputPath, stat: stat2 }) => {
10203
+ let content = "";
10204
+ try {
10205
+ content = await readFile2(outputPath, "utf-8");
10206
+ } catch {
10207
+ }
10208
+ const metadata = parseWebResearchArtifactMetadata(content);
10209
+ return {
10210
+ outputPath,
10211
+ fileName,
10212
+ query: metadata.query ?? "",
10213
+ title: deriveWebResearchTitle(content, metadata.query ?? ""),
10214
+ provider: metadata.provider ?? "",
10215
+ status: metadata.status ?? "unknown",
10216
+ startedAt: metadata.startedAt ?? "",
10217
+ completedAt: metadata.completedAt ?? "",
10218
+ elapsedMs: computeHistoryElapsedMs(metadata),
10219
+ mtimeMs: stat2.mtimeMs
10220
+ };
10221
+ })
10222
+ );
10223
+ }
10224
+ function parseWebResearchArtifactMetadata(content) {
10225
+ return parseWebResearchFrontmatter(content) ?? parseLegacyWebResearchArtifactMetadata(content);
10226
+ }
10227
+ function parseWebResearchFrontmatter(content) {
10228
+ if (!content.startsWith("---\n")) {
10229
+ return void 0;
10230
+ }
10231
+ const end = content.indexOf("\n---", 4);
10232
+ if (end === -1) {
10233
+ return void 0;
10234
+ }
10235
+ const result = {};
10236
+ const frontmatter = content.slice(4, end);
10237
+ for (const line of frontmatter.split(/\r?\n/u)) {
10238
+ const match = /^([A-Za-z][A-Za-z0-9_-]*):\s*(.*)$/u.exec(line);
10239
+ if (!match) {
10240
+ continue;
10241
+ }
10242
+ result[match[1] ?? ""] = parseYamlScalar(match[2] ?? "");
10243
+ }
10244
+ return result;
10245
+ }
10246
+ function parseYamlScalar(value) {
10247
+ const trimmed = value.trim();
10248
+ if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
10249
+ try {
10250
+ const parsed = JSON.parse(trimmed);
10251
+ return typeof parsed === "string" ? parsed : String(parsed);
10252
+ } catch {
10253
+ }
10254
+ }
10255
+ return trimmed;
10256
+ }
10257
+ function parseLegacyWebResearchArtifactMetadata(content) {
10258
+ const result = {};
10259
+ const metadataHeadings = /* @__PURE__ */ new Map([
10260
+ ["Query", "query"],
10261
+ ["Provider", "provider"],
10262
+ ["Status", "status"],
10263
+ ["Started", "startedAt"],
10264
+ ["Completed", "completedAt"]
10265
+ ]);
10266
+ const artifactBodyHeadings = /* @__PURE__ */ new Set(["Elapsed", "Items", "Error", "Report"]);
10267
+ const lines = content.split(/\r?\n/u);
10268
+ for (let index = 0; index < lines.length; index++) {
10269
+ const match = /^##\s+(.+)$/u.exec(lines[index] ?? "");
10270
+ if (!match) continue;
10271
+ const heading = match[1] ?? "";
10272
+ if (artifactBodyHeadings.has(heading)) break;
10273
+ const key = metadataHeadings.get(heading);
10274
+ if (key === void 0 || result[key] !== void 0) continue;
10275
+ const values = [];
10276
+ for (let next = index + 1; next < lines.length; next++) {
10277
+ if (/^##\s+/u.test(lines[next] ?? "")) break;
10278
+ if ((lines[next] ?? "").trim() || values.length > 0)
10279
+ values.push(lines[next] ?? "");
10280
+ }
10281
+ result[key] = values.join("\n").trim();
10282
+ }
10283
+ return result;
10284
+ }
10285
+ var RESEARCH_TITLE_MAX_LENGTH = 80;
10286
+ var NON_TITLE_HEADINGS = /* @__PURE__ */ new Set([
10287
+ "Web research report",
10288
+ "Query",
10289
+ "Provider",
10290
+ "Status",
10291
+ "Started",
10292
+ "Completed",
10293
+ "Elapsed",
10294
+ "Items",
10295
+ "Error",
10296
+ "Report"
10297
+ ]);
10298
+ function deriveWebResearchTitle(content, query2) {
10299
+ const body = getWebResearchBody(content);
10300
+ for (const line of body.split(/\r?\n/u)) {
10301
+ const match = /^#{1,2}\s+(.+?)\s*$/u.exec(line);
10302
+ if (!match) continue;
10303
+ const heading = (match[1] ?? "").trim();
10304
+ if (heading.length > 0 && !NON_TITLE_HEADINGS.has(heading)) {
10305
+ return heading;
10306
+ }
10307
+ }
10308
+ const fallback = query2.replace(/\s+/g, " ").trim();
10309
+ if (fallback.length <= RESEARCH_TITLE_MAX_LENGTH) {
10310
+ return fallback;
10311
+ }
10312
+ return `${fallback.slice(0, RESEARCH_TITLE_MAX_LENGTH - 1)}\u2026`;
10313
+ }
10314
+ function computeHistoryElapsedMs(metadata) {
10315
+ if (metadata.elapsedMs !== void 0) {
10316
+ const parsed = Number(metadata.elapsedMs);
10317
+ if (Number.isFinite(parsed) && parsed >= 0) {
10318
+ return parsed;
10319
+ }
10320
+ }
10321
+ const started = Date.parse(metadata.startedAt ?? "");
10322
+ const completed = Date.parse(metadata.completedAt ?? "");
10323
+ if (Number.isFinite(started) && Number.isFinite(completed)) {
10324
+ return Math.max(0, completed - started);
10325
+ }
10326
+ return void 0;
10327
+ }
10328
+ function getWebResearchBody(content) {
10329
+ if (!content.startsWith("---\n")) {
10330
+ return content;
10331
+ }
10332
+ const end = content.indexOf("\n---", 4);
10333
+ if (end === -1) {
10334
+ return content;
10335
+ }
10336
+ const afterClose = content.indexOf("\n", end + 4);
10337
+ return afterClose === -1 ? "" : content.slice(afterClose + 1);
10338
+ }
10339
+ async function loadWebResearchReport(outputPath) {
10340
+ const buffer = await readFile2(outputPath);
10341
+ const truncated = buffer.byteLength > RESEARCH_REPORT_MAX_BYTES;
10342
+ const content = buffer.subarray(0, RESEARCH_REPORT_MAX_BYTES).toString("utf-8");
10343
+ return {
10344
+ body: getWebResearchBody(content).trim(),
10345
+ metadata: parseWebResearchArtifactMetadata(content),
10346
+ truncated
10347
+ };
10348
+ }
10349
+ async function loadWebResearchPreview(outputPath) {
10350
+ const buffer = await readFile2(outputPath);
10351
+ const truncated = buffer.byteLength > RESEARCH_PREVIEW_MAX_BYTES;
10352
+ const text = buffer.subarray(0, RESEARCH_PREVIEW_MAX_BYTES).toString("utf-8");
10353
+ return truncated ? `${text}
10354
+
10355
+ ---
10356
+ Preview truncated. Open the full report at \`${outputPath}\`.` : `${text}
10357
+
10358
+ ---
10359
+ Full report: \`${outputPath}\``;
10360
+ }
10361
+ function trackPendingResearchTask(task) {
10362
+ const tracked = task.catch(() => {
10363
+ }).finally(() => {
10364
+ pendingResearchTasks.delete(tracked);
10365
+ });
10366
+ pendingResearchTasks.add(tracked);
10367
+ }
10368
+ async function waitForPendingResearchTasks() {
10369
+ await Promise.all([...pendingResearchTasks]);
10370
+ }
10371
+
10372
+ // src/web-research-view.ts
10373
+ import { getMarkdownTheme } from "@earendil-works/pi-coding-agent";
10374
+ import {
10375
+ getKeybindings,
10376
+ Key,
10377
+ Markdown,
10378
+ matchesKey,
10379
+ truncateToWidth,
10380
+ visibleWidth
10381
+ } from "@earendil-works/pi-tui";
10382
+ var STATUS_MESSAGE_TTL_MS = 1500;
10383
+ var PAGE_JUMP = 10;
10384
+ var WebResearchManagerView = class {
10385
+ constructor(tui, theme, done, cwd, tasks, onChange, actions, loadHistory = loadWebResearchHistory) {
10386
+ this.tui = tui;
10387
+ this.theme = theme;
10388
+ this.done = done;
10389
+ this.cwd = cwd;
10390
+ this.tasks = tasks;
10391
+ this.onChange = onChange;
10392
+ this.actions = actions;
10393
+ this.loadHistory = loadHistory;
10394
+ void this.reloadHistory();
10395
+ }
10396
+ tui;
10397
+ theme;
10398
+ done;
10399
+ cwd;
10400
+ tasks;
10401
+ onChange;
10402
+ actions;
10403
+ loadHistory;
10404
+ selectedIndex = 0;
10405
+ history = [];
10406
+ confirmCancelId;
10407
+ open;
10408
+ scrollOffset = 0;
10409
+ statusMessage;
10410
+ statusMessageTimer;
10411
+ isReportOpen() {
10412
+ return this.open !== void 0;
10413
+ }
10414
+ render(width) {
10415
+ if (this.open?.kind === "report") {
10416
+ return this.renderReport(this.open, width);
10417
+ }
10418
+ if (this.open?.kind === "detail") {
10419
+ const snapshot = this.findSnapshot(this.open.taskId);
10420
+ if (snapshot) return this.renderDetail(snapshot, width);
10421
+ this.open = void 0;
10422
+ }
10423
+ return this.renderTable(width);
10424
+ }
10425
+ invalidate() {
10426
+ }
10427
+ dispose() {
10428
+ if (this.statusMessageTimer) clearTimeout(this.statusMessageTimer);
10429
+ }
10430
+ handleInput(data) {
10431
+ if (this.open) {
10432
+ this.handleOpenInput(data);
10433
+ } else {
10434
+ this.handleTableInput(data);
10435
+ }
10436
+ this.tui.requestRender();
10437
+ }
10438
+ refresh() {
10439
+ if (this.open?.kind === "detail" && !this.findSnapshot(this.open.taskId)) {
10440
+ this.open = void 0;
10441
+ this.confirmCancelId = void 0;
10442
+ }
10443
+ void this.reloadHistory();
10444
+ this.tui.requestRender();
10445
+ }
10446
+ // --- table mode ---
10447
+ getRows() {
10448
+ const snapshots = getWebResearchTaskSnapshots(this.tasks).sort(
10449
+ (a, b) => a.request.startedAt.localeCompare(b.request.startedAt)
10450
+ );
10451
+ const runningPaths = new Set(
10452
+ snapshots.map((snapshot) => snapshot.request.outputPath)
10453
+ );
10454
+ const rows = snapshots.map((snapshot) => ({
10455
+ kind: "running",
10456
+ snapshot
10457
+ }));
10458
+ for (const item of this.history) {
10459
+ if (runningPaths.has(item.outputPath)) continue;
10460
+ rows.push({ kind: "history", item });
10461
+ }
10462
+ return rows;
10463
+ }
10464
+ border(width) {
10465
+ return this.theme.fg("border", "\u2500".repeat(Math.max(1, width)));
10466
+ }
10467
+ renderTable(width) {
10468
+ const rows = this.getRows();
10469
+ this.selectedIndex = clamp2(this.selectedIndex, 0, rows.length - 1);
10470
+ const lines = [
10471
+ this.border(width),
10472
+ this.theme.fg("accent", " Web research"),
10473
+ ""
10474
+ ];
10475
+ if (rows.length === 0) {
10476
+ lines.push(this.theme.fg("muted", " No research jobs or reports"));
10477
+ } else {
10478
+ const layout = computeTableLayout(rows, width);
10479
+ const now = Date.now();
10480
+ rows.forEach((row, index) => {
10481
+ lines.push(
10482
+ formatResearchTableRow(
10483
+ row,
10484
+ layout,
10485
+ this.theme,
10486
+ index === this.selectedIndex,
10487
+ now
10488
+ )
10489
+ );
10490
+ if (row.kind === "running" && this.confirmCancelId === row.snapshot.request.id) {
10491
+ lines.push(
10492
+ this.theme.fg(
10493
+ "warning",
10494
+ truncateToWidth(
10495
+ ` Press c again to cancel this ${providerLabel(row.snapshot.request.provider)} research`,
10496
+ width
10497
+ )
10498
+ )
10499
+ );
10500
+ }
10501
+ });
10502
+ }
10503
+ lines.push("");
10504
+ if (this.statusMessage) {
10505
+ lines.push(this.theme.fg("accent", ` ${this.statusMessage}`));
10506
+ }
10507
+ lines.push(
10508
+ this.theme.fg(
10509
+ "dim",
10510
+ " \u2191\u2193 move \xB7 Enter open \xB7 c cancel running \xB7 Esc close"
10511
+ ),
10512
+ this.border(width)
10513
+ );
10514
+ return lines;
10515
+ }
10516
+ handleTableInput(data) {
10517
+ const kb = getKeybindings();
10518
+ const rows = this.getRows();
10519
+ if (kb.matches(data, "tui.select.up")) this.move(rows.length, -1);
10520
+ else if (kb.matches(data, "tui.select.down")) this.move(rows.length, 1);
10521
+ else if (kb.matches(data, "tui.select.pageUp"))
10522
+ this.move(rows.length, -PAGE_JUMP, false);
10523
+ else if (kb.matches(data, "tui.select.pageDown"))
10524
+ this.move(rows.length, PAGE_JUMP, false);
10525
+ else if (kb.matches(data, "tui.select.confirm"))
10526
+ void this.openRow(rows[this.selectedIndex]);
10527
+ else if (data === "c") this.cancelRow(rows[this.selectedIndex]);
10528
+ else if (kb.matches(data, "tui.select.cancel")) {
10529
+ if (this.confirmCancelId) this.confirmCancelId = void 0;
10530
+ else this.done(void 0);
10531
+ }
10532
+ }
10533
+ move(count, delta, wrap = true) {
10534
+ this.confirmCancelId = void 0;
10535
+ if (count === 0) return;
10536
+ const next = this.selectedIndex + delta;
10537
+ this.selectedIndex = wrap ? (next + count) % count : clamp2(next, 0, count - 1);
10538
+ }
10539
+ async openRow(row) {
10540
+ if (!row) return;
10541
+ this.confirmCancelId = void 0;
10542
+ if (row.kind === "running") {
10543
+ this.open = { kind: "detail", taskId: row.snapshot.request.id };
10544
+ this.scrollOffset = 0;
10545
+ return;
10546
+ }
10547
+ try {
10548
+ const report = await loadWebResearchReport(row.item.outputPath);
10549
+ this.open = {
10550
+ kind: "report",
10551
+ title: row.item.title || row.item.query,
10552
+ body: report.body,
10553
+ truncated: report.truncated,
10554
+ item: row.item,
10555
+ markdown: new Markdown(report.body, 1, 0, getMarkdownTheme())
10556
+ };
10557
+ this.scrollOffset = 0;
10558
+ } catch (error) {
10559
+ this.showStatusMessage(
10560
+ `Failed to read ${row.item.outputPath}: ${formatErrorMessage(error)}`
10561
+ );
10562
+ }
10563
+ this.tui.requestRender();
10564
+ }
10565
+ cancelRow(row) {
10566
+ if (!row || row.kind !== "running") return;
10567
+ const id = row.snapshot.request.id;
10568
+ if (this.confirmCancelId !== id) {
10569
+ this.confirmCancelId = id;
10570
+ return;
10571
+ }
10572
+ cancelWebResearchTask(this.tasks, id);
10573
+ this.confirmCancelId = void 0;
10574
+ this.onChange();
10575
+ }
10576
+ // --- report / detail mode ---
10577
+ renderReport(open, width) {
10578
+ const lines = [
10579
+ this.border(width),
10580
+ this.theme.fg("accent", truncateToWidth(` ${open.title}`, width)),
10581
+ this.theme.fg(
10582
+ "dim",
10583
+ " c copy markdown \xB7 i inject into context \xB7 \u2191\u2193/PgUp/PgDn scroll \xB7 Esc back"
10584
+ ),
10585
+ this.border(width),
10586
+ ""
10587
+ ];
10588
+ let body;
10589
+ try {
10590
+ body = open.markdown.render(Math.max(20, width - 2));
10591
+ } catch {
10592
+ body = open.body.split("\n").map((line) => truncateToWidth(line, width));
10593
+ }
10594
+ const viewport = this.viewportHeight();
10595
+ const maxOffset = Math.max(0, body.length - viewport);
10596
+ this.scrollOffset = clamp2(this.scrollOffset, 0, maxOffset);
10597
+ lines.push(...body.slice(this.scrollOffset, this.scrollOffset + viewport));
10598
+ lines.push("");
10599
+ const footer = [];
10600
+ if (body.length > viewport) {
10601
+ const end = Math.min(body.length, this.scrollOffset + viewport);
10602
+ footer.push(`lines ${this.scrollOffset + 1}\u2013${end} of ${body.length}`);
10603
+ }
10604
+ if (open.truncated) {
10605
+ footer.push(`report truncated \xB7 full text: ${open.item.outputPath}`);
10606
+ }
10607
+ if (this.statusMessage) {
10608
+ lines.push(this.theme.fg("accent", ` ${this.statusMessage}`));
10609
+ }
10610
+ if (footer.length > 0) {
10611
+ lines.push(
10612
+ this.theme.fg("dim", truncateToWidth(` ${footer.join(" \xB7 ")}`, width))
10613
+ );
10614
+ }
10615
+ lines.push(this.border(width));
10616
+ return lines;
10617
+ }
10618
+ renderDetail(snapshot, width) {
10619
+ const request = snapshot.request;
10620
+ const elapsed = formatCompactElapsed(
10621
+ Date.now() - Date.parse(request.startedAt)
10622
+ );
10623
+ const progress = snapshot.cancelRequestedAt ? "cancelling" : request.progress ?? "running";
10624
+ const lines = [
10625
+ this.border(width),
10626
+ this.theme.fg(
10627
+ "accent",
10628
+ truncateToWidth(
10629
+ ` Running research via ${providerLabel(request.provider)}`,
10630
+ width
10631
+ )
10632
+ ),
10633
+ this.theme.fg("dim", " c cancel \xB7 Esc back"),
10634
+ this.border(width),
10635
+ "",
10636
+ truncateToWidth(` Status: ${progress} (${elapsed} elapsed)`, width),
10637
+ truncateToWidth(` Report path: ${request.outputPath}`, width),
10638
+ "",
10639
+ this.theme.fg("accent", " Research brief"),
10640
+ ""
10641
+ ];
10642
+ for (const line of request.input.split("\n").slice(0, 100)) {
10643
+ lines.push(truncateToWidth(` ${line}`, width));
10644
+ }
10645
+ if (this.confirmCancelId === request.id) {
10646
+ lines.push(
10647
+ "",
10648
+ this.theme.fg("warning", " Press c again to cancel this research")
10649
+ );
10650
+ }
10651
+ if (this.statusMessage) {
10652
+ lines.push("", this.theme.fg("accent", ` ${this.statusMessage}`));
10653
+ }
10654
+ lines.push(this.border(width));
10655
+ return lines;
10656
+ }
10657
+ handleOpenInput(data) {
10658
+ const kb = getKeybindings();
10659
+ const open = this.open;
10660
+ if (!open) return;
10661
+ if (kb.matches(data, "tui.select.cancel")) {
10662
+ if (this.confirmCancelId) {
10663
+ this.confirmCancelId = void 0;
10664
+ return;
10665
+ }
10666
+ this.open = void 0;
10667
+ this.scrollOffset = 0;
10668
+ return;
10669
+ }
10670
+ if (open.kind === "detail") {
10671
+ if (data === "c") {
10672
+ const snapshot = this.findSnapshot(open.taskId);
10673
+ if (!snapshot) return;
10674
+ if (this.confirmCancelId !== open.taskId) {
10675
+ this.confirmCancelId = open.taskId;
10676
+ return;
10677
+ }
10678
+ cancelWebResearchTask(this.tasks, open.taskId);
10679
+ this.confirmCancelId = void 0;
10680
+ this.onChange();
10681
+ this.showStatusMessage("Cancellation requested");
10682
+ }
10683
+ return;
10684
+ }
10685
+ if (kb.matches(data, "tui.select.up")) this.scrollOffset -= 1;
10686
+ else if (kb.matches(data, "tui.select.down")) this.scrollOffset += 1;
10687
+ else if (kb.matches(data, "tui.select.pageUp"))
10688
+ this.scrollOffset -= this.viewportHeight();
10689
+ else if (kb.matches(data, "tui.select.pageDown"))
10690
+ this.scrollOffset += this.viewportHeight();
10691
+ else if (matchesKey(data, Key.home)) this.scrollOffset = 0;
10692
+ else if (matchesKey(data, Key.end))
10693
+ this.scrollOffset = Number.MAX_SAFE_INTEGER;
10694
+ else if (data === "c") {
10695
+ void this.actions.copyToClipboard(open.body).then(() => this.showStatusMessage("Copied report to clipboard")).catch((error) => {
10696
+ const message = `Copy failed: ${formatErrorMessage(error)}`;
10697
+ this.showStatusMessage(message);
10698
+ this.actions.notify(message, "error");
10699
+ });
10700
+ } else if (data === "i") {
10701
+ this.actions.injectReport({
10702
+ title: open.title,
10703
+ body: open.body,
10704
+ item: open.item
10705
+ });
10706
+ this.showStatusMessage("Report added to conversation context");
10707
+ }
10708
+ }
10709
+ viewportHeight() {
10710
+ return Math.max(5, Math.floor(this.tui.terminal.rows * 0.85) - 6);
10711
+ }
10712
+ showStatusMessage(message) {
10713
+ this.statusMessage = message;
10714
+ if (this.statusMessageTimer) clearTimeout(this.statusMessageTimer);
10715
+ this.statusMessageTimer = setTimeout(() => {
10716
+ this.statusMessage = void 0;
10717
+ this.statusMessageTimer = void 0;
10718
+ this.tui.requestRender();
10719
+ }, STATUS_MESSAGE_TTL_MS);
10720
+ }
10721
+ findSnapshot(taskId) {
10722
+ return getWebResearchTaskSnapshots(this.tasks).find(
10723
+ (snapshot) => snapshot.request.id === taskId
10724
+ );
10725
+ }
10726
+ async reloadHistory() {
10727
+ this.history = await this.loadHistory(this.cwd);
10728
+ this.tui.requestRender();
10729
+ }
10730
+ };
10731
+ function computeTableLayout(rows, width) {
10732
+ const providerWidth = clamp2(
10733
+ Math.max(0, ...rows.map((row) => visibleWidth(rowProvider(row)))),
10734
+ 4,
10735
+ 12
10736
+ );
10737
+ const date = 10;
10738
+ const duration = 7;
10739
+ const fixed = 2 + 2 + date + 1 + providerWidth + 1 + duration + 1;
10740
+ return {
10741
+ date,
10742
+ provider: providerWidth,
10743
+ duration,
10744
+ title: Math.max(10, width - fixed)
10745
+ };
10746
+ }
10747
+ function formatResearchTableRow(row, layout, theme, selected, now = Date.now()) {
10748
+ const cursor = selected ? "\u203A " : " ";
10749
+ const glyph = statusGlyph(row, theme);
10750
+ const date = padCell(
10751
+ formatRelativeDate(rowTimestampMs(row), now),
10752
+ layout.date
10753
+ );
10754
+ const provider = padCell(
10755
+ truncateToWidth(rowProvider(row), layout.provider),
10756
+ layout.provider
10757
+ );
10758
+ const duration = padCell(rowDuration(row, now), layout.duration, "right");
10759
+ const title = truncateToWidth(rowTitle(row), layout.title);
10760
+ const dim = (text) => row.kind === "history" ? text : theme.fg("dim", text);
10761
+ return `${cursor}${glyph} ${dim(date)} ${provider} ${theme.fg("muted", duration)} ${title}`;
10762
+ }
10763
+ function statusGlyph(row, theme) {
10764
+ if (row.kind === "running") {
10765
+ const icon = row.snapshot.cancelRequestedAt ? "\u25CC" : getWebResearchProgressIcon(row.snapshot.request);
10766
+ return theme.fg("accent", icon);
10767
+ }
10768
+ switch (row.item.status) {
10769
+ case "completed":
10770
+ return theme.fg("success", "\u2714");
10771
+ case "failed":
10772
+ return theme.fg("error", "\u2718");
10773
+ case "cancelled":
10774
+ return theme.fg("warning", "\u2718");
10775
+ default:
10776
+ return theme.fg("dim", "?");
10777
+ }
10778
+ }
10779
+ function rowProvider(row) {
10780
+ return row.kind === "running" ? providerLabel(row.snapshot.request.provider) : row.item.provider || "?";
10781
+ }
10782
+ function rowTimestampMs(row) {
10783
+ if (row.kind === "running") {
10784
+ return Date.parse(row.snapshot.request.startedAt);
10785
+ }
10786
+ const completed = Date.parse(row.item.completedAt);
10787
+ if (Number.isFinite(completed)) return completed;
10788
+ const started = Date.parse(row.item.startedAt);
10789
+ if (Number.isFinite(started)) return started;
10790
+ return row.item.mtimeMs;
10791
+ }
10792
+ function rowDuration(row, now) {
10793
+ if (row.kind === "running") {
10794
+ return formatCompactElapsed(
10795
+ now - Date.parse(row.snapshot.request.startedAt)
10796
+ );
10797
+ }
10798
+ return row.item.elapsedMs === void 0 ? "\u2014" : formatCompactElapsed(row.item.elapsedMs);
10799
+ }
10800
+ var REDUNDANT_PROGRESS = /* @__PURE__ */ new Set([
10801
+ "in_progress",
10802
+ "running",
10803
+ "starting",
10804
+ "queued",
10805
+ "cancelling"
10806
+ ]);
10807
+ function rowTitle(row) {
10808
+ if (row.kind === "running") {
10809
+ const request = row.snapshot.request;
10810
+ const progress = row.snapshot.cancelRequestedAt ? void 0 : request.progress;
10811
+ const prefix = progress && !REDUNDANT_PROGRESS.has(progress) ? `${progress} \u2014 ` : "";
10812
+ return `${prefix}${cleanSingleLine(request.input)}`;
10813
+ }
10814
+ return row.item.title || cleanSingleLine(row.item.query);
10815
+ }
10816
+ function formatRelativeDate(timestampMs, now) {
10817
+ if (!Number.isFinite(timestampMs)) return "?";
10818
+ const diff = Math.max(0, now - timestampMs);
10819
+ if (diff < 6e4) return "now";
10820
+ if (diff < 36e5) return `${Math.floor(diff / 6e4)}m ago`;
10821
+ if (diff < 864e5) return `${Math.floor(diff / 36e5)}h ago`;
10822
+ if (diff < 7 * 864e5) return `${Math.floor(diff / 864e5)}d ago`;
10823
+ const date = new Date(timestampMs);
10824
+ const month = String(date.getMonth() + 1).padStart(2, "0");
10825
+ const day = String(date.getDate()).padStart(2, "0");
10826
+ if (date.getFullYear() === new Date(now).getFullYear()) {
10827
+ return `${month}-${day}`;
10828
+ }
10829
+ return `${date.getFullYear()}-${month}-${day}`;
10830
+ }
10831
+ function providerLabel(providerId) {
10832
+ return PROVIDERS_BY_ID[providerId]?.label ?? providerId;
10833
+ }
10834
+ function padCell(text, width, align = "left") {
10835
+ const pad = " ".repeat(Math.max(0, width - visibleWidth(text)));
10836
+ return align === "left" ? `${text}${pad}` : `${pad}${text}`;
10837
+ }
10838
+ function clamp2(value, min, max) {
10839
+ return Math.min(Math.max(value, min), Math.max(min, max));
10840
+ }
10841
+ function formatCompactElapsed(ms) {
10842
+ const totalSeconds = Math.max(0, Math.floor(ms / 1e3));
10843
+ const minutes = Math.floor(totalSeconds / 60);
10844
+ const seconds = totalSeconds % 60;
10845
+ if (minutes > 0) {
10846
+ return `${minutes}m${seconds}s`;
10847
+ }
10848
+ return `${totalSeconds}s`;
10849
+ }
10850
+ function cleanSingleLine(text) {
10851
+ return text.replace(/\s+/g, " ").trim();
10852
+ }
10853
+
10854
+ // src/index.ts
10855
+ var DEFAULT_MAX_RESULTS = 5;
10856
+ var MAX_ALLOWED_RESULTS = 20;
10857
+ var MAX_SEARCH_QUERIES = 10;
10858
+ var RESEARCH_HEARTBEAT_MS = 15e3;
10859
+ var WEB_RESEARCH_RESULT_MESSAGE_TYPE = "web-research-result";
10860
+ var WEB_RESEARCH_REPORT_MESSAGE_TYPE = "web-research-report";
10861
+ var WEB_RESEARCH_WIDGET_KEY = "web-research-jobs";
10862
+ var DEFAULT_SUMMARY_SYMBOLS = {
10863
+ success: "\u2714",
10864
+ failure: "\u2718"
10865
+ };
10866
+ function webProvidersExtension(pi) {
10867
+ const activeWebResearchRequests = /* @__PURE__ */ new Map();
10868
+ let latestWidgetContext;
10869
+ let webResearchWidgetTimer;
10870
+ const stopWebResearchWidgetTimer = () => {
10871
+ if (webResearchWidgetTimer) {
10872
+ clearInterval(webResearchWidgetTimer);
10873
+ webResearchWidgetTimer = void 0;
10874
+ }
10875
+ };
10876
+ const ensureWebResearchWidgetTimer = () => {
10877
+ if (webResearchWidgetTimer || activeWebResearchRequests.size === 0) {
10878
+ return;
10879
+ }
10880
+ webResearchWidgetTimer = setInterval(() => {
10881
+ updateWebResearchWidget();
10882
+ }, 1e3);
10883
+ };
10884
+ const updateWebResearchWidget = (ctx) => {
10885
+ const widgetContext = ctx ?? latestWidgetContext;
10886
+ if (!widgetContext) {
10887
+ return;
10888
+ }
10889
+ latestWidgetContext = widgetContext;
10890
+ if (!widgetContext.hasUI) {
9840
10891
  stopWebResearchWidgetTimer();
9841
10892
  return;
9842
10893
  }
@@ -9849,7 +10900,7 @@ function webProvidersExtension(pi) {
9849
10900
  widgetContext.ui.setWidget(
9850
10901
  WEB_RESEARCH_WIDGET_KEY,
9851
10902
  buildWebResearchWidgetLines(
9852
- [...activeWebResearchRequests.values()],
10903
+ getActiveWebResearchRequests(activeWebResearchRequests),
9853
10904
  widgetContext.ui.theme
9854
10905
  )
9855
10906
  );
@@ -9859,6 +10910,10 @@ function webProvidersExtension(pi) {
9859
10910
  WEB_RESEARCH_RESULT_MESSAGE_TYPE,
9860
10911
  (message, state, theme) => renderWebResearchResultMessage(message, state, theme)
9861
10912
  );
10913
+ pi.registerMessageRenderer(
10914
+ WEB_RESEARCH_REPORT_MESSAGE_TYPE,
10915
+ (message, state, theme) => renderWebResearchReportMessage(message, state, theme)
10916
+ );
9862
10917
  }
9863
10918
  pi.registerCommand("web-providers", {
9864
10919
  description: "Configure web search providers",
@@ -9874,11 +10929,71 @@ function webProvidersExtension(pi) {
9874
10929
  );
9875
10930
  }
9876
10931
  });
10932
+ pi.registerCommand("web-research", {
10933
+ description: "Browse, inspect, and manage web researches",
10934
+ handler: async (_args, ctx) => {
10935
+ if (!ctx.hasUI) {
10936
+ ctx.ui.notify("web-research requires interactive mode", "error");
10937
+ return;
10938
+ }
10939
+ const actions = {
10940
+ copyToClipboard,
10941
+ injectReport: ({ title, body, item }) => {
10942
+ pi.sendMessage(
10943
+ {
10944
+ customType: WEB_RESEARCH_REPORT_MESSAGE_TYPE,
10945
+ content: formatWebResearchReportMessage(title, body, item),
10946
+ display: true,
10947
+ details: {
10948
+ title,
10949
+ outputPath: item.outputPath,
10950
+ provider: item.provider,
10951
+ query: item.query,
10952
+ status: item.status
10953
+ }
10954
+ },
10955
+ { triggerTurn: false }
10956
+ );
10957
+ },
10958
+ notify: (message, type) => ctx.ui.notify(message, type ?? "info")
10959
+ };
10960
+ let timer;
10961
+ let view;
10962
+ try {
10963
+ await ctx.ui.custom(
10964
+ (tui, theme, _keybindings, done) => {
10965
+ view = new WebResearchManagerView(
10966
+ tui,
10967
+ theme,
10968
+ done,
10969
+ ctx.cwd,
10970
+ activeWebResearchRequests,
10971
+ () => updateWebResearchWidget(ctx),
10972
+ actions
10973
+ );
10974
+ timer = setInterval(() => view?.refresh(), 1e3);
10975
+ return view;
10976
+ },
10977
+ {
10978
+ overlay: true,
10979
+ overlayOptions: () => view?.isReportOpen() ? { anchor: "center", width: "85%", maxHeight: "85%" } : {
10980
+ anchor: "center",
10981
+ width: "75%",
10982
+ maxHeight: "60%",
10983
+ minWidth: 60
10984
+ }
10985
+ }
10986
+ );
10987
+ } finally {
10988
+ if (timer) clearInterval(timer);
10989
+ }
10990
+ }
10991
+ });
9877
10992
  pi.on("session_start", async (_event, ctx) => {
9878
10993
  latestWidgetContext = ctx;
9879
10994
  resetContentStore();
9880
10995
  updateWebResearchWidget(ctx);
9881
- await refreshManagedToolsOnStartup(
10996
+ await refreshManagedToolsOnStartup2(
9882
10997
  pi,
9883
10998
  { activeWebResearchRequests, updateWebResearchWidget },
9884
10999
  ctx.cwd,
@@ -9889,7 +11004,7 @@ function webProvidersExtension(pi) {
9889
11004
  latestWidgetContext = ctx;
9890
11005
  await cleanupContentStore();
9891
11006
  updateWebResearchWidget(ctx);
9892
- await refreshManagedToolsOnStartup(
11007
+ await refreshManagedToolsOnStartup2(
9893
11008
  pi,
9894
11009
  { activeWebResearchRequests, updateWebResearchWidget },
9895
11010
  ctx.cwd,
@@ -10112,7 +11227,7 @@ function registerWebResearchTool(pi, webResearchLifecycle, providerIds) {
10112
11227
  "Do not expect the final report in the same turn; tell the user that web research has started and wait for the completion message with the saved report path."
10113
11228
  ]),
10114
11229
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
10115
- return dispatchWebResearch({
11230
+ return dispatchWebResearch2({
10116
11231
  pi,
10117
11232
  activeWebResearchRequests: webResearchLifecycle.activeWebResearchRequests,
10118
11233
  updateWebResearchWidget: webResearchLifecycle.updateWebResearchWidget,
@@ -10137,109 +11252,38 @@ function registerWebResearchTool(pi, webResearchLifecycle, providerIds) {
10137
11252
  }
10138
11253
  });
10139
11254
  }
10140
- async function runWebProvidersConfig(pi, webResearchLifecycle, ctx) {
10141
- const config = await loadConfig();
10142
- const activeProvider = getInitialProviderSelection(config);
10143
- await ctx.ui.custom(
10144
- (tui, theme, _keybindings, done) => new WebProvidersSettingsView(
10145
- tui,
10146
- theme,
10147
- done,
10148
- ctx,
10149
- config,
10150
- activeProvider
10151
- )
10152
- );
10153
- await refreshManagedTools(pi, webResearchLifecycle, ctx.cwd, {
10154
- addAvailable: true
10155
- });
10156
- }
10157
- function formatStartupConfigError(error) {
10158
- const detail = error instanceof Error ? error.message : String(error);
10159
- return `web-providers config error: ${detail.replace(getConfigPath(), "~/.pi/agent/web-providers.json")}`;
10160
- }
10161
- function getAvailableProviderIdsForCapability(config, cwd, capability) {
10162
- const providerId = getMappedProviderIdForTool(config, capability);
10163
- if (!providerId) {
10164
- return [];
10165
- }
10166
- const provider = PROVIDERS_BY_ID[providerId];
10167
- if (!supportsTool2(provider, capability)) {
10168
- return [];
10169
- }
10170
- const status = getProviderCapabilityStatus(
10171
- config,
10172
- cwd,
10173
- providerId,
10174
- capability,
10175
- {
10176
- resolveSecrets: false
10177
- }
10178
- );
10179
- return isProviderCapabilityExposable(status) ? [providerId] : [];
10180
- }
10181
- function getProviderStatusForTool(config, cwd, providerId, capability) {
10182
- return getProviderCapabilityStatus(config, cwd, providerId, capability);
10183
- }
10184
- function getAvailableManagedToolNames(config, cwd) {
10185
- return Object.keys(CAPABILITY_TOOL_NAMES).filter(
10186
- (capability) => getAvailableProviderIdsForCapability(config, cwd, capability).length > 0
10187
- ).map((capability) => CAPABILITY_TOOL_NAMES[capability]);
10188
- }
10189
- function getSyncedActiveTools(config, cwd, activeToolNames, options) {
10190
- const availableToolNames = new Set(getAvailableManagedToolNames(config, cwd));
10191
- const nextActiveTools = new Set(activeToolNames);
10192
- for (const toolName of MANAGED_TOOL_NAMES) {
10193
- if (availableToolNames.has(toolName)) {
10194
- if (options.addAvailable) {
10195
- nextActiveTools.add(toolName);
10196
- }
10197
- continue;
10198
- }
10199
- nextActiveTools.delete(toolName);
10200
- }
10201
- return nextActiveTools;
10202
- }
10203
- async function refreshManagedTools(pi, webResearchLifecycle, cwd, options) {
11255
+ async function runWebProvidersConfig(pi, webResearchLifecycle, ctx) {
10204
11256
  const config = await loadConfig();
10205
- const nextActiveTools = getSyncedActiveTools(
10206
- config,
10207
- cwd,
10208
- pi.getActiveTools(),
10209
- options
11257
+ const activeProvider = getInitialProviderSelection(config);
11258
+ await ctx.ui.custom(
11259
+ (tui, theme, _keybindings, done) => new WebProvidersSettingsView(
11260
+ tui,
11261
+ theme,
11262
+ done,
11263
+ ctx,
11264
+ config,
11265
+ activeProvider
11266
+ )
10210
11267
  );
10211
- registerManagedTools(pi, webResearchLifecycle, {
10212
- search: getAvailableProviderIdsForCapability(config, cwd, "search"),
10213
- contents: getAvailableProviderIdsForCapability(config, cwd, "contents"),
10214
- answer: getAvailableProviderIdsForCapability(config, cwd, "answer"),
10215
- research: getAvailableProviderIdsForCapability(config, cwd, "research")
11268
+ await refreshManagedTools2(pi, webResearchLifecycle, ctx.cwd, {
11269
+ addAvailable: true
10216
11270
  });
10217
- await syncManagedToolAvailability(pi, nextActiveTools);
10218
11271
  }
10219
- async function refreshManagedToolsOnStartup(pi, webResearchLifecycle, cwd, options) {
10220
- try {
10221
- await refreshManagedTools(pi, webResearchLifecycle, cwd, options);
10222
- } catch (error) {
10223
- const message = formatStartupConfigError(error);
10224
- pi.sendMessage({
10225
- customType: "web-providers-config-error",
10226
- content: message,
10227
- display: true
10228
- });
10229
- await syncManagedToolAvailability(
10230
- pi,
10231
- new Set(
10232
- pi.getActiveTools().filter((toolName) => !MANAGED_TOOL_NAMES.includes(toolName))
10233
- )
10234
- );
10235
- }
11272
+ async function refreshManagedTools2(pi, webResearchLifecycle, cwd, options) {
11273
+ await refreshManagedTools(
11274
+ pi,
11275
+ (providerIdsByCapability) => registerManagedTools(pi, webResearchLifecycle, providerIdsByCapability),
11276
+ cwd,
11277
+ options
11278
+ );
10236
11279
  }
10237
- async function syncManagedToolAvailability(pi, nextActiveTools) {
10238
- const activeTools = pi.getActiveTools();
10239
- const changed = activeTools.length !== nextActiveTools.size || activeTools.some((toolName) => !nextActiveTools.has(toolName));
10240
- if (changed) {
10241
- pi.setActiveTools(Array.from(nextActiveTools));
10242
- }
11280
+ async function refreshManagedToolsOnStartup2(pi, webResearchLifecycle, cwd, options) {
11281
+ await refreshManagedToolsOnStartup(
11282
+ pi,
11283
+ (providerIdsByCapability) => registerManagedTools(pi, webResearchLifecycle, providerIdsByCapability),
11284
+ cwd,
11285
+ options
11286
+ );
10243
11287
  }
10244
11288
  function getSearchMaxResultsLimit(providerId) {
10245
11289
  const capabilities = PROVIDERS_BY_ID[providerId].capabilities;
@@ -10456,12 +11500,12 @@ async function executeRawProviderRequest({
10456
11500
  input
10457
11501
  });
10458
11502
  }
10459
- function buildSearchBatchError(outcomes, providerLabel) {
11503
+ function buildSearchBatchError(outcomes, providerLabel2) {
10460
11504
  const failed = outcomes.filter((outcome) => outcome.error !== void 0);
10461
11505
  if (failed.length === 1) {
10462
11506
  return new Error(
10463
11507
  formatProviderCapabilityFailure(
10464
- providerLabel,
11508
+ providerLabel2,
10465
11509
  "search",
10466
11510
  failed[0]?.error ?? ""
10467
11511
  )
@@ -10471,7 +11515,7 @@ function buildSearchBatchError(outcomes, providerLabel) {
10471
11515
  (outcome, index) => `${index + 1}. ${formatQuotedPreview(outcome.query, 40)} \u2014 ${outcome.error}`
10472
11516
  ).join("; ");
10473
11517
  return new Error(
10474
- `${providerLabel} search failed for ${failed.length} queries: ${summary}`
11518
+ `${providerLabel2} search failed for ${failed.length} queries: ${summary}`
10475
11519
  );
10476
11520
  }
10477
11521
  async function executeSingleSearchQuery({
@@ -10608,12 +11652,12 @@ async function executeAnswerToolInternal({
10608
11652
  })
10609
11653
  };
10610
11654
  }
10611
- function buildAnswerBatchError(outcomes, providerLabel) {
11655
+ function buildAnswerBatchError(outcomes, providerLabel2) {
10612
11656
  const failed = outcomes.filter((outcome) => outcome.error !== void 0);
10613
11657
  if (failed.length === 1) {
10614
11658
  return new Error(
10615
11659
  formatProviderCapabilityFailure(
10616
- providerLabel,
11660
+ providerLabel2,
10617
11661
  "answer",
10618
11662
  failed[0]?.error ?? ""
10619
11663
  )
@@ -10623,7 +11667,7 @@ function buildAnswerBatchError(outcomes, providerLabel) {
10623
11667
  (outcome, index) => `${index + 1}. ${formatQuotedPreview(outcome.query, 40)} \u2014 ${outcome.error}`
10624
11668
  ).join("; ");
10625
11669
  return new Error(
10626
- `${providerLabel} answer failed for ${failed.length} questions: ${summary}`
11670
+ `${providerLabel2} answer failed for ${failed.length} questions: ${summary}`
10627
11671
  );
10628
11672
  }
10629
11673
  function formatAnswerResponses(outcomes) {
@@ -10839,7 +11883,7 @@ async function executeProviderToolInternal({
10839
11883
  })
10840
11884
  };
10841
11885
  }
10842
- async function dispatchWebResearch({
11886
+ async function dispatchWebResearch2({
10843
11887
  pi,
10844
11888
  activeWebResearchRequests,
10845
11889
  updateWebResearchWidget,
@@ -10868,188 +11912,59 @@ async function dispatchWebResearchInternal({
10868
11912
  input,
10869
11913
  executionOverride
10870
11914
  }) {
10871
- await cleanupContentStore();
10872
- const provider = resolveProviderForTool(
11915
+ return dispatchWebResearch({
11916
+ activeWebResearchRequests,
10873
11917
  config,
10874
- ctx.cwd,
10875
- "research",
10876
- explicitProvider
10877
- );
10878
- const request = createWebResearchRequest(ctx.cwd, provider.id, input);
10879
- const providerConfig = getEffectiveProviderConfig(config, provider.id);
10880
- activeWebResearchRequests.set(request.id, request);
10881
- updateWebResearchWidget(ctx);
10882
- trackPendingResearchTask(
10883
- runDispatchedWebResearch({
10884
- pi,
10885
- activeWebResearchRequests,
10886
- updateWebResearchWidget,
10887
- request,
10888
- config,
11918
+ explicitProvider,
11919
+ ctx: { cwd: ctx.cwd },
11920
+ options: providerOptions,
11921
+ input,
11922
+ executionOverride,
11923
+ executeResearch: async ({
11924
+ config: config2,
10889
11925
  provider,
10890
11926
  providerConfig,
10891
- ctx,
10892
- options: providerOptions,
10893
- executionOverride
10894
- })
10895
- );
10896
- return {
10897
- content: [
10898
- {
10899
- type: "text",
10900
- text: `Started web research via ${provider.label}.`
10901
- }
10902
- ],
10903
- details: request,
10904
- display: buildProviderToolDisplay2({
10905
- capability: "research",
10906
- providerId: provider.id,
10907
- details: { tool: "web_research", provider: provider.id },
10908
- text: "started"
10909
- })
10910
- };
10911
- }
10912
- async function runDispatchedWebResearch({
10913
- pi,
10914
- activeWebResearchRequests,
10915
- updateWebResearchWidget,
10916
- request,
10917
- config,
10918
- provider,
10919
- providerConfig,
10920
- ctx,
10921
- options,
10922
- executionOverride
10923
- }) {
10924
- let result;
10925
- let reportText = "";
10926
- try {
10927
- const response = await executeProviderOperation({
11927
+ ctx: ctx2,
11928
+ signal,
11929
+ options,
11930
+ input: input2,
11931
+ onProgress,
11932
+ executionOverride: executionOverride2
11933
+ }) => executeProviderOperation({
10928
11934
  capability: "research",
10929
- config,
11935
+ config: config2,
10930
11936
  provider,
10931
11937
  providerConfig,
10932
- ctx,
10933
- signal: void 0,
11938
+ ctx: ctx2,
11939
+ signal,
10934
11940
  options,
10935
- input: request.input,
10936
- onProgress: (message) => {
10937
- request.progress = summarizeWebResearchProgress(
10938
- message,
10939
- provider.label
10940
- );
10941
- updateWebResearchWidget();
10942
- },
10943
- executionOverride
10944
- });
10945
- const completedAt = (/* @__PURE__ */ new Date()).toISOString();
10946
- result = {
10947
- ...request,
10948
- status: "completed",
10949
- completedAt,
10950
- elapsedMs: Math.max(
10951
- 0,
10952
- Date.parse(completedAt) - Date.parse(request.startedAt)
10953
- ),
10954
- itemCount: response.itemCount
10955
- };
10956
- reportText = response.text;
10957
- } catch (error) {
10958
- const completedAt = (/* @__PURE__ */ new Date()).toISOString();
10959
- result = {
10960
- ...request,
10961
- status: "failed",
10962
- completedAt,
10963
- elapsedMs: Math.max(
10964
- 0,
10965
- Date.parse(completedAt) - Date.parse(request.startedAt)
10966
- ),
10967
- error: formatErrorMessage(error)
10968
- };
10969
- }
10970
- try {
10971
- await writeWebResearchArtifact(result, reportText);
10972
- pi.sendMessage({
10973
- customType: WEB_RESEARCH_RESULT_MESSAGE_TYPE,
10974
- content: formatWebResearchResultMessage(result, reportText),
10975
- display: true,
10976
- details: result
10977
- });
10978
- } finally {
10979
- activeWebResearchRequests.delete(request.id);
10980
- updateWebResearchWidget();
10981
- }
10982
- }
10983
- function createWebResearchRequest(cwd, provider, input) {
10984
- const startedAt = (/* @__PURE__ */ new Date()).toISOString();
10985
- return {
10986
- tool: "web_research",
10987
- id: randomUUID(),
10988
- provider,
10989
- input,
10990
- outputPath: buildWebResearchArtifactPath(cwd, input, startedAt),
10991
- startedAt
10992
- };
10993
- }
10994
- function buildWebResearchArtifactPath(cwd, input, startedAt) {
10995
- const timestamp = startedAt.replaceAll(":", "-").replace(".", "-");
10996
- const slug = slugifyWebResearchInput(input);
10997
- return join2(cwd, RESEARCH_ARTIFACTS_DIR, `${timestamp}-${slug}.md`);
10998
- }
10999
- function slugifyWebResearchInput(input) {
11000
- const slug = input.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60).replace(/-+$/g, "");
11001
- return slug.length > 0 ? slug : "research";
11941
+ input: input2,
11942
+ onProgress,
11943
+ executionOverride: executionOverride2
11944
+ }),
11945
+ deliverResult: (message) => pi.sendMessage(message),
11946
+ onJobsChanged: () => updateWebResearchWidget(ctx),
11947
+ resultMessageType: WEB_RESEARCH_RESULT_MESSAGE_TYPE
11948
+ });
11002
11949
  }
11003
11950
  function buildWebResearchWidgetLines(requests, theme, now = Date.now()) {
11004
- const lines = [theme.fg("accent", "Research jobs:")];
11005
- for (const request of requests.slice().sort((left, right) => left.startedAt.localeCompare(right.startedAt)).slice(0, 3)) {
11006
- const providerLabel = PROVIDERS_BY_ID[request.provider]?.label ?? request.provider;
11007
- const elapsed = formatCompactElapsed(now - Date.parse(request.startedAt));
11008
- const icon = getWebResearchWidgetIcon(request, now);
11009
- lines.push(
11010
- `${icon}${providerLabel} ${theme.fg("muted", `(${elapsed}): `)}${truncateInline(cleanSingleLine(request.input), 70)}`
11011
- );
11012
- }
11013
- if (requests.length > 3) {
11014
- lines.push(theme.fg("muted", `+${requests.length - 3} more`));
11015
- }
11016
- return lines;
11017
- }
11018
- function getWebResearchWidgetIcon(request, _now) {
11019
- if (request.progress === "poll retrying after transient errors") {
11020
- return "\u27F3 ";
11021
- }
11022
- if (request.progress === "queued") {
11023
- return "\u25CC ";
11024
- }
11025
- if (request.progress === "starting") {
11026
- return "\u25D4 ";
11027
- }
11028
- if (request.progress?.startsWith("started:")) {
11029
- return "\u25D1 ";
11030
- }
11031
- return "\u25CF ";
11032
- }
11033
- function summarizeWebResearchProgress(message, providerLabel) {
11034
- const startingMessage = `Starting research via ${providerLabel}`;
11035
- if (message === startingMessage) {
11036
- return "starting";
11037
- }
11038
- const startedPrefix = `${providerLabel} research started: `;
11039
- if (message.startsWith(startedPrefix)) {
11040
- return `started: ${message.slice(startedPrefix.length)}`;
11041
- }
11042
- const statusPrefix = `Research via ${providerLabel}: `;
11043
- if (message.startsWith(statusPrefix)) {
11044
- return message.slice(statusPrefix.length).replace(/\s+\([^)]* elapsed\)$/u, "").trim();
11045
- }
11046
- const retryPrefix = `${providerLabel} research poll is still retrying after transient errors`;
11047
- if (message.startsWith(retryPrefix)) {
11048
- return "poll retrying after transient errors";
11951
+ const sorted = requests.slice().sort((left, right) => left.startedAt.localeCompare(right.startedAt));
11952
+ const jobs = sorted.slice(0, 3).map((request) => {
11953
+ const providerLabel2 = PROVIDERS_BY_ID[request.provider]?.label ?? request.provider;
11954
+ const elapsed = formatCompactElapsed2(now - Date.parse(request.startedAt));
11955
+ const icon = getWebResearchProgressIcon(request);
11956
+ const status = request.progress === "cancelling" ? " cancelling" : "";
11957
+ return `${icon} ${providerLabel2} ${elapsed}${status}`;
11958
+ });
11959
+ if (sorted.length > 3) {
11960
+ jobs.push(`+${sorted.length - 3} more`);
11049
11961
  }
11050
- return message.trim();
11962
+ const count = sorted.length === 1 ? "1 research" : `${sorted.length} researches`;
11963
+ return [
11964
+ `${theme.fg("accent", `${count} running`)}${theme.fg("muted", ` \xB7 ${jobs.join(" \xB7 ")} \xB7 /web-research`)}`
11965
+ ];
11051
11966
  }
11052
- function formatCompactElapsed(ms) {
11967
+ function formatCompactElapsed2(ms) {
11053
11968
  const totalSeconds = Math.max(0, Math.floor(ms / 1e3));
11054
11969
  const minutes = Math.floor(totalSeconds / 60);
11055
11970
  const seconds = totalSeconds % 60;
@@ -11058,68 +11973,6 @@ function formatCompactElapsed(ms) {
11058
11973
  }
11059
11974
  return `${totalSeconds}s`;
11060
11975
  }
11061
- function formatWebResearchResultMessage(result, reportText) {
11062
- const text = reportText.trim();
11063
- if (text.length > 0) {
11064
- return `${text}
11065
- `;
11066
- }
11067
- if (result.error) {
11068
- return `${result.error}
11069
- `;
11070
- }
11071
- return "";
11072
- }
11073
- async function writeWebResearchArtifact(result, reportText) {
11074
- await mkdir2(dirname2(result.outputPath), { recursive: true });
11075
- await writeFile2(
11076
- result.outputPath,
11077
- formatWebResearchArtifact(result, reportText),
11078
- "utf-8"
11079
- );
11080
- }
11081
- function formatWebResearchArtifact(result, reportText) {
11082
- const providerLabel = PROVIDERS_BY_ID[result.provider]?.label ?? result.provider;
11083
- const lines = [
11084
- "# Web research report",
11085
- "",
11086
- "## Query",
11087
- result.input,
11088
- "",
11089
- "## Provider",
11090
- providerLabel,
11091
- "",
11092
- "## Status",
11093
- result.status,
11094
- "",
11095
- "## Started",
11096
- result.startedAt,
11097
- "",
11098
- "## Completed",
11099
- result.completedAt,
11100
- "",
11101
- "## Elapsed",
11102
- formatElapsed(result.elapsedMs)
11103
- ];
11104
- if (typeof result.itemCount === "number") {
11105
- lines.push("", "## Items", String(result.itemCount));
11106
- }
11107
- if (result.error) {
11108
- lines.push("", "## Error", result.error);
11109
- }
11110
- if (reportText) {
11111
- lines.push("", "## Report", reportText);
11112
- }
11113
- return `${lines.join("\n")}
11114
- `;
11115
- }
11116
- function trackPendingResearchTask(task) {
11117
- const tracked = task.catch(() => {
11118
- }).finally(() => {
11119
- pendingResearchTasks.delete(tracked);
11120
- });
11121
- pendingResearchTasks.add(tracked);
11122
- }
11123
11976
  async function executeBatchedContentsTool({
11124
11977
  config,
11125
11978
  provider,
@@ -11281,10 +12134,10 @@ function createToolProgressReporter(capability, providerId, progress) {
11281
12134
  if (Date.now() - lastUpdateAt < RESEARCH_HEARTBEAT_MS) {
11282
12135
  return;
11283
12136
  }
11284
- const providerLabel = PROVIDERS_BY_ID[providerId]?.label ?? providerId;
12137
+ const providerLabel2 = PROVIDERS_BY_ID[providerId]?.label ?? providerId;
11285
12138
  const elapsed = formatElapsed(Date.now() - startedAt);
11286
12139
  emit(
11287
- `Researching via ${providerLabel} (${elapsed} elapsed)`,
12140
+ `Researching via ${providerLabel2} (${elapsed} elapsed)`,
11288
12141
  buildProgressDisplay2(providerId, `Researching ${elapsed}`)
11289
12142
  );
11290
12143
  lastUpdateAt = Date.now();
@@ -11307,38 +12160,38 @@ function renderListCallHeader(toolName, items, theme, suffix, options = {}) {
11307
12160
  invalidate() {
11308
12161
  },
11309
12162
  render(width) {
11310
- const normalizedItems = items.map((item) => cleanSingleLine(item)).filter((item) => item.length > 0);
12163
+ const normalizedItems = items.map((item) => cleanSingleLine2(item)).filter((item) => item.length > 0);
11311
12164
  const toolTitle = theme.fg("toolTitle", theme.bold(toolName));
11312
12165
  const mutedSuffix = suffix ? theme.fg("muted", suffix) : "";
11313
12166
  if (!options.forceMultiline && normalizedItems.length === 1) {
11314
12167
  const singleItem = options.quoteSingleItem ? formatQuotedPreview(normalizedItems[0], 80) : truncateInline(normalizedItems[0], 120);
11315
12168
  const inline = `${toolTitle} ${theme.fg("accent", singleItem)}${mutedSuffix}`;
11316
- const line = truncateToWidth(inline.trimEnd(), width);
11317
- return [line + " ".repeat(Math.max(0, width - visibleWidth(line)))];
12169
+ const line = truncateToWidth2(inline.trimEnd(), width);
12170
+ return [line + " ".repeat(Math.max(0, width - visibleWidth2(line)))];
11318
12171
  }
11319
12172
  let header = toolTitle;
11320
12173
  if (mutedSuffix) {
11321
12174
  header += mutedSuffix;
11322
12175
  }
11323
12176
  const lines = [];
11324
- const headerLine = truncateToWidth(header.trimEnd(), width);
12177
+ const headerLine = truncateToWidth2(header.trimEnd(), width);
11325
12178
  lines.push(
11326
- headerLine + " ".repeat(Math.max(0, width - visibleWidth(headerLine)))
12179
+ headerLine + " ".repeat(Math.max(0, width - visibleWidth2(headerLine)))
11327
12180
  );
11328
12181
  for (const item of normalizedItems) {
11329
12182
  const itemLines = options.forceMultiline ? wrapTextWithAnsi(
11330
12183
  theme.fg("accent", item),
11331
12184
  Math.max(1, width - 2)
11332
12185
  ).map((line) => ` ${line}`) : [
11333
- truncateToWidth(
12186
+ truncateToWidth2(
11334
12187
  ` ${theme.fg("accent", truncateInline(item, 120))}`,
11335
12188
  width
11336
12189
  )
11337
12190
  ];
11338
12191
  for (const itemLine of itemLines) {
11339
- const line = truncateToWidth(itemLine, width);
12192
+ const line = truncateToWidth2(itemLine, width);
11340
12193
  lines.push(
11341
- line + " ".repeat(Math.max(0, width - visibleWidth(line)))
12194
+ line + " ".repeat(Math.max(0, width - visibleWidth2(line)))
11342
12195
  );
11343
12196
  }
11344
12197
  }
@@ -11433,7 +12286,8 @@ function renderWebResearchResultMessage(message, { expanded }, theme, symbols =
11433
12286
  const text = typeof message.content === "string" ? message.content : extractTextContent(message.content);
11434
12287
  const details = isWebResearchResult(message.details) ? message.details : void 0;
11435
12288
  const isSuccess = details?.status === "completed";
11436
- const accent = isSuccess ? "success" : "error";
12289
+ const isCancelled = details?.status === "cancelled";
12290
+ const accent = isSuccess ? "success" : isCancelled ? "warning" : "error";
11437
12291
  const box = new Box(1, 1, (value) => theme.bg("customMessageBg", value));
11438
12292
  if (!expanded) {
11439
12293
  const summary = details ? buildWebResearchResultSummaryLine(details, theme, symbols) : theme.fg(accent, "Web research update");
@@ -11443,10 +12297,42 @@ function renderWebResearchResultMessage(message, { expanded }, theme, symbols =
11443
12297
  return box;
11444
12298
  }
11445
12299
  box.addChild(
11446
- details ? renderMarkdownBlock(renderWebResearchResultMarkdown(details)) : isSuccess ? renderMarkdownBlock(text ?? "") : renderBlockText(text ?? "", theme, "error")
12300
+ details ? renderMarkdownBlock(renderWebResearchResultMarkdown(details)) : isSuccess ? renderMarkdownBlock(text ?? "") : renderBlockText(
12301
+ text ?? "",
12302
+ theme,
12303
+ isCancelled ? "toolOutput" : "error"
12304
+ )
11447
12305
  );
11448
12306
  return box;
11449
12307
  }
12308
+ function formatWebResearchReportMessage(title, body, item) {
12309
+ const provenance = `Saved web research report "${title}" (provider: ${item.provider || "unknown"}, status: ${item.status}, artifact: \`${item.outputPath}\`):`;
12310
+ return `${provenance}
12311
+
12312
+ ${body}
12313
+ `;
12314
+ }
12315
+ function renderWebResearchReportMessage(message, { expanded }, theme) {
12316
+ const text = typeof message.content === "string" ? message.content : extractTextContent(message.content);
12317
+ const details = isWebResearchReportDetails(message.details) ? message.details : void 0;
12318
+ const box = new Box(1, 1, (value) => theme.bg("customMessageBg", value));
12319
+ if (!expanded) {
12320
+ const title = details?.title ?? "saved report";
12321
+ box.addChild(
12322
+ new Text(
12323
+ `${theme.fg("success", `Injected research report: ${title}`)}${theme.fg("muted", ` (${getExpandHint()})`)}`,
12324
+ 0,
12325
+ 0
12326
+ )
12327
+ );
12328
+ return box;
12329
+ }
12330
+ box.addChild(renderMarkdownBlock(text ?? ""));
12331
+ return box;
12332
+ }
12333
+ function isWebResearchReportDetails(details) {
12334
+ return typeof details === "object" && details !== null && "title" in details && "outputPath" in details && !("tool" in details);
12335
+ }
11450
12336
  function renderWebResearchRequestMarkdown(request) {
11451
12337
  return [
11452
12338
  "### Web research",
@@ -11472,7 +12358,7 @@ function renderWebResearchResultMarkdown(result) {
11472
12358
  ].join("\n");
11473
12359
  }
11474
12360
  function buildWebResearchResultSummaryLine(result, theme, symbols) {
11475
- const providerLabel = PROVIDERS_BY_ID[result.provider]?.label ?? result.provider;
12361
+ const providerLabel2 = PROVIDERS_BY_ID[result.provider]?.label ?? result.provider;
11476
12362
  if (result.status === "completed") {
11477
12363
  return renderSuccessSummary(
11478
12364
  `${formatSummaryElapsed(result.elapsedMs)} \xB7 ${basename(result.outputPath)}`,
@@ -11480,8 +12366,8 @@ function buildWebResearchResultSummaryLine(result, theme, symbols) {
11480
12366
  symbols
11481
12367
  );
11482
12368
  }
11483
- const statusText = result.status === "cancelled" ? `${providerLabel} research canceled after ${formatSummaryElapsed(result.elapsedMs)}` : `${providerLabel} research failed after ${formatSummaryElapsed(result.elapsedMs)}`;
11484
- const errorSuffix = result.error ? `: ${normalizeProviderFailureDetail(providerLabel, result.error)}` : "";
12369
+ const statusText = result.status === "cancelled" ? `${providerLabel2} research canceled after ${formatSummaryElapsed(result.elapsedMs)}` : `${providerLabel2} research failed after ${formatSummaryElapsed(result.elapsedMs)}`;
12370
+ const errorSuffix = result.error ? `: ${normalizeProviderFailureDetail(providerLabel2, result.error)}` : "";
11485
12371
  return renderFailureSummary(`${statusText}${errorSuffix}`, theme, symbols);
11486
12372
  }
11487
12373
  function isWebResearchRequest(details) {
@@ -11580,7 +12466,7 @@ function renderEntryList(width, theme, entries, selection) {
11580
12466
  const paddedLabel = entry.label.padEnd(labelWidth, " ");
11581
12467
  const label = selected ? theme.fg("accent", paddedLabel) : paddedLabel;
11582
12468
  const value = selected ? theme.fg("accent", entry.currentValue) : theme.fg("muted", entry.currentValue);
11583
- return truncateToWidth(`${prefix}${label} ${value}`, width);
12469
+ return truncateToWidth2(`${prefix}${label} ${value}`, width);
11584
12470
  });
11585
12471
  }
11586
12472
  function renderSelectedEntryDescription(width, theme, entry) {
@@ -11588,7 +12474,7 @@ function renderSelectedEntryDescription(width, theme, entry) {
11588
12474
  return [];
11589
12475
  }
11590
12476
  return wrapTextWithAnsi(entry.description, Math.max(10, width - 2)).map(
11591
- (line) => truncateToWidth(theme.fg("dim", line), width)
12477
+ (line) => truncateToWidth2(theme.fg("dim", line), width)
11592
12478
  );
11593
12479
  }
11594
12480
  function formatProviderCapabilityChecks(providerId, theme) {
@@ -11769,7 +12655,7 @@ var WebProvidersSettingsView = class {
11769
12655
  }
11770
12656
  lines.push("");
11771
12657
  lines.push(
11772
- truncateToWidth(
12658
+ truncateToWidth2(
11773
12659
  this.theme.fg(
11774
12660
  "dim",
11775
12661
  "\u2191\u2193 move \xB7 Tab/Shift+Tab switch section \xB7 Enter edit/open \xB7 Esc close"
@@ -11788,7 +12674,7 @@ var WebProvidersSettingsView = class {
11788
12674
  this.tui.requestRender();
11789
12675
  return;
11790
12676
  }
11791
- const kb = getKeybindings();
12677
+ const kb = getKeybindings2();
11792
12678
  const entries = this.getActiveSectionEntries();
11793
12679
  if (kb.matches(data, "tui.select.up")) {
11794
12680
  if (entries.length > 0) {
@@ -11798,9 +12684,9 @@ var WebProvidersSettingsView = class {
11798
12684
  if (entries.length > 0) {
11799
12685
  this.moveSelection(1);
11800
12686
  }
11801
- } else if (matchesKey(data, Key.tab)) {
12687
+ } else if (matchesKey2(data, Key2.tab)) {
11802
12688
  this.moveSection(1);
11803
- } else if (matchesKey(data, Key.shift("tab"))) {
12689
+ } else if (matchesKey2(data, Key2.shift("tab"))) {
11804
12690
  this.moveSection(-1);
11805
12691
  } else if (kb.matches(data, "tui.select.confirm") || data === " ") {
11806
12692
  void this.activateCurrentEntry();
@@ -11933,14 +12819,14 @@ var WebProvidersSettingsView = class {
11933
12819
  Math.max(20, Math.floor(width * 0.45))
11934
12820
  );
11935
12821
  const lines = [
11936
- truncateToWidth(
12822
+ truncateToWidth2(
11937
12823
  this.activeSection === section ? this.theme.fg("accent", this.theme.bold(title)) : this.theme.bold(title),
11938
12824
  width
11939
12825
  )
11940
12826
  ];
11941
12827
  if (section === "provider") {
11942
12828
  lines.push(
11943
- truncateToWidth(
12829
+ truncateToWidth2(
11944
12830
  this.theme.fg(
11945
12831
  "dim",
11946
12832
  ` ${"Provider".padEnd(labelWidth, " ")} S C A R Status`
@@ -11955,15 +12841,15 @@ var WebProvidersSettingsView = class {
11955
12841
  const paddedLabel = entry.label.padEnd(labelWidth, " ");
11956
12842
  const label = selected ? this.theme.fg("accent", paddedLabel) : paddedLabel;
11957
12843
  if (entry.currentValue.trim().length === 0) {
11958
- lines.push(truncateToWidth(`${prefix}${label}`, width));
12844
+ lines.push(truncateToWidth2(`${prefix}${label}`, width));
11959
12845
  continue;
11960
12846
  }
11961
12847
  const value = entry.preserveValueStyle ? entry.currentValue : selected ? this.theme.fg("accent", entry.currentValue) : this.theme.fg("muted", entry.currentValue);
11962
- lines.push(truncateToWidth(`${prefix}${label} ${value}`, width));
12848
+ lines.push(truncateToWidth2(`${prefix}${label} ${value}`, width));
11963
12849
  }
11964
12850
  if (section === "provider") {
11965
12851
  lines.push(
11966
- truncateToWidth(
12852
+ truncateToWidth2(
11967
12853
  this.theme.fg("dim", " S=Search C=Contents A=Answer R=Research"),
11968
12854
  width
11969
12855
  )
@@ -12098,7 +12984,7 @@ var ToolSettingsSubmenu = class {
12098
12984
  }
12099
12985
  const entries = this.getEntries();
12100
12986
  const lines = [
12101
- truncateToWidth(
12987
+ truncateToWidth2(
12102
12988
  this.theme.fg("accent", TOOL_INFO[this.toolId].label),
12103
12989
  width
12104
12990
  ),
@@ -12114,7 +13000,7 @@ var ToolSettingsSubmenu = class {
12114
13000
  }
12115
13001
  lines.push("");
12116
13002
  lines.push(
12117
- truncateToWidth(
13003
+ truncateToWidth2(
12118
13004
  this.theme.fg("dim", "\u2191\u2193 move \xB7 Enter edit/toggle \xB7 Esc back"),
12119
13005
  width
12120
13006
  )
@@ -12130,7 +13016,7 @@ var ToolSettingsSubmenu = class {
12130
13016
  this.tui.requestRender();
12131
13017
  return;
12132
13018
  }
12133
- const kb = getKeybindings();
13019
+ const kb = getKeybindings2();
12134
13020
  const entries = this.getEntries();
12135
13021
  if (kb.matches(data, "tui.select.up")) {
12136
13022
  if (this.selection > 0) {
@@ -12333,7 +13219,7 @@ var ProviderSettingsSubmenu = class {
12333
13219
  const providerConfig = this.getProviderConfig();
12334
13220
  const entries = this.getEntries();
12335
13221
  const lines = [
12336
- truncateToWidth(this.theme.fg("accent", provider.label), width),
13222
+ truncateToWidth2(this.theme.fg("accent", provider.label), width),
12337
13223
  "",
12338
13224
  ...renderEntryList(width, this.theme, entries, this.selection)
12339
13225
  ];
@@ -12350,10 +13236,10 @@ var ProviderSettingsSubmenu = class {
12350
13236
  );
12351
13237
  lines.push("");
12352
13238
  lines.push(
12353
- truncateToWidth(this.theme.fg("dim", `Status: ${status}`), width)
13239
+ truncateToWidth2(this.theme.fg("dim", `Status: ${status}`), width)
12354
13240
  );
12355
13241
  lines.push(
12356
- truncateToWidth(
13242
+ truncateToWidth2(
12357
13243
  this.theme.fg("dim", "\u2191\u2193 move \xB7 Enter edit/toggle \xB7 Esc back"),
12358
13244
  width
12359
13245
  )
@@ -12369,7 +13255,7 @@ var ProviderSettingsSubmenu = class {
12369
13255
  this.tui.requestRender();
12370
13256
  return;
12371
13257
  }
12372
- const kb = getKeybindings();
13258
+ const kb = getKeybindings2();
12373
13259
  const entries = this.getEntries();
12374
13260
  if (kb.matches(data, "tui.select.up")) {
12375
13261
  if (this.selection > 0) {
@@ -12504,12 +13390,12 @@ var TextValueSubmenu = class {
12504
13390
  editor;
12505
13391
  render(width) {
12506
13392
  return [
12507
- truncateToWidth(this.theme.fg("accent", this.title), width),
13393
+ truncateToWidth2(this.theme.fg("accent", this.title), width),
12508
13394
  "",
12509
13395
  ...this.editor.render(width),
12510
13396
  "",
12511
- truncateToWidth(this.theme.fg("dim", this.help), width),
12512
- truncateToWidth(
13397
+ truncateToWidth2(this.theme.fg("dim", this.help), width),
13398
+ truncateToWidth2(
12513
13399
  this.theme.fg(
12514
13400
  "dim",
12515
13401
  "Enter to save \xB7 Shift+Enter for newline \xB7 Esc to cancel"
@@ -12522,7 +13408,7 @@ var TextValueSubmenu = class {
12522
13408
  this.editor.invalidate();
12523
13409
  }
12524
13410
  handleInput(data) {
12525
- if (matchesKey(data, Key.escape)) {
13411
+ if (matchesKey2(data, Key2.escape)) {
12526
13412
  this.done(void 0);
12527
13413
  return;
12528
13414
  }
@@ -12646,7 +13532,7 @@ function getSearchQueriesForDisplay(queries) {
12646
13532
  function getAnswerQueriesForDisplay(queries) {
12647
13533
  return getSearchQueriesForDisplay(queries);
12648
13534
  }
12649
- function createBatchCompletionReporter(verb, providerId, providerLabel, total, report) {
13535
+ function createBatchCompletionReporter(verb, providerId, providerLabel2, total, report) {
12650
13536
  if (!report) {
12651
13537
  return {
12652
13538
  start: () => {
@@ -12660,7 +13546,7 @@ function createBatchCompletionReporter(verb, providerId, providerLabel, total, r
12660
13546
  let completedCount = 0;
12661
13547
  let failedCount = 0;
12662
13548
  const emit = () => {
12663
- let message = `${verb} via ${providerLabel}: ${completedCount}/${total} completed`;
13549
+ let message = `${verb} via ${providerLabel2}: ${completedCount}/${total} completed`;
12664
13550
  if (failedCount > 0) {
12665
13551
  message += `, ${failedCount} failed`;
12666
13552
  }
@@ -12712,8 +13598,8 @@ function renderMarkdownBlock(text) {
12712
13598
  if (!text) {
12713
13599
  return new Text("", 0, 0);
12714
13600
  }
12715
- return new Markdown(`
12716
- ${text}`, 0, 0, getMarkdownTheme());
13601
+ return new Markdown2(`
13602
+ ${text}`, 0, 0, getMarkdownTheme2());
12717
13603
  }
12718
13604
  function renderBlockText(text, theme, color) {
12719
13605
  if (!text) {
@@ -12747,12 +13633,12 @@ function prefixWithSymbol(text, symbol) {
12747
13633
  }
12748
13634
  function renderToolProgress(display, fallbackText, theme) {
12749
13635
  const progress = display?.progress;
12750
- const providerLabel = display?.provider?.label;
12751
- if (!progress || !providerLabel) {
13636
+ const providerLabel2 = display?.provider?.label;
13637
+ if (!progress || !providerLabel2) {
12752
13638
  return renderSimpleText(fallbackText ?? "Working\u2026", theme, "warning");
12753
13639
  }
12754
13640
  return new Text(
12755
- `${theme.fg("warning", progress.action)} ${theme.fg("muted", `via ${providerLabel}`)}`,
13641
+ `${theme.fg("warning", progress.action)} ${theme.fg("muted", `via ${providerLabel2}`)}`,
12756
13642
  0,
12757
13643
  0
12758
13644
  );
@@ -12804,15 +13690,15 @@ function buildFailureSummary({
12804
13690
  fallback
12805
13691
  }) {
12806
13692
  const detail = stripTrailingSentencePunctuation(getFirstLine2(text) ?? "");
12807
- const providerLabel = details?.provider !== void 0 ? PROVIDERS_BY_ID[details.provider]?.label ?? details.provider : void 0;
12808
- if (!providerLabel) {
13693
+ const providerLabel2 = details?.provider !== void 0 ? PROVIDERS_BY_ID[details.provider]?.label ?? details.provider : void 0;
13694
+ if (!providerLabel2) {
12809
13695
  return detail || fallback;
12810
13696
  }
12811
- return formatProviderCapabilityFailure(providerLabel, capability, detail);
13697
+ return formatProviderCapabilityFailure(providerLabel2, capability, detail);
12812
13698
  }
12813
- function formatProviderCapabilityFailure(providerLabel, capability, detail) {
13699
+ function formatProviderCapabilityFailure(providerLabel2, capability, detail) {
12814
13700
  const action = getFailureAction(capability);
12815
- const base2 = `${providerLabel} ${action} failed`;
13701
+ const base2 = `${providerLabel2} ${action} failed`;
12816
13702
  if (!detail || detail === base2) {
12817
13703
  return base2;
12818
13704
  }
@@ -12820,14 +13706,14 @@ function formatProviderCapabilityFailure(providerLabel, capability, detail) {
12820
13706
  return detail;
12821
13707
  }
12822
13708
  const normalizedDetail = normalizeProviderFailureDetail(
12823
- providerLabel,
13709
+ providerLabel2,
12824
13710
  detail
12825
13711
  );
12826
13712
  return `${base2}: ${normalizedDetail}`;
12827
13713
  }
12828
- function normalizeProviderFailureDetail(providerLabel, detail) {
13714
+ function normalizeProviderFailureDetail(providerLabel2, detail) {
12829
13715
  const normalized = stripTrailingSentencePunctuation(detail);
12830
- const providerPrefix = `${providerLabel}:`;
13716
+ const providerPrefix = `${providerLabel2}:`;
12831
13717
  return normalized.toLowerCase().startsWith(providerPrefix.toLowerCase()) ? normalized.slice(providerPrefix.length).trim() : normalized;
12832
13718
  }
12833
13719
  function getFailureAction(capability) {
@@ -12876,7 +13762,7 @@ function getFirstLine2(text) {
12876
13762
  }
12877
13763
  function getExpandHint() {
12878
13764
  try {
12879
- const keys = getKeybindings().getKeys("app.tools.expand");
13765
+ const keys = getKeybindings2().getKeys("app.tools.expand");
12880
13766
  if (keys.length > 0) {
12881
13767
  return `${keys.join("/")} to expand`;
12882
13768
  }
@@ -12884,11 +13770,11 @@ function getExpandHint() {
12884
13770
  }
12885
13771
  return "ctrl+o to expand";
12886
13772
  }
12887
- function cleanSingleLine(text) {
13773
+ function cleanSingleLine2(text) {
12888
13774
  return text.replace(/\s+/g, " ").trim();
12889
13775
  }
12890
13776
  function formatQuotedPreview(text, maxLength = 80) {
12891
- return `"${truncateInline(cleanSingleLine(text), maxLength)}"`;
13777
+ return `"${truncateInline(cleanSingleLine2(text), maxLength)}"`;
12892
13778
  }
12893
13779
  function formatSearchResponses(outcomes, prefetch) {
12894
13780
  const body = outcomes.map(
@@ -12914,10 +13800,10 @@ function formatSearchOutcomeSection(outcome, index, total) {
12914
13800
  ${body}`;
12915
13801
  }
12916
13802
  function formatSearchHeading(query2) {
12917
- return `"${escapeMarkdownText(cleanSingleLine(query2))}"`;
13803
+ return `"${escapeMarkdownText(cleanSingleLine2(query2))}"`;
12918
13804
  }
12919
13805
  function formatAnswerHeading(query2) {
12920
- return `"${escapeMarkdownText(cleanSingleLine(query2))}"`;
13806
+ return `"${escapeMarkdownText(cleanSingleLine2(query2))}"`;
12921
13807
  }
12922
13808
  function collectSearchResultUrls(outcomes) {
12923
13809
  return outcomes.flatMap(
@@ -12933,7 +13819,7 @@ function formatSearchResponseMarkdown(response) {
12933
13819
  `${index + 1}. ${formatMarkdownLink(result.title, result.url)}`
12934
13820
  ];
12935
13821
  if (result.snippet) {
12936
- lines.push(` ${escapeMarkdownText(cleanSingleLine(result.snippet))}`);
13822
+ lines.push(` ${escapeMarkdownText(cleanSingleLine2(result.snippet))}`);
12937
13823
  }
12938
13824
  return lines.join("\n");
12939
13825
  }).join("\n\n");
@@ -12942,7 +13828,7 @@ function formatMarkdownLink(label, url2) {
12942
13828
  return `[${escapeMarkdownLinkLabel(label)}](<${url2}>)`;
12943
13829
  }
12944
13830
  function escapeMarkdownLinkLabel(text) {
12945
- return cleanSingleLine(text).replaceAll("\\", "\\\\").replaceAll("]", "\\]");
13831
+ return cleanSingleLine2(text).replaceAll("\\", "\\\\").replaceAll("]", "\\]");
12946
13832
  }
12947
13833
  function escapeMarkdownText(text) {
12948
13834
  return text.replaceAll("\\", "\\\\").replaceAll("*", "\\*").replaceAll("_", "\\_").replaceAll("`", "\\`").replaceAll("#", "\\#").replaceAll("[", "\\[").replaceAll("]", "\\]");
@@ -12963,10 +13849,10 @@ async function truncateAndSaveWithMetadata(text, prefix) {
12963
13849
  truncated: false
12964
13850
  };
12965
13851
  }
12966
- const dir = join2(tmpdir(), `pi-web-providers-${prefix}-${Date.now()}`);
12967
- await mkdir2(dir, { recursive: true });
12968
- const fullPath = join2(dir, "output.txt");
12969
- await writeFile2(fullPath, text, "utf-8");
13852
+ const dir = join3(tmpdir(), `pi-web-providers-${prefix}-${Date.now()}`);
13853
+ await mkdir3(dir, { recursive: true });
13854
+ const fullPath = join3(dir, "output.txt");
13855
+ await writeFile3(fullPath, text, "utf-8");
12970
13856
  return {
12971
13857
  text: truncation.content + `
12972
13858
 
@@ -13073,6 +13959,12 @@ var __test__ = {
13073
13959
  }),
13074
13960
  extractTextContent,
13075
13961
  formatWebResearchResultMessage,
13962
+ getActiveWebResearchRequests,
13963
+ getWebResearchTaskSnapshots,
13964
+ cancelWebResearchTask,
13965
+ loadWebResearchHistory,
13966
+ loadWebResearchPreview,
13967
+ loadWebResearchReport,
13076
13968
  getAvailableManagedToolNames,
13077
13969
  getReadyCompatibleProvidersForTool,
13078
13970
  getEnabledCompatibleProvidersForTool: getReadyCompatibleProvidersForTool,
@@ -13090,9 +13982,10 @@ var __test__ = {
13090
13982
  renderProviderToolResult,
13091
13983
  renderWebResearchDispatchResult,
13092
13984
  renderWebResearchResultMessage,
13093
- waitForPendingResearchTasks: async () => {
13094
- await Promise.all([...pendingResearchTasks]);
13095
- },
13985
+ renderWebResearchReportMessage,
13986
+ formatWebResearchReportMessage,
13987
+ buildWebResearchWidgetLines,
13988
+ waitForPendingResearchTasks,
13096
13989
  formatSearchResponses,
13097
13990
  formatAnswerResponses
13098
13991
  };