feat(coding-agent/mcp): added TUI rendering for MCP tool calls and results with JSON tree display
- Added renderCall and renderResult methods to MCPTool and DeferredMCPTool classes for TUI rendering of tool calls and results. - Created new mcp/render.ts module providing JSON tree rendering functionality with configurable depth and line limits for terminal display. - Updated renderResult signature in custom tool and extension types to accept optional args parameter for accessing original tool arguments during result rendering. - Fixed method binding in extension and hook tool wrappers by applying .bind(tool) to renderCall and renderResult methods to preserve correct 'this' context. - Added filter to skip .git directories in fuzzy_find_sync function to improve search performance.
This commit is contained in:
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.3.13/schema.json",
|
||||
"$schema": "https://biomejs.dev/schemas/2.3.14/schema.json",
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"@sinclair/typebox": "^0.34.48",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.3.13",
|
||||
"@biomejs/biome": "2.3.14",
|
||||
"@bufbuild/protoc-gen-es": "^2.11.0",
|
||||
"@types/bun": "^1.3.7",
|
||||
"@types/node": "^25.0.10",
|
||||
|
||||
@@ -619,6 +619,9 @@ fn collect_files(
|
||||
.sort_by_file_path(|a, b| a.cmp(b));
|
||||
|
||||
let mut entries = Vec::new();
|
||||
// Skip .git directories entirely
|
||||
builder.filter_entry(|entry| entry.file_name().to_str() != Some(".git"));
|
||||
|
||||
for entry in builder.build() {
|
||||
let Ok(entry) = entry else { continue };
|
||||
let file_type = entry.file_type();
|
||||
@@ -1176,6 +1179,9 @@ fn fuzzy_find_sync(config: FuzzyFindConfig, ct: task::CancelToken) -> Result<Fuz
|
||||
.follow_links(false)
|
||||
.sort_by_file_path(|a, b| a.cmp(b));
|
||||
|
||||
// Skip .git directories entirely
|
||||
builder.filter_entry(|entry| entry.file_name().to_str() != Some(".git"));
|
||||
|
||||
let mut matches = Vec::with_capacity(max_results.min(256));
|
||||
let mut total_matches = 0u32;
|
||||
|
||||
|
||||
+1
-1
@@ -37,7 +37,7 @@
|
||||
"generate-models": "bun --cwd=packages/ai scripts/generate-models.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.3.13",
|
||||
"@biomejs/biome": "2.3.14",
|
||||
"@bufbuild/protoc-gen-es": "^2.11.0",
|
||||
"@types/bun": "^1.3.7",
|
||||
"@types/node": "^25.0.10",
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
### Added
|
||||
|
||||
- Added `renderCall` and `renderResult` methods to MCP tools for structured TUI display of tool calls and results
|
||||
- Added new `mcp/render.ts` module providing JSON tree rendering for MCP tool output with collapsible/expandable views
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated `renderResult` signature in custom tools and extensions to accept optional `args` parameter for context-aware rendering
|
||||
- Changed environment variable from `ENV_AGENT_DIR` constant to hardcoded `OMP_CODING_AGENT_DIR` string in config and CLI help text
|
||||
- Fixed method binding in extension and hook tool wrappers to preserve `this` context for `renderCall` and `renderResult` methods
|
||||
|
||||
## [10.3.1] - 2026-02-03
|
||||
### Fixed
|
||||
|
||||
@@ -8,7 +8,7 @@ This document shows how each file uses the config module and what subpaths they
|
||||
┌─────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ config.ts exports │
|
||||
├─────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ Constants: APP_NAME, CONFIG_DIR_NAME, VERSION, ENV_AGENT_DIR │
|
||||
│ Constants: APP_NAME, CONFIG_DIR_NAME, VERSION │
|
||||
│ Single paths: getAgentDir, getAuthPath, getModelsPath, getCommandsDir, ... │
|
||||
│ Multi-config: getConfigDirs, getConfigDirPaths, findConfigFile, │
|
||||
│ readConfigFile, findNearestProjectConfigDir, ... │
|
||||
@@ -19,51 +19,51 @@ This document shows how each file uses the config module and what subpaths they
|
||||
|
||||
### 1. Display/Branding Only (no file I/O)
|
||||
|
||||
| File | Imports | Purpose |
|
||||
|------|---------|---------|
|
||||
| `cli/args.ts` | `APP_NAME`, `CONFIG_DIR_NAME`, `ENV_AGENT_DIR` | Help text, env var names |
|
||||
| `cli/plugin-cli.ts` | `APP_NAME` | Command output |
|
||||
| `cli/update-cli.ts` | `APP_NAME`, `VERSION` | Update messages |
|
||||
| `core/export-html/index.ts` | `APP_NAME` | HTML export title |
|
||||
| `modes/interactive/components/welcome.ts` | `APP_NAME` | Welcome banner |
|
||||
| `utils/tools-manager.ts` | `APP_NAME` | Tool download messages |
|
||||
| File | Imports | Purpose |
|
||||
| ----------------------------------------- | ----------------------------- | ------------------------ |
|
||||
| `cli/args.ts` | `APP_NAME`, `CONFIG_DIR_NAME` | Help text, env var names |
|
||||
| `cli/plugin-cli.ts` | `APP_NAME` | Command output |
|
||||
| `cli/update-cli.ts` | `APP_NAME`, `VERSION` | Update messages |
|
||||
| `core/export-html/index.ts` | `APP_NAME` | HTML export title |
|
||||
| `modes/interactive/components/welcome.ts` | `APP_NAME` | Welcome banner |
|
||||
| `utils/tools-manager.ts` | `APP_NAME` | Tool download messages |
|
||||
|
||||
### 2. Single Fixed Paths (user-level only)
|
||||
|
||||
| File | Imports | Path | Purpose |
|
||||
|------|---------|------|---------|
|
||||
| `core/logger.ts` | `CONFIG_DIR_NAME` | `~/.omp/logs/` | Log file directory |
|
||||
| `core/agent-session.ts` | `getAuthPath` | `~/.omp/agent/auth.json` | Error messages |
|
||||
| `core/session-manager.ts` | `getAgentDir` | `~/.omp/agent/sessions/` | Session storage |
|
||||
| `modes/interactive/theme/theme.ts` | `getCustomThemesDir` | `~/.omp/agent/themes/` | Custom themes |
|
||||
| `modes/interactive/interactive-mode.ts` | `getAuthPath`, `getDebugLogPath` | auth.json, debug log | Status messages |
|
||||
| `utils/changelog.ts` | `getChangelogPath` | Package CHANGELOG.md | Re-exports |
|
||||
| `core/system-prompt.ts` | `getAgentDir`, `getDocsPath`, `getExamplesPath`, `getReadmePath` | Package assets + AGENTS.md | System prompt building |
|
||||
| `migrations.ts` | `getAgentDir` | `~/.omp/agent/` | Auth/session migration |
|
||||
| `core/plugins/installer.ts` | `getAgentDir` | `~/.omp/agent/` | Plugin installation |
|
||||
| `core/plugins/paths.ts` | `CONFIG_DIR_NAME` | `~/.omp/plugins/` | Plugin directories |
|
||||
| File | Imports | Path | Purpose |
|
||||
| --------------------------------------- | ---------------------------------------------------------------- | -------------------------- | ---------------------- |
|
||||
| `core/logger.ts` | `CONFIG_DIR_NAME` | `~/.omp/logs/` | Log file directory |
|
||||
| `core/agent-session.ts` | `getAuthPath` | `~/.omp/agent/auth.json` | Error messages |
|
||||
| `core/session-manager.ts` | `getAgentDir` | `~/.omp/agent/sessions/` | Session storage |
|
||||
| `modes/interactive/theme/theme.ts` | `getCustomThemesDir` | `~/.omp/agent/themes/` | Custom themes |
|
||||
| `modes/interactive/interactive-mode.ts` | `getAuthPath`, `getDebugLogPath` | auth.json, debug log | Status messages |
|
||||
| `utils/changelog.ts` | `getChangelogPath` | Package CHANGELOG.md | Re-exports |
|
||||
| `core/system-prompt.ts` | `getAgentDir`, `getDocsPath`, `getExamplesPath`, `getReadmePath` | Package assets + AGENTS.md | System prompt building |
|
||||
| `migrations.ts` | `getAgentDir` | `~/.omp/agent/` | Auth/session migration |
|
||||
| `core/plugins/installer.ts` | `getAgentDir` | `~/.omp/agent/` | Plugin installation |
|
||||
| `core/plugins/paths.ts` | `CONFIG_DIR_NAME` | `~/.omp/plugins/` | Plugin directories |
|
||||
|
||||
### 3. Multi-Config Discovery (with fallbacks)
|
||||
|
||||
These use the new helpers to check `.omp`, `.pi`, `.claude` directories:
|
||||
|
||||
| File | Helper Used | Subpath(s) | Levels |
|
||||
|------|-------------|------------|--------|
|
||||
| `main.ts` | `findConfigFile` | `SYSTEM.md` | project |
|
||||
| `core/sdk.ts` | `getConfigDirPaths` | `auth.json`, `models.json` | user |
|
||||
| `core/settings-manager.ts` | `readConfigFile` | `settings.json` | user+project |
|
||||
| `core/skills.ts` | `getConfigDirPaths` | `skills/` | user+project |
|
||||
| `core/slash-commands.ts` | `getConfigDirPaths` | `commands/` | project |
|
||||
| `core/hooks/loader.ts` | `getConfigDirPaths` | `hooks/` | project |
|
||||
| `core/custom-tools/loader.ts` | `getConfigDirPaths` | `tools/` | project |
|
||||
| `core/custom-commands/loader.ts` | `getConfigDirPaths` | `commands/` | project |
|
||||
| `core/plugins/paths.ts` | `getConfigDirPaths` | `plugin-overrides.json` | project |
|
||||
| `core/mcp/config.ts` | `getConfigDirPaths` | `mcp.json` | user+project |
|
||||
| `core/tools/lsp/config.ts` | `getConfigDirPaths` | `lsp.json`, `.lsp.json` | user+project |
|
||||
| `core/tools/task/commands.ts` | `getConfigDirPaths`, `findAllNearestProjectConfigDirs` | `commands/` | user+project |
|
||||
| `core/tools/task/discovery.ts` | `getConfigDirs`, `findAllNearestProjectConfigDirs` | `agents/` | user+project |
|
||||
| `core/tools/task/model-resolver.ts` | `readConfigFile` | `settings.json` | user |
|
||||
| `core/tools/web-search/auth.ts` | `getConfigDirPaths` | `` (root for models.json, auth.json) | user |
|
||||
| File | Helper Used | Subpath(s) | Levels |
|
||||
| ----------------------------------- | ------------------------------------------------------ | ------------------------------------ | ------------ |
|
||||
| `main.ts` | `findConfigFile` | `SYSTEM.md` | project |
|
||||
| `core/sdk.ts` | `getConfigDirPaths` | `auth.json`, `models.json` | user |
|
||||
| `core/settings-manager.ts` | `readConfigFile` | `settings.json` | user+project |
|
||||
| `core/skills.ts` | `getConfigDirPaths` | `skills/` | user+project |
|
||||
| `core/slash-commands.ts` | `getConfigDirPaths` | `commands/` | project |
|
||||
| `core/hooks/loader.ts` | `getConfigDirPaths` | `hooks/` | project |
|
||||
| `core/custom-tools/loader.ts` | `getConfigDirPaths` | `tools/` | project |
|
||||
| `core/custom-commands/loader.ts` | `getConfigDirPaths` | `commands/` | project |
|
||||
| `core/plugins/paths.ts` | `getConfigDirPaths` | `plugin-overrides.json` | project |
|
||||
| `core/mcp/config.ts` | `getConfigDirPaths` | `mcp.json` | user+project |
|
||||
| `core/tools/lsp/config.ts` | `getConfigDirPaths` | `lsp.json`, `.lsp.json` | user+project |
|
||||
| `core/tools/task/commands.ts` | `getConfigDirPaths`, `findAllNearestProjectConfigDirs` | `commands/` | user+project |
|
||||
| `core/tools/task/discovery.ts` | `getConfigDirs`, `findAllNearestProjectConfigDirs` | `agents/` | user+project |
|
||||
| `core/tools/task/model-resolver.ts` | `readConfigFile` | `settings.json` | user |
|
||||
| `core/tools/web-search/auth.ts` | `getConfigDirPaths` | `` (root for models.json, auth.json) | user |
|
||||
|
||||
## Subpath Summary
|
||||
|
||||
@@ -107,7 +107,7 @@ Special paths (not under agent/):
|
||||
|
||||
These files construct paths manually because they only use the primary config dir:
|
||||
|
||||
| File | Current Approach | Reason |
|
||||
|------|------------------|--------|
|
||||
| `core/logger.ts` | `CONFIG_DIR_NAME` for logs dir | Logs only written to primary (~/.omp/logs/) |
|
||||
| File | Current Approach | Reason |
|
||||
| ----------------------- | --------------------------------- | --------------------------------------------------- |
|
||||
| `core/logger.ts` | `CONFIG_DIR_NAME` for logs dir | Logs only written to primary (~/.omp/logs/) |
|
||||
| `core/plugins/paths.ts` | `CONFIG_DIR_NAME` for plugins dir | Plugins only installed in primary (~/.omp/plugins/) |
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
||||
import chalk from "chalk";
|
||||
import { APP_NAME, CONFIG_DIR_NAME, ENV_AGENT_DIR } from "../config";
|
||||
import { APP_NAME, CONFIG_DIR_NAME } from "../config";
|
||||
import { BUILTIN_TOOLS } from "../tools";
|
||||
|
||||
export type Mode = "text" | "json" | "rpc";
|
||||
@@ -286,7 +286,7 @@ ${chalk.bold("Environment Variables:")}
|
||||
PERPLEXITY_API_KEY - Perplexity search API key
|
||||
|
||||
${chalk.dim("# Configuration")}
|
||||
${ENV_AGENT_DIR.padEnd(23)} - Session storage directory (default: ~/${CONFIG_DIR_NAME}/agent)
|
||||
OMP_CODING_AGENT_DIR - Session storage directory (default: ~/${CONFIG_DIR_NAME}/agent)
|
||||
|
||||
${chalk.bold("Available Tools (all enabled by default):")}
|
||||
read - Read file contents
|
||||
|
||||
@@ -22,9 +22,6 @@ const priorityList = [
|
||||
{ dir: ".gemini" },
|
||||
];
|
||||
|
||||
// e.g., OMP_CODING_AGENT_DIR
|
||||
export const ENV_AGENT_DIR = `${APP_NAME.toUpperCase()}_CODING_AGENT_DIR`;
|
||||
|
||||
// =============================================================================
|
||||
// Package Directory (for optional external docs/examples)
|
||||
// =============================================================================
|
||||
@@ -56,7 +53,7 @@ export function getChangelogPath(): string {
|
||||
|
||||
/** Get the agent config directory (e.g., ~/.omp/agent/) */
|
||||
export function getAgentDir(): string {
|
||||
return process.env[ENV_AGENT_DIR] || path.join(os.homedir(), CONFIG_DIR_NAME, "agent");
|
||||
return process.env.OMP_CODING_AGENT_DIR || path.join(os.homedir(), CONFIG_DIR_NAME, "agent");
|
||||
}
|
||||
|
||||
/** Get path to user's custom themes directory */
|
||||
|
||||
@@ -147,7 +147,12 @@ export interface CustomTool<TParams extends TSchema = TSchema, TDetails = any> {
|
||||
renderCall?: (args: Static<TParams>, theme: Theme) => Component;
|
||||
|
||||
/** Custom rendering for tool result display - return a Component */
|
||||
renderResult?: (result: CustomToolResult<TDetails>, options: RenderResultOptions, theme: Theme) => Component;
|
||||
renderResult?: (
|
||||
result: CustomToolResult<TDetails>,
|
||||
options: RenderResultOptions,
|
||||
theme: Theme,
|
||||
args?: Static<TParams>,
|
||||
) => Component;
|
||||
}
|
||||
|
||||
/** Factory function that creates a custom tool or array of tools */
|
||||
|
||||
@@ -47,8 +47,13 @@ export class CustomToolAdapter<TParams extends TSchema = TSchema, TDetails = any
|
||||
}
|
||||
|
||||
/** Optional custom rendering for tool result display (returns UI component) */
|
||||
renderResult(result: AgentToolResult<TDetails>, options: RenderResultOptions, theme: TTheme): Component | undefined {
|
||||
return this.tool.renderResult?.(result, options, theme);
|
||||
renderResult(
|
||||
result: AgentToolResult<TDetails>,
|
||||
options: RenderResultOptions,
|
||||
theme: TTheme,
|
||||
args?: Static<TParams>,
|
||||
): Component | undefined {
|
||||
return this.tool.renderResult?.(result, options, theme, args);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -255,7 +255,12 @@ export interface ToolDefinition<TParams extends TSchema = TSchema, TDetails = un
|
||||
renderCall?: (args: Static<TParams>, theme: Theme) => Component;
|
||||
|
||||
/** Custom rendering for tool result display */
|
||||
renderResult?: (result: AgentToolResult<TDetails>, options: ToolRenderResultOptions, theme: Theme) => Component;
|
||||
renderResult?: (
|
||||
result: AgentToolResult<TDetails>,
|
||||
options: ToolRenderResultOptions,
|
||||
theme: Theme,
|
||||
args?: Static<TParams>,
|
||||
) => Component;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@@ -42,11 +42,12 @@ export class RegisteredToolAdapter implements AgentTool<any, any, any> {
|
||||
return this.registeredTool.definition.renderCall?.(args, theme as Theme);
|
||||
}
|
||||
|
||||
renderResult?(result: any, options: any, theme: any) {
|
||||
renderResult?(result: any, options: any, theme: any, args?: any) {
|
||||
return this.registeredTool.definition.renderResult?.(
|
||||
result,
|
||||
{ expanded: options.expanded, isPartial: options.isPartial, spinnerFrame: options.spinnerFrame },
|
||||
theme as Theme,
|
||||
args,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -82,8 +83,8 @@ export class ExtensionToolWrapper<TParameters extends TSchema = TSchema, TDetail
|
||||
private tool: AgentTool<TParameters, TDetails>,
|
||||
private runner: ExtensionRunner,
|
||||
) {
|
||||
this.renderCall = tool.renderCall;
|
||||
this.renderResult = tool.renderResult;
|
||||
this.renderCall = tool.renderCall?.bind(tool);
|
||||
this.renderResult = tool.renderResult?.bind(tool);
|
||||
this.mergeCallAndResult = (tool as { mergeCallAndResult?: boolean }).mergeCallAndResult;
|
||||
this.inline = (tool as { inline?: boolean }).inline;
|
||||
}
|
||||
|
||||
@@ -34,8 +34,8 @@ export class HookToolWrapper<TParameters extends TSchema = TSchema, TDetails = u
|
||||
this.label = tool.label ?? "";
|
||||
this.description = tool.description;
|
||||
this.parameters = tool.parameters;
|
||||
this.renderCall = tool.renderCall;
|
||||
this.renderResult = tool.renderResult;
|
||||
this.renderCall = tool.renderCall?.bind(tool);
|
||||
this.renderResult = tool.renderResult?.bind(tool);
|
||||
this.mergeCallAndResult = (tool as { mergeCallAndResult?: boolean }).mergeCallAndResult;
|
||||
this.inline = (tool as { inline?: boolean }).inline;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,339 @@
|
||||
/**
|
||||
* TUI rendering for MCP tools.
|
||||
*
|
||||
* Provides structured display of MCP tool calls and results,
|
||||
* showing args and output in JSON tree format similar to task tool.
|
||||
*/
|
||||
import type { Component } from "@oh-my-pi/pi-tui";
|
||||
import { Text } from "@oh-my-pi/pi-tui";
|
||||
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
||||
import type { Theme } from "../modes/theme/theme";
|
||||
import { formatExpandHint, truncateToWidth } from "../tools/render-utils";
|
||||
import { renderStatusLine } from "../tui";
|
||||
import type { MCPToolDetails } from "./tool-bridge";
|
||||
|
||||
/** Max depth for JSON tree rendering */
|
||||
const JSON_TREE_MAX_DEPTH_COLLAPSED = 2;
|
||||
const JSON_TREE_MAX_DEPTH_EXPANDED = 6;
|
||||
const JSON_TREE_MAX_LINES_COLLAPSED = 6;
|
||||
const JSON_TREE_MAX_LINES_EXPANDED = 200;
|
||||
const JSON_TREE_SCALAR_LEN_COLLAPSED = 60;
|
||||
const JSON_TREE_SCALAR_LEN_EXPANDED = 2000;
|
||||
|
||||
/**
|
||||
* Format a scalar value for inline display.
|
||||
*/
|
||||
function formatScalar(value: unknown, maxLen: number): string {
|
||||
if (value === null) return "null";
|
||||
if (value === undefined) return "undefined";
|
||||
if (typeof value === "boolean") return String(value);
|
||||
if (typeof value === "number") return String(value);
|
||||
if (typeof value === "string") {
|
||||
const escaped = value.replace(/\n/g, "\\n").replace(/\t/g, "\\t");
|
||||
const truncated = truncateToWidth(escaped, maxLen);
|
||||
return `"${truncated}"`;
|
||||
}
|
||||
if (Array.isArray(value)) return `[${value.length} items]`;
|
||||
if (typeof value === "object") {
|
||||
const keys = Object.keys(value);
|
||||
return `{${keys.length} keys}`;
|
||||
}
|
||||
return String(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format args inline for collapsed view.
|
||||
*/
|
||||
function formatArgsInline(args: Record<string, unknown>, maxWidth: number): string {
|
||||
const entries = Object.entries(args);
|
||||
if (entries.length === 0) return "";
|
||||
|
||||
// Single arg: show key=value
|
||||
if (entries.length === 1) {
|
||||
const [key, value] = entries[0];
|
||||
return `${key}=${formatScalar(value, maxWidth - key.length - 1)}`;
|
||||
}
|
||||
|
||||
// Multiple args: show key=value, key=value...
|
||||
const pairs: string[] = [];
|
||||
let totalLen = 0;
|
||||
|
||||
for (const [key, value] of entries) {
|
||||
const valueStr = formatScalar(value, 24);
|
||||
const pairStr = `${key}=${valueStr}`;
|
||||
const addLen = pairs.length > 0 ? pairStr.length + 2 : pairStr.length;
|
||||
|
||||
if (totalLen + addLen > maxWidth && pairs.length > 0) {
|
||||
pairs.push("…");
|
||||
break;
|
||||
}
|
||||
|
||||
pairs.push(pairStr);
|
||||
totalLen += addLen;
|
||||
}
|
||||
|
||||
return pairs.join(", ");
|
||||
}
|
||||
|
||||
/**
|
||||
* Build tree prefix for nested rendering.
|
||||
*/
|
||||
function buildTreePrefix(ancestors: boolean[], theme: Theme): string {
|
||||
return ancestors.map(hasNext => (hasNext ? `${theme.tree.vertical} ` : " ")).join("");
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a JSON value as tree lines.
|
||||
*/
|
||||
function renderJsonTreeLines(
|
||||
value: unknown,
|
||||
theme: Theme,
|
||||
maxDepth: number,
|
||||
maxLines: number,
|
||||
maxScalarLen: number,
|
||||
): { lines: string[]; truncated: boolean } {
|
||||
const lines: string[] = [];
|
||||
let truncated = false;
|
||||
|
||||
const iconObject = theme.styledSymbol("icon.folder", "muted");
|
||||
const iconArray = theme.styledSymbol("icon.package", "muted");
|
||||
const iconScalar = theme.styledSymbol("icon.file", "muted");
|
||||
|
||||
const pushLine = (line: string): boolean => {
|
||||
if (lines.length >= maxLines) {
|
||||
truncated = true;
|
||||
return false;
|
||||
}
|
||||
lines.push(line);
|
||||
return true;
|
||||
};
|
||||
|
||||
const renderNode = (val: unknown, key: string | undefined, ancestors: boolean[], isLast: boolean, depth: number) => {
|
||||
if (lines.length >= maxLines) {
|
||||
truncated = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const connector = isLast ? theme.tree.last : theme.tree.branch;
|
||||
const prefix = `${buildTreePrefix(ancestors, theme)}${theme.fg("dim", connector)} `;
|
||||
|
||||
// Handle scalars
|
||||
if (val === null || val === undefined || typeof val !== "object") {
|
||||
const label = key ? theme.fg("muted", key) : theme.fg("muted", "value");
|
||||
|
||||
// Special handling for multiline strings
|
||||
if (typeof val === "string" && val.includes("\n")) {
|
||||
const strLines = val.split("\n");
|
||||
const maxStrLines = Math.min(strLines.length, Math.max(1, maxLines - lines.length - 1));
|
||||
const continuePrefix = buildTreePrefix([...ancestors, !isLast], theme);
|
||||
|
||||
// First line with label
|
||||
const firstLine = truncateToWidth(strLines[0], maxScalarLen);
|
||||
pushLine(`${prefix}${iconScalar} ${label}: ${theme.fg("dim", `"${firstLine}`)}`);
|
||||
|
||||
// Subsequent lines indented
|
||||
for (let i = 1; i < maxStrLines; i++) {
|
||||
if (lines.length >= maxLines) {
|
||||
truncated = true;
|
||||
break;
|
||||
}
|
||||
const line = truncateToWidth(strLines[i], maxScalarLen);
|
||||
pushLine(`${continuePrefix} ${theme.fg("dim", ` ${line}`)}`);
|
||||
}
|
||||
|
||||
// Show truncation and closing quote
|
||||
if (strLines.length > maxStrLines) {
|
||||
truncated = true;
|
||||
pushLine(`${continuePrefix} ${theme.fg("dim", ` …(${strLines.length - maxStrLines} more lines)"`)}`);
|
||||
} else {
|
||||
// Add closing quote to last line - need to modify the last pushed line
|
||||
const lastIdx = lines.length - 1;
|
||||
lines[lastIdx] = `${lines[lastIdx]}${theme.fg("dim", '"')}`;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const scalar = formatScalar(val, maxScalarLen);
|
||||
pushLine(`${prefix}${iconScalar} ${label}: ${theme.fg("dim", scalar)}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle arrays
|
||||
if (Array.isArray(val)) {
|
||||
const header = key ? theme.fg("muted", key) : theme.fg("muted", "array");
|
||||
pushLine(`${prefix}${iconArray} ${header}`);
|
||||
if (val.length === 0) {
|
||||
pushLine(
|
||||
`${buildTreePrefix([...ancestors, !isLast], theme)}${theme.fg("dim", theme.tree.last)} ${theme.fg("dim", "[]")}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (depth >= maxDepth) {
|
||||
pushLine(
|
||||
`${buildTreePrefix([...ancestors, !isLast], theme)}${theme.fg("dim", theme.tree.last)} ${theme.fg("dim", "…")}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
const nextAncestors = [...ancestors, !isLast];
|
||||
for (let i = 0; i < val.length; i++) {
|
||||
renderNode(val[i], `[${i}]`, nextAncestors, i === val.length - 1, depth + 1);
|
||||
if (lines.length >= maxLines) {
|
||||
truncated = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle objects
|
||||
const header = key ? theme.fg("muted", key) : theme.fg("muted", "object");
|
||||
pushLine(`${prefix}${iconObject} ${header}`);
|
||||
const entries = Object.entries(val as Record<string, unknown>);
|
||||
if (entries.length === 0) {
|
||||
pushLine(
|
||||
`${buildTreePrefix([...ancestors, !isLast], theme)}${theme.fg("dim", theme.tree.last)} ${theme.fg("dim", "{}")}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (depth >= maxDepth) {
|
||||
pushLine(
|
||||
`${buildTreePrefix([...ancestors, !isLast], theme)}${theme.fg("dim", theme.tree.last)} ${theme.fg("dim", "…")}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
const nextAncestors = [...ancestors, !isLast];
|
||||
for (let i = 0; i < entries.length; i++) {
|
||||
const [childKey, child] = entries[i];
|
||||
renderNode(child, childKey, nextAncestors, i === entries.length - 1, depth + 1);
|
||||
if (lines.length >= maxLines) {
|
||||
truncated = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Render root level
|
||||
if (value && typeof value === "object" && !Array.isArray(value)) {
|
||||
const entries = Object.entries(value as Record<string, unknown>);
|
||||
for (let i = 0; i < entries.length; i++) {
|
||||
const [childKey, child] = entries[i];
|
||||
renderNode(child, childKey, [], i === entries.length - 1, 1);
|
||||
if (lines.length >= maxLines) {
|
||||
truncated = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (Array.isArray(value)) {
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
renderNode(value[i], `[${i}]`, [], i === value.length - 1, 1);
|
||||
if (lines.length >= maxLines) {
|
||||
truncated = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
renderNode(value, undefined, [], true, 0);
|
||||
}
|
||||
|
||||
return { lines, truncated };
|
||||
}
|
||||
|
||||
/**
|
||||
* Render MCP tool call.
|
||||
*/
|
||||
export function renderMCPCall(args: Record<string, unknown>, theme: Theme, label: string): Component {
|
||||
const lines: string[] = [];
|
||||
lines.push(renderStatusLine({ icon: "pending", title: label }, theme));
|
||||
|
||||
if (args && typeof args === "object" && Object.keys(args).length > 0) {
|
||||
// Show args inline preview
|
||||
const preview = formatArgsInline(args, 70);
|
||||
if (preview) {
|
||||
lines.push(` ${theme.fg("dim", theme.tree.last)} ${theme.fg("dim", preview)}`);
|
||||
}
|
||||
}
|
||||
|
||||
return new Text(lines.join("\n"), 0, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render MCP tool result.
|
||||
*/
|
||||
export function renderMCPResult(
|
||||
result: { content: Array<{ type: string; text?: string }>; details?: MCPToolDetails; isError?: boolean },
|
||||
options: RenderResultOptions,
|
||||
theme: Theme,
|
||||
args?: Record<string, unknown>,
|
||||
): Component {
|
||||
const { expanded } = options;
|
||||
const lines: string[] = [];
|
||||
|
||||
// Args section (when expanded)
|
||||
if (expanded && args && typeof args === "object" && Object.keys(args).length > 0) {
|
||||
lines.push(`${theme.fg("dim", "Args")}`);
|
||||
const maxDepth = JSON_TREE_MAX_DEPTH_EXPANDED;
|
||||
const maxLines = JSON_TREE_MAX_LINES_EXPANDED;
|
||||
const tree = renderJsonTreeLines(args, theme, maxDepth, maxLines, JSON_TREE_SCALAR_LEN_EXPANDED);
|
||||
for (const line of tree.lines) {
|
||||
lines.push(line);
|
||||
}
|
||||
if (tree.truncated) {
|
||||
lines.push(theme.fg("dim", "…"));
|
||||
}
|
||||
lines.push(""); // Blank line before output
|
||||
}
|
||||
|
||||
// Output section
|
||||
const textContent = result.content?.find(c => c.type === "text")?.text ?? "";
|
||||
const trimmedOutput = textContent.trimEnd();
|
||||
|
||||
if (!trimmedOutput) {
|
||||
lines.push(theme.fg("dim", "(no output)"));
|
||||
return new Text(lines.join("\n"), 0, 0);
|
||||
}
|
||||
|
||||
// Try to parse as JSON for structured display
|
||||
if (trimmedOutput.startsWith("{") || trimmedOutput.startsWith("[")) {
|
||||
try {
|
||||
const parsed = JSON.parse(trimmedOutput);
|
||||
const maxDepth = expanded ? JSON_TREE_MAX_DEPTH_EXPANDED : JSON_TREE_MAX_DEPTH_COLLAPSED;
|
||||
const maxLines = expanded ? JSON_TREE_MAX_LINES_EXPANDED : JSON_TREE_MAX_LINES_COLLAPSED;
|
||||
const maxScalarLen = expanded ? JSON_TREE_SCALAR_LEN_EXPANDED : JSON_TREE_SCALAR_LEN_COLLAPSED;
|
||||
const tree = renderJsonTreeLines(parsed, theme, maxDepth, maxLines, maxScalarLen);
|
||||
|
||||
if (tree.lines.length > 0) {
|
||||
for (const line of tree.lines) {
|
||||
lines.push(line);
|
||||
}
|
||||
// Always show expand hint when collapsed (expanded view shows longer values and deeper nesting)
|
||||
if (!expanded) {
|
||||
lines.push(formatExpandHint(theme, expanded, true));
|
||||
} else if (tree.truncated) {
|
||||
lines.push(theme.fg("dim", "…"));
|
||||
}
|
||||
return new Text(lines.join("\n"), 0, 0);
|
||||
}
|
||||
} catch {
|
||||
// Fall through to raw output
|
||||
}
|
||||
}
|
||||
|
||||
// Raw text output
|
||||
const outputLines = trimmedOutput.split("\n");
|
||||
const maxOutputLines = expanded ? 12 : 4;
|
||||
const displayLines = outputLines.slice(0, maxOutputLines);
|
||||
|
||||
for (const line of displayLines) {
|
||||
lines.push(theme.fg("toolOutput", truncateToWidth(line, 80)));
|
||||
}
|
||||
|
||||
if (outputLines.length > maxOutputLines) {
|
||||
const remaining = outputLines.length - maxOutputLines;
|
||||
lines.push(`${theme.fg("dim", `… ${remaining} more lines`)} ${formatExpandHint(theme, expanded, true)}`);
|
||||
} else if (!expanded) {
|
||||
// Show expand hint when collapsed even if all lines shown (lines may be truncated)
|
||||
lines.push(formatExpandHint(theme, expanded, true));
|
||||
}
|
||||
|
||||
return new Text(lines.join("\n"), 0, 0);
|
||||
}
|
||||
@@ -6,8 +6,15 @@
|
||||
import type { AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
||||
import type { TSchema } from "@sinclair/typebox";
|
||||
import type { SourceMeta } from "../capability/types";
|
||||
import type { CustomTool, CustomToolContext, CustomToolResult } from "../extensibility/custom-tools/types";
|
||||
import type {
|
||||
CustomTool,
|
||||
CustomToolContext,
|
||||
CustomToolResult,
|
||||
RenderResultOptions,
|
||||
} from "../extensibility/custom-tools/types";
|
||||
import type { Theme } from "../modes/theme/theme";
|
||||
import { callTool } from "./client";
|
||||
import { renderMCPCall, renderMCPResult } from "./render";
|
||||
import type { MCPContent, MCPServerConnection, MCPToolDefinition } from "./types";
|
||||
|
||||
/** Details included in MCP tool results for rendering */
|
||||
@@ -135,6 +142,14 @@ export class MCPTool implements CustomTool<TSchema, MCPToolDetails> {
|
||||
this.mcpServerName = connection.name;
|
||||
}
|
||||
|
||||
renderCall(args: unknown, theme: Theme) {
|
||||
return renderMCPCall((args ?? {}) as Record<string, unknown>, theme, this.label);
|
||||
}
|
||||
|
||||
renderResult(result: CustomToolResult<MCPToolDetails>, options: RenderResultOptions, theme: Theme, args?: unknown) {
|
||||
return renderMCPResult(result, options, theme, (args ?? {}) as Record<string, unknown>);
|
||||
}
|
||||
|
||||
async execute(
|
||||
_toolCallId: string,
|
||||
params: unknown,
|
||||
@@ -223,6 +238,14 @@ export class DeferredMCPTool implements CustomTool<TSchema, MCPToolDetails> {
|
||||
this.fallbackProviderName = source?.providerName;
|
||||
}
|
||||
|
||||
renderCall(args: unknown, theme: Theme) {
|
||||
return renderMCPCall((args ?? {}) as Record<string, unknown>, theme, this.label);
|
||||
}
|
||||
|
||||
renderResult(result: CustomToolResult<MCPToolDetails>, options: RenderResultOptions, theme: Theme, args?: unknown) {
|
||||
return renderMCPResult(result, options, theme, (args ?? {}) as Record<string, unknown>);
|
||||
}
|
||||
|
||||
async execute(
|
||||
_toolCallId: string,
|
||||
params: unknown,
|
||||
|
||||
@@ -33,7 +33,6 @@ import type { Component } from "@oh-my-pi/pi-tui";
|
||||
import { logger, postmortem } from "@oh-my-pi/pi-utils";
|
||||
import { YAML } from "bun";
|
||||
import chalk from "chalk";
|
||||
// Import discovery to register all providers on startup
|
||||
import { loadCapability } from "./capability";
|
||||
import { type Rule, ruleCapability } from "./capability/rule";
|
||||
import { getAgentDir, getConfigDirPaths } from "./config";
|
||||
|
||||
Reference in New Issue
Block a user