@aliou/pi-synthetic 0.18.1 → 0.18.2

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.2",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "private": false,
@@ -33,8 +33,9 @@
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": "*"
38
39
  },
39
40
  "dependencies": {
40
41
  "@aliou/pi-utils-settings": "^0.15.0",
@@ -44,12 +45,13 @@
44
45
  "@aliou/biome-plugins": "^0.8.1",
45
46
  "@biomejs/biome": "^2.4.15",
46
47
  "@changesets/cli": "^2.27.11",
47
- "@earendil-works/pi-coding-agent": "0.74.0",
48
+ "@earendil-works/pi-coding-agent": "0.77.0",
48
49
  "typebox": "^1.1.37",
49
50
  "@types/node": "^25.0.10",
50
51
  "husky": "^9.1.7",
51
52
  "typescript": "^5.9.3",
52
- "vitest": "^4.0.18"
53
+ "vitest": "^4.0.18",
54
+ "@earendil-works/pi-tui": "0.77.0"
53
55
  },
54
56
  "peerDependenciesMeta": {
55
57
  "@earendil-works/pi-coding-agent": {
@@ -57,6 +59,9 @@
57
59
  },
58
60
  "@earendil-works/pi-tui": {
59
61
  "optional": true
62
+ },
63
+ "typebox": {
64
+ "optional": true
60
65
  }
61
66
  },
