@aliou/pi-synthetic 0.18.1 → 0.18.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -41,7 +41,7 @@ pi install npm:@aliou/pi-synthetic
41
41
  pi install git:github.com/aliou/pi-synthetic
42
42
 
43
43
  # Local development
44
- pi -e ./src/index.ts
44
+ pi -e .
45
45
  ```
46
46
 
47
47
  ## Usage
@@ -165,7 +165,7 @@ pnpm run test
165
165
  ### Test Locally
166
166
 
167
167
  ```bash
168
- pi -e ./src/index.ts
168
+ pi -e .
169
169
  ```
170
170
 
171
171
  ## Release
@@ -180,7 +180,7 @@ This repository uses [Changesets](https://github.com/changesets/changesets) for
180
180
 
181
181
  ## Requirements
182
182
 
183
- - Pi coding agent v0.72.0+
183
+ - Pi coding agent v0.77.0+
184
184
  - Synthetic API key (configured in `~/.pi/agent/auth.json` or via `SYNTHETIC_API_KEY`)
185
185
 
186
186
  ## Links
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aliou/pi-synthetic",
3
- "version": "0.18.1",
3
+ "version": "0.18.3",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "private": false,
@@ -33,8 +33,21 @@
33
33
  "!src/**/*.test.ts"
34
34
  ],
35
35
  "peerDependencies": {
36
- "@earendil-works/pi-coding-agent": "0.74.0",
37
- "@earendil-works/pi-tui": "0.74.0"
36
+ "@earendil-works/pi-coding-agent": "*",
37
+ "@earendil-works/pi-tui": "*",
38
+ "typebox": "*"
39
+ },
40
+ "scripts": {
41
+ "typecheck": "tsc --noEmit",
42
+ "lint": "biome check",
43
+ "format": "biome check --write",
44
+ "test": "vitest run",
45
+ "test:watch": "vitest",
46
+ "check:lockfile": "pnpm install --frozen-lockfile --ignore-scripts",
47
+ "prepare": "[ -d .git ] && husky || true",
48
+ "changeset": "changeset",
49
+ "version": "changeset version",
50
+ "release": "pnpm changeset publish"
38
51
  },
39
52
  "dependencies": {
40
53
  "@aliou/pi-utils-settings": "^0.15.0",
@@ -44,12 +57,19 @@
44
57
  "@aliou/biome-plugins": "^0.8.1",
45
58
  "@biomejs/biome": "^2.4.15",
46
59
  "@changesets/cli": "^2.27.11",
47
- "@earendil-works/pi-coding-agent": "0.74.0",
60
+ "@earendil-works/pi-coding-agent": "0.77.0",
48
61
  "typebox": "^1.1.37",
49
62
  "@types/node": "^25.0.10",
50
63
  "husky": "^9.1.7",
51
64
  "typescript": "^5.9.3",
52
- "vitest": "^4.0.18"
65
+ "vitest": "^4.0.18",
66
+ "@earendil-works/pi-tui": "0.77.0"
67
+ },
68
+ "pnpm": {
69
+ "overrides": {
70
+ "@earendil-works/pi-ai": "$@earendil-works/pi-coding-agent",
71
+ "@earendil-works/pi-tui": "$@earendil-works/pi-coding-agent"
72
+ }
53
73
  },
54
74
  "peerDependenciesMeta": {
55
75
  "@earendil-works/pi-coding-agent": {
@@ -57,17 +77,10 @@
57
77
  },
58
78
  "@earendil-works/pi-tui": {
59
79
  "optional": true
80
+ },
81
+ "typebox": {
82
+ "optional": true
60
83
  }
61
84
  },
62
- "scripts": {
63
- "typecheck": "tsc --noEmit",
64
- "lint": "biome check",
65
- "format": "biome check --write",
66
- "test": "vitest run",
67
- "test:watch": "vitest",
68
- "check:lockfile": "pnpm install --frozen-lockfile --ignore-scripts",
69
- "changeset": "changeset",
70
- "version": "changeset version",
71
- "release": "pnpm changeset publish"
72
- }
73
- }
85
+ "packageManager": "pnpm@10.26.1"
86
+ }
@@ -80,7 +80,7 @@ export function registerSyntheticProvider(
80
80
  ): void {
81
81
  pi.registerProvider("synthetic", {
82
82
  baseUrl: "https://api.synthetic.new/openai/v1",
83
- apiKey: "SYNTHETIC_API_KEY",
83
+ apiKey: "$SYNTHETIC_API_KEY",
84
84
  api: "openai-completions",
85
85
  headers: {
86
86
  Referer: "https://pi.dev",
@@ -51,11 +51,11 @@ export const SYNTHETIC_MODELS: SyntheticModelEntry[] = [
51
51
  name: "syn:large:vision",
52
52
  aliasFor: "hf:moonshotai/Kimi-K2.6",
53
53
  },
54
- // API: syn:small:vision → alias for hf:moonshotai/Kimi-K2.6
54
+ // API: syn:small:vision → alias for hf:Qwen/Qwen3.6-27B
55
55
  {
56
56
  id: "syn:small:vision",
57
57
  name: "syn:small:vision",
58
- aliasFor: "hf:moonshotai/Kimi-K2.6",
58
+ aliasFor: "hf:Qwen/Qwen3.6-27B",
59
59
  },
60
60
  // API: hf:zai-org/GLM-4.7 → ctx=202752
61
61
  {
@@ -84,33 +84,6 @@ export const SYNTHETIC_MODELS: SyntheticModelEntry[] = [
84
84
  contextWindow: 202752,
85
85
  maxTokens: 65536,
86
86
  },
87
- // API: hf:zai-org/GLM-5 → ctx=196608, out=65536
88
- {
89
- id: "hf:zai-org/GLM-5",
90
- name: "zai-org/GLM-5",
91
- provider: "synthetic",
92
- reasoning: true,
93
- thinkingLevelMap: {
94
- off: "none",
95
- minimal: null,
96
- low: null,
97
- medium: "medium",
98
- high: null,
99
- xhigh: null,
100
- },
101
- compat: {
102
- supportsReasoningEffort: true,
103
- },
104
- input: ["text"],
105
- cost: {
106
- input: 1,
107
- output: 3,
108
- cacheRead: 1,
109
- cacheWrite: 0,
110
- },
111
- contextWindow: 196608,
112
- maxTokens: 65536,
113
- },
114
87
  // API: hf:zai-org/GLM-5.1 → ctx=196608, out=65536
115
88
  {
116
89
  id: "hf:zai-org/GLM-5.1",
@@ -166,33 +139,6 @@ export const SYNTHETIC_MODELS: SyntheticModelEntry[] = [
166
139
  contextWindow: 196608,
167
140
  maxTokens: 65536,
168
141
  },
169
- // models.dev: synthetic/hf:deepseek-ai/DeepSeek-V3.2 → ctx=162816, out=8000
170
- {
171
- id: "hf:deepseek-ai/DeepSeek-V3.2",
172
- name: "deepseek-ai/DeepSeek-V3.2",
173
- provider: "fireworks",
174
- reasoning: true,
175
- thinkingLevelMap: {
176
- off: "none",
177
- minimal: null,
178
- low: null,
179
- medium: "medium",
180
- high: null,
181
- xhigh: null,
182
- },
183
- compat: {
184
- supportsReasoningEffort: true,
185
- },
186
- input: ["text"],
187
- cost: {
188
- input: 0.56,
189
- output: 1.68,
190
- cacheRead: 0.56,
191
- cacheWrite: 0,
192
- },
193
- contextWindow: 162816,
194
- maxTokens: 8000,
195
- },
196
142
  // models.dev: synthetic/hf:openai/gpt-oss-120b → ctx=128000, out=32768
197
143
  {
198
144
  id: "hf:openai/gpt-oss-120b",
@@ -209,22 +155,6 @@ export const SYNTHETIC_MODELS: SyntheticModelEntry[] = [
209
155
  contextWindow: 131072,
210
156
  maxTokens: 32768,
211
157
  },
212
- // API: hf:Qwen/Qwen3-Coder-480B-A35B-Instruct → ctx=262144, out=65536
213
- {
214
- id: "hf:Qwen/Qwen3-Coder-480B-A35B-Instruct",
215
- name: "Qwen/Qwen3-Coder-480B-A35B-Instruct",
216
- provider: "together",
217
- reasoning: false,
218
- input: ["text"],
219
- cost: {
220
- input: 2,
221
- output: 2,
222
- cacheRead: 2,
223
- cacheWrite: 0,
224
- },
225
- contextWindow: 262144,
226
- maxTokens: 65536,
227
- },
228
158
  // API: hf:moonshotai/Kimi-K2.6 → ctx=262144, out=65536
229
159
  {
230
160
  id: "hf:moonshotai/Kimi-K2.6",
@@ -279,6 +209,33 @@ export const SYNTHETIC_MODELS: SyntheticModelEntry[] = [
279
209
  contextWindow: 262144,
280
210
  maxTokens: 65536,
281
211
  },
212
+ // API: hf:Qwen/Qwen3.6-27B → ctx=262144, out=65536
213
+ {
214
+ id: "hf:Qwen/Qwen3.6-27B",
215
+ name: "Qwen/Qwen3.6-27B",
216
+ provider: "synthetic",
217
+ reasoning: true,
218
+ thinkingLevelMap: {
219
+ off: "none",
220
+ minimal: null,
221
+ low: null,
222
+ medium: "medium",
223
+ high: null,
224
+ xhigh: null,
225
+ },
226
+ compat: {
227
+ supportsReasoningEffort: true,
228
+ },
229
+ input: ["text", "image"],
230
+ cost: {
231
+ input: 0.45,
232
+ output: 3.6,
233
+ cacheRead: 0.45,
234
+ cacheWrite: 0,
235
+ },
236
+ contextWindow: 262144,
237
+ maxTokens: 65536,
238
+ },
282
239
  // API: hf:MiniMaxAI/MiniMax-M2.5 → ctx=191488, out=65536
283
240
  {
284
241
  id: "hf:MiniMaxAI/MiniMax-M2.5",
@@ -3,16 +3,9 @@ import { writeFile } from "node:fs/promises";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { ToolCallHeader, ToolFooter } from "@aliou/pi-utils-ui";
6
- import type {
7
- AgentToolResult,
8
- ExtensionAPI,
9
- ExtensionContext,
10
- Theme,
11
- ToolRenderResultOptions,
12
- } from "@earendil-works/pi-coding-agent";
6
+ import type { ExtensionAPI, Theme } from "@earendil-works/pi-coding-agent";
13
7
  import {
14
- DEFAULT_MAX_BYTES,
15
- DEFAULT_MAX_LINES,
8
+ defineTool,
16
9
  formatSize,
17
10
  keyHint,
18
11
  truncateHead,
@@ -43,8 +36,6 @@ interface WebSearchResultDetails {
43
36
  tempFilePath?: string;
44
37
  totalLines: number;
45
38
  totalBytes: number;
46
- outputLines: number;
47
- outputBytes: number;
48
39
  }
49
40
 
50
41
  interface WebSearchDetails {
@@ -60,261 +51,251 @@ const SearchParams = Type.Object({
60
51
 
61
52
  type SearchParamsType = Static<typeof SearchParams>;
62
53
 
63
- export function registerSyntheticWebSearchTool(pi: ExtensionAPI): void {
64
- pi.registerTool<typeof SearchParams, WebSearchDetails>({
65
- name: SYNTHETIC_WEB_SEARCH_TOOL,
66
- label: "Synthetic: Web Search",
67
- description: `Search the web using Synthetic's zero-data-retention API. Returns search results with titles, URLs, content snippets, and publication dates. Use for finding documentation, articles, recent information, or any web content. Results are fresh and not cached by Synthetic. Results are truncated to ${DEFAULT_MAX_LINES} lines or ${formatSize(DEFAULT_MAX_BYTES)} (whichever is hit first). If truncated, full output is saved to a temp file.`,
68
- promptSnippet: "Search the web using Synthetic's zero-data-retention API",
69
- promptGuidelines: [
70
- "Use synthetic_web_search for finding documentation, articles, recent information, or any web content.",
71
- "Write specific queries with names, dates, versions, or locations for synthetic_web_search.",
72
- "synthetic_web_search results are fresh and not cached by Synthetic.",
73
- ],
74
- parameters: SearchParams,
75
-
76
- async execute(
77
- _toolCallId: string,
78
- params: SearchParamsType,
79
- signal: AbortSignal | undefined,
80
- onUpdate:
81
- | ((result: AgentToolResult<WebSearchDetails>) => void)
82
- | undefined,
83
- ctx: ExtensionContext,
84
- ): Promise<AgentToolResult<WebSearchDetails>> {
85
- onUpdate?.({
86
- content: [{ type: "text", text: "Searching..." }],
87
- details: { query: params.query },
54
+ export const syntheticWebSearchTool = defineTool({
55
+ name: SYNTHETIC_WEB_SEARCH_TOOL,
56
+ label: "Synthetic: Web Search",
57
+ description: `Search the web using Synthetic's zero-data-retention API. Returns search results with titles, URLs, content snippets, and publication dates. Use for finding documentation, articles, recent information, or any web content. Results are fresh and not cached by Synthetic. Results exceeding 1000 lines or 25KB are saved to temp files and referenced inline (use the read tool to inspect). Shorter results are included inline.`,
58
+ promptSnippet: "Search the web using Synthetic's zero-data-retention API",
59
+ promptGuidelines: [
60
+ "Use synthetic_web_search for finding documentation, articles, recent information, or any web content.",
61
+ "Write specific queries with names, dates, versions, or locations for synthetic_web_search.",
62
+ "synthetic_web_search results are fresh and not cached by Synthetic.",
63
+ ],
64
+ parameters: SearchParams,
65
+
66
+ async execute(_toolCallId, params, signal, onUpdate, ctx) {
67
+ onUpdate?.({
68
+ content: [{ type: "text", text: "Searching..." }],
69
+ details: { query: params.query },
70
+ });
71
+
72
+ if (!configLoader.getConfig().webSearch) {
73
+ throw new Error(
74
+ "Synthetic web search is disabled. Re-enable it with synthetic:settings or pi config.",
75
+ );
76
+ }
77
+
78
+ const apiKey = await getSyntheticApiKey(ctx.modelRegistry.authStorage);
79
+ if (!apiKey) {
80
+ throw new Error(
81
+ "Synthetic web search requires a Synthetic subscription. Add credentials to ~/.pi/agent/auth.json or set SYNTHETIC_API_KEY environment variable.",
82
+ );
83
+ }
84
+
85
+ const response = await fetch("https://api.synthetic.new/v2/search", {
86
+ method: "POST",
87
+ headers: {
88
+ Authorization: `Bearer ${apiKey}`,
89
+ "Content-Type": "application/json",
90
+ },
91
+ body: JSON.stringify({ query: params.query }),
92
+ signal,
93
+ });
94
+
95
+ if (!response.ok) {
96
+ const errorText = await response.text();
97
+ throw new Error(`Search API error: ${response.status} ${errorText}`);
98
+ }
99
+
100
+ let data: SyntheticSearchResponse;
101
+ try {
102
+ data = await response.json();
103
+ } catch (parseError) {
104
+ throw new Error(
105
+ parseError instanceof Error
106
+ ? `Failed to parse search results: ${parseError.message}`
107
+ : "Failed to parse search results",
108
+ );
109
+ }
110
+
111
+ let content = `Found ${data.results.length} result(s):\n\n`;
112
+ const resultDetails: WebSearchResultDetails[] = [];
113
+
114
+ for (let i = 0; i < data.results.length; i++) {
115
+ const result = data.results[i];
116
+ const slug = result.title
117
+ .toLowerCase()
118
+ .replace(/[^a-z0-9]+/g, "-")
119
+ .replace(/(^-|-$)/g, "")
120
+ .slice(0, 40);
121
+ const truncation = truncateHead(result.text, {
122
+ maxLines: 1000,
123
+ maxBytes: 25_000,
88
124
  });
89
125
 
90
- if (!configLoader.getConfig().webSearch) {
91
- throw new Error(
92
- "Synthetic web search is disabled. Re-enable it with synthetic:settings or pi config.",
93
- );
94
- }
126
+ let inline: string;
127
+ let tempFilePath: string | undefined;
95
128
 
96
- const apiKey = await getSyntheticApiKey(ctx.modelRegistry.authStorage);
97
- if (!apiKey) {
98
- throw new Error(
99
- "Synthetic web search requires a Synthetic subscription. Add credentials to ~/.pi/agent/auth.json or set SYNTHETIC_API_KEY environment variable.",
129
+ if (truncation.truncated) {
130
+ // Result exceeds limits — write full content to a temp file
131
+ // and only reference it inline to avoid eating LLM context
132
+ tempFilePath = join(
133
+ tmpdir(),
134
+ `pi-synthetic-search-${slug}-${randomBytes(4).toString("hex")}.md`,
100
135
  );
136
+ await writeFile(tempFilePath, result.text, "utf8");
137
+ inline = `[Result too large: ${truncation.totalLines} lines, ${formatSize(truncation.totalBytes)}. Full result saved to: ${tempFilePath}. Use the read tool to inspect it.]`;
138
+ } else {
139
+ // Result fits within limits — include inline
140
+ inline = truncation.content;
101
141
  }
102
142
 
103
- const response = await fetch("https://api.synthetic.new/v2/search", {
104
- method: "POST",
105
- headers: {
106
- Authorization: `Bearer ${apiKey}`,
107
- "Content-Type": "application/json",
108
- },
109
- body: JSON.stringify({ query: params.query }),
110
- signal,
143
+ content += `## ${result.title}\n`;
144
+ content += `URL: ${result.url}\n`;
145
+ content += `Published: ${result.published}\n`;
146
+ content += `\n${inline}\n`;
147
+ content += "\n---\n\n";
148
+
149
+ resultDetails.push({
150
+ title: result.title,
151
+ url: result.url,
152
+ published: result.published,
153
+ truncated: truncation.truncated,
154
+ tempFilePath,
155
+ totalLines: truncation.totalLines,
156
+ totalBytes: truncation.totalBytes,
111
157
  });
158
+ }
159
+
160
+ return {
161
+ content: [{ type: "text", text: content }],
162
+ details: {
163
+ results: resultDetails,
164
+ query: params.query,
165
+ },
166
+ };
167
+ },
168
+
169
+ renderCall(args: SearchParamsType, theme: Theme) {
170
+ return new ToolCallHeader(
171
+ {
172
+ toolName: "Synthetic: WebSearch",
173
+ mainArg: `"${args.query}"`,
174
+ showColon: true,
175
+ },
176
+ theme,
177
+ );
178
+ },
179
+
180
+ renderResult(result, options, theme: Theme) {
181
+ const { expanded, isPartial } = options;
182
+
183
+ if (isPartial) {
184
+ return new Text(
185
+ theme.fg("muted", "Synthetic: WebSearch: fetching..."),
186
+ 0,
187
+ 0,
188
+ );
189
+ }
190
+
191
+ const details = result.details as WebSearchDetails | undefined;
192
+ const results = details?.results || [];
193
+ const container = new Container();
194
+
195
+ // When the tool throws, the framework calls renderResult with
196
+ // details={} (empty object) and the error message in content.
197
+ // Detect this by checking for missing results in details.
198
+ if (!details?.results) {
199
+ const textBlock = result.content.find((c) => c.type === "text");
200
+ const errorMsg =
201
+ (textBlock?.type === "text" && textBlock.text) || "Search failed";
202
+ container.addChild(new Text(theme.fg("error", errorMsg), 0, 0));
203
+ return container;
204
+ }
112
205
 
113
- if (!response.ok) {
114
- const errorText = await response.text();
115
- throw new Error(`Search API error: ${response.status} ${errorText}`);
116
- }
206
+ const hasTruncation = results.some((r) => r.truncated);
117
207
 
118
- let data: SyntheticSearchResponse;
119
- try {
120
- data = await response.json();
121
- } catch (parseError) {
122
- throw new Error(
123
- parseError instanceof Error
124
- ? `Failed to parse search results: ${parseError.message}`
125
- : "Failed to parse search results",
126
- );
208
+ if (results.length === 0) {
209
+ container.addChild(
210
+ new Text(theme.fg("muted", "Synthetic: WebSearch: no results"), 0, 0),
211
+ );
212
+ } else if (!expanded) {
213
+ // Collapsed: show result count + first result title
214
+ let text = theme.fg("success", `Found ${results.length} result(s)`);
215
+ if (hasTruncation) {
216
+ text += theme.fg("warning", " (offloaded)");
127
217
  }
128
-
129
- let content = `Found ${data.results.length} result(s):\n\n`;
130
- const resultDetails: WebSearchResultDetails[] = [];
131
-
132
- for (let i = 0; i < data.results.length; i++) {
133
- const result = data.results[i];
134
- const slug = result.title
135
- .toLowerCase()
136
- .replace(/[^a-z0-9]+/g, "-")
137
- .replace(/(^-|-$)/g, "")
138
- .slice(0, 40);
139
- const truncation = truncateHead(result.text, {
140
- maxLines: DEFAULT_MAX_LINES,
141
- maxBytes: DEFAULT_MAX_BYTES,
142
- });
143
-
144
- let preview = truncation.content;
145
- let tempFilePath: string | undefined;
146
-
147
- if (truncation.truncated) {
148
- tempFilePath = join(
149
- tmpdir(),
150
- `pi-synthetic-search-${slug}-${randomBytes(4).toString("hex")}.md`,
151
- );
152
- await writeFile(tempFilePath, result.text, "utf8");
153
- preview += `\n\n[Result truncated: ${truncation.outputLines} of ${truncation.totalLines} lines (${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)}). Full result: ${tempFilePath}]`;
218
+ const first = results[0];
219
+ if (first) {
220
+ text += `\n ${theme.fg("dim", first.title)}`;
221
+ if (results.length > 1) {
222
+ text += theme.fg("dim", ` (+${results.length - 1} more)`);
154
223
  }
155
-
156
- content += `## ${result.title}\n`;
157
- content += `URL: ${result.url}\n`;
158
- content += `Published: ${result.published}\n`;
159
- content += `\n${preview}\n`;
160
- content += "\n---\n\n";
161
-
162
- resultDetails.push({
163
- title: result.title,
164
- url: result.url,
165
- published: result.published,
166
- truncated: truncation.truncated,
167
- tempFilePath,
168
- totalLines: truncation.totalLines,
169
- totalBytes: truncation.totalBytes,
170
- outputLines: truncation.outputLines,
171
- outputBytes: truncation.outputBytes,
172
- });
173
224
  }
174
-
175
- return {
176
- content: [{ type: "text", text: content }],
177
- details: {
178
- results: resultDetails,
179
- query: params.query,
180
- },
181
- };
182
- },
183
-
184
- renderCall(args: SearchParamsType, theme: Theme) {
185
- return new ToolCallHeader(
186
- {
187
- toolName: "Synthetic: WebSearch",
188
- mainArg: `"${args.query}"`,
189
- showColon: true,
190
- },
191
- theme,
192
- );
193
- },
194
-
195
- renderResult(
196
- result: AgentToolResult<WebSearchDetails>,
197
- options: ToolRenderResultOptions,
198
- theme: Theme,
199
- ) {
200
- const { expanded, isPartial } = options;
201
-
202
- if (isPartial) {
203
- return new Text(
204
- theme.fg("muted", "Synthetic: WebSearch: fetching..."),
225
+ text += theme.fg("muted", ` ${keyHint("app.tools.expand", "to expand")}`);
226
+ container.addChild(new Text(text, 0, 0));
227
+ } else {
228
+ // Expanded: show each result with title, URL, date, and snippet
229
+ container.addChild(
230
+ new Text(
231
+ theme.fg("success", `Found ${results.length} result(s)`),
205
232
  0,
206
233
  0,
207
- );
208
- }
209
-
210
- const details = result.details;
211
- const results = details?.results || [];
212
- const container = new Container();
213
-
214
- // When the tool throws, the framework calls renderResult with
215
- // details={} (empty object) and the error message in content.
216
- // Detect this by checking for missing results in details.
217
- if (!details?.results) {
218
- const textBlock = result.content.find((c) => c.type === "text");
219
- const errorMsg =
220
- (textBlock?.type === "text" && textBlock.text) || "Search failed";
221
- container.addChild(new Text(theme.fg("error", errorMsg), 0, 0));
222
- return container;
223
- }
224
-
225
- const hasTruncation = results.some((r) => r.truncated);
234
+ ),
235
+ );
226
236
 
227
- if (results.length === 0) {
228
- container.addChild(
229
- new Text(theme.fg("muted", "Synthetic: WebSearch: no results"), 0, 0),
230
- );
231
- } else if (!expanded) {
232
- // Collapsed: show result count + first result title
233
- let text = theme.fg("success", `Found ${results.length} result(s)`);
234
- if (hasTruncation) {
235
- text += theme.fg("warning", " (truncated)");
236
- }
237
- const first = results[0];
238
- if (first) {
239
- text += `\n ${theme.fg("dim", first.title)}`;
240
- if (results.length > 1) {
241
- text += theme.fg("dim", ` (+${results.length - 1} more)`);
242
- }
243
- }
244
- text += theme.fg(
245
- "muted",
246
- ` ${keyHint("app.tools.expand", "to expand")}`,
247
- );
248
- container.addChild(new Text(text, 0, 0));
249
- } else {
250
- // Expanded: show each result with title, URL, date, and snippet
237
+ for (const r of results) {
238
+ container.addChild(new Text("", 0, 0));
251
239
  container.addChild(
252
240
  new Text(
253
- theme.fg("success", `Found ${results.length} result(s)`),
241
+ `${theme.fg("dim", ">")} ${theme.fg("accent", theme.bold(r.title))}`,
254
242
  0,
255
243
  0,
256
244
  ),
257
245
  );
246
+ container.addChild(new Text(` ${theme.fg("dim", r.url)}`, 0, 0));
247
+ if (r.published) {
248
+ container.addChild(
249
+ new Text(
250
+ ` ${theme.fg("muted", `Published: ${r.published}`)}`,
251
+ 0,
252
+ 0,
253
+ ),
254
+ );
255
+ }
258
256
 
259
- for (const r of results) {
260
- container.addChild(new Text("", 0, 0));
257
+ if (r.truncated) {
261
258
  container.addChild(
262
259
  new Text(
263
- `${theme.fg("dim", ">")} ${theme.fg("accent", theme.bold(r.title))}`,
260
+ ` ${theme.fg("warning", `Offloaded: ${r.totalLines} lines, ${formatSize(r.totalBytes)}. Full result: ${r.tempFilePath}`)}`,
264
261
  0,
265
262
  0,
266
263
  ),
267
264
  );
268
- container.addChild(new Text(` ${theme.fg("dim", r.url)}`, 0, 0));
269
- if (r.published) {
270
- container.addChild(
271
- new Text(
272
- ` ${theme.fg("muted", `Published: ${r.published}`)}`,
273
- 0,
274
- 0,
275
- ),
276
- );
277
- }
278
-
279
- if (r.truncated) {
280
- container.addChild(
281
- new Text(
282
- ` ${theme.fg("warning", `Truncated: ${r.outputLines} of ${r.totalLines} lines (${formatSize(r.outputBytes)} of ${formatSize(r.totalBytes)}). Full content: ${r.tempFilePath}`)}`,
283
- 0,
284
- 0,
285
- ),
286
- );
287
- }
288
265
  }
289
266
  }
290
-
291
- const footerItems: { label: string; value: string }[] = [];
267
+ }
268
+
269
+ const footerItems: { label: string; value: string }[] = [];
270
+ footerItems.push({
271
+ label: "results",
272
+ value: `${results.length} result(s)`,
273
+ });
274
+ if (hasTruncation) {
275
+ const truncatedCount = results.filter((r) => r.truncated).length;
292
276
  footerItems.push({
293
- label: "results",
294
- value: `${results.length} result(s)`,
277
+ label: "offloaded",
278
+ value: `${truncatedCount}`,
295
279
  });
296
- if (hasTruncation) {
297
- const truncatedCount = results.filter((r) => r.truncated).length;
298
- footerItems.push({
299
- label: "truncated",
300
- value: `${truncatedCount}`,
301
- });
302
- }
303
- if (!expanded) {
304
- footerItems.push({
305
- label: "",
306
- value: keyHint("app.tools.expand", "to expand"),
307
- });
308
- }
309
- container.addChild(new Text("", 0, 0));
310
- container.addChild(
311
- new ToolFooter(theme, {
312
- items: footerItems,
313
- separator: " | ",
314
- }),
315
- );
280
+ }
281
+ if (!expanded) {
282
+ footerItems.push({
283
+ label: "",
284
+ value: keyHint("app.tools.expand", "to expand"),
285
+ });
286
+ }
287
+ container.addChild(new Text("", 0, 0));
288
+ container.addChild(
289
+ new ToolFooter(theme, {
290
+ items: footerItems,
291
+ separator: " | ",
292
+ }),
293
+ );
294
+
295
+ return container;
296
+ },
297
+ });
316
298
 
317
- return container;
318
- },
319
- });
299
+ export function registerSyntheticWebSearchTool(pi: ExtensionAPI): void {
300
+ pi.registerTool(syntheticWebSearchTool);
320
301
  }