@oh-my-pi/exa 1.3.371 → 1.3.372
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 +6 -6
- package/package.json +1 -1
- package/tools/company.ts +2 -2
- package/tools/researcher.ts +2 -2
- package/tools/search.ts +4 -4
- package/tools/shared.ts +321 -4
package/README.md
CHANGED
|
@@ -59,10 +59,10 @@ Get your API key from: https://dashboard.exa.ai/api-keys
|
|
|
59
59
|
|
|
60
60
|
| Tool | Description |
|
|
61
61
|
| ------------------------- | ---------------------------------------------------- |
|
|
62
|
-
| `
|
|
62
|
+
| `web_search` | Real-time web searches with content extraction |
|
|
63
63
|
| `web_search_deep` | Natural language web search with synthesized results |
|
|
64
64
|
| `web_search_code_context` | Search code snippets, docs, and examples |
|
|
65
|
-
| `
|
|
65
|
+
| `web_search_crawl` | Extract content from specific URLs |
|
|
66
66
|
|
|
67
67
|
### linkedin
|
|
68
68
|
|
|
@@ -72,16 +72,16 @@ Get your API key from: https://dashboard.exa.ai/api-keys
|
|
|
72
72
|
|
|
73
73
|
### company
|
|
74
74
|
|
|
75
|
-
| Tool
|
|
76
|
-
|
|
|
77
|
-
| `
|
|
75
|
+
| Tool | Description |
|
|
76
|
+
| -------------------- | ------------------------------ |
|
|
77
|
+
| `web_search_company` | Comprehensive company research |
|
|
78
78
|
|
|
79
79
|
### researcher
|
|
80
80
|
|
|
81
81
|
| Tool | Description |
|
|
82
82
|
| ----------------------------- | -------------------------------------------- |
|
|
83
83
|
| `web_search_researcher_start` | Start comprehensive AI-powered research task |
|
|
84
|
-
| `
|
|
84
|
+
| `web_search_researcher_poll` | Check research task status and get results |
|
|
85
85
|
|
|
86
86
|
### websets
|
|
87
87
|
|
package/package.json
CHANGED
package/tools/company.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Exa Company Research Tool
|
|
3
3
|
*
|
|
4
4
|
* Tools:
|
|
5
|
-
* -
|
|
5
|
+
* - web_search_company: Comprehensive company research
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { CustomAgentTool, CustomToolFactory, ToolAPI } from '@mariozechner/pi-coding-agent'
|
|
@@ -14,7 +14,7 @@ const TOOL_NAMES = ['company_research_exa']
|
|
|
14
14
|
|
|
15
15
|
// Tool name mapping: MCP name -> exposed name
|
|
16
16
|
const NAME_MAP: Record<string, string> = {
|
|
17
|
-
company_research_exa: '
|
|
17
|
+
company_research_exa: 'web_search_company',
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
const factory: CustomToolFactory = async (_toolApi: ToolAPI): Promise<CustomAgentTool<TSchema, unknown>[] | null> => {
|
package/tools/researcher.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Tools:
|
|
5
5
|
* - web_search_researcher_start: Start comprehensive AI research tasks
|
|
6
|
-
* -
|
|
6
|
+
* - web_search_researcher_poll: Check research task status
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { CustomAgentTool, CustomToolFactory, ToolAPI } from '@mariozechner/pi-coding-agent'
|
|
@@ -16,7 +16,7 @@ const TOOL_NAMES = ['deep_researcher_start', 'deep_researcher_check']
|
|
|
16
16
|
// Tool name mapping: MCP name -> exposed name
|
|
17
17
|
const NAME_MAP: Record<string, string> = {
|
|
18
18
|
deep_researcher_start: 'web_search_researcher_start',
|
|
19
|
-
deep_researcher_check: '
|
|
19
|
+
deep_researcher_check: 'web_search_researcher_poll',
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
const factory: CustomToolFactory = async (_toolApi: ToolAPI): Promise<CustomAgentTool<TSchema, unknown>[] | null> => {
|
package/tools/search.ts
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
* Exa Search Tools - Core web search capabilities
|
|
3
3
|
*
|
|
4
4
|
* Tools:
|
|
5
|
-
* -
|
|
5
|
+
* - web_search: Real-time web searches
|
|
6
6
|
* - web_search_deep: Natural language web search with synthesis
|
|
7
7
|
* - web_search_code_context: Code search for libraries, docs, examples
|
|
8
|
-
* -
|
|
8
|
+
* - web_search_crawl: Extract content from specific URLs
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import type { CustomAgentTool, CustomToolFactory, ToolAPI } from '@mariozechner/pi-coding-agent'
|
|
@@ -17,10 +17,10 @@ const TOOL_NAMES = ['web_search_exa', 'deep_search_exa', 'get_code_context_exa',
|
|
|
17
17
|
|
|
18
18
|
// Tool name mapping: MCP name -> exposed name
|
|
19
19
|
const NAME_MAP: Record<string, string> = {
|
|
20
|
-
web_search_exa: '
|
|
20
|
+
web_search_exa: 'web_search',
|
|
21
21
|
deep_search_exa: 'web_search_deep',
|
|
22
22
|
get_code_context_exa: 'web_search_code_context',
|
|
23
|
-
crawling_exa: '
|
|
23
|
+
crawling_exa: 'web_search_crawl',
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
const factory: CustomToolFactory = async (_toolApi: ToolAPI): Promise<CustomAgentTool<TSchema, unknown>[] | null> => {
|
package/tools/shared.ts
CHANGED
|
@@ -6,12 +6,25 @@ import * as fs from 'node:fs'
|
|
|
6
6
|
import * as os from 'node:os'
|
|
7
7
|
import * as path from 'node:path'
|
|
8
8
|
import type { CustomAgentTool } from '@mariozechner/pi-coding-agent'
|
|
9
|
+
import { Text } from '@mariozechner/pi-tui'
|
|
9
10
|
import type { TSchema } from '@sinclair/typebox'
|
|
10
11
|
|
|
11
12
|
// MCP endpoints
|
|
12
13
|
export const EXA_MCP_URL = 'https://mcp.exa.ai/mcp'
|
|
13
14
|
export const WEBSETS_MCP_URL = 'https://websetsmcp.exa.ai/mcp'
|
|
14
15
|
|
|
16
|
+
// Log paths
|
|
17
|
+
const EXA_ERROR_LOG = path.join(os.homedir(), '.pi/exa_errors.log')
|
|
18
|
+
const VIEW_ERROR_LOG = path.join(os.homedir(), '.pi/view_errors.log')
|
|
19
|
+
|
|
20
|
+
function logExaError(msg: string): void {
|
|
21
|
+
fs.appendFileSync(EXA_ERROR_LOG, `[${new Date().toISOString()}] ${msg}\n`)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function logViewError(msg: string): void {
|
|
25
|
+
fs.appendFileSync(VIEW_ERROR_LOG, `[${new Date().toISOString()}] ${msg}\n`)
|
|
26
|
+
}
|
|
27
|
+
|
|
15
28
|
export interface MCPTool {
|
|
16
29
|
name: string
|
|
17
30
|
description: string
|
|
@@ -164,7 +177,8 @@ export async function fetchExaTools(apiKey: string, toolNames: string[]): Promis
|
|
|
164
177
|
}
|
|
165
178
|
return response.result?.tools ?? []
|
|
166
179
|
} catch (error) {
|
|
167
|
-
|
|
180
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
181
|
+
logExaError(`Failed to fetch Exa tools: ${msg}`)
|
|
168
182
|
return []
|
|
169
183
|
}
|
|
170
184
|
}
|
|
@@ -182,7 +196,8 @@ export async function fetchWebsetsTools(apiKey: string): Promise<MCPTool[]> {
|
|
|
182
196
|
}
|
|
183
197
|
return response.result?.tools ?? []
|
|
184
198
|
} catch (error) {
|
|
185
|
-
|
|
199
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
200
|
+
logExaError(`Failed to fetch Websets tools: ${msg}`)
|
|
186
201
|
return []
|
|
187
202
|
}
|
|
188
203
|
}
|
|
@@ -237,6 +252,178 @@ async function callMCPTool(url: string, toolName: string, args: Record<string, u
|
|
|
237
252
|
return response.result
|
|
238
253
|
}
|
|
239
254
|
|
|
255
|
+
interface SearchResult {
|
|
256
|
+
id?: string
|
|
257
|
+
title?: string
|
|
258
|
+
url?: string
|
|
259
|
+
author?: string
|
|
260
|
+
publishedDate?: string
|
|
261
|
+
text?: string
|
|
262
|
+
image?: string
|
|
263
|
+
favicon?: string
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
interface SearchResponse {
|
|
267
|
+
results?: SearchResult[]
|
|
268
|
+
statuses?: Array<{ id: string; status: string; source?: string }>
|
|
269
|
+
costDollars?: { total: number }
|
|
270
|
+
searchTime?: number
|
|
271
|
+
requestId?: string
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Format search results as readable markdown (for LLM consumption)
|
|
276
|
+
*/
|
|
277
|
+
function formatSearchResults(data: SearchResponse): string {
|
|
278
|
+
const lines: string[] = []
|
|
279
|
+
|
|
280
|
+
if (data.results && data.results.length > 0) {
|
|
281
|
+
for (const result of data.results) {
|
|
282
|
+
// Title with link
|
|
283
|
+
if (result.title && result.url) {
|
|
284
|
+
lines.push(`### [${result.title}](${result.url})`)
|
|
285
|
+
} else if (result.title) {
|
|
286
|
+
lines.push(`### ${result.title}`)
|
|
287
|
+
} else if (result.url) {
|
|
288
|
+
lines.push(`### ${result.url}`)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Author if present
|
|
292
|
+
if (result.author) {
|
|
293
|
+
lines.push(`*by ${result.author}*`)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
lines.push('')
|
|
297
|
+
|
|
298
|
+
// Content - truncate if very long
|
|
299
|
+
if (result.text) {
|
|
300
|
+
const text = result.text.trim()
|
|
301
|
+
const maxLen = 2000
|
|
302
|
+
if (text.length > maxLen) {
|
|
303
|
+
lines.push(`${text.slice(0, maxLen)}...`)
|
|
304
|
+
} else {
|
|
305
|
+
lines.push(text)
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
lines.push('')
|
|
310
|
+
lines.push('---')
|
|
311
|
+
lines.push('')
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Footer with metadata
|
|
316
|
+
const meta: string[] = []
|
|
317
|
+
if (data.results) meta.push(`${data.results.length} result(s)`)
|
|
318
|
+
if (data.searchTime) meta.push(`${(data.searchTime / 1000).toFixed(2)}s`)
|
|
319
|
+
if (data.costDollars?.total) meta.push(`$${data.costDollars.total.toFixed(4)}`)
|
|
320
|
+
|
|
321
|
+
if (meta.length > 0) {
|
|
322
|
+
lines.push(`*${meta.join(' • ')}*`)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return lines.join('\n')
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Check if result looks like a search response
|
|
330
|
+
*/
|
|
331
|
+
function isSearchResponse(data: unknown): data is SearchResponse {
|
|
332
|
+
if (!data || typeof data !== 'object') return false
|
|
333
|
+
const obj = data as Record<string, unknown>
|
|
334
|
+
return Array.isArray(obj.results) || 'searchTime' in obj || 'costDollars' in obj
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Parse Exa's markdown text format into a SearchResponse structure
|
|
339
|
+
* Format: Title: ...\nURL: ...\nAuthor: ...\nPublished Date: ...\nText: ...\n\n (repeated)
|
|
340
|
+
*/
|
|
341
|
+
function parseExaMarkdown(text: string): SearchResponse | null {
|
|
342
|
+
const results: SearchResult[] = []
|
|
343
|
+
|
|
344
|
+
// Split by double newlines to separate results, but be careful with Text: blocks
|
|
345
|
+
// Each result starts with "Title:"
|
|
346
|
+
const parts = text.split(/\n(?=Title:)/g)
|
|
347
|
+
|
|
348
|
+
for (const part of parts) {
|
|
349
|
+
if (!part.trim()) continue
|
|
350
|
+
|
|
351
|
+
const result: SearchResult = {}
|
|
352
|
+
const lines = part.split('\n')
|
|
353
|
+
|
|
354
|
+
let currentField: string | null = null
|
|
355
|
+
let textLines: string[] = []
|
|
356
|
+
|
|
357
|
+
for (const line of lines) {
|
|
358
|
+
// Check for field prefixes
|
|
359
|
+
if (line.startsWith('Title: ')) {
|
|
360
|
+
result.title = line.slice(7).trim()
|
|
361
|
+
currentField = null
|
|
362
|
+
} else if (line.startsWith('URL: ')) {
|
|
363
|
+
result.url = line.slice(5).trim()
|
|
364
|
+
currentField = null
|
|
365
|
+
} else if (line.startsWith('Author: ')) {
|
|
366
|
+
result.author = line.slice(8).trim()
|
|
367
|
+
currentField = null
|
|
368
|
+
} else if (line.startsWith('Published Date: ')) {
|
|
369
|
+
result.publishedDate = line.slice(16).trim()
|
|
370
|
+
currentField = null
|
|
371
|
+
} else if (line.startsWith('Text: ')) {
|
|
372
|
+
textLines = [line.slice(6)]
|
|
373
|
+
currentField = 'text'
|
|
374
|
+
} else if (currentField === 'text') {
|
|
375
|
+
textLines.push(line)
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (textLines.length > 0) {
|
|
380
|
+
result.text = textLines.join('\n').trim()
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (result.title || result.url) {
|
|
384
|
+
results.push(result)
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (results.length === 0) return null
|
|
389
|
+
return { results }
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Tree formatting helpers
|
|
393
|
+
const TREE_MID = '├─'
|
|
394
|
+
const TREE_END = '└─'
|
|
395
|
+
const TREE_PIPE = '│'
|
|
396
|
+
const TREE_SPACE = ' '
|
|
397
|
+
const TREE_HOOK = '⎿'
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Truncate text to max length with ellipsis
|
|
401
|
+
*/
|
|
402
|
+
function truncate(text: string, maxLen: number): string {
|
|
403
|
+
if (text.length <= maxLen) return text
|
|
404
|
+
return `${text.slice(0, maxLen - 1)}…`
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Extract domain from URL
|
|
409
|
+
*/
|
|
410
|
+
function getDomain(url: string): string {
|
|
411
|
+
try {
|
|
412
|
+
const u = new URL(url)
|
|
413
|
+
return u.hostname.replace(/^www\./, '')
|
|
414
|
+
} catch {
|
|
415
|
+
return url
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Get first N lines of text as preview
|
|
421
|
+
*/
|
|
422
|
+
function getPreviewLines(text: string, maxLines: number, maxLineLen: number): string[] {
|
|
423
|
+
const lines = text.split('\n').filter(l => l.trim())
|
|
424
|
+
return lines.slice(0, maxLines).map(l => truncate(l.trim(), maxLineLen))
|
|
425
|
+
}
|
|
426
|
+
|
|
240
427
|
/**
|
|
241
428
|
* Create a tool wrapper for an MCP tool
|
|
242
429
|
*/
|
|
@@ -244,16 +431,28 @@ export function createToolWrapper(
|
|
|
244
431
|
mcpTool: MCPTool,
|
|
245
432
|
renamedName: string,
|
|
246
433
|
callFn: (toolName: string, args: Record<string, unknown>) => Promise<unknown>
|
|
247
|
-
): CustomAgentTool<TSchema, unknown> {
|
|
434
|
+
): CustomAgentTool<TSchema, SearchResponse | { error: string } | unknown> {
|
|
248
435
|
return {
|
|
249
436
|
name: renamedName,
|
|
250
437
|
label: renamedName.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase()),
|
|
251
438
|
description: mcpTool.description,
|
|
252
439
|
parameters: normalizeInputSchema(mcpTool.inputSchema) as TSchema,
|
|
440
|
+
|
|
253
441
|
async execute(_toolCallId, params) {
|
|
254
442
|
try {
|
|
255
443
|
const result = await callFn(mcpTool.name, (params ?? {}) as Record<string, unknown>)
|
|
256
|
-
|
|
444
|
+
|
|
445
|
+
let text: string
|
|
446
|
+
if (typeof result === 'string') {
|
|
447
|
+
text = result
|
|
448
|
+
} else if (result == null) {
|
|
449
|
+
text = 'No results'
|
|
450
|
+
} else if (isSearchResponse(result)) {
|
|
451
|
+
text = formatSearchResults(result)
|
|
452
|
+
} else {
|
|
453
|
+
text = JSON.stringify(result, null, 2) ?? String(result)
|
|
454
|
+
}
|
|
455
|
+
|
|
257
456
|
return {
|
|
258
457
|
content: [{ type: 'text' as const, text }],
|
|
259
458
|
details: result,
|
|
@@ -266,5 +465,123 @@ export function createToolWrapper(
|
|
|
266
465
|
}
|
|
267
466
|
}
|
|
268
467
|
},
|
|
468
|
+
|
|
469
|
+
renderResult(result, { expanded }, theme) {
|
|
470
|
+
let { details } = result
|
|
471
|
+
|
|
472
|
+
// Handle error case
|
|
473
|
+
if (details && typeof details === 'object' && 'error' in details) {
|
|
474
|
+
const errDetails = details as { error: string }
|
|
475
|
+
return new Text(theme.fg('error', `Error: ${errDetails.error}`), 0, 0)
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// If details is a string (Exa markdown format), try to parse it
|
|
479
|
+
if (typeof details === 'string') {
|
|
480
|
+
const parsed = parseExaMarkdown(details)
|
|
481
|
+
if (parsed) {
|
|
482
|
+
details = parsed
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Handle non-search responses (plain text/JSON)
|
|
487
|
+
if (!isSearchResponse(details)) {
|
|
488
|
+
const text = result.content[0]
|
|
489
|
+
if (text?.type === 'text') {
|
|
490
|
+
// For non-search content, show truncated in collapsed, full in expanded
|
|
491
|
+
if (expanded) {
|
|
492
|
+
return new Text(text.text, 0, 0)
|
|
493
|
+
}
|
|
494
|
+
const preview = getPreviewLines(text.text, 5, 100)
|
|
495
|
+
const lines = preview.map(l => theme.fg('dim', l)).join('\n')
|
|
496
|
+
return new Text(lines + (text.text.split('\n').length > 5 ? theme.fg('muted', '\n …') : ''), 0, 0)
|
|
497
|
+
}
|
|
498
|
+
return new Text('', 0, 0)
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Search response - render as tree
|
|
502
|
+
const data = details as SearchResponse
|
|
503
|
+
const resultCount = data.results?.length ?? 0
|
|
504
|
+
|
|
505
|
+
// Build header with metadata
|
|
506
|
+
const meta: string[] = []
|
|
507
|
+
meta.push(`${resultCount} result${resultCount !== 1 ? 's' : ''}`)
|
|
508
|
+
if (data.searchTime) meta.push(`${(data.searchTime / 1000).toFixed(2)}s`)
|
|
509
|
+
if (data.costDollars?.total) meta.push(`$${data.costDollars.total.toFixed(4)}`)
|
|
510
|
+
|
|
511
|
+
const icon = resultCount > 0 ? theme.fg('success', '●') : theme.fg('warning', '●')
|
|
512
|
+
const expandHint = expanded ? '' : theme.fg('dim', ' (Ctrl+O to expand)')
|
|
513
|
+
let text = `${icon} ${theme.fg('toolTitle', 'Web Search')} ${theme.fg('dim', meta.join(' • '))}${expandHint}`
|
|
514
|
+
|
|
515
|
+
if (!data.results || data.results.length === 0) {
|
|
516
|
+
text += `\n ${theme.fg('dim', TREE_END)} ${theme.fg('muted', 'No results found')}`
|
|
517
|
+
return new Text(text, 0, 0)
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Render each result
|
|
521
|
+
try {
|
|
522
|
+
for (let i = 0; i < data.results.length; i++) {
|
|
523
|
+
const r = data.results[i]
|
|
524
|
+
const isLast = i === data.results.length - 1
|
|
525
|
+
const branch = isLast ? TREE_END : TREE_MID
|
|
526
|
+
const cont = isLast ? TREE_SPACE : TREE_PIPE
|
|
527
|
+
|
|
528
|
+
// Title line
|
|
529
|
+
const title = r.title ? truncate(r.title, 80) : 'Untitled'
|
|
530
|
+
const domain = r.url ? getDomain(r.url) : ''
|
|
531
|
+
text += `\n ${theme.fg('dim', branch)} ${theme.fg('accent', title)}`
|
|
532
|
+
if (domain) {
|
|
533
|
+
text += theme.fg('dim', ` (${domain})`)
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// URL line (if different from domain)
|
|
537
|
+
if (r.url) {
|
|
538
|
+
text += `\n ${theme.fg('dim', `${cont} ${TREE_HOOK} `)}${theme.fg('link', r.url)}`
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Author/date metadata
|
|
542
|
+
const metaParts: string[] = []
|
|
543
|
+
if (r.author) metaParts.push(`by ${r.author}`)
|
|
544
|
+
if (r.publishedDate) {
|
|
545
|
+
try {
|
|
546
|
+
const date = new Date(r.publishedDate)
|
|
547
|
+
metaParts.push(date.toLocaleDateString())
|
|
548
|
+
} catch {
|
|
549
|
+
// ignore invalid dates
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
if (metaParts.length > 0) {
|
|
553
|
+
text += `\n ${theme.fg('dim', `${cont} `)}${theme.fg('muted', metaParts.join(' • '))}`
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Content preview (collapsed) or full content (expanded)
|
|
557
|
+
if (r.text) {
|
|
558
|
+
if (expanded) {
|
|
559
|
+
// Show full content with proper indentation
|
|
560
|
+
const lines = r.text.split('\n')
|
|
561
|
+
for (const line of lines) {
|
|
562
|
+
if (line.trim()) {
|
|
563
|
+
text += `\n ${theme.fg('dim', `${cont} `)}${theme.fg('dim', line)}`
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
} else {
|
|
567
|
+
// Show preview (first 2 non-empty lines)
|
|
568
|
+
const preview = getPreviewLines(r.text, 2, 100)
|
|
569
|
+
for (const line of preview) {
|
|
570
|
+
text += `\n ${theme.fg('dim', `${cont} `)}${theme.fg('dim', line)}`
|
|
571
|
+
}
|
|
572
|
+
const totalLines = r.text.split('\n').filter(l => l.trim()).length
|
|
573
|
+
if (totalLines > 2) {
|
|
574
|
+
text += `\n ${theme.fg('dim', `${cont} `)}${theme.fg('muted', `… ${totalLines - 2} more lines`)}`
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
} catch (err) {
|
|
580
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
581
|
+
logViewError(`exa renderResult error: ${msg}`)
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
return new Text(text, 0, 0)
|
|
585
|
+
},
|
|
269
586
|
}
|
|
270
587
|
}
|