62
67
  "scripts": {
@@ -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",
@@ -279,6 +225,33 @@ export const SYNTHETIC_MODELS: SyntheticModelEntry[] = [
279
225
  contextWindow: 262144,
280
226
  maxTokens: 65536,
281
227
  },
228
+ // API: hf:Qwen/Qwen3.6-27B → ctx=262144, out=65536
229
+ {
230
+ id: "hf:Qwen/Qwen3.6-27B",
231
+ name: "Qwen/Qwen3.6-27B",
232
+ provider: "synthetic",
233
+ reasoning: true,
234
+ thinkingLevelMap: {
235
+ off: "none",
236
+ minimal: null,
237
+ low: null,
238
+ medium: "medium",
239
+ high: null,
240
+ xhigh: null,
241
+ },
242
+ compat: {
243
+ supportsReasoningEffort: true,
244
+ },
245
+ input: ["text", "image"],
246
+ cost: {
247
+ input: 0.45,
248
+ output: 3.6,
249
+ cacheRead: 0.45,
250
+ cacheWrite: 0,
251
+ },
252
+ contextWindow: 262144,
253
+ maxTokens: 65536,
254
+ },
282
255
  // API: hf:MiniMaxAI/MiniMax-M2.5 → ctx=191488, out=65536
283
256
  {
284
257
  id: "hf:MiniMaxAI/MiniMax-M2.5",
@@ -3,16 +3,11 @@ 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
8
  DEFAULT_MAX_BYTES,
15
9
  DEFAULT_MAX_LINES,
10
+ defineTool,
16
11
  formatSize,
17
12
  keyHint,
18
13
  truncateHead,
@@ -60,261 +55,248 @@ const SearchParams = Type.Object({
60
55
 
61
56
  type SearchParamsType = Static<typeof SearchParams>;
62
57
 
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 },
58
+ export const syntheticWebSearchTool = defineTool({
59
+ name: SYNTHETIC_WEB_SEARCH_TOOL,
60
+ label: "Synthetic: Web Search",
61
+ 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.`,
62
+ promptSnippet: "Search the web using Synthetic's zero-data-retention API",
63
+ promptGuidelines: [
64
+ "Use synthetic_web_search for finding documentation, articles, recent information, or any web content.",
65
+ "Write specific queries with names, dates, versions, or locations for synthetic_web_search.",
66
+ "synthetic_web_search results are fresh and not cached by Synthetic.",
67
+ ],
68
+ parameters: SearchParams,
69
+
70
+ async execute(_toolCallId, params, signal, onUpdate, ctx) {
71
+ onUpdate?.({
72
+ content: [{ type: "text", text: "Searching..." }],
73
+ details: { query: params.query },
74
+ });
75
+
76
+ if (!configLoader.getConfig().webSearch) {
77
+ throw new Error(
78
+ "Synthetic web search is disabled. Re-enable it with synthetic:settings or pi config.",
79
+ );
80
+ }
81
+
82
+ const apiKey = await getSyntheticApiKey(ctx.modelRegistry.authStorage);
83
+ if (!apiKey) {
84
+ throw new Error(
85
+ "Synthetic web search requires a Synthetic subscription. Add credentials to ~/.pi/agent/auth.json or set SYNTHETIC_API_KEY environment variable.",
86
+ );
87
+ }
88
+
89
+ const response = await fetch("https://api.synthetic.new/v2/search", {
90
+ method: "POST",
91
+ headers: {
92
+ Authorization: `Bearer ${apiKey}`,
93
+ "Content-Type": "application/json",
94
+ },
95
+ body: JSON.stringify({ query: params.query }),
96
+ signal,
97
+ });
98
+
99
+ if (!response.ok) {
100
+ const errorText = await response.text();
101
+ throw new Error(`Search API error: ${response.status} ${errorText}`);
102
+ }
103
+
104
+ let data: SyntheticSearchResponse;
105
+ try {
106
+ data = await response.json();
107
+ } catch (parseError) {
108
+ throw new Error(
109
+ parseError instanceof Error
110
+ ? `Failed to parse search results: ${parseError.message}`
111
+ : "Failed to parse search results",
112
+ );
113
+ }
114
+
115
+ let content = `Found ${data.results.length} result(s):\n\n`;
116
+ const resultDetails: WebSearchResultDetails[] = [];
117
+
118
+ for (let i = 0; i < data.results.length; i++) {
119
+ const result = data.results[i];
120
+ const slug = result.title
121
+ .toLowerCase()
122
+ .replace(/[^a-z0-9]+/g, "-")
123
+ .replace(/(^-|-$)/g, "")
124
+ .slice(0, 40);
125
+ const truncation = truncateHead(result.text, {
126
+ maxLines: DEFAULT_MAX_LINES,
127
+ maxBytes: DEFAULT_MAX_BYTES,
88
128
  });
89
129
 
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
- }
130
+ let preview = truncation.content;
131
+ let tempFilePath: string | undefined;
95
132
 
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.",
133
+ if (truncation.truncated) {
134
+ tempFilePath = join(
135
+ tmpdir(),
136
+ `pi-synthetic-search-${slug}-${randomBytes(4).toString("hex")}.md`,
100
137
  );
138
+ await writeFile(tempFilePath, result.text, "utf8");
139
+ preview += `\n\n[Result truncated: ${truncation.outputLines} of ${truncation.totalLines} lines (${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)}). Full result: ${tempFilePath}]`;
101
140
  }
102
141
 
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,
142
+ content += `## ${result.title}\n`;
143
+ content += `URL: ${result.url}\n`;
144
+ content += `Published: ${result.published}\n`;
145
+ content += `\n${preview}\n`;
146
+ content += "\n---\n\n";
147
+
148
+ resultDetails.push({
149
+ title: result.title,
150
+ url: result.url,
151
+ published: result.published,
152
+ truncated: truncation.truncated,
153
+ tempFilePath,
154
+ totalLines: truncation.totalLines,
155
+ totalBytes: truncation.totalBytes,
156
+ outputLines: truncation.outputLines,
157
+ outputBytes: truncation.outputBytes,
111
158
  });
159
+ }
160
+
161
+ return {
162
+ content: [{ type: "text", text: content }],
163
+ details: {
164
+ results: resultDetails,
165
+ query: params.query,
166
+ },
167
+ };
168
+ },
169
+
170
+ renderCall(args: SearchParamsType, theme: Theme) {
171
+ return new ToolCallHeader(
172
+ {
173
+ toolName: "Synthetic: WebSearch",
174
+ mainArg: `"${args.query}"`,
175
+ showColon: true,
176
+ },
177
+ theme,
178
+ );
179
+ },
180
+
181
+ renderResult(result, options, theme: Theme) {
182
+ const { expanded, isPartial } = options;
183
+
184
+ if (isPartial) {
185
+ return new Text(
186
+ theme.fg("muted", "Synthetic: WebSearch: fetching..."),
187
+ 0,
188
+ 0,
189
+ );
190
+ }
191
+
192
+ const details = result.details as WebSearchDetails | undefined;
193
+ const results = details?.results || [];
194
+ const container = new Container();
195
+
196
+ // When the tool throws, the framework calls renderResult with
197
+ // details={} (empty object) and the error message in content.
198
+ // Detect this by checking for missing results in details.
199
+ if (!details?.results) {
200
+ const textBlock = result.content.find((c) => c.type === "text");
201
+ const errorMsg =
202
+ (textBlock?.type === "text" && textBlock.text) || "Search failed";
203
+ container.addChild(new Text(theme.fg("error", errorMsg), 0, 0));
204
+ return container;
205
+ }
112
206
 
113
- if (!response.ok) {
114
- const errorText = await response.text();
115
- throw new Error(`Search API error: ${response.status} ${errorText}`);
116
- }
207
+ const hasTruncation = results.some((r) => r.truncated);
117
208
 
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
- );
209
+ if (results.length === 0) {
210
+ container.addChild(
211
+ new Text(theme.fg("muted", "Synthetic: WebSearch: no results"), 0, 0),
212
+ );
213
+ } else if (!expanded) {
214
+ // Collapsed: show result count + first result title
215
+ let text = theme.fg("success", `Found ${results.length} result(s)`);
216
+ if (hasTruncation) {
217
+ text += theme.fg("warning", " (truncated)");
127
218
  }
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}]`;
219
+ const first = results[0];
220
+ if (first) {
221
+ text += `\n ${theme.fg("dim", first.title)}`;
222
+ if (results.length > 1) {
223
+ text += theme.fg("dim", ` (+${results.length - 1} more)`);
154
224
  }
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
225
  }
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..."),
226
+ text += theme.fg("muted", ` ${keyHint("app.tools.expand", "to expand")}`);
227
+ container.addChild(new Text(text, 0, 0));
228
+ } else {
229
+ // Expanded: show each result with title, URL, date, and snippet
230
+ container.addChild(
231
+ new Text(
232
+ theme.fg("success", `Found ${results.length} result(s)`),
205
233
  0,
206
234
  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);
235
+ ),
236
+ );
226
237
 
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
238
+ for (const r of results) {
239
+ container.addChild(new Text("", 0, 0));
251
240
  container.addChild(
252
241
  new Text(
253
- theme.fg("success", `Found ${results.length} result(s)`),
242
+ `${theme.fg("dim", ">")} ${theme.fg("accent", theme.bold(r.title))}`,
254
243
  0,
255
244
  0,
256
245
  ),
257
246
  );
247
+ container.addChild(new Text(` ${theme.fg("dim", r.url)}`, 0, 0));
248
+ if (r.published) {
249
+ container.addChild(
250
+ new Text(
251
+ ` ${theme.fg("muted", `Published: ${r.published}`)}`,
252
+ 0,
253
+ 0,
254
+ ),
255
+ );
256
+ }
258
257
 
259
- for (const r of results) {
260
- container.addChild(new Text("", 0, 0));
258
+ if (r.truncated) {
261
259
  container.addChild(
262
260
  new Text(
263
- `${theme.fg("dim", ">")} ${theme.fg("accent", theme.bold(r.title))}`,
261
+ ` ${theme.fg("warning", `Truncated: ${r.outputLines} of ${r.totalLines} lines (${formatSize(r.outputBytes)} of ${formatSize(r.totalBytes)}). Full content: ${r.tempFilePath}`)}`,
264
262
  0,
265
263
  0,
266
264
  ),
267
265
  );
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
266
  }
289
267
  }
290
-
291
- const footerItems: { label: string; value: string }[] = [];
268
+ }
269
+
270
+ const footerItems: { label: string; value: string }[] = [];
271
+ footerItems.push({
272
+ label: "results",
273
+ value: `${results.length} result(s)`,
274
+ });
275
+ if (hasTruncation) {
276
+ const truncatedCount = results.filter((r) => r.truncated).length;
292
277
  footerItems.push({
293
- label: "results",
294
- value: `${results.length} result(s)`,
278
+ label: "truncated",
279
+ value: `${truncatedCount}`,
295
280
  });
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
- );
281
+ }
282
+ if (!expanded) {
283
+ footerItems.push({
284
+ label: "",
285
+ value: keyHint("app.tools.expand", "to expand"),
286
+ });
287
+ }
288
+ container.addChild(new Text("", 0, 0));
289
+ container.addChild(
290
+ new ToolFooter(theme, {
291
+ items: footerItems,
292
+ separator: " | ",
293
+ }),
294
+ );
295
+
296
+ return container;
297
+ },
298
+ });
316
299
 
317
- return container;
318
- },
319
- });
300
+ export function registerSyntheticWebSearchTool(pi: ExtensionAPI): void {
301
+ pi.registerTool(syntheticWebSearchTool);
320
302
  }