@oh-my-pi/pi-coding-agent 3.15.0 → 3.20.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.
Files changed (193) hide show
  1. package/CHANGELOG.md +61 -1
  2. package/docs/extensions.md +1055 -0
  3. package/docs/rpc.md +69 -13
  4. package/docs/session-tree-plan.md +1 -1
  5. package/examples/extensions/README.md +141 -0
  6. package/examples/extensions/api-demo.ts +87 -0
  7. package/examples/extensions/chalk-logger.ts +26 -0
  8. package/examples/extensions/hello.ts +33 -0
  9. package/examples/extensions/pirate.ts +44 -0
  10. package/examples/extensions/plan-mode.ts +551 -0
  11. package/examples/extensions/subagent/agents/reviewer.md +35 -0
  12. package/examples/extensions/todo.ts +299 -0
  13. package/examples/extensions/tools.ts +145 -0
  14. package/examples/extensions/with-deps/index.ts +36 -0
  15. package/examples/extensions/with-deps/package-lock.json +31 -0
  16. package/examples/extensions/with-deps/package.json +16 -0
  17. package/examples/sdk/02-custom-model.ts +3 -3
  18. package/examples/sdk/05-tools.ts +7 -3
  19. package/examples/sdk/06-extensions.ts +81 -0
  20. package/examples/sdk/06-hooks.ts +14 -13
  21. package/examples/sdk/08-prompt-templates.ts +42 -0
  22. package/examples/sdk/08-slash-commands.ts +17 -12
  23. package/examples/sdk/09-api-keys-and-oauth.ts +2 -2
  24. package/examples/sdk/12-full-control.ts +6 -6
  25. package/package.json +11 -7
  26. package/src/capability/extension-module.ts +34 -0
  27. package/src/cli/args.ts +22 -7
  28. package/src/cli/file-processor.ts +38 -67
  29. package/src/cli/list-models.ts +1 -1
  30. package/src/config.ts +25 -14
  31. package/src/core/agent-session.ts +505 -242
  32. package/src/core/auth-storage.ts +33 -21
  33. package/src/core/compaction/branch-summarization.ts +4 -4
  34. package/src/core/compaction/compaction.ts +3 -3
  35. package/src/core/custom-commands/bundled/wt/index.ts +430 -0
  36. package/src/core/custom-commands/loader.ts +9 -0
  37. package/src/core/custom-tools/wrapper.ts +5 -0
  38. package/src/core/event-bus.ts +59 -0
  39. package/src/core/export-html/vendor/highlight.min.js +1213 -0
  40. package/src/core/export-html/vendor/marked.min.js +6 -0
  41. package/src/core/extensions/index.ts +100 -0
  42. package/src/core/extensions/loader.ts +501 -0
  43. package/src/core/extensions/runner.ts +477 -0
  44. package/src/core/extensions/types.ts +712 -0
  45. package/src/core/extensions/wrapper.ts +147 -0
  46. package/src/core/hooks/types.ts +2 -2
  47. package/src/core/index.ts +10 -21
  48. package/src/core/keybindings.ts +199 -0
  49. package/src/core/messages.ts +26 -7
  50. package/src/core/model-registry.ts +123 -46
  51. package/src/core/model-resolver.ts +7 -5
  52. package/src/core/prompt-templates.ts +242 -0
  53. package/src/core/sdk.ts +378 -295
  54. package/src/core/session-manager.ts +72 -58
  55. package/src/core/settings-manager.ts +118 -22
  56. package/src/core/system-prompt.ts +24 -1
  57. package/src/core/terminal-notify.ts +37 -0
  58. package/src/core/tools/context.ts +4 -4
  59. package/src/core/tools/exa/mcp-client.ts +5 -4
  60. package/src/core/tools/exa/render.ts +176 -131
  61. package/src/core/tools/gemini-image.ts +361 -0
  62. package/src/core/tools/git.ts +216 -0
  63. package/src/core/tools/index.ts +28 -15
  64. package/src/core/tools/lsp/config.ts +5 -4
  65. package/src/core/tools/lsp/index.ts +17 -12
  66. package/src/core/tools/lsp/render.ts +39 -47
  67. package/src/core/tools/read.ts +66 -29
  68. package/src/core/tools/render-utils.ts +268 -0
  69. package/src/core/tools/renderers.ts +243 -225
  70. package/src/core/tools/task/discovery.ts +2 -2
  71. package/src/core/tools/task/executor.ts +66 -58
  72. package/src/core/tools/task/index.ts +29 -10
  73. package/src/core/tools/task/model-resolver.ts +8 -13
  74. package/src/core/tools/task/omp-command.ts +24 -0
  75. package/src/core/tools/task/render.ts +35 -60
  76. package/src/core/tools/task/types.ts +3 -0
  77. package/src/core/tools/web-fetch.ts +29 -28
  78. package/src/core/tools/web-search/index.ts +6 -5
  79. package/src/core/tools/web-search/providers/exa.ts +6 -5
  80. package/src/core/tools/web-search/render.ts +66 -111
  81. package/src/core/voice-controller.ts +135 -0
  82. package/src/core/voice-supervisor.ts +1003 -0
  83. package/src/core/voice.ts +308 -0
  84. package/src/discovery/builtin.ts +75 -1
  85. package/src/discovery/claude.ts +47 -1
  86. package/src/discovery/codex.ts +54 -2
  87. package/src/discovery/gemini.ts +55 -2
  88. package/src/discovery/helpers.ts +100 -1
  89. package/src/discovery/index.ts +2 -0
  90. package/src/index.ts +14 -9
  91. package/src/lib/worktree/collapse.ts +179 -0
  92. package/src/lib/worktree/constants.ts +14 -0
  93. package/src/lib/worktree/errors.ts +23 -0
  94. package/src/lib/worktree/git.ts +110 -0
  95. package/src/lib/worktree/index.ts +23 -0
  96. package/src/lib/worktree/operations.ts +216 -0
  97. package/src/lib/worktree/session.ts +114 -0
  98. package/src/lib/worktree/stats.ts +67 -0
  99. package/src/main.ts +61 -37
  100. package/src/migrations.ts +37 -7
  101. package/src/modes/interactive/components/bash-execution.ts +6 -4
  102. package/src/modes/interactive/components/custom-editor.ts +55 -0
  103. package/src/modes/interactive/components/custom-message.ts +95 -0
  104. package/src/modes/interactive/components/extensions/extension-list.ts +5 -0
  105. package/src/modes/interactive/components/extensions/inspector-panel.ts +18 -12
  106. package/src/modes/interactive/components/extensions/state-manager.ts +12 -0
  107. package/src/modes/interactive/components/extensions/types.ts +1 -0
  108. package/src/modes/interactive/components/footer.ts +324 -0
  109. package/src/modes/interactive/components/hook-editor.ts +1 -0
  110. package/src/modes/interactive/components/hook-selector.ts +3 -3
  111. package/src/modes/interactive/components/model-selector.ts +7 -6
  112. package/src/modes/interactive/components/oauth-selector.ts +3 -3
  113. package/src/modes/interactive/components/settings-defs.ts +55 -6
  114. package/src/modes/interactive/components/status-line/separators.ts +4 -4
  115. package/src/modes/interactive/components/status-line.ts +45 -35
  116. package/src/modes/interactive/components/tool-execution.ts +95 -23
  117. package/src/modes/interactive/interactive-mode.ts +644 -113
  118. package/src/modes/interactive/theme/defaults/alabaster.json +99 -0
  119. package/src/modes/interactive/theme/defaults/amethyst.json +103 -0
  120. package/src/modes/interactive/theme/defaults/anthracite.json +100 -0
  121. package/src/modes/interactive/theme/defaults/basalt.json +90 -0
  122. package/src/modes/interactive/theme/defaults/birch.json +101 -0
  123. package/src/modes/interactive/theme/defaults/dark-abyss.json +97 -0
  124. package/src/modes/interactive/theme/defaults/dark-aurora.json +94 -0
  125. package/src/modes/interactive/theme/defaults/dark-cavern.json +97 -0
  126. package/src/modes/interactive/theme/defaults/dark-copper.json +94 -0
  127. package/src/modes/interactive/theme/defaults/dark-cosmos.json +96 -0
  128. package/src/modes/interactive/theme/defaults/dark-eclipse.json +97 -0
  129. package/src/modes/interactive/theme/defaults/dark-ember.json +94 -0
  130. package/src/modes/interactive/theme/defaults/dark-equinox.json +96 -0
  131. package/src/modes/interactive/theme/defaults/dark-lavender.json +94 -0
  132. package/src/modes/interactive/theme/defaults/dark-lunar.json +95 -0
  133. package/src/modes/interactive/theme/defaults/dark-midnight.json +94 -0
  134. package/src/modes/interactive/theme/defaults/dark-nebula.json +96 -0
  135. package/src/modes/interactive/theme/defaults/dark-rainforest.json +97 -0
  136. package/src/modes/interactive/theme/defaults/dark-reef.json +97 -0
  137. package/src/modes/interactive/theme/defaults/dark-sakura.json +94 -0
  138. package/src/modes/interactive/theme/defaults/dark-slate.json +94 -0
  139. package/src/modes/interactive/theme/defaults/dark-solstice.json +96 -0
  140. package/src/modes/interactive/theme/defaults/dark-starfall.json +97 -0
  141. package/src/modes/interactive/theme/defaults/dark-swamp.json +96 -0
  142. package/src/modes/interactive/theme/defaults/dark-taiga.json +97 -0
  143. package/src/modes/interactive/theme/defaults/dark-terminal.json +94 -0
  144. package/src/modes/interactive/theme/defaults/dark-tundra.json +97 -0
  145. package/src/modes/interactive/theme/defaults/dark-twilight.json +97 -0
  146. package/src/modes/interactive/theme/defaults/dark-volcanic.json +97 -0
  147. package/src/modes/interactive/theme/defaults/graphite.json +99 -0
  148. package/src/modes/interactive/theme/defaults/index.ts +128 -0
  149. package/src/modes/interactive/theme/defaults/light-aurora-day.json +97 -0
  150. package/src/modes/interactive/theme/defaults/light-canyon.json +97 -0
  151. package/src/modes/interactive/theme/defaults/light-cirrus.json +96 -0
  152. package/src/modes/interactive/theme/defaults/light-coral.json +94 -0
  153. package/src/modes/interactive/theme/defaults/light-dawn.json +96 -0
  154. package/src/modes/interactive/theme/defaults/light-dunes.json +97 -0
  155. package/src/modes/interactive/theme/defaults/light-eucalyptus.json +94 -0
  156. package/src/modes/interactive/theme/defaults/light-frost.json +94 -0
  157. package/src/modes/interactive/theme/defaults/light-glacier.json +97 -0
  158. package/src/modes/interactive/theme/defaults/light-haze.json +96 -0
  159. package/src/modes/interactive/theme/defaults/light-honeycomb.json +94 -0
  160. package/src/modes/interactive/theme/defaults/light-lagoon.json +97 -0
  161. package/src/modes/interactive/theme/defaults/light-lavender.json +94 -0
  162. package/src/modes/interactive/theme/defaults/light-meadow.json +97 -0
  163. package/src/modes/interactive/theme/defaults/light-mint.json +94 -0
  164. package/src/modes/interactive/theme/defaults/light-opal.json +97 -0
  165. package/src/modes/interactive/theme/defaults/light-orchard.json +97 -0
  166. package/src/modes/interactive/theme/defaults/light-paper.json +94 -0
  167. package/src/modes/interactive/theme/defaults/light-prism.json +96 -0
  168. package/src/modes/interactive/theme/defaults/light-sand.json +94 -0
  169. package/src/modes/interactive/theme/defaults/light-savanna.json +97 -0
  170. package/src/modes/interactive/theme/defaults/light-soleil.json +96 -0
  171. package/src/modes/interactive/theme/defaults/light-wetland.json +97 -0
  172. package/src/modes/interactive/theme/defaults/light-zenith.json +95 -0
  173. package/src/modes/interactive/theme/defaults/limestone.json +100 -0
  174. package/src/modes/interactive/theme/defaults/mahogany.json +104 -0
  175. package/src/modes/interactive/theme/defaults/marble.json +99 -0
  176. package/src/modes/interactive/theme/defaults/obsidian.json +90 -0
  177. package/src/modes/interactive/theme/defaults/onyx.json +90 -0
  178. package/src/modes/interactive/theme/defaults/pearl.json +99 -0
  179. package/src/modes/interactive/theme/defaults/porcelain.json +90 -0
  180. package/src/modes/interactive/theme/defaults/quartz.json +102 -0
  181. package/src/modes/interactive/theme/defaults/sandstone.json +101 -0
  182. package/src/modes/interactive/theme/defaults/titanium.json +89 -0
  183. package/src/modes/print-mode.ts +14 -72
  184. package/src/modes/rpc/rpc-client.ts +23 -9
  185. package/src/modes/rpc/rpc-mode.ts +137 -125
  186. package/src/modes/rpc/rpc-types.ts +46 -24
  187. package/src/prompts/task.md +1 -0
  188. package/src/prompts/tools/gemini-image.md +4 -0
  189. package/src/prompts/tools/git.md +9 -0
  190. package/src/prompts/voice-summary.md +12 -0
  191. package/src/utils/image-convert.ts +26 -0
  192. package/src/utils/image-resize.ts +215 -0
  193. package/src/utils/shell-snapshot.ts +22 -20
