@oh-my-pi/pi-coding-agent 10.3.1 → 10.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [10.3.2] - 2026-02-03
6
+ ### Added
7
+
8
+ - Added `renderCall` and `renderResult` methods to MCP tools for structured TUI display of tool calls and results
9
+ - Added new `mcp/render.ts` module providing JSON tree rendering for MCP tool output with collapsible/expandable views
10
+
11
+ ### Changed
12
+
13
+ - Updated `renderResult` signature in custom tools and extensions to accept optional `args` parameter for context-aware rendering
14
+ - Changed environment variable from `ENV_AGENT_DIR` constant to hardcoded `OMP_CODING_AGENT_DIR` string in config and CLI help text
15
+ - Fixed method binding in extension and hook tool wrappers to preserve `this` context for `renderCall` and `renderResult` methods
16
+
5
17
  ## [10.3.1] - 2026-02-03
6
18
  ### Fixed
7
19
 
@@ -8,7 +8,7 @@ This document shows how each file uses the config module and what subpaths they
8
8
  ┌─────────────────────────────────────────────────────────────────────────────────┐
9
9
  │ config.ts exports │
10
10
  ├─────────────────────────────────────────────────────────────────────────────────┤
11
- │ Constants: APP_NAME, CONFIG_DIR_NAME, VERSION, ENV_AGENT_DIR
11
+ │ Constants: APP_NAME, CONFIG_DIR_NAME, VERSION
12
12
  │ Single paths: getAgentDir, getAuthPath, getModelsPath, getCommandsDir, ... │
13
13
  │ Multi-config: getConfigDirs, getConfigDirPaths, findConfigFile, │
14
14
  │ readConfigFile, findNearestProjectConfigDir, ... │
@@ -19,51 +19,51 @@ This document shows how each file uses the config module and what subpaths they
19
19
 
20
20
  ### 1. Display/Branding Only (no file I/O)
21
21
 
22
- | File | Imports | Purpose |
23
- |------|---------|---------|
24
- | `cli/args.ts` | `APP_NAME`, `CONFIG_DIR_NAME`, `ENV_AGENT_DIR` | Help text, env var names |
25
- | `cli/plugin-cli.ts` | `APP_NAME` | Command output |
26
- | `cli/update-cli.ts` | `APP_NAME`, `VERSION` | Update messages |
27
- | `core/export-html/index.ts` | `APP_NAME` | HTML export title |
28
- | `modes/interactive/components/welcome.ts` | `APP_NAME` | Welcome banner |
29
- | `utils/tools-manager.ts` | `APP_NAME` | Tool download messages |
22
+ | File | Imports | Purpose |
23
+ | ----------------------------------------- | ----------------------------- | ------------------------ |
24
+ | `cli/args.ts` | `APP_NAME`, `CONFIG_DIR_NAME` | Help text, env var names |
25
+ | `cli/plugin-cli.ts` | `APP_NAME` | Command output |
26
+ | `cli/update-cli.ts` | `APP_NAME`, `VERSION` | Update messages |
27
+ | `core/export-html/index.ts` | `APP_NAME` | HTML export title |
28
+ | `modes/interactive/components/welcome.ts` | `APP_NAME` | Welcome banner |
29
+ | `utils/tools-manager.ts` | `APP_NAME` | Tool download messages |
30
30
 
31
31
  ### 2. Single Fixed Paths (user-level only)
32
32
 
