snow-ai 0.2.23 → 0.2.25

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 (43) hide show
  1. package/dist/agents/compactAgent.d.ts +55 -0
  2. package/dist/agents/compactAgent.js +301 -0
  3. package/dist/api/chat.d.ts +0 -8
  4. package/dist/api/chat.js +1 -144
  5. package/dist/api/responses.d.ts +0 -11
  6. package/dist/api/responses.js +1 -189
  7. package/dist/api/systemPrompt.d.ts +1 -1
  8. package/dist/api/systemPrompt.js +80 -206
  9. package/dist/app.d.ts +2 -1
  10. package/dist/app.js +11 -13
  11. package/dist/cli.js +23 -3
  12. package/dist/hooks/useConversation.js +51 -7
  13. package/dist/hooks/useGlobalNavigation.d.ts +1 -1
  14. package/dist/hooks/useKeyboardInput.js +14 -8
  15. package/dist/mcp/filesystem.d.ts +49 -6
  16. package/dist/mcp/filesystem.js +243 -86
  17. package/dist/mcp/websearch.d.ts +118 -0
  18. package/dist/mcp/websearch.js +451 -0
  19. package/dist/ui/components/ToolResultPreview.js +60 -1
  20. package/dist/ui/pages/ChatScreen.d.ts +4 -2
  21. package/dist/ui/pages/ChatScreen.js +62 -14
  22. package/dist/ui/pages/{ApiConfigScreen.d.ts → ConfigScreen.d.ts} +1 -1
  23. package/dist/ui/pages/ConfigScreen.js +549 -0
  24. package/dist/ui/pages/{ModelConfigScreen.d.ts → ProxyConfigScreen.d.ts} +1 -1
  25. package/dist/ui/pages/ProxyConfigScreen.js +143 -0
  26. package/dist/ui/pages/WelcomeScreen.js +15 -15
  27. package/dist/utils/apiConfig.d.ts +8 -2
  28. package/dist/utils/apiConfig.js +21 -0
  29. package/dist/utils/commandExecutor.d.ts +1 -1
  30. package/dist/utils/contextCompressor.js +363 -49
  31. package/dist/utils/mcpToolsManager.d.ts +1 -1
  32. package/dist/utils/mcpToolsManager.js +106 -6
  33. package/dist/utils/resourceMonitor.d.ts +65 -0
  34. package/dist/utils/resourceMonitor.js +175 -0
  35. package/dist/utils/retryUtils.js +6 -0
  36. package/dist/utils/sessionManager.d.ts +1 -0
  37. package/dist/utils/sessionManager.js +10 -0
  38. package/dist/utils/textBuffer.js +7 -2
  39. package/dist/utils/toolExecutor.d.ts +2 -2
  40. package/dist/utils/toolExecutor.js +4 -4
  41. package/package.json +5 -1
  42. package/dist/ui/pages/ApiConfigScreen.js +0 -161
  43. package/dist/ui/pages/ModelConfigScreen.js +0 -467