@@ -1,21 +1,21 @@
1
1
  import type { AgentToolContext } from "@oh-my-pi/pi-agent-core";
2
2
  import type { CustomToolContext } from "../custom-tools/types";
3
- import type { HookUIContext } from "../hooks/types";
3
+ import type { ExtensionUIContext } from "../extensions/types";
4
4
 
5
5
  declare module "@oh-my-pi/pi-agent-core" {
6
6
  interface AgentToolContext extends CustomToolContext {
7
- ui?: HookUIContext;
7
+ ui?: ExtensionUIContext;
8
8
  hasUI?: boolean;
9
9
  }
10
10
  }
11
11
 
12
12
  export interface ToolContextStore {
13
13
  getContext(): AgentToolContext;
14
- setUIContext(uiContext: HookUIContext, hasUI: boolean): void;
14
+ setUIContext(uiContext: ExtensionUIContext, hasUI: boolean): void;
15
15
  }
16
16
 
17
17
  export function createToolContextStore(getBaseContext: () => CustomToolContext): ToolContextStore {
18
- let uiContext: HookUIContext | undefined;
18
+ let uiContext: ExtensionUIContext | undefined;
19
19
  let hasUI = false;
20
20
 
21
21
  return {
@@ -4,6 +4,8 @@
4
4
  * Client for interacting with Exa MCP servers via JSON-RPC 2.0 over HTTPS.
5
5
  */
6
6
 
7
+ import { existsSync, readFileSync } from "node:fs";
8
+ import { homedir } from "node:os";
7
9
  import type { TSchema } from "@sinclair/typebox";
8
10
  import type { CustomTool } from "../../custom-tools/types";
9
11
  import { logger } from "../../logger";
@@ -26,14 +28,13 @@ export async function findApiKey(): Promise<string | null> {
26
28
 
27
29
  // Try loading from .env files in cwd and home
28
30
  const cwd = process.cwd();
29
- const home = process.env.HOME ?? process.env.USERPROFILE ?? "~";
31
+ const home = homedir();
30
32
 
31
33
  for (const dir of [cwd, home]) {
32
34
  const envPath = `${dir}/.env`;
33
35
  try {
34
- const file = Bun.file(envPath);
35
- if (await file.exists()) {
36
- const content = await file.text();
36
+ if (existsSync(envPath)) {
37
+ const content = readFileSync(envPath, "utf-8");
37
38
  const match = content.match(/^EXA_API_KEY=(.+)$/m);
38
39
  if (match?.[1]) {
39
40
  return match[1].trim().replace(/^["']|["']$/g, "");
@@ -9,29 +9,37 @@ import { Text } from "@oh-my-pi/pi-tui";
9
9
  import type { Theme } from "../../../modes/interactive/theme/theme";
10
10
  import type { RenderResultOptions } from "../../custom-tools/types";
11
11
  import { logger } from "../../logger";
12
+ import {
13
+ formatCount,
14
+ formatExpandHint,
15
+ formatMoreItems,
16
+ getDomain,
17
+ getPreviewLines,
18
+ getStyledStatusIcon,
19
+ PREVIEW_LIMITS,
20
+ TRUNCATE_LENGTHS,
21
+ truncate,
22
+ } from "../render-utils";
12
23
  import type { ExaRenderDetails } from "./types";
13
24
 
14
- /** Truncate text to max length with ellipsis */
15
- function truncate(text: string, maxLen: number, ellipsis: string): string {
16
- if (text.length <= maxLen) return text;
17
- const sliceLen = Math.max(0, maxLen - ellipsis.length);
18
- return `${text.slice(0, sliceLen)}${ellipsis}`;
25
+ const COLLAPSED_PREVIEW_LINES = PREVIEW_LIMITS.COLLAPSED_LINES;
26
+ const COLLAPSED_PREVIEW_LINE_LEN = TRUNCATE_LENGTHS.LONG;
27
+ const EXPANDED_TEXT_LINES = 5;
28
+ const EXPANDED_TEXT_LINE_LEN = 90;
29
+ const MAX_TITLE_LEN = TRUNCATE_LENGTHS.TITLE;
30
+ const MAX_HIGHLIGHT_LEN = TRUNCATE_LENGTHS.CONTENT;
31
+
32
+ function renderErrorMessage(message: string, theme: Theme): Text {
33
+ const clean = message.replace(/^Error:\s*/, "").trim();
34
+ return new Text(
35
+ `${getStyledStatusIcon("error", theme)} ${theme.fg("error", `Error: ${clean || "Unknown error"}`)}`,
36
+ 0,
37
+ 0,
38
+ );
19
39
  }
20
40
 
21
- /** Extract domain from URL */
22
- function getDomain(url: string): string {
23
- try {
24
- const u = new URL(url);
25
- return u.hostname.replace(/^www\./, "");
26
- } catch {
27
- return url;
28
- }
29
- }
30
-
31
- /** Get first N lines of text as preview */
32
- function getPreviewLines(text: string, maxLines: number, maxLineLen: number, ellipsis: string): string[] {
33
- const lines = text.split("\n").filter((l) => l.trim());
34
- return lines.slice(0, maxLines).map((l) => truncate(l.trim(), maxLineLen, ellipsis));
41
+ function renderEmptyMessage(message: string, theme: Theme): Text {
42
+ return new Text(`${getStyledStatusIcon("warning", theme)} ${theme.fg("muted", message)}`, 0, 0);
35
43
  }
36
44
 
37
45
  /** Render Exa result with tree-based layout */
@@ -43,26 +51,42 @@ export function renderExaResult(
43
51
  const { expanded } = options;
44
52
  const details = result.details;
45
53
 
46
- // Handle error case
47
54
  if (details?.error) {
48
55
  logger.error("Exa render error", { error: details.error, toolName: details.toolName });
49
- return new Text(uiTheme.fg("error", `Error: ${details.error}`), 0, 0);
56
+ return renderErrorMessage(details.error, uiTheme);
50
57
  }
51
58
 
52
59
  const response = details?.response;
53
60
  if (!response) {
54
- // Non-search response: show raw result
55
61
  if (details?.raw) {
56
62
  const rawText = typeof details.raw === "string" ? details.raw : JSON.stringify(details.raw, null, 2);
57
- const preview = expanded ? rawText : truncate(rawText, 200, uiTheme.format.ellipsis);
58
- const toolLabel = details?.toolName ?? "Exa";
59
- return new Text(
60
- `${uiTheme.fg("success", uiTheme.format.bullet)} ${uiTheme.fg("toolTitle", toolLabel)}\n ${uiTheme.fg("dim", uiTheme.tree.vertical)} ${preview}`,
61
- 0,
62
- 0,
63
- );
63
+ const rawLines = rawText.split("\n").filter((l) => l.trim());
64
+ const maxLines = expanded ? rawLines.length : Math.min(rawLines.length, COLLAPSED_PREVIEW_LINES);
65
+ const displayLines = rawLines.slice(0, maxLines);
66
+ const remaining = rawLines.length - maxLines;
67
+ const expandHint = formatExpandHint(expanded, remaining > 0, uiTheme);
68
+
69
+ let text = `${getStyledStatusIcon("info", uiTheme)} ${uiTheme.fg("dim", "Raw response")}${expandHint}`;
70
+
71
+ for (let i = 0; i < displayLines.length; i++) {
72
+ const isLast = i === displayLines.length - 1 && remaining === 0;
73
+ const branch = isLast ? uiTheme.tree.last : uiTheme.tree.branch;
74
+ text += `\n ${uiTheme.fg("dim", branch)} ${uiTheme.fg(
75
+ "toolOutput",
76
+ truncate(displayLines[i], COLLAPSED_PREVIEW_LINE_LEN, uiTheme.format.ellipsis),
77
+ )}`;
78
+ }
79
+
80
+ if (remaining > 0) {
81
+ text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg(
82
+ "muted",
83
+ formatMoreItems(remaining, "line", uiTheme),
84
+ )}`;
85
+ }
86
+
87
+ return new Text(text, 0, 0);
64
88
  }
65
- return new Text(uiTheme.fg("error", "No response data"), 0, 0);
89
+ return renderEmptyMessage("No response data", uiTheme);
66
90
  }
67
91
 
68
92
  const results = response.results ?? [];
@@ -70,118 +94,135 @@ export function renderExaResult(
70
94
  const cost = response.costDollars?.total;
71
95
  const time = response.searchTime;
72
96
 
73
- // Build header: Exa Search · N results · $X.XX · Xs
74
- const icon =
75
- resultCount > 0 ? uiTheme.fg("success", uiTheme.format.bullet) : uiTheme.fg("warning", uiTheme.format.bullet);
76
- const expandHint = expanded ? "" : uiTheme.fg("dim", " (Ctrl+O for full results)");
77
- const toolLabel = details?.toolName ?? "Exa Search";
97
+ const icon = getStyledStatusIcon(resultCount > 0 ? "success" : "warning", uiTheme);
78
98
 
79
- let headerParts = `${icon} ${uiTheme.fg("toolTitle", toolLabel)}${uiTheme.sep.dot}${uiTheme.fg(
80
- "dim",
81
- `${resultCount} result${resultCount !== 1 ? "s" : ""}`,
82
- )}`;
99
+ const metaParts = [formatCount("result", resultCount)];
100
+ if (cost !== undefined) metaParts.push(`cost:$${cost.toFixed(4)}`);
101
+ if (time !== undefined) metaParts.push(`time:${time.toFixed(2)}s`);
102
+ const summaryText = metaParts.join(uiTheme.sep.dot);
83
103
 
84
- if (cost !== undefined) {
85
- headerParts += `${uiTheme.sep.dot}${uiTheme.fg("muted", `$${cost.toFixed(4)}`)}`;
86
- }
87
- if (time !== undefined) {
88
- headerParts += `${uiTheme.sep.dot}${uiTheme.fg("muted", `${time.toFixed(2)}s`)}`;
104
+ let hasMorePreview = false;
105
+ if (!expanded && resultCount > 0) {
106
+ const previewText = results[0].text ?? results[0].title ?? "";
107
+ const totalLines = previewText.split("\n").filter((l) => l.trim()).length;
108
+ hasMorePreview = totalLines > COLLAPSED_PREVIEW_LINES || resultCount > 1;
89
109
  }
110
+ const expandHint = formatExpandHint(expanded, hasMorePreview, uiTheme);
90
111
 
91
- let text = headerParts + expandHint;
112
+ let text = `${icon} ${uiTheme.fg("dim", summaryText)}${expandHint}`;
92
113
 
93
114
  if (!expanded) {
94
- // Collapsed view: show 3-line preview from first result
95
- if (resultCount > 0) {
96
- const first = results[0];
97
- const previewText = first.text ?? first.title ?? "";
98
- const previewLines = getPreviewLines(previewText, 3, 100, uiTheme.format.ellipsis);
99
-
100
- for (const line of previewLines) {
101
- text += `\n ${uiTheme.fg("dim", uiTheme.tree.vertical)} ${uiTheme.fg("dim", line)}`;
102
- }
115
+ if (resultCount === 0) {
116
+ text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg("muted", "No results")}`;
117
+ return new Text(text, 0, 0);
118
+ }
119
+
120
+ const first = results[0];
121
+ const previewText = first.text ?? first.title ?? "";
122
+ const previewLines = previewText
123
+ ? getPreviewLines(previewText, COLLAPSED_PREVIEW_LINES, COLLAPSED_PREVIEW_LINE_LEN, uiTheme.format.ellipsis)
124
+ : [];
125
+ const safePreviewLines = previewLines.length > 0 ? previewLines : ["No preview text"];
126
+ const totalLines = previewText.split("\n").filter((l) => l.trim()).length;
127
+ const remainingLines = Math.max(0, totalLines - previewLines.length);
128
+ const extraItems: string[] = [];
129
+ if (remainingLines > 0) {
130
+ extraItems.push(formatMoreItems(remainingLines, "line", uiTheme));
131
+ }
132
+ if (resultCount > 1) {
133
+ extraItems.push(formatMoreItems(resultCount - 1, "result", uiTheme));
134
+ }
135
+
136
+ for (let i = 0; i < safePreviewLines.length; i++) {
137
+ const isLast = i === safePreviewLines.length - 1 && extraItems.length === 0;
138
+ const branch = isLast ? uiTheme.tree.last : uiTheme.tree.branch;
139
+ const line = safePreviewLines[i];
140
+ const color = line === "No preview text" ? "muted" : "toolOutput";
141
+ text += `\n ${uiTheme.fg("dim", branch)} ${uiTheme.fg(color, line)}`;
142
+ }
143
+
144
+ for (let i = 0; i < extraItems.length; i++) {
145
+ const isLast = i === extraItems.length - 1;
146
+ const branch = isLast ? uiTheme.tree.last : uiTheme.tree.branch;
147
+ text += `\n ${uiTheme.fg("dim", branch)} ${uiTheme.fg("muted", extraItems[i])}`;
148
+ }
103
149
 
104
- const totalLines = previewText.split("\n").filter((l) => l.trim()).length;
105
- if (totalLines > 3) {
106
- text += `\n ${uiTheme.fg("dim", uiTheme.tree.vertical)} ${uiTheme.fg(
150
+ return new Text(text, 0, 0);
151
+ }
152
+
153
+ if (resultCount === 0) {
154
+ text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg("muted", "No results")}`;
155
+ return new Text(text, 0, 0);
156
+ }
157
+
158
+ for (let i = 0; i < results.length; i++) {
159
+ const res = results[i];
160
+ const isLast = i === results.length - 1;
161
+ const branch = isLast ? uiTheme.tree.last : uiTheme.tree.branch;
162
+ const cont = isLast ? " " : uiTheme.tree.vertical;
163
+
164
+ const title = truncate(res.title ?? "Untitled", MAX_TITLE_LEN, uiTheme.format.ellipsis);
165
+ const domain = res.url ? getDomain(res.url) : "";
166
+ const domainPart = domain ? uiTheme.fg("dim", ` (${domain})`) : "";
167
+
168
+ text += `\n ${uiTheme.fg("dim", branch)} ${uiTheme.fg("accent", title)}${domainPart}`;
169
+
170
+ if (res.url) {
171
+ text += `\n ${uiTheme.fg("dim", cont)} ${uiTheme.fg("dim", uiTheme.tree.hook)} ${uiTheme.fg(
172
+ "mdLinkUrl",
173
+ res.url,
174
+ )}`;
175
+ }
176
+
177
+ if (res.author) {
178
+ text += `\n ${uiTheme.fg("dim", cont)} ${uiTheme.fg("dim", uiTheme.tree.hook)} ${uiTheme.fg(
179
+ "muted",
180
+ `Author: ${res.author}`,
181
+ )}`;
182
+ }
183
+
184
+ if (res.publishedDate) {
185
+ text += `\n ${uiTheme.fg("dim", cont)} ${uiTheme.fg("dim", uiTheme.tree.hook)} ${uiTheme.fg(
186
+ "muted",
187
+ `Published: ${res.publishedDate}`,
188
+ )}`;
189
+ }
190
+
191
+ if (res.text) {
192
+ const textLines = res.text.split("\n").filter((l) => l.trim());
193
+ const displayLines = textLines.slice(0, EXPANDED_TEXT_LINES);
194
+ for (const line of displayLines) {
195
+ text += `\n ${uiTheme.fg("dim", cont)} ${uiTheme.fg("dim", uiTheme.tree.hook)} ${uiTheme.fg(
196
+ "toolOutput",
197
+ truncate(line.trim(), EXPANDED_TEXT_LINE_LEN, uiTheme.format.ellipsis),
198
+ )}`;
199
+ }
200
+ if (textLines.length > EXPANDED_TEXT_LINES) {
201
+ text += `\n ${uiTheme.fg("dim", cont)} ${uiTheme.fg("dim", uiTheme.tree.hook)} ${uiTheme.fg(
107
202
  "muted",
108
- `${uiTheme.format.ellipsis} ${totalLines - 3} more lines`,
203
+ formatMoreItems(textLines.length - EXPANDED_TEXT_LINES, "line", uiTheme),
109
204
  )}`;
110
205
  }
206
+ }
111
207
 
112
- if (resultCount > 1) {
113
- text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg(
208
+ if (res.highlights?.length) {
209
+ text += `\n ${uiTheme.fg("dim", cont)} ${uiTheme.fg("dim", uiTheme.tree.hook)} ${uiTheme.fg(
210
+ "accent",
211
+ "Highlights",
212
+ )}`;
213
+ const maxHighlights = Math.min(res.highlights.length, 3);
214
+ for (let j = 0; j < maxHighlights; j++) {
215
+ const h = res.highlights[j];
216
+ text += `\n ${uiTheme.fg("dim", cont)} ${uiTheme.fg("dim", uiTheme.tree.hook)} ${uiTheme.fg(
114
217
  "muted",
115
- `${resultCount - 1} more result${resultCount !== 2 ? "s" : ""}`,
218
+ `${uiTheme.format.dash} ${truncate(h, MAX_HIGHLIGHT_LEN, uiTheme.format.ellipsis)}`,
116
219
  )}`;
117
220
  }
118
- }
119
- } else {
120
- // Expanded view: full results tree
121
- if (resultCount > 0) {
122
- text += `\n ${uiTheme.fg("dim", uiTheme.tree.vertical)}`;
123
- text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg("accent", "Results")}`;
124
-
125
- for (let i = 0; i < results.length; i++) {
126
- const res = results[i];
127
- const isLast = i === results.length - 1;
128
- const branch = isLast ? uiTheme.tree.last : uiTheme.tree.branch;
129
- const cont = isLast ? " " : uiTheme.tree.vertical;
130
-
131
- // Title + domain
132
- const title = truncate(res.title ?? "Untitled", 60, uiTheme.format.ellipsis);
133
- const domain = res.url ? getDomain(res.url) : "";
134
- const domainPart = domain ? uiTheme.fg("dim", ` (${domain})`) : "";
135
-
136
- text += `\n ${uiTheme.fg("dim", " ")} ${uiTheme.fg("dim", branch)} ${uiTheme.fg("accent", title)}${domainPart}`;
137
-
138
- // URL
139
- if (res.url) {
140
- text += `\n ${uiTheme.fg("dim", cont)} ${uiTheme.fg("dim", uiTheme.tree.hook)} ${uiTheme.fg("mdLinkUrl", res.url)}`;
141
- }
142
-
143
- // Author
144
- if (res.author) {
145
- text += `\n ${uiTheme.fg("dim", cont)} ${uiTheme.fg("muted", `Author: ${res.author}`)}`;
146
- }
147
-
148
- // Published date
149
- if (res.publishedDate) {
150
- text += `\n ${uiTheme.fg("dim", cont)} ${uiTheme.fg("muted", `Published: ${res.publishedDate}`)}`;
151
- }
152
-
153
- // Text content
154
- if (res.text) {
155
- const textLines = res.text.split("\n").filter((l) => l.trim());
156
- const displayLines = textLines.slice(0, 5); // Show first 5 lines
157
- for (const line of displayLines) {
158
- text += `\n ${uiTheme.fg("dim", cont)} ${truncate(line.trim(), 90, uiTheme.format.ellipsis)}`;
159
- }
160
- if (textLines.length > 5) {
161
- text += `\n ${uiTheme.fg("dim", cont)} ${uiTheme.fg(
162
- "muted",
163
- `${uiTheme.format.ellipsis} ${textLines.length - 5} more lines`,
164
- )}`;
165
- }
166
- }
167
-
168
- // Highlights
169
- if (res.highlights?.length) {
170
- text += `\n ${uiTheme.fg("dim", cont)} ${uiTheme.fg("accent", "Highlights:")}`;
171
- for (let j = 0; j < Math.min(res.highlights.length, 3); j++) {
172
- const h = res.highlights[j];
173
- text += `\n ${uiTheme.fg("dim", cont)} ${uiTheme.fg(
174
- "muted",
175
- `${uiTheme.format.bullet} ${truncate(h, 80, uiTheme.format.ellipsis)}`,
176
- )}`;
177
- }
178
- if (res.highlights.length > 3) {
179
- text += `\n ${uiTheme.fg("dim", cont)} ${uiTheme.fg(
180
- "muted",
181
- `${uiTheme.format.ellipsis} ${res.highlights.length - 3} more`,
182
- )}`;
183
- }
184
- }
221
+ if (res.highlights.length > maxHighlights) {
222
+ text += `\n ${uiTheme.fg("dim", cont)} ${uiTheme.fg("dim", uiTheme.tree.hook)} ${uiTheme.fg(
223
+ "muted",
224
+ formatMoreItems(res.highlights.length - maxHighlights, "highlight", uiTheme),
225
+ )}`;
185
226
  }
186
227
  }
187
228
  }
@@ -191,10 +232,14 @@ export function renderExaResult(
191
232
 
192
233
  /** Render Exa call (query/args preview) */
193
234
  export function renderExaCall(args: Record<string, unknown>, toolName: string, uiTheme: Theme): Component {
194
- const query = typeof args.query === "string" ? truncate(args.query, 80, uiTheme.format.ellipsis) : "";
235
+ const toolLabel = toolName || "Exa Search";
236
+ const query = typeof args.query === "string" ? truncate(args.query, 80, uiTheme.format.ellipsis) : "?";
195
237
  const numResults = typeof args.num_results === "number" ? args.num_results : undefined;
196
- const detail = numResults ? uiTheme.fg("dim", ` (${numResults} results)`) : "";
197
238
 
198
- const text = `${uiTheme.fg("toolTitle", toolName)} ${uiTheme.fg("muted", query)}${detail}`;
239
+ let text = `${uiTheme.fg("toolTitle", toolLabel)} ${uiTheme.fg("accent", query)}`;
240
+ if (numResults !== undefined) {
241
+ text += ` ${uiTheme.fg("muted", `results:${numResults}`)}`;
242
+ }
243
+
199
244
  return new Text(text, 0, 0);
200
245
  }