@f5xc-salesdemos/xcsh 18.40.1 → 18.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@f5xc-salesdemos/xcsh",
4
- "version": "18.40.1",
4
+ "version": "18.41.0",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://github.com/f5xc-salesdemos/xcsh",
7
7
  "author": "Can Boluk",
@@ -48,12 +48,12 @@
48
48
  "dependencies": {
49
49
  "@agentclientprotocol/sdk": "0.16.1",
50
50
  "@mozilla/readability": "^0.6",
51
- "@f5xc-salesdemos/xcsh-stats": "18.40.1",
52
- "@f5xc-salesdemos/pi-agent-core": "18.40.1",
53
- "@f5xc-salesdemos/pi-ai": "18.40.1",
54
- "@f5xc-salesdemos/pi-natives": "18.40.1",
55
- "@f5xc-salesdemos/pi-tui": "18.40.1",
56
- "@f5xc-salesdemos/pi-utils": "18.40.1",
51
+ "@f5xc-salesdemos/xcsh-stats": "18.41.0",
52
+ "@f5xc-salesdemos/pi-agent-core": "18.41.0",
53
+ "@f5xc-salesdemos/pi-ai": "18.41.0",
54
+ "@f5xc-salesdemos/pi-natives": "18.41.0",
55
+ "@f5xc-salesdemos/pi-tui": "18.41.0",
56
+ "@f5xc-salesdemos/pi-utils": "18.41.0",
57
57
  "@sinclair/typebox": "^0.34",
58
58
  "@xterm/headless": "^6.0",
59
59
  "ajv": "^8.18",
@@ -1,7 +1,7 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { StringEnum } from "@f5xc-salesdemos/pi-ai";
4
- import { Text } from "@f5xc-salesdemos/pi-tui";
4
+ import { type Component, Text } from "@f5xc-salesdemos/pi-tui";
5
5
  import { Type } from "@sinclair/typebox";
6
6
  import type { ToolDefinition } from "../../extensibility/extensions";
7
7
  import type { Theme } from "../../modes/theme/theme";
@@ -370,8 +370,13 @@ export function createInitExperimentTool(
370
370
  details: { state: cloneExperimentState(state) },
371
371
  };
372
372
  },