33
- | File | Imports | Path | Purpose |
34
- |------|---------|------|---------|
35
- | `core/logger.ts` | `CONFIG_DIR_NAME` | `~/.omp/logs/` | Log file directory |
36
- | `core/agent-session.ts` | `getAuthPath` | `~/.omp/agent/auth.json` | Error messages |
37
- | `core/session-manager.ts` | `getAgentDir` | `~/.omp/agent/sessions/` | Session storage |
38
- | `modes/interactive/theme/theme.ts` | `getCustomThemesDir` | `~/.omp/agent/themes/` | Custom themes |
39
- | `modes/interactive/interactive-mode.ts` | `getAuthPath`, `getDebugLogPath` | auth.json, debug log | Status messages |
40
- | `utils/changelog.ts` | `getChangelogPath` | Package CHANGELOG.md | Re-exports |
41
- | `core/system-prompt.ts` | `getAgentDir`, `getDocsPath`, `getExamplesPath`, `getReadmePath` | Package assets + AGENTS.md | System prompt building |
42
- | `migrations.ts` | `getAgentDir` | `~/.omp/agent/` | Auth/session migration |
43
- | `core/plugins/installer.ts` | `getAgentDir` | `~/.omp/agent/` | Plugin installation |
44
- | `core/plugins/paths.ts` | `CONFIG_DIR_NAME` | `~/.omp/plugins/` | Plugin directories |
33
+ | File | Imports | Path | Purpose |
34
+ | --------------------------------------- | ---------------------------------------------------------------- | -------------------------- | ---------------------- |
35
+ | `core/logger.ts` | `CONFIG_DIR_NAME` | `~/.omp/logs/` | Log file directory |
36
+ | `core/agent-session.ts` | `getAuthPath` | `~/.omp/agent/auth.json` | Error messages |
37
+ | `core/session-manager.ts` | `getAgentDir` | `~/.omp/agent/sessions/` | Session storage |
38
+ | `modes/interactive/theme/theme.ts` | `getCustomThemesDir` | `~/.omp/agent/themes/` | Custom themes |
39
+ | `modes/interactive/interactive-mode.ts` | `getAuthPath`, `getDebugLogPath` | auth.json, debug log | Status messages |
40
+ | `utils/changelog.ts` | `getChangelogPath` | Package CHANGELOG.md | Re-exports |
41
+ | `core/system-prompt.ts` | `getAgentDir`, `getDocsPath`, `getExamplesPath`, `getReadmePath` | Package assets + AGENTS.md | System prompt building |
42
+ | `migrations.ts` | `getAgentDir` | `~/.omp/agent/` | Auth/session migration |
43
+ | `core/plugins/installer.ts` | `getAgentDir` | `~/.omp/agent/` | Plugin installation |
44
+ | `core/plugins/paths.ts` | `CONFIG_DIR_NAME` | `~/.omp/plugins/` | Plugin directories |
45
45
 
46
46
  ### 3. Multi-Config Discovery (with fallbacks)
47
47
 
48
48
  These use the new helpers to check `.omp`, `.pi`, `.claude` directories:
49
49
 
50
- | File | Helper Used | Subpath(s) | Levels |
51
- |------|-------------|------------|--------|
52
- | `main.ts` | `findConfigFile` | `SYSTEM.md` | project |
53
- | `core/sdk.ts` | `getConfigDirPaths` | `auth.json`, `models.json` | user |
54
- | `core/settings-manager.ts` | `readConfigFile` | `settings.json` | user+project |
55
- | `core/skills.ts` | `getConfigDirPaths` | `skills/` | user+project |
56
- | `core/slash-commands.ts` | `getConfigDirPaths` | `commands/` | project |
57
- | `core/hooks/loader.ts` | `getConfigDirPaths` | `hooks/` | project |
58
- | `core/custom-tools/loader.ts` | `getConfigDirPaths` | `tools/` | project |
59
- | `core/custom-commands/loader.ts` | `getConfigDirPaths` | `commands/` | project |
60
- | `core/plugins/paths.ts` | `getConfigDirPaths` | `plugin-overrides.json` | project |
61
- | `core/mcp/config.ts` | `getConfigDirPaths` | `mcp.json` | user+project |
62
- | `core/tools/lsp/config.ts` | `getConfigDirPaths` | `lsp.json`, `.lsp.json` | user+project |
63
- | `core/tools/task/commands.ts` | `getConfigDirPaths`, `findAllNearestProjectConfigDirs` | `commands/` | user+project |
64
- | `core/tools/task/discovery.ts` | `getConfigDirs`, `findAllNearestProjectConfigDirs` | `agents/` | user+project |
65
- | `core/tools/task/model-resolver.ts` | `readConfigFile` | `settings.json` | user |
66
- | `core/tools/web-search/auth.ts` | `getConfigDirPaths` | `` (root for models.json, auth.json) | user |
50
+ | File | Helper Used | Subpath(s) | Levels |
51
+ | ----------------------------------- | ------------------------------------------------------ | ------------------------------------ | ------------ |
52
+ | `main.ts` | `findConfigFile` | `SYSTEM.md` | project |
53
+ | `core/sdk.ts` | `getConfigDirPaths` | `auth.json`, `models.json` | user |
54
+ | `core/settings-manager.ts` | `readConfigFile` | `settings.json` | user+project |
55
+ | `core/skills.ts` | `getConfigDirPaths` | `skills/` | user+project |
56
+ | `core/slash-commands.ts` | `getConfigDirPaths` | `commands/` | project |
57
+ | `core/hooks/loader.ts` | `getConfigDirPaths` | `hooks/` | project |
58
+ | `core/custom-tools/loader.ts` | `getConfigDirPaths` | `tools/` | project |
59
+ | `core/custom-commands/loader.ts` | `getConfigDirPaths` | `commands/` | project |
60
+ | `core/plugins/paths.ts` | `getConfigDirPaths` | `plugin-overrides.json` | project |
61
+ | `core/mcp/config.ts` | `getConfigDirPaths` | `mcp.json` | user+project |
62
+ | `core/tools/lsp/config.ts` | `getConfigDirPaths` | `lsp.json`, `.lsp.json` | user+project |
63
+ | `core/tools/task/commands.ts` | `getConfigDirPaths`, `findAllNearestProjectConfigDirs` | `commands/` | user+project |
64
+ | `core/tools/task/discovery.ts` | `getConfigDirs`, `findAllNearestProjectConfigDirs` | `agents/` | user+project |
65
+ | `core/tools/task/model-resolver.ts` | `readConfigFile` | `settings.json` | user |
66
+ | `core/tools/web-search/auth.ts` | `getConfigDirPaths` | `` (root for models.json, auth.json) | user |
67
67
 