@@ -0,0 +1,118 @@
1
+ interface SearchResult {
2
+ title: string;
3
+ url: string;
4
+ snippet: string;
5
+ displayUrl: string;
6
+ }
7
+ interface SearchResponse {
8
+ query: string;
9
+ results: SearchResult[];
10
+ totalResults: number;
11
+ }
12
+ interface WebPageContent {
13
+ url: string;
14
+ title: string;
15
+ content: string;
16
+ textLength: number;
17
+ contentPreview: string;
18
+ }
19
+ /**
20
+ * Web Search Service using DuckDuckGo Lite with Puppeteer Core
21
+ * Provides web search functionality with real browser support and proxy
22
+ * Uses system-installed Chrome/Edge to reduce package size
23
+ */
24
+ export declare class WebSearchService {
25
+ private maxResults;
26
+ private browser;
27
+ private executablePath;
28
+ constructor(maxResults?: number);
29
+ /**
30
+ * Launch browser with proxy settings from config
31
+ */
32
+ private launchBrowser;
33
+ /**
34
+ * Close browser instance
35
+ */
36
+ closeBrowser(): Promise<void>;
37
+ /**
38
+ * Perform a web search using DuckDuckGo
39
+ * @param query - Search query string
40
+ * @param maxResults - Maximum number of results to return (default: 10)
41
+ * @returns Search results with title, URL, and snippet
42
+ */
43
+ search(query: string, maxResults?: number): Promise<SearchResponse>;
44
+ /**
45
+ * Fetch and extract content from a web page
46
+ * @param url - URL of the web page to fetch
47
+ * @param maxLength - Maximum content length (default: 50000 characters)
48
+ * @param userQuery - Optional user query for content extraction using compact model agent
49
+ * @param abortSignal - Optional abort signal from main flow
50
+ * @param onTokenUpdate - Optional callback to update token count during compression
51
+ * @returns Cleaned page content
52
+ */
53
+ fetchPage(url: string, maxLength?: number, userQuery?: string, abortSignal?: AbortSignal, onTokenUpdate?: (tokenCount: number) => void): Promise<WebPageContent>;
54
+ /**
55
+ * Clean text by removing extra whitespace and HTML entities
56
+ * @param text - Raw text to clean
57
+ * @returns Cleaned text
58
+ */
59
+ private cleanText;
60
+ /**
61
+ * Format search results as readable text for AI consumption
62
+ * @param searchResponse - Search response object
63
+ * @returns Formatted text representation
64
+ */
65
+ formatResults(searchResponse: SearchResponse): string;
66
+ }
67
+ export declare const webSearchService: WebSearchService;
68
+ export declare const mcpTools: ({
69
+ name: string;
70
+ description: string;
71
+ inputSchema: {
72
+ type: string;
73
+ properties: {
74
+ query: {
75
+ type: string;
76
+ description: string;
77
+ };
78
+ maxResults: {
79
+ type: string;
80
+ description: string;
81
+ default: number;
82
+ minimum: number;
83
+ maximum: number;
84
+ };
85
+ url?: undefined;
86
+ maxLength?: undefined;
87
+ userQuery?: undefined;
88
+ };
89
+ required: string[];
90
+ };
91
+ } | {
92
+ name: string;
93
+ description: string;
94
+ inputSchema: {
95
+ type: string;
96
+ properties: {
97
+ url: {
98
+ type: string;
99
+ description: string;
100
+ };
101
+ maxLength: {
102
+ type: string;
103
+ description: string;
104
+ default: number;
105
+ minimum: number;
106
+ maximum: number;
107
+ };
108
+ userQuery: {
109
+ type: string;
110
+ description: string;
111
+ };
112
+ query?: undefined;
113
+ maxResults?: undefined;
114
+ };
115
+ required: string[];
116
+ };
117
+ })[];
118
+ export {};
@@ -0,0 +1,451 @@
1
+ import puppeteer from 'puppeteer-core';
2
+ import { getProxyConfig } from '../utils/apiConfig.js';
3
+ import { execSync } from 'node:child_process';
4
+ import { existsSync } from 'node:fs';
5
+ import { platform } from 'node:os';
6
+ /**
7
+ * Detect system Chrome/Edge browser executable path
8
+ */
9
+ function findBrowserExecutable() {
10
+ const os = platform();
11
+ const paths = [];
12
+ if (os === 'win32') {
13
+ // Windows: Prioritize Edge (built-in), then Chrome
14
+ const edgePaths = [
15
+ 'C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe',
16
+ 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
17
+ ];
18
+ const chromePaths = [
19
+ 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
20
+ 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
21
+ process.env['LOCALAPPDATA'] + '\\Google\\Chrome\\Application\\chrome.exe',
22
+ ];
23
+ paths.push(...edgePaths, ...chromePaths);
24
+ }
25
+ else if (os === 'darwin') {
26
+ // macOS
27
+ paths.push('/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', '/Applications/Chromium.app/Contents/MacOS/Chromium', '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge');
28
+ }
29
+ else {
30
+ // Linux
31
+ const binPaths = ['google-chrome', 'chromium', 'chromium-browser', 'microsoft-edge'];
32
+ for (const bin of binPaths) {
33
+ try {
34
+ const path = execSync(`which ${bin}`, { encoding: 'utf8' }).trim();
35
+ if (path) {
36
+ return path;
37
+ }
38
+ }
39
+ catch {
40
+ // Continue to next binary
41
+ }
42
+ }
43
+ }
44
+ // Check if any path exists
45
+ for (const path of paths) {
46
+ if (path && existsSync(path)) {
47
+ return path;
48
+ }
49
+ }
50
+ return null;
51
+ }
52
+ /**
53
+ * Web Search Service using DuckDuckGo Lite with Puppeteer Core
54
+ * Provides web search functionality with real browser support and proxy
55
+ * Uses system-installed Chrome/Edge to reduce package size
56
+ */
57
+ export class WebSearchService {
58
+ constructor(maxResults = 10) {
59
+ Object.defineProperty(this, "maxResults", {
60
+ enumerable: true,
61
+ configurable: true,
62
+ writable: true,
63
+ value: void 0
64
+ });
65
+ Object.defineProperty(this, "browser", {
66
+ enumerable: true,
67
+ configurable: true,
68
+ writable: true,
69
+ value: null
70
+ });
71
+ Object.defineProperty(this, "executablePath", {
72
+ enumerable: true,
73
+ configurable: true,
74
+ writable: true,
75
+ value: null
76
+ });
77
+ this.maxResults = maxResults;
78
+ }
79
+ /**
80
+ * Launch browser with proxy settings from config
81
+ */
82
+ async launchBrowser() {
83
+ if (this.browser && this.browser.connected) {
84
+ return this.browser;
85
+ }
86
+ const proxyConfig = getProxyConfig();
87
+ // Find browser executable path (cache it)
88
+ // Priority: 1. User-configured path, 2. Auto-detect
89
+ if (!this.executablePath) {
90
+ // First try user-configured browser path
91
+ if (proxyConfig.browserPath && existsSync(proxyConfig.browserPath)) {
92
+ this.executablePath = proxyConfig.browserPath;
93
+ }
94
+ else {
95
+ // Fallback to auto-detection
96
+ this.executablePath = findBrowserExecutable();
97
+ if (!this.executablePath) {
98
+ throw new Error('No system browser found. Please install Chrome or Edge browser, or configure browser path in Proxy settings.');
99
+ }
100
+ }
101
+ }
102
+ const launchArgs = [
103
+ '--no-sandbox',
104
+ '--disable-setuid-sandbox',
105
+ '--disable-dev-shm-usage',
106
+ '--disable-accelerated-2d-canvas',
107
+ '--disable-gpu',
108
+ ];
109
+ // Only add proxy if enabled
110
+ if (proxyConfig.enabled) {
111
+ launchArgs.unshift(`--proxy-server=http://127.0.0.1:${proxyConfig.port}`);
112
+ }
113
+ this.browser = await puppeteer.launch({
114
+ executablePath: this.executablePath,
115
+ headless: true,
116
+ args: launchArgs,
117
+ });
118
+ return this.browser;
119
+ }
120
+ /**
121
+ * Close browser instance
122
+ */
123
+ async closeBrowser() {
124
+ if (this.browser) {
125
+ await this.browser.close();
126
+ this.browser = null;
127
+ }
128
+ }
129
+ /**
130
+ * Perform a web search using DuckDuckGo
131
+ * @param query - Search query string
132
+ * @param maxResults - Maximum number of results to return (default: 10)
133
+ * @returns Search results with title, URL, and snippet
134
+ */
135
+ async search(query, maxResults) {
136
+ const limit = maxResults || this.maxResults;
137
+ let page = null;
138
+ try {
139
+ // Launch browser with proxy
140
+ const browser = await this.launchBrowser();
141
+ page = await browser.newPage();
142
+ // Set realistic user agent
143
+ await page.setUserAgent('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36');
144
+ // Encode query for URL
145
+ const encodedQuery = encodeURIComponent(query);
146
+ const searchUrl = `https://lite.duckduckgo.com/lite?q=${encodedQuery}`;
147
+ // Navigate to search page with timeout
148
+ await page.goto(searchUrl, {
149
+ waitUntil: 'networkidle2',
150
+ timeout: 30000,
151
+ });
152
+ // Extract search results from the page
153
+ const results = await page.evaluate((maxLimit) => {
154
+ const searchResults = [];
155
+ const rows = document.querySelectorAll('table tr');
156
+ let currentResult = {};
157
+ let resultCount = 0;
158
+ for (const row of rows) {
159
+ if (resultCount >= maxLimit)
160
+ break;
161
+ // Check if this row contains a link (title row)
162
+ const linkElement = row.querySelector('a.result-link');
163
+ if (linkElement) {
164
+ // Save previous result if exists
165
+ if (currentResult.title && currentResult.url) {
166
+ searchResults.push(currentResult);
167
+ resultCount++;
168
+ if (resultCount >= maxLimit)
169
+ break;
170
+ }
171
+ // Start new result
172
+ const title = linkElement.textContent?.trim() || '';
173
+ const href = linkElement.getAttribute('href') || '';
174
+ // Extract actual URL from DuckDuckGo redirect
175
+ let actualUrl = href;
176
+ if (href.includes('uddg=')) {
177
+ const match = href.match(/uddg=([^&]+)/);
178
+ if (match && match[1]) {
179
+ actualUrl = decodeURIComponent(match[1]);
180
+ }
181
+ }
182
+ currentResult = {
183
+ title: title,
184
+ url: actualUrl,
185
+ snippet: '',
186
+ displayUrl: '',
187
+ };
188
+ continue;
189
+ }
190
+ // Check if this row contains snippet
191
+ const snippetElement = row.querySelector('td.result-snippet');
192
+ if (snippetElement && currentResult.title) {
193
+ currentResult.snippet = snippetElement.textContent?.trim() || '';
194
+ continue;
195
+ }
196
+ // Check if this row contains display URL
197
+ const displayUrlElement = row.querySelector('span.link-text');
198
+ if (displayUrlElement && currentResult.title) {
199
+ currentResult.displayUrl = displayUrlElement.textContent?.trim() || '';
200
+ }
201
+ }
202
+ // Add last result if exists
203
+ if (currentResult.title && currentResult.url && resultCount < maxLimit) {
204
+ searchResults.push(currentResult);
205
+ }
206
+ return searchResults;
207
+ }, limit);
208
+ // Clean text in results
209
+ const cleanedResults = results.map(result => ({
210
+ title: this.cleanText(result.title),
211
+ url: result.url,
212
+ snippet: this.cleanText(result.snippet),
213
+ displayUrl: this.cleanText(result.displayUrl),
214
+ }));
215
+ // Close the page
216
+ await page.close();
217
+ return {
218
+ query,
219
+ results: cleanedResults,
220
+ totalResults: cleanedResults.length,
221
+ };
222
+ }
223
+ catch (error) {
224
+ // Clean up page on error
225
+ if (page) {
226
+ try {
227
+ await page.close();
228
+ }
229
+ catch {
230
+ // Ignore close errors
231
+ }
232
+ }
233
+ throw new Error(`Web search failed: ${error.message}`);
234
+ }
235
+ }
236
+ /**
237
+ * Fetch and extract content from a web page
238
+ * @param url - URL of the web page to fetch
239
+ * @param maxLength - Maximum content length (default: 50000 characters)
240
+ * @param userQuery - Optional user query for content extraction using compact model agent
241
+ * @param abortSignal - Optional abort signal from main flow
242
+ * @param onTokenUpdate - Optional callback to update token count during compression
243
+ * @returns Cleaned page content
244
+ */
245
+ async fetchPage(url, maxLength = 50000, userQuery, abortSignal, onTokenUpdate) {
246
+ let page = null;
247
+ try {
248
+ // Launch browser with proxy
249
+ const browser = await this.launchBrowser();
250
+ page = await browser.newPage();
251
+ // Set realistic user agent
252
+ await page.setUserAgent('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36');
253
+ // Navigate to page with timeout
254
+ await page.goto(url, {
255
+ waitUntil: 'networkidle2',
256
+ timeout: 30000,
257
+ });
258
+ // Extract content using browser context
259
+ const pageData = await page.evaluate(() => {
260
+ // Remove unwanted elements
261
+ const selectorsToRemove = [
262
+ 'script',
263
+ 'style',
264
+ 'nav',
265
+ 'header',
266
+ 'footer',
267
+ 'iframe',
268
+ 'noscript',
269
+ 'svg',
270
+ '.advertisement',
271
+ '.ad',
272
+ '.ads',
273
+ '#cookie-banner',
274
+ '.cookie-notice',
275
+ '.social-share',
276
+ '.comments',
277
+ '.sidebar',
278
+ '[role="banner"]',
279
+ '[role="navigation"]',
280
+ '[role="complementary"]',
281
+ ];
282
+ selectorsToRemove.forEach(selector => {
283
+ document.querySelectorAll(selector).forEach(el => el.remove());
284
+ });
285
+ // Get title
286
+ const title = document.title || '';
287
+ // Try to find main content area
288
+ let mainContent = null;
289
+ const mainSelectors = [
290
+ 'article',
291
+ 'main',
292
+ '[role="main"]',
293
+ '.main-content',
294
+ '.content',
295
+ '#content',
296
+ '.article-body',
297
+ '.post-content',
298
+ ];
299
+ for (const selector of mainSelectors) {
300
+ mainContent = document.querySelector(selector);
301
+ if (mainContent)
302
+ break;
303
+ }
304
+ // Fallback to body if no main content found
305
+ const contentElement = mainContent || document.body;
306
+ // Extract text content
307
+ const textContent = contentElement.textContent || '';
308
+ return {
309
+ title,
310
+ textContent,
311
+ };
312
+ });
313
+ // Clean and process the text
314
+ let cleanedContent = pageData.textContent
315
+ .replace(/\s+/g, ' ') // Replace multiple spaces with single space
316
+ .replace(/\n\s*\n/g, '\n') // Remove empty lines
317
+ .trim();
318
+ // Limit content length
319
+ if (cleanedContent.length > maxLength) {
320
+ cleanedContent = cleanedContent.slice(0, maxLength) + '\n\n[Content truncated...]';
321
+ }
322
+ // Create preview (first 500 characters)
323
+ const contentPreview = cleanedContent.slice(0, 500) + (cleanedContent.length > 500 ? '...' : '');
324
+ // Close the page
325
+ await page.close();
326
+ // Use compact agent to extract key information if userQuery is provided
327
+ let finalContent = cleanedContent;
328
+ if (userQuery) {
329
+ try {
330
+ const { compactAgent } = await import('../agents/compactAgent.js');
331
+ const isAvailable = await compactAgent.isAvailable();
332
+ if (isAvailable) {
333
+ // Use compact model to extract relevant information
334
+ // No timeout - let it run as long as needed
335
+ finalContent = await compactAgent.extractWebPageContent(cleanedContent, userQuery, url, abortSignal, onTokenUpdate);
336
+ }
337
+ }
338
+ catch (error) {
339
+ // If compact agent fails, fallback to original content
340
+ // Error is already logged in compactAgent
341
+ }
342
+ }
343
+ return {
344
+ url,
345
+ title: this.cleanText(pageData.title),
346
+ content: finalContent,
347
+ textLength: finalContent.length,
348
+ contentPreview,
349
+ };
350
+ }
351
+ catch (error) {
352
+ // Clean up page on error
353
+ if (page) {
354
+ try {
355
+ await page.close();
356
+ }
357
+ catch {
358
+ // Ignore close errors
359
+ }
360
+ }
361
+ throw new Error(`Failed to fetch page: ${error.message}`);
362
+ }
363
+ }
364
+ /**
365
+ * Clean text by removing extra whitespace and HTML entities
366
+ * @param text - Raw text to clean
367
+ * @returns Cleaned text
368
+ */
369
+ cleanText(text) {
370
+ return text
371
+ .replace(/\s+/g, ' ') // Replace multiple spaces with single space
372
+ .replace(/&quot;/g, '"')
373
+ .replace(/&amp;/g, '&')
374
+ .replace(/&lt;/g, '<')
375
+ .replace(/&gt;/g, '>')
376
+ .replace(/<b>/g, '')
377
+ .replace(/<\/b>/g, '')
378
+ .trim();
379
+ }
380
+ /**
381
+ * Format search results as readable text for AI consumption
382
+ * @param searchResponse - Search response object
383
+ * @returns Formatted text representation
384
+ */
385
+ formatResults(searchResponse) {
386
+ const { query, results, totalResults } = searchResponse;
387
+ let output = `Search Results for: "${query}"\n`;
388
+ output += `Found ${totalResults} results\n\n`;
389
+ output += '='.repeat(80) + '\n\n';
390
+ results.forEach((result, index) => {
391
+ output += `${index + 1}. ${result.title}\n`;
392
+ output += ` URL: ${result.url}\n`;
393
+ if (result.snippet) {
394
+ output += ` ${result.snippet}\n`;
395
+ }
396
+ output += '\n';
397
+ });
398
+ return output;
399
+ }
400
+ }
401
+ // Export a default instance
402
+ export const webSearchService = new WebSearchService();
403
+ // MCP Tool definitions
404
+ export const mcpTools = [
405
+ {
406
+ name: 'websearch_search',
407
+ description: 'Search the web using DuckDuckGo. Returns a list of search results with titles, URLs, and snippets. Best for finding current information, documentation, news, or general web content. **IMPORTANT WORKFLOW**: After getting search results, analyze them and choose ONLY ONE most credible and relevant page to fetch. Do NOT fetch multiple pages - reading one high-quality source is sufficient and more efficient.',
408
+ inputSchema: {
409
+ type: 'object',
410
+ properties: {
411
+ query: {
412
+ type: 'string',
413
+ description: 'Search query string (e.g., "Claude latest model", "TypeScript best practices")',
414
+ },
415
+ maxResults: {
416
+ type: 'number',
417
+ description: 'Maximum number of results to return (default: 10, max: 20)',
418
+ default: 10,
419
+ minimum: 1,
420
+ maximum: 20,
421
+ },
422
+ },
423
+ required: ['query'],
424
+ },
425
+ },
426
+ {
427
+ name: 'websearch_fetch',
428
+ description: 'Fetch and read the full content of a web page. Automatically cleans HTML and extracts the main text content, removing ads, navigation, and other noise. **USAGE RULE**: Only fetch ONE page per search - choose the most credible and relevant result (prefer official documentation, reputable tech sites, or well-known sources). **OPTIMIZATION**: ALWAYS provide the userQuery parameter with the user\'s original question. This enables automatic extraction of only relevant information using a compact AI model, which dramatically saves context and improves response quality.',
429
+ inputSchema: {
430
+ type: 'object',
431
+ properties: {
432
+ url: {
433
+ type: 'string',
434
+ description: 'Full URL of the web page to fetch (e.g., "https://example.com/article")',
435
+ },
436
+ maxLength: {
437
+ type: 'number',
438
+ description: 'Maximum content length in characters (default: 50000, max: 100000)',
439
+ default: 50000,
440
+ minimum: 1000,
441
+ maximum: 100000,
442
+ },
443
+ userQuery: {
444
+ type: 'string',
445
+ description: 'REQUIRED: User\'s original question or query. This is essential for intelligent content extraction - the compact AI model will extract only information relevant to this query, reducing content size by 80-95%. Always provide this parameter.',
446
+ },
447
+ },
448
+ required: ['url'],
449
+ },
450
+ },
451
+ ];
@@ -21,6 +21,12 @@ export default function ToolResultPreview({ toolName, result, maxLines = 5 }) {
21
21
  else if (toolName === 'filesystem-edit_search') {
22
22
  return renderEditSearchPreview(data);
23
23
  }
24
+ else if (toolName === 'websearch-search') {
25
+ return renderWebSearchPreview(data, maxLines);
26
+ }
27
+ else if (toolName === 'websearch-fetch') {
28
+ return renderWebFetchPreview(data);
29
+ }
24
30
  else if (toolName.startsWith('ace-')) {
25
31
  return renderACEPreview(toolName, data, maxLines);
26
32
  }
@@ -219,6 +225,59 @@ function renderEditSearchPreview(data) {
219
225
  "\u2514\u2500 Total lines: ",
220
226
  data.totalLines))));