373
- renderCall(args, _options, theme): Text {
374
- return new Text(renderInitCall(args.name, theme), 0, 0);
373
+ renderCall(args, _options, theme): Component {
374
+ return {
375
+ render(width: number): string[] {
376
+ return [renderInitCall(args.name, theme, width)];
377
+ },
378
+ invalidate() {},
379
+ };
375
380
  },
376
381
  renderResult(result): Text {
377
382
  const text = replaceTabs(result.content.find(part => part.type === "text")?.text ?? "");
@@ -380,8 +385,8 @@ export function createInitExperimentTool(
380
385
  };
381
386
  }
382
387
 
383
- function renderInitCall(name: string, theme: Theme): string {
384
- return `${theme.fg("toolTitle", theme.bold("init_experiment"))} ${theme.fg("contentAccent", truncateToWidth(replaceTabs(name), 100))}`;
388
+ function renderInitCall(name: string, theme: Theme, width?: number): string {
389
+ return `${theme.fg("toolTitle", theme.bold("init_experiment"))} ${theme.fg("contentAccent", truncateToWidth(replaceTabs(name), Math.max(20, (width ?? 100) - 20)))}`;
385
390
  }
386
391
 
387
392
  function collectLoggedRunNumbers(results: ExperimentState["results"]): Set<number> {
@@ -1,7 +1,7 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { StringEnum } from "@f5xc-salesdemos/pi-ai";
4
- import { Text } from "@f5xc-salesdemos/pi-tui";
4
+ import { type Component, Text } from "@f5xc-salesdemos/pi-tui";
5
5
  import { logger } from "@f5xc-salesdemos/pi-utils";
6
6
  import { Type } from "@sinclair/typebox";
7
7
  import type { ToolDefinition } from "../../extensibility/extensions";
@@ -358,22 +358,29 @@ export function createLogExperimentTool(
358
358
  },
359
359
  };
360
360
  },
361
- renderCall(args, _options, theme): Text {
361
+ renderCall(args, _options, theme): Component {
362
362
  const color = args.status === "keep" ? "success" : args.status === "discard" ? "warning" : "error";
363
- const description = truncateToWidth(replaceTabs(args.description), 100);
364
- return new Text(
365
- `${theme.fg("toolTitle", theme.bold("log_experiment"))} ${theme.fg(color, args.status)} ${theme.fg("muted", description)}`,
366
- 0,
367
- 0,
368
- );
363
+ return {
364
+ render(width: number): string[] {
365
+ const description = truncateToWidth(replaceTabs(args.description), Math.max(20, width - 30));
366
+ return [
367
+ `${theme.fg("toolTitle", theme.bold("log_experiment"))} ${theme.fg(color, args.status)} ${theme.fg("muted", description)}`,
368
+ ];
369
+ },
370
+ invalidate() {},
371
+ };
369
372
  },
370
- renderResult(result, _options, theme): Text {
373
+ renderResult(result, _options, theme): Component {
371
374
  const details = result.details;
372
375
  if (!details) {
373
376
  return new Text(replaceTabs(result.content.find(part => part.type === "text")?.text ?? ""), 0, 0);
374
377
  }
375
- const summary = renderSummary(details, theme);
376
- return new Text(summary, 0, 0);
378
+ return {
379
+ render(width: number): string[] {
380
+ return [renderSummary(details, theme, width)];
381
+ },
382
+ invalidate() {},
383
+ };
377
384
  },
378
385
  };
379
386
  }
@@ -763,10 +770,10 @@ function truncateAsiValue(value: ASIData[string]): string {
763
770
  return text.length > 120 ? `${text.slice(0, 117)}...` : text;
764
771
  }
765
772
 
766
- function renderSummary(details: LogDetails, theme: Theme): string {
773
+ function renderSummary(details: LogDetails, theme: Theme, width?: number): string {
767
774
  const { experiment, state } = details;
768
775
  const color = experiment.status === "keep" ? "success" : experiment.status === "discard" ? "warning" : "error";
769
- let summary = `${theme.fg(color, experiment.status.toUpperCase())} ${theme.fg("muted", truncateToWidth(replaceTabs(experiment.description), 100))}`;
776
+ let summary = `${theme.fg(color, experiment.status.toUpperCase())} ${theme.fg("muted", truncateToWidth(replaceTabs(experiment.description), Math.max(20, (width ?? 100) - 30)))}`;
770
777
  summary += ` ${theme.fg("contentAccent", `${state.metricName}=${formatNum(experiment.metric, state.metricUnit)}`)}`;
771
778
  if (state.bestMetric !== null) {
772
779
  summary += ` ${theme.fg("dim", `baseline ${formatNum(state.bestMetric, state.metricUnit)}`)}`;
@@ -1,7 +1,7 @@
1
1
  import * as childProcess from "node:child_process";
2
2
  import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
- import { Text } from "@f5xc-salesdemos/pi-tui";
4
+ import { type Component, Text } from "@f5xc-salesdemos/pi-tui";
5
5
  import { formatBytes } from "@f5xc-salesdemos/pi-utils";
6
6
  import { Type } from "@sinclair/typebox";
7
7
  import type { ToolDefinition } from "../../extensibility/extensions";
@@ -380,13 +380,14 @@ export function createRunExperimentTool(
380
380
  details: resultDetails,
381
381
  };
382
382
  },
383
- renderCall(args, _options, theme): Text {
384
- const commandPreview = truncateToWidth(replaceTabs(args.command), 100);
385
- return new Text(
386
- `${theme.fg("toolTitle", theme.bold("run_experiment"))} ${theme.fg("muted", commandPreview)}`,
387
- 0,
388
- 0,
389
- );
383
+ renderCall(args, _options, theme): Component {
384
+ return {
385
+ render(width: number): string[] {
386
+ const commandPreview = truncateToWidth(replaceTabs(args.command), Math.max(20, width - 20));
387
+ return [`${theme.fg("toolTitle", theme.bold("run_experiment"))} ${theme.fg("muted", commandPreview)}`];
388
+ },
389
+ invalidate() {},
390
+ };
390
391
  },
391
392
  renderResult(result, options, theme): Text {
392
393
  if (isProgressDetails(result.details)) {
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import type { ToolCallContext } from "@f5xc-salesdemos/pi-agent-core";
5
5
  import type { Component } from "@f5xc-salesdemos/pi-tui";
6
- import { Text, visibleWidth, wrapTextWithAnsi } from "@f5xc-salesdemos/pi-tui";
6
+ import { visibleWidth, wrapTextWithAnsi } from "@f5xc-salesdemos/pi-tui";
7
7
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
8
8
  import type { FileDiagnosticsResult } from "../lsp";
9
9
  import { renderDiff as renderDiffColored } from "../modes/components/diff";
@@ -145,8 +145,6 @@ export interface EditRenderContext {
145
145
 
146
146
  const EDIT_STREAMING_PREVIEW_LINES = 12;
147
147
  const CALL_TEXT_PREVIEW_LINES = 6;
148
- const CALL_TEXT_PREVIEW_WIDTH = 80;
149
- const STREAMING_EDIT_PREVIEW_WIDTH = 120;
150
148
  const STREAMING_EDIT_PREVIEW_LIMIT = 4;
151
149
  const STREAMING_EDIT_PREVIEW_DST_LINE_LIMIT = 8;
152
150
 
@@ -207,11 +205,11 @@ function formatEditDescription(
207
205
  };
208
206
  }
209
207
 
210
- function renderPlainTextPreview(text: string, uiTheme: Theme): string {
208
+ function renderPlainTextPreview(text: string, uiTheme: Theme, width: number): string {
211
209
  const previewLines = text.split("\n");
212
210
  let preview = "\n\n";
213
211
  for (const line of previewLines.slice(0, CALL_TEXT_PREVIEW_LINES)) {
214
- preview += `${uiTheme.fg("toolOutput", truncateToWidth(replaceTabs(line), CALL_TEXT_PREVIEW_WIDTH))}\n`;
212
+ preview += `${uiTheme.fg("toolOutput", truncateToWidth(replaceTabs(line), width))}\n`;
215
213
  }
216
214
  if (previewLines.length > CALL_TEXT_PREVIEW_LINES) {
217
215
  preview += uiTheme.fg("dim", `… ${previewLines.length - CALL_TEXT_PREVIEW_LINES} more lines`);
@@ -299,7 +297,11 @@ function formatChunkStreamingEdit(edit: Partial<ChunkToolEdit>): FormattedStream
299
297
  return { srcLabel: `\u2022 edit ${target}`, dst: "" };
300
298
  }
301
299
 
302
- function formatStreamingHashlineEdits(edits: Partial<HashlineToolEdit | ChunkToolEdit>[], uiTheme: Theme): string {
300
+ function formatStreamingHashlineEdits(
301
+ edits: Partial<HashlineToolEdit | ChunkToolEdit>[],
302
+ uiTheme: Theme,
303
+ width: number,
304
+ ): string {
303
305
  let text = "\n\n";
304
306
 
305
307
  // Detect whether these are chunk edits (target field) or hashline edits (loc field)
@@ -314,17 +316,17 @@ function formatStreamingHashlineEdits(edits: Partial<HashlineToolEdit | ChunkToo
314
316
  shownEdits++;
315
317
  if (shownEdits > STREAMING_EDIT_PREVIEW_LIMIT) break;
316
318
  const formatted = formatEdit(edit as never);
317
- text += uiTheme.fg("toolOutput", truncateToWidth(replaceTabs(formatted.srcLabel), STREAMING_EDIT_PREVIEW_WIDTH));
319
+ text += uiTheme.fg("toolOutput", truncateToWidth(replaceTabs(formatted.srcLabel), width));
318
320
  text += "\n";
319
321
  if (formatted.dst === "") {
320
- text += uiTheme.fg("dim", truncateToWidth(" (delete)", STREAMING_EDIT_PREVIEW_WIDTH));
322
+ text += uiTheme.fg("dim", truncateToWidth(" (delete)", width));
321
323
  text += "\n";
322
324
  continue;
323
325
  }
324
326
  for (const dstLine of formatted.dst.split("\n")) {
325
327
  shownDstLines++;
326
328
  if (shownDstLines > STREAMING_EDIT_PREVIEW_DST_LINE_LIMIT) break;
327
- text += uiTheme.fg("toolOutput", truncateToWidth(replaceTabs(`+ ${dstLine}`), STREAMING_EDIT_PREVIEW_WIDTH));
329
+ text += uiTheme.fg("toolOutput", truncateToWidth(replaceTabs(`+ ${dstLine}`), width));
328
330
  text += "\n";
329
331
  }
330
332
  if (shownDstLines > STREAMING_EDIT_PREVIEW_DST_LINE_LIMIT) break;
@@ -347,7 +349,7 @@ function formatMetadataLine(lineCount: number | null, language: string | undefin
347
349
  return uiTheme.fg("dim", `${icon}`);
348
350
  }
349
351
 
350
- function getCallPreview(args: EditRenderArgs, rawPath: string, uiTheme: Theme): string {
352
+ function getCallPreview(args: EditRenderArgs, rawPath: string, uiTheme: Theme, width: number): string {
351
353
  if (args.previewDiff) {
352
354
  return formatStreamingDiff(args.previewDiff, rawPath, uiTheme, "preview");
353
355
  }
@@ -358,14 +360,14 @@ function getCallPreview(args: EditRenderArgs, rawPath: string, uiTheme: Theme):
358
360
  // Only show hashline/chunk streaming edits — replace/patch use previewDiff above
359
361
  const first = args.edits[0];
360
362
  if (first && typeof first === "object" && ("loc" in first || isChunkStreamingEdit(first))) {
361
- return formatStreamingHashlineEdits(args.edits, uiTheme);
363
+ return formatStreamingHashlineEdits(args.edits, uiTheme, width);
362
364
  }
363
365
  }
364
366
  if (args.diff) {
365
- return renderPlainTextPreview(args.diff, uiTheme);
367
+ return renderPlainTextPreview(args.diff, uiTheme, width);
366
368
  }
367
369
  if (args.newText || args.patch) {
368
- return renderPlainTextPreview(args.newText ?? args.patch ?? "", uiTheme);
370
+ return renderPlainTextPreview(args.newText ?? args.patch ?? "", uiTheme, width);
369
371
  }
370
372
  return "";
371
373
  }
@@ -445,15 +447,20 @@ export const editToolRenderer = {
445
447
  const { description } = formatEditDescription(rawPath, uiTheme, { rename });
446
448
  const spinner =
447
449
  options?.spinnerFrame !== undefined ? formatStatusIcon("running", uiTheme, options.spinnerFrame) : "";
448
- let text = `${formatTitle(getOperationTitle(op), uiTheme)} ${spinner ? `${spinner} ` : ""}${description}`;
450
+ let header = `${formatTitle(getOperationTitle(op), uiTheme)} ${spinner ? `${spinner} ` : ""}${description}`;
449
451
  // Show file count hint for multi-file edits
450
452
  const fileCount = Array.isArray(args.edits) ? countEditFiles(args.edits as any[]) : 0;
451
453
  if (fileCount > 1) {
452
- text += uiTheme.fg("dim", ` (+${fileCount - 1} more)`);
454
+ header += uiTheme.fg("dim", ` (+${fileCount - 1} more)`);
453
455
  }
454
- text += getCallPreview(args, rawPath, uiTheme);
455
456
 
456
- return new Text(text, 0, 0);
457
+ return {
458
+ render(width: number) {
459
+ const text = header + getCallPreview(args, rawPath, uiTheme, width);
460
+ return text.split("\n");
461
+ },
462
+ invalidate() {},
463
+ };
457
464
  },
458
465
 
459
466
  renderResult(
package/src/exa/render.ts CHANGED
@@ -15,17 +15,12 @@ import {
15
15
  getDomain,
16
16
  getPreviewLines,
17
17
  PREVIEW_LIMITS,
18
- TRUNCATE_LENGTHS,
19
18
  truncateToWidth,
20
19
  } from "../tools/render-utils";
21
20
  import type { ExaRenderDetails } from "./types";
22
21
 
23
22
  const COLLAPSED_PREVIEW_LINES = PREVIEW_LIMITS.COLLAPSED_LINES;
24
- const COLLAPSED_PREVIEW_LINE_LEN = TRUNCATE_LENGTHS.LONG;
25
23
  const EXPANDED_TEXT_LINES = 5;
26
- const EXPANDED_TEXT_LINE_LEN = 90;
27
- const MAX_TITLE_LEN = TRUNCATE_LENGTHS.TITLE;
28
- const MAX_HIGHLIGHT_LEN = TRUNCATE_LENGTHS.CONTENT;
29
24
 
30
25
  function renderErrorMessage(message: string, theme: Theme): Text {
31
26
  const clean = message.replace(/^Error:\s*/, "").trim();
@@ -42,7 +37,6 @@ export function renderExaResult(
42
37
  options: RenderResultOptions,
43
38
  uiTheme: Theme,
44
39
  ): Component {
45
- const { expanded } = options;
46
40
  const details = result.details;
47
41
 
48
42
  if (details?.error) {
@@ -51,187 +45,188 @@ export function renderExaResult(
51
45
  }
52
46
 
53
47
  const response = details?.response;
54
- if (!response) {
55
- if (details?.raw) {
56
- const rawText = typeof details.raw === "string" ? details.raw : JSON.stringify(details.raw, null, 2);
57
- const rawLines = rawText.split("\n").filter(l => l.trim());
58
- const maxLines = expanded ? rawLines.length : Math.min(rawLines.length, COLLAPSED_PREVIEW_LINES);
59
- const displayLines = rawLines.slice(0, maxLines);
60
- const remaining = rawLines.length - maxLines;
61
- const expandHint = formatExpandHint(uiTheme, expanded, remaining > 0);
62
-
63
- let text = `${uiTheme.fg("dim", "Raw response")}${expandHint}`;
64
-
65
- for (let i = 0; i < displayLines.length; i++) {
66
- const isLast = i === displayLines.length - 1 && remaining === 0;
67
- const branch = isLast ? uiTheme.tree.last : uiTheme.tree.branch;
68
- text += `\n ${uiTheme.fg("dim", branch)} ${uiTheme.fg(
69
- "toolOutput",
70
- truncateToWidth(displayLines[i], COLLAPSED_PREVIEW_LINE_LEN),
71
- )}`;
72
- }
73
-
74
- if (remaining > 0) {
75
- text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg(
76
- "muted",
77
- formatMoreItems(remaining, "line"),
78
- )}`;
79
- }
80
-
81
- return new Text(text, 0, 0);
82
- }
48
+ if (!response && !details?.raw) {
83
49
  return renderEmptyMessage("No response data", uiTheme);
84
50
  }
85
51
 
86
- const results = response.results ?? [];
87
- const resultCount = results.length;
88
- const cost = response.costDollars?.total;
89
- const time = response.searchTime;
90
-
91
- const metaParts = [formatCount("result", resultCount)];
92
- if (cost !== undefined) metaParts.push(`cost:$${cost.toFixed(4)}`);
93
- if (time !== undefined) metaParts.push(`time:${time.toFixed(2)}s`);
94
- const summaryText = metaParts.join(uiTheme.sep.dot);
95
-
96
- let hasMorePreview = false;
97
- if (!expanded && resultCount > 0) {
98
- const previewText = results[0].text ?? results[0].title ?? "";
99
- const totalLines = previewText.split("\n").filter(l => l.trim()).length;
100
- hasMorePreview = totalLines > COLLAPSED_PREVIEW_LINES || resultCount > 1;
101
- }
102
- const expandHint = formatExpandHint(uiTheme, expanded, hasMorePreview);
103
-
104
- let text = `${uiTheme.fg("dim", summaryText)}${expandHint}`;
105
-
106
- if (!expanded) {
107
- if (resultCount === 0) {
108
- text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg("muted", "No results")}`;
109
- return new Text(text, 0, 0);
110
- }
111
-
112
- const first = results[0];
113
- const previewText = first.text ?? first.title ?? "";
114
- const previewLines = previewText
115
- ? getPreviewLines(previewText, COLLAPSED_PREVIEW_LINES, COLLAPSED_PREVIEW_LINE_LEN)
116
- : [];
117
- const safePreviewLines = previewLines.length > 0 ? previewLines : ["No preview text"];
118
- const totalLines = previewText.split("\n").filter(l => l.trim()).length;
119
- const remainingLines = Math.max(0, totalLines - previewLines.length);
120
- const extraItems: string[] = [];
121
- if (remainingLines > 0) {
122
- extraItems.push(formatMoreItems(remainingLines, "line"));
123
- }
124
- if (resultCount > 1) {
125
- extraItems.push(formatMoreItems(resultCount - 1, "result"));
126
- }
127
-
128
- for (let i = 0; i < safePreviewLines.length; i++) {
129
- const isLast = i === safePreviewLines.length - 1 && extraItems.length === 0;
130
- const branch = isLast ? uiTheme.tree.last : uiTheme.tree.branch;
131
- const line = safePreviewLines[i];
132
- const color = line === "No preview text" ? "muted" : "toolOutput";
133
- text += `\n ${uiTheme.fg("dim", branch)} ${uiTheme.fg(color, line)}`;
134
- }
135
-
136
- for (let i = 0; i < extraItems.length; i++) {
137
- const isLast = i === extraItems.length - 1;
138
- const branch = isLast ? uiTheme.tree.last : uiTheme.tree.branch;
139
- text += `\n ${uiTheme.fg("dim", branch)} ${uiTheme.fg("muted", extraItems[i])}`;
140
- }
141
-
142
- return new Text(text, 0, 0);
143
- }
144
-
145
- if (resultCount === 0) {
146
- text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg("muted", "No results")}`;
147
- return new Text(text, 0, 0);
148
- }
52
+ return {
53
+ render(width: number): string[] {
54
+ const { expanded } = options;
55
+ const contentWidth = Math.max(20, width - 6);
56
+
57
+ if (!response) {
58
+ const rawText = typeof details?.raw === "string" ? details.raw : JSON.stringify(details?.raw, null, 2);
59
+ const rawLines = rawText.split("\n").filter(l => l.trim());
60
+ const maxLines = expanded ? rawLines.length : Math.min(rawLines.length, COLLAPSED_PREVIEW_LINES);
61
+ const displayLines = rawLines.slice(0, maxLines);
62
+ const remaining = rawLines.length - maxLines;
63
+ const expandHint = formatExpandHint(uiTheme, expanded, remaining > 0);
64
+
65
+ const lines: string[] = [`${uiTheme.fg("dim", "Raw response")}${expandHint}`];
66
+
67
+ for (let i = 0; i < displayLines.length; i++) {
68
+ const isLast = i === displayLines.length - 1 && remaining === 0;
69
+ const branch = isLast ? uiTheme.tree.last : uiTheme.tree.branch;
70
+ lines.push(
71
+ ` ${uiTheme.fg("dim", branch)} ${uiTheme.fg("toolOutput", truncateToWidth(displayLines[i], contentWidth))}`,
72
+ );
73
+ }
74
+
75
+ if (remaining > 0) {
76
+ lines.push(
77
+ ` ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg("muted", formatMoreItems(remaining, "line"))}`,
78
+ );
79
+ }
80
+
81
+ return lines;
82
+ }
149
83
 
150
- for (let i = 0; i < results.length; i++) {
151
- const res = results[i];
152
- const isLast = i === results.length - 1;
153
- const branch = isLast ? uiTheme.tree.last : uiTheme.tree.branch;
154
- const cont = isLast ? " " : uiTheme.tree.vertical;
155
-
156
- const title = truncateToWidth(res.title ?? "Untitled", MAX_TITLE_LEN);
157
- const domain = res.url ? getDomain(res.url) : "";
158
- const domainPart = domain ? uiTheme.fg("dim", ` (${domain})`) : "";
159
-
160
- text += `\n ${uiTheme.fg("dim", branch)} ${uiTheme.fg("contentAccent", title)}${domainPart}`;
161
-
162
- if (res.url) {
163
- text += `\n ${uiTheme.fg("dim", cont)} ${uiTheme.fg("dim", uiTheme.tree.hook)} ${uiTheme.fg(
164
- "mdLinkUrl",
165
- res.url,
166
- )}`;
167
- }
168
-
169
- if (res.author) {
170
- text += `\n ${uiTheme.fg("dim", cont)} ${uiTheme.fg("dim", uiTheme.tree.hook)} ${uiTheme.fg(
171
- "muted",
172
- `Author: ${res.author}`,
173
- )}`;
174
- }
175
-
176
- if (res.publishedDate) {
177
- text += `\n ${uiTheme.fg("dim", cont)} ${uiTheme.fg("dim", uiTheme.tree.hook)} ${uiTheme.fg(
178
- "muted",
179
- `Published: ${res.publishedDate}`,
180
- )}`;
181
- }
182
-
183
- if (res.text) {
184
- const textLines = res.text.split("\n").filter(l => l.trim());
185
- const displayLines = textLines.slice(0, EXPANDED_TEXT_LINES);
186
- for (const line of displayLines) {
187
- text += `\n ${uiTheme.fg("dim", cont)} ${uiTheme.fg("dim", uiTheme.tree.hook)} ${uiTheme.fg(
188
- "toolOutput",
189
- truncateToWidth(line.trim(), EXPANDED_TEXT_LINE_LEN),
190
- )}`;
84
+ const results = response.results ?? [];
85
+ const resultCount = results.length;
86
+ const cost = response.costDollars?.total;
87
+ const time = response.searchTime;
88
+
89
+ const metaParts = [formatCount("result", resultCount)];
90
+ if (cost !== undefined) metaParts.push(`cost:$${cost.toFixed(4)}`);
91
+ if (time !== undefined) metaParts.push(`time:${time.toFixed(2)}s`);
92
+ const summaryText = metaParts.join(uiTheme.sep.dot);
93
+
94
+ let hasMorePreview = false;
95
+ if (!expanded && resultCount > 0) {
96
+ const previewText = results[0].text ?? results[0].title ?? "";
97
+ const totalLines = previewText.split("\n").filter(l => l.trim()).length;
98
+ hasMorePreview = totalLines > COLLAPSED_PREVIEW_LINES || resultCount > 1;
191
99
  }
192
- if (textLines.length > EXPANDED_TEXT_LINES) {
193
- text += `\n ${uiTheme.fg("dim", cont)} ${uiTheme.fg("dim", uiTheme.tree.hook)} ${uiTheme.fg(
194
- "muted",
195
- formatMoreItems(textLines.length - EXPANDED_TEXT_LINES, "line"),
196
- )}`;
100
+ const expandHint = formatExpandHint(uiTheme, expanded, hasMorePreview);
101
+
102
+ const lines: string[] = [`${uiTheme.fg("dim", summaryText)}${expandHint}`];
103
+
104
+ if (!expanded) {
105
+ if (resultCount === 0) {
106
+ lines.push(` ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg("muted", "No results")}`);
107
+ return lines;
108
+ }
109
+
110
+ const first = results[0];
111
+ const previewText = first.text ?? first.title ?? "";
112
+ const previewLines = previewText ? getPreviewLines(previewText, COLLAPSED_PREVIEW_LINES, contentWidth) : [];
113
+ const safePreviewLines = previewLines.length > 0 ? previewLines : ["No preview text"];
114
+ const totalLines = previewText.split("\n").filter(l => l.trim()).length;
115
+ const remainingLines = Math.max(0, totalLines - previewLines.length);
116
+ const extraItems: string[] = [];
117
+ if (remainingLines > 0) {
118
+ extraItems.push(formatMoreItems(remainingLines, "line"));
119
+ }
120
+ if (resultCount > 1) {
121
+ extraItems.push(formatMoreItems(resultCount - 1, "result"));
122
+ }
123
+
124
+ for (let i = 0; i < safePreviewLines.length; i++) {
125
+ const isLast = i === safePreviewLines.length - 1 && extraItems.length === 0;
126
+ const branch = isLast ? uiTheme.tree.last : uiTheme.tree.branch;
127
+ const line = safePreviewLines[i];
128
+ const color = line === "No preview text" ? "muted" : "toolOutput";
129
+ lines.push(` ${uiTheme.fg("dim", branch)} ${uiTheme.fg(color, line)}`);
130
+ }
131
+
132
+ for (let i = 0; i < extraItems.length; i++) {
133
+ const isLast = i === extraItems.length - 1;
134
+ const branch = isLast ? uiTheme.tree.last : uiTheme.tree.branch;
135
+ lines.push(` ${uiTheme.fg("dim", branch)} ${uiTheme.fg("muted", extraItems[i])}`);
136
+ }
137
+
138
+ return lines;
197
139
  }
198
- }
199
-
200
- if (res.highlights?.length) {
201
- text += `\n ${uiTheme.fg("dim", cont)} ${uiTheme.fg("dim", uiTheme.tree.hook)} ${uiTheme.fg(
202
- "contentAccent",
203
- "Highlights",
204
- )}`;
205
- const maxHighlights = Math.min(res.highlights.length, 3);
206
- for (let j = 0; j < maxHighlights; j++) {
207
- const h = res.highlights[j];
208
- text += `\n ${uiTheme.fg("dim", cont)} ${uiTheme.fg("dim", uiTheme.tree.hook)} ${uiTheme.fg(
209
- "muted",
210
- `${uiTheme.format.dash} ${truncateToWidth(h, MAX_HIGHLIGHT_LEN)}`,
211
- )}`;
140
+
141
+ if (resultCount === 0) {
142
+ lines.push(` ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg("muted", "No results")}`);
143
+ return lines;
212
144
  }
213
- if (res.highlights.length > maxHighlights) {
214
- text += `\n ${uiTheme.fg("dim", cont)} ${uiTheme.fg("dim", uiTheme.tree.hook)} ${uiTheme.fg(
215
- "muted",
216
- formatMoreItems(res.highlights.length - maxHighlights, "highlight"),
217
- )}`;
145
+
146
+ for (let i = 0; i < results.length; i++) {
147
+ const res = results[i];
148
+ const isLast = i === results.length - 1;
149
+ const branch = isLast ? uiTheme.tree.last : uiTheme.tree.branch;
150
+ const cont = isLast ? " " : uiTheme.tree.vertical;
151
+
152
+ const title = truncateToWidth(res.title ?? "Untitled", contentWidth);
153
+ const domain = res.url ? getDomain(res.url) : "";
154
+ const domainPart = domain ? uiTheme.fg("dim", ` (${domain})`) : "";
155
+
156
+ lines.push(` ${uiTheme.fg("dim", branch)} ${uiTheme.fg("contentAccent", title)}${domainPart}`);
157
+
158
+ if (res.url) {
159
+ lines.push(
160
+ ` ${uiTheme.fg("dim", cont)} ${uiTheme.fg("dim", uiTheme.tree.hook)} ${uiTheme.fg("mdLinkUrl", res.url)}`,
161
+ );
162
+ }
163
+
164
+ if (res.author) {
165
+ lines.push(
166
+ ` ${uiTheme.fg("dim", cont)} ${uiTheme.fg("dim", uiTheme.tree.hook)} ${uiTheme.fg("muted", `Author: ${res.author}`)}`,
167
+ );
168
+ }
169
+
170
+ if (res.publishedDate) {
171
+ lines.push(
172
+ ` ${uiTheme.fg("dim", cont)} ${uiTheme.fg("dim", uiTheme.tree.hook)} ${uiTheme.fg("muted", `Published: ${res.publishedDate}`)}`,
173
+ );
174
+ }
175
+
176
+ if (res.text) {
177
+ const textLines = res.text.split("\n").filter(l => l.trim());
178
+ const displayLines = textLines.slice(0, EXPANDED_TEXT_LINES);
179
+ for (const line of displayLines) {
180
+ lines.push(
181
+ ` ${uiTheme.fg("dim", cont)} ${uiTheme.fg("dim", uiTheme.tree.hook)} ${uiTheme.fg("toolOutput", truncateToWidth(line.trim(), contentWidth))}`,
182
+ );
183
+ }
184
+ if (textLines.length > EXPANDED_TEXT_LINES) {
185
+ lines.push(
186
+ ` ${uiTheme.fg("dim", cont)} ${uiTheme.fg("dim", uiTheme.tree.hook)} ${uiTheme.fg("muted", formatMoreItems(textLines.length - EXPANDED_TEXT_LINES, "line"))}`,
187
+ );
188
+ }
189
+ }
190
+
191
+ if (res.highlights?.length) {
192
+ lines.push(
193
+ ` ${uiTheme.fg("dim", cont)} ${uiTheme.fg("dim", uiTheme.tree.hook)} ${uiTheme.fg("contentAccent", "Highlights")}`,
194
+ );
195
+ const maxHighlights = Math.min(res.highlights.length, 3);
196
+ for (let j = 0; j < maxHighlights; j++) {
197
+ const h = res.highlights[j];
198
+ lines.push(
199
+ ` ${uiTheme.fg("dim", cont)} ${uiTheme.fg("dim", uiTheme.tree.hook)} ${uiTheme.fg("muted", `${uiTheme.format.dash} ${truncateToWidth(h, contentWidth)}`)}`,
200
+ );
201
+ }
202
+ if (res.highlights.length > maxHighlights) {
203
+ lines.push(
204
+ ` ${uiTheme.fg("dim", cont)} ${uiTheme.fg("dim", uiTheme.tree.hook)} ${uiTheme.fg("muted", formatMoreItems(res.highlights.length - maxHighlights, "highlight"))}`,
205
+ );
206
+ }
207
+ }
218
208
  }
219
- }
220
- }
221
209
 
222
- return new Text(text, 0, 0);
210
+ return lines;
211
+ },
212
+ invalidate() {},
213
+ };
223
214
  }
224
215
 
225
216
  /** Render Exa call (query/args preview) */
226
217
  export function renderExaCall(args: Record<string, unknown>, toolName: string, uiTheme: Theme): Component {
227
218
  const toolLabel = toolName || "Exa Search";
228
- const query = typeof args.query === "string" ? truncateToWidth(args.query, 80) : "?";
229
219
  const numResults = typeof args.num_results === "number" ? args.num_results : undefined;
230
220
 
231
- let text = `${uiTheme.fg("toolTitle", toolLabel)} ${uiTheme.fg("contentAccent", query)}`;
232
- if (numResults !== undefined) {
233
- text += ` ${uiTheme.fg("muted", `results:${numResults}`)}`;
234
- }
235
-
236
- return new Text(text, 0, 0);
221
+ return {
222
+ render(width: number): string[] {
223
+ const query = typeof args.query === "string" ? truncateToWidth(args.query, Math.max(20, width - 30)) : "?";
224
+ let text = `${uiTheme.fg("toolTitle", toolLabel)} ${uiTheme.fg("contentAccent", query)}`;
225
+ if (numResults !== undefined) {
226
+ text += ` ${uiTheme.fg("muted", `results:${numResults}`)}`;
227
+ }
228
+ return [truncateToWidth(text, width)];
229
+ },
230
+ invalidate() {},
231
+ };
237
232
  }