68
68
  ## Subpath Summary
69
69
 
@@ -107,7 +107,7 @@ Special paths (not under agent/):
107
107
 
108
108
  These files construct paths manually because they only use the primary config dir:
109
109
 
110
- | File | Current Approach | Reason |
111
- |------|------------------|--------|
112
- | `core/logger.ts` | `CONFIG_DIR_NAME` for logs dir | Logs only written to primary (~/.omp/logs/) |
110
+ | File | Current Approach | Reason |
111
+ | ----------------------- | --------------------------------- | --------------------------------------------------- |
112
+ | `core/logger.ts` | `CONFIG_DIR_NAME` for logs dir | Logs only written to primary (~/.omp/logs/) |
113
113
  | `core/plugins/paths.ts` | `CONFIG_DIR_NAME` for plugins dir | Plugins only installed in primary (~/.omp/plugins/) |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-coding-agent",
3
- "version": "10.3.1",
3
+ "version": "10.3.2",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "ompConfig": {
@@ -79,12 +79,12 @@
79
79
  "test": "bun test"
80
80
  },
81
81
  "dependencies": {
82
- "@oh-my-pi/omp-stats": "10.3.1",
83
- "@oh-my-pi/pi-agent-core": "10.3.1",
84
- "@oh-my-pi/pi-ai": "10.3.1",
85
- "@oh-my-pi/pi-natives": "10.3.1",
86
- "@oh-my-pi/pi-tui": "10.3.1",
87
- "@oh-my-pi/pi-utils": "10.3.1",
82
+ "@oh-my-pi/omp-stats": "10.3.2",
83
+ "@oh-my-pi/pi-agent-core": "10.3.2",
84
+ "@oh-my-pi/pi-ai": "10.3.2",
85
+ "@oh-my-pi/pi-natives": "10.3.2",
86
+ "@oh-my-pi/pi-tui": "10.3.2",
87
+ "@oh-my-pi/pi-utils": "10.3.2",
88
88
  "@openai/agents": "^0.4.4",
89
89
  "@sinclair/typebox": "^0.34.48",
90
90
  "ajv": "^8.17.1",
package/src/cli/args.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
5
5
  import chalk from "chalk";
6
- import { APP_NAME, CONFIG_DIR_NAME, ENV_AGENT_DIR } from "../config";
6
+ import { APP_NAME, CONFIG_DIR_NAME } from "../config";
7
7
  import { BUILTIN_TOOLS } from "../tools";
8
8
 
9
9
  export type Mode = "text" | "json" | "rpc";
@@ -286,7 +286,7 @@ ${chalk.bold("Environment Variables:")}
286
286
  PERPLEXITY_API_KEY - Perplexity search API key
287
287
 
288
288
  ${chalk.dim("# Configuration")}
289
- ${ENV_AGENT_DIR.padEnd(23)} - Session storage directory (default: ~/${CONFIG_DIR_NAME}/agent)
289
+ OMP_CODING_AGENT_DIR - Session storage directory (default: ~/${CONFIG_DIR_NAME}/agent)
290
290
 
291
291
  ${chalk.bold("Available Tools (all enabled by default):")}
292
292
  read - Read file contents
package/src/config.ts CHANGED
@@ -22,9 +22,6 @@ const priorityList = [
22
22
  { dir: ".gemini" },
23
23
  ];
24
24
 
25
- // e.g., OMP_CODING_AGENT_DIR
26
- export const ENV_AGENT_DIR = `${APP_NAME.toUpperCase()}_CODING_AGENT_DIR`;
27
-
28
25
  // =============================================================================
29
26
  // Package Directory (for optional external docs/examples)
30
27
  // =============================================================================
@@ -56,7 +53,7 @@ export function getChangelogPath(): string {
56
53
 
57
54
  /** Get the agent config directory (e.g., ~/.omp/agent/) */
58
55
  export function getAgentDir(): string {
59
- return process.env[ENV_AGENT_DIR] || path.join(os.homedir(), CONFIG_DIR_NAME, "agent");
56
+ return process.env.OMP_CODING_AGENT_DIR || path.join(os.homedir(), CONFIG_DIR_NAME, "agent");
60
57
  }
61
58
 
62
59
  /** Get path to user's custom themes directory */
@@ -147,7 +147,12 @@ export interface CustomTool<TParams extends TSchema = TSchema, TDetails = any> {
147
147
  renderCall?: (args: Static<TParams>, theme: Theme) => Component;
148
148
 
149
149
  /** Custom rendering for tool result display - return a Component */
150
- renderResult?: (result: CustomToolResult<TDetails>, options: RenderResultOptions, theme: Theme) => Component;
150
+ renderResult?: (
151
+ result: CustomToolResult<TDetails>,
152
+ options: RenderResultOptions,
153
+ theme: Theme,
154
+ args?: Static<TParams>,
155
+ ) => Component;
151
156
  }
152
157
 
153
158
  /** Factory function that creates a custom tool or array of tools */
@@ -47,8 +47,13 @@ export class CustomToolAdapter<TParams extends TSchema = TSchema, TDetails = any
47
47
  }
48
48
 
49
49
  /** Optional custom rendering for tool result display (returns UI component) */
50
- renderResult(result: AgentToolResult<TDetails>, options: RenderResultOptions, theme: TTheme): Component | undefined {
51
- return this.tool.renderResult?.(result, options, theme);
50
+ renderResult(
51
+ result: AgentToolResult<TDetails>,
52
+ options: RenderResultOptions,
53
+ theme: TTheme,
54
+ args?: Static<TParams>,
55
+ ): Component | undefined {
56
+ return this.tool.renderResult?.(result, options, theme, args);
52
57
  }
53
58
 
54
59
  /**
@@ -255,7 +255,12 @@ export interface ToolDefinition<TParams extends TSchema = TSchema, TDetails = un
255
255
  renderCall?: (args: Static<TParams>, theme: Theme) => Component;
256
256
 
257
257
  /** Custom rendering for tool result display */
258
- renderResult?: (result: AgentToolResult<TDetails>, options: ToolRenderResultOptions, theme: Theme) => Component;
258
+ renderResult?: (
259
+ result: AgentToolResult<TDetails>,
260
+ options: ToolRenderResultOptions,
261
+ theme: Theme,
262
+ args?: Static<TParams>,
263
+ ) => Component;
259
264
  }
260
265
 
261
266
  // ============================================================================
@@ -42,11 +42,12 @@ export class RegisteredToolAdapter implements AgentTool<any, any, any> {
42
42
  return this.registeredTool.definition.renderCall?.(args, theme as Theme);
43
43
  }
44
44
 
45
- renderResult?(result: any, options: any, theme: any) {
45
+ renderResult?(result: any, options: any, theme: any, args?: any) {
46
46
  return this.registeredTool.definition.renderResult?.(
47
47
  result,
48
48
  { expanded: options.expanded, isPartial: options.isPartial, spinnerFrame: options.spinnerFrame },
49
49
  theme as Theme,
50
+ args,
50
51
  );
51
52
  }
52
53
  }
@@ -82,8 +83,8 @@ export class ExtensionToolWrapper<TParameters extends TSchema = TSchema, TDetail
82
83
  private tool: AgentTool<TParameters, TDetails>,
83
84
  private runner: ExtensionRunner,
84
85
  ) {
85
- this.renderCall = tool.renderCall;
86
- this.renderResult = tool.renderResult;
86
+ this.renderCall = tool.renderCall?.bind(tool);
87
+ this.renderResult = tool.renderResult?.bind(tool);
87
88
  this.mergeCallAndResult = (tool as { mergeCallAndResult?: boolean }).mergeCallAndResult;
88
89
  this.inline = (tool as { inline?: boolean }).inline;
89
90
  }