221
227
  }
228
+ function renderWebSearchPreview(data, maxLines) {
229
+ if (!data.results || data.results.length === 0) {
230
+ return (React.createElement(Box, { marginLeft: 2 },
231
+ React.createElement(Text, { color: "gray", dimColor: true },
232
+ "\u2514\u2500 No search results found for \"",
233
+ data.query,
234
+ "\"")));
235
+ }
236
+ const previewResults = data.results.slice(0, maxLines);
237
+ const hasMore = data.results.length > maxLines;
238
+ return (React.createElement(Box, { flexDirection: "column", marginLeft: 2 },
239
+ React.createElement(Text, { color: "cyan", dimColor: true },
240
+ "\u251C\u2500 Query: ",
241
+ data.query),
242
+ React.createElement(Text, { color: "cyan", dimColor: true },
243
+ "\u251C\u2500 Found ",
244
+ data.totalResults,
245
+ " results"),
246
+ previewResults.map((result, idx) => (React.createElement(Box, { key: idx, flexDirection: "column" },
247
+ React.createElement(Text, { color: "gray", dimColor: true },
248
+ idx === previewResults.length - 1 && !hasMore ? '└─ ' : '├─ ',
249
+ "[",
250
+ idx + 1,
251
+ "] ",
252
+ result.title.slice(0, 20),
253
+ result.title.length > 20 ? '...' : ''),
254
+ result.snippet && (React.createElement(Box, { marginLeft: 3 },
255
+ React.createElement(Text, { color: "gray", dimColor: true },
256
+ result.snippet.slice(0, 20),
257
+ result.snippet.length > 20 ? '...' : '')))))),
258
+ hasMore && (React.createElement(Text, { color: "gray", dimColor: true },
259
+ "\u2514\u2500 ... (",
260
+ data.results.length - maxLines,
261
+ " more results)"))));
262
+ }
263
+ function renderWebFetchPreview(data) {
264
+ return (React.createElement(Box, { flexDirection: "column", marginLeft: 2 },
265
+ React.createElement(Text, { color: "cyan", dimColor: true },
266
+ "\u251C\u2500 Page: ",
267
+ data.title || 'Untitled'),
268
+ React.createElement(Text, { color: "cyan", dimColor: true },
269
+ "\u251C\u2500 URL: ",
270
+ data.url.slice(0, 20),
271
+ data.url.length > 20 ? '...' : ''),
272
+ React.createElement(Text, { color: "gray", dimColor: true },
273
+ "\u251C\u2500 Content length: ",
274
+ data.textLength,
275
+ " characters"),
276
+ React.createElement(Text, { color: "gray", dimColor: true },
277
+ "\u2514\u2500 Preview: ",
278
+ data.contentPreview.slice(0, 20),
279
+ data.contentPreview.length > 20 ? '...' : '')));
280
+ }
222
281
  function renderGenericPreview(data, maxLines) {
223
282
  // For unknown tool types, show first few properties
224
283
  const entries = Object.entries(data).slice(0, maxLines);
@@ -226,7 +285,7 @@ function renderGenericPreview(data, maxLines) {
226
285
  return null;
227
286
  return (React.createElement(Box, { flexDirection: "column", marginLeft: 2 }, entries.map(([key, value], idx) => {
228
287
  const valueStr = typeof value === 'string'
229
- ? value.slice(0, 60) + (value.length > 60 ? '...' : '')
288
+ ? value.slice(0, 20) + (value.length > 20 ? '...' : '')
230
289
  : JSON.stringify(value).slice(0, 60);
231
290
  return (React.createElement(Text, { key: idx, color: "gray", dimColor: true },
232
291
  idx === entries.length - 1 ? '└─ ' : '├─ ',
@@ -6,6 +6,8 @@ import '../../utils/commands/yolo.js';
6
6
  import '../../utils/commands/init.js';
7
7
  import '../../utils/commands/ide.js';
8
8
  import '../../utils/commands/compact.js';
9
- type Props = {};
10
- export default function ChatScreen({}: Props): React.JSX.Element;
9
+ type Props = {
10
+ skipWelcome?: boolean;
11
+ };
12
+ export default function ChatScreen({ skipWelcome }: Props): React.JSX.Element;
11
13
  export {};