@@ -34,8 +34,8 @@ export class HookToolWrapper<TParameters extends TSchema = TSchema, TDetails = u
34
34
  this.label = tool.label ?? "";
35
35
  this.description = tool.description;
36
36
  this.parameters = tool.parameters;
37
- this.renderCall = tool.renderCall;
38
- this.renderResult = tool.renderResult;
37
+ this.renderCall = tool.renderCall?.bind(tool);
38
+ this.renderResult = tool.renderResult?.bind(tool);
39
39
  this.mergeCallAndResult = (tool as { mergeCallAndResult?: boolean }).mergeCallAndResult;
40
40
  this.inline = (tool as { inline?: boolean }).inline;
41
41
  }
@@ -0,0 +1,339 @@
1
+ /**
2
+ * TUI rendering for MCP tools.
3
+ *
4
+ * Provides structured display of MCP tool calls and results,
5
+ * showing args and output in JSON tree format similar to task tool.
6
+ */
7
+ import type { Component } from "@oh-my-pi/pi-tui";
8
+ import { Text } from "@oh-my-pi/pi-tui";
9
+ import type { RenderResultOptions } from "../extensibility/custom-tools/types";
10
+ import type { Theme } from "../modes/theme/theme";
11
+ import { formatExpandHint, truncateToWidth } from "../tools/render-utils";
12
+ import { renderStatusLine } from "../tui";
13
+ import type { MCPToolDetails } from "./tool-bridge";
14
+
15
+ /** Max depth for JSON tree rendering */
16
+ const JSON_TREE_MAX_DEPTH_COLLAPSED = 2;
17
+ const JSON_TREE_MAX_DEPTH_EXPANDED = 6;
18
+ const JSON_TREE_MAX_LINES_COLLAPSED = 6;
19
+ const JSON_TREE_MAX_LINES_EXPANDED = 200;
20
+ const JSON_TREE_SCALAR_LEN_COLLAPSED = 60;
21
+ const JSON_TREE_SCALAR_LEN_EXPANDED = 2000;
22
+
23
+ /**
24
+ * Format a scalar value for inline display.
25
+ */
26
+ function formatScalar(value: unknown, maxLen: number): string {
27
+ if (value === null) return "null";
28
+ if (value === undefined) return "undefined";
29
+ if (typeof value === "boolean") return String(value);
30
+ if (typeof value === "number") return String(value);
31
+ if (typeof value === "string") {
32
+ const escaped = value.replace(/\n/g, "\\n").replace(/\t/g, "\\t");
33
+ const truncated = truncateToWidth(escaped, maxLen);
34
+ return `"${truncated}"`;
35
+ }
36
+ if (Array.isArray(value)) return `[${value.length} items]`;
37
+ if (typeof value === "object") {
38
+ const keys = Object.keys(value);
39
+ return `{${keys.length} keys}`;
40
+ }
41
+ return String(value);
42
+ }
43
+
44
+ /**
45
+ * Format args inline for collapsed view.
46
+ */
47
+ function formatArgsInline(args: Record<string, unknown>, maxWidth: number): string {
48
+ const entries = Object.entries(args);
49
+ if (entries.length === 0) return "";
50
+
51
+ // Single arg: show key=value
52
+ if (entries.length === 1) {
53
+ const [key, value] = entries[0];
54
+ return `${key}=${formatScalar(value, maxWidth - key.length - 1)}`;
55
+ }
56
+
57
+ // Multiple args: show key=value, key=value...
58
+ const pairs: string[] = [];
59
+ let totalLen = 0;
60
+
61
+ for (const [key, value] of entries) {
62
+ const valueStr = formatScalar(value, 24);
63
+ const pairStr = `${key}=${valueStr}`;
64
+ const addLen = pairs.length > 0 ? pairStr.length + 2 : pairStr.length;
65
+
66
+ if (totalLen + addLen > maxWidth && pairs.length > 0) {
67
+ pairs.push("…");
68
+ break;
69
+ }
70
+
71
+ pairs.push(pairStr);
72
+ totalLen += addLen;
73
+ }
74
+
75
+ return pairs.join(", ");
76
+ }
77
+
78
+ /**
79
+ * Build tree prefix for nested rendering.
80
+ */
81
+ function buildTreePrefix(ancestors: boolean[], theme: Theme): string {
82
+ return ancestors.map(hasNext => (hasNext ? `${theme.tree.vertical} ` : " ")).join("");
83
+ }
84
+
85
+ /**
86
+ * Render a JSON value as tree lines.
87
+ */
88
+ function renderJsonTreeLines(
89
+ value: unknown,
90
+ theme: Theme,
91
+ maxDepth: number,
92
+ maxLines: number,
93
+ maxScalarLen: number,
94
+ ): { lines: string[]; truncated: boolean } {
95
+ const lines: string[] = [];
96
+ let truncated = false;
97
+
98
+ const iconObject = theme.styledSymbol("icon.folder", "muted");
99
+ const iconArray = theme.styledSymbol("icon.package", "muted");
100
+ const iconScalar = theme.styledSymbol("icon.file", "muted");
101
+
102
+ const pushLine = (line: string): boolean => {
103
+ if (lines.length >= maxLines) {
104
+ truncated = true;
105
+ return false;
106
+ }
107
+ lines.push(line);
108
+ return true;
109
+ };
110
+
111
+ const renderNode = (val: unknown, key: string | undefined, ancestors: boolean[], isLast: boolean, depth: number) => {
112
+ if (lines.length >= maxLines) {
113
+ truncated = true;
114
+ return;
115
+ }
116
+
117
+ const connector = isLast ? theme.tree.last : theme.tree.branch;
118
+ const prefix = `${buildTreePrefix(ancestors, theme)}${theme.fg("dim", connector)} `;
119
+
120
+ // Handle scalars
121
+ if (val === null || val === undefined || typeof val !== "object") {
122
+ const label = key ? theme.fg("muted", key) : theme.fg("muted", "value");
123
+
124
+ // Special handling for multiline strings
125
+ if (typeof val === "string" && val.includes("\n")) {
126
+ const strLines = val.split("\n");
127
+ const maxStrLines = Math.min(strLines.length, Math.max(1, maxLines - lines.length - 1));
128
+ const continuePrefix = buildTreePrefix([...ancestors, !isLast], theme);
129
+
130
+ // First line with label
131
+ const firstLine = truncateToWidth(strLines[0], maxScalarLen);
132
+ pushLine(`${prefix}${iconScalar} ${label}: ${theme.fg("dim", `"${firstLine}`)}`);
133
+
134
+ // Subsequent lines indented
135
+ for (let i = 1; i < maxStrLines; i++) {
136
+ if (lines.length >= maxLines) {
137
+ truncated = true;
138
+ break;
139
+ }
140
+ const line = truncateToWidth(strLines[i], maxScalarLen);
141
+ pushLine(`${continuePrefix} ${theme.fg("dim", ` ${line}`)}`);
142
+ }
143
+
144
+ // Show truncation and closing quote
145
+ if (strLines.length > maxStrLines) {
146
+ truncated = true;
147
+ pushLine(`${continuePrefix} ${theme.fg("dim", ` …(${strLines.length - maxStrLines} more lines)"`)}`);
148
+ } else {
149
+ // Add closing quote to last line - need to modify the last pushed line
150
+ const lastIdx = lines.length - 1;
151
+ lines[lastIdx] = `${lines[lastIdx]}${theme.fg("dim", '"')}`;
152
+ }
153
+ return;
154
+ }
155
+
156
+ const scalar = formatScalar(val, maxScalarLen);
157
+ pushLine(`${prefix}${iconScalar} ${label}: ${theme.fg("dim", scalar)}`);
158
+ return;
159
+ }
160
+
161
+ // Handle arrays
162
+ if (Array.isArray(val)) {
163
+ const header = key ? theme.fg("muted", key) : theme.fg("muted", "array");
164
+ pushLine(`${prefix}${iconArray} ${header}`);
165
+ if (val.length === 0) {
166
+ pushLine(
167
+ `${buildTreePrefix([...ancestors, !isLast], theme)}${theme.fg("dim", theme.tree.last)} ${theme.fg("dim", "[]")}`,
168
+ );
169
+ return;
170
+ }
171
+ if (depth >= maxDepth) {
172
+ pushLine(
173
+ `${buildTreePrefix([...ancestors, !isLast], theme)}${theme.fg("dim", theme.tree.last)} ${theme.fg("dim", "…")}`,
174
+ );
175
+ return;
176
+ }
177
+ const nextAncestors = [...ancestors, !isLast];
178
+ for (let i = 0; i < val.length; i++) {
179
+ renderNode(val[i], `[${i}]`, nextAncestors, i === val.length - 1, depth + 1);
180
+ if (lines.length >= maxLines) {
181
+ truncated = true;
182
+ return;
183
+ }
184
+ }
185
+ return;
186
+ }
187
+
188
+ // Handle objects
189
+ const header = key ? theme.fg("muted", key) : theme.fg("muted", "object");
190
+ pushLine(`${prefix}${iconObject} ${header}`);
191
+ const entries = Object.entries(val as Record<string, unknown>);
192
+ if (entries.length === 0) {
193
+ pushLine(
194
+ `${buildTreePrefix([...ancestors, !isLast], theme)}${theme.fg("dim", theme.tree.last)} ${theme.fg("dim", "{}")}`,
195
+ );
196
+ return;
197
+ }
198
+ if (depth >= maxDepth) {
199
+ pushLine(
200
+ `${buildTreePrefix([...ancestors, !isLast], theme)}${theme.fg("dim", theme.tree.last)} ${theme.fg("dim", "…")}`,
201
+ );
202
+ return;
203
+ }
204
+ const nextAncestors = [...ancestors, !isLast];
205
+ for (let i = 0; i < entries.length; i++) {
206
+ const [childKey, child] = entries[i];
207
+ renderNode(child, childKey, nextAncestors, i === entries.length - 1, depth + 1);
208
+ if (lines.length >= maxLines) {
209
+ truncated = true;
210
+ return;
211
+ }
212
+ }
213
+ };
214
+
215
+ // Render root level
216
+ if (value && typeof value === "object" && !Array.isArray(value)) {
217
+ const entries = Object.entries(value as Record<string, unknown>);
218
+ for (let i = 0; i < entries.length; i++) {
219
+ const [childKey, child] = entries[i];
220
+ renderNode(child, childKey, [], i === entries.length - 1, 1);
221
+ if (lines.length >= maxLines) {
222
+ truncated = true;
223
+ break;
224
+ }
225
+ }
226
+ } else if (Array.isArray(value)) {
227
+ for (let i = 0; i < value.length; i++) {
228
+ renderNode(value[i], `[${i}]`, [], i === value.length - 1, 1);
229
+ if (lines.length >= maxLines) {
230
+ truncated = true;
231
+ break;
232
+ }
233
+ }
234
+ } else {
235
+ renderNode(value, undefined, [], true, 0);
236
+ }
237
+
238
+ return { lines, truncated };
239
+ }
240
+
241
+ /**
242
+ * Render MCP tool call.
243
+ */
244
+ export function renderMCPCall(args: Record<string, unknown>, theme: Theme, label: string): Component {
245
+ const lines: string[] = [];
246
+ lines.push(renderStatusLine({ icon: "pending", title: label }, theme));
247
+
248
+ if (args && typeof args === "object" && Object.keys(args).length > 0) {
249
+ // Show args inline preview
250
+ const preview = formatArgsInline(args, 70);
251
+ if (preview) {
252
+ lines.push(` ${theme.fg("dim", theme.tree.last)} ${theme.fg("dim", preview)}`);
253
+ }
254
+ }
255
+
256
+ return new Text(lines.join("\n"), 0, 0);
257
+ }
258
+
259
+ /**
260
+ * Render MCP tool result.
261
+ */
262
+ export function renderMCPResult(
263
+ result: { content: Array<{ type: string; text?: string }>; details?: MCPToolDetails; isError?: boolean },
264
+ options: RenderResultOptions,
265
+ theme: Theme,
266
+ args?: Record<string, unknown>,
267
+ ): Component {
268
+ const { expanded } = options;
269
+ const lines: string[] = [];
270
+
271
+ // Args section (when expanded)
272
+ if (expanded && args && typeof args === "object" && Object.keys(args).length > 0) {
273
+ lines.push(`${theme.fg("dim", "Args")}`);
274
+ const maxDepth = JSON_TREE_MAX_DEPTH_EXPANDED;
275
+ const maxLines = JSON_TREE_MAX_LINES_EXPANDED;
276
+ const tree = renderJsonTreeLines(args, theme, maxDepth, maxLines, JSON_TREE_SCALAR_LEN_EXPANDED);
277
+ for (const line of tree.lines) {
278
+ lines.push(line);
279
+ }
280
+ if (tree.truncated) {
281
+ lines.push(theme.fg("dim", "…"));
282
+ }
283
+ lines.push(""); // Blank line before output
284
+ }
285
+
286
+ // Output section
287
+ const textContent = result.content?.find(c => c.type === "text")?.text ?? "";
288
+ const trimmedOutput = textContent.trimEnd();
289
+
290
+ if (!trimmedOutput) {
291
+ lines.push(theme.fg("dim", "(no output)"));
292
+ return new Text(lines.join("\n"), 0, 0);
293
+ }
294
+
295
+ // Try to parse as JSON for structured display
296
+ if (trimmedOutput.startsWith("{") || trimmedOutput.startsWith("[")) {
297
+ try {
298
+ const parsed = JSON.parse(trimmedOutput);
299
+ const maxDepth = expanded ? JSON_TREE_MAX_DEPTH_EXPANDED : JSON_TREE_MAX_DEPTH_COLLAPSED;
300
+ const maxLines = expanded ? JSON_TREE_MAX_LINES_EXPANDED : JSON_TREE_MAX_LINES_COLLAPSED;
301
+ const maxScalarLen = expanded ? JSON_TREE_SCALAR_LEN_EXPANDED : JSON_TREE_SCALAR_LEN_COLLAPSED;
302
+ const tree = renderJsonTreeLines(parsed, theme, maxDepth, maxLines, maxScalarLen);
303
+
304
+ if (tree.lines.length > 0) {
305
+ for (const line of tree.lines) {
306
+ lines.push(line);
307
+ }
308
+ // Always show expand hint when collapsed (expanded view shows longer values and deeper nesting)
309
+ if (!expanded) {
310
+ lines.push(formatExpandHint(theme, expanded, true));
311
+ } else if (tree.truncated) {
312
+ lines.push(theme.fg("dim", "…"));
313
+ }
314
+ return new Text(lines.join("\n"), 0, 0);
315
+ }
316
+ } catch {
317
+ // Fall through to raw output
318
+ }
319
+ }
320
+
321
+ // Raw text output
322
+ const outputLines = trimmedOutput.split("\n");
323
+ const maxOutputLines = expanded ? 12 : 4;
324
+ const displayLines = outputLines.slice(0, maxOutputLines);
325
+
326
+ for (const line of displayLines) {
327
+ lines.push(theme.fg("toolOutput", truncateToWidth(line, 80)));
328
+ }
329
+
330
+ if (outputLines.length > maxOutputLines) {
331
+ const remaining = outputLines.length - maxOutputLines;
332
+ lines.push(`${theme.fg("dim", `… ${remaining} more lines`)} ${formatExpandHint(theme, expanded, true)}`);
333
+ } else if (!expanded) {
334
+ // Show expand hint when collapsed even if all lines shown (lines may be truncated)
335
+ lines.push(formatExpandHint(theme, expanded, true));
336
+ }
337
+
338
+ return new Text(lines.join("\n"), 0, 0);
339
+ }
@@ -6,8 +6,15 @@
6
6
  import type { AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
7
7
  import type { TSchema } from "@sinclair/typebox";
8
8
  import type { SourceMeta } from "../capability/types";
9
- import type { CustomTool, CustomToolContext, CustomToolResult } from "../extensibility/custom-tools/types";
9
+ import type {
10
+ CustomTool,
11
+ CustomToolContext,
12
+ CustomToolResult,
13
+ RenderResultOptions,
14
+ } from "../extensibility/custom-tools/types";
15
+ import type { Theme } from "../modes/theme/theme";
10
16
  import { callTool } from "./client";
17
+ import { renderMCPCall, renderMCPResult } from "./render";
11
18
  import type { MCPContent, MCPServerConnection, MCPToolDefinition } from "./types";
12
19
 
13
20
  /** Details included in MCP tool results for rendering */
@@ -135,6 +142,14 @@ export class MCPTool implements CustomTool<TSchema, MCPToolDetails> {
135
142
  this.mcpServerName = connection.name;
136
143
  }
137
144
 
145
+ renderCall(args: unknown, theme: Theme) {
146
+ return renderMCPCall((args ?? {}) as Record<string, unknown>, theme, this.label);
147
+ }
148
+
149
+ renderResult(result: CustomToolResult<MCPToolDetails>, options: RenderResultOptions, theme: Theme, args?: unknown) {
150
+ return renderMCPResult(result, options, theme, (args ?? {}) as Record<string, unknown>);
151
+ }
152
+
138
153
  async execute(
139
154
  _toolCallId: string,
140
155
  params: unknown,
@@ -223,6 +238,14 @@ export class DeferredMCPTool implements CustomTool<TSchema, MCPToolDetails> {
223
238
  this.fallbackProviderName = source?.providerName;
224
239
  }
225
240
 
241
+ renderCall(args: unknown, theme: Theme) {
242
+ return renderMCPCall((args ?? {}) as Record<string, unknown>, theme, this.label);
243
+ }
244
+
245
+ renderResult(result: CustomToolResult<MCPToolDetails>, options: RenderResultOptions, theme: Theme, args?: unknown) {
246
+ return renderMCPResult(result, options, theme, (args ?? {}) as Record<string, unknown>);
247
+ }
248
+
226
249
  async execute(
227
250
  _toolCallId: string,
228
251
  params: unknown,
package/src/sdk.ts CHANGED
@@ -33,7 +33,6 @@ import type { Component } from "@oh-my-pi/pi-tui";
33
33
  import { logger, postmortem } from "@oh-my-pi/pi-utils";
34
34
  import { YAML } from "bun";
35
35
  import chalk from "chalk";
36
- // Import discovery to register all providers on startup
37
36
  import { loadCapability } from "./capability";
38
37
  import { type Rule, ruleCapability } from "./capability/rule";
39
38
  import { getAgentDir, getConfigDirPaths } from "./config";