Files
oh-my-pi/packages/collab-web/src/tool-render/ToolView.tsx
T
can1357 5ff277349c refactor(coding-agent): consolidated tool surface onto xd:// devices and hub
- Added the `xd://` virtual device protocol (`internal-urls/xd-protocol.ts`, `tools/xdev.ts`): tools declaring `loadMode: "discoverable"` are unmounted from the request tools array and driven via `read xd://` (list/docs+schema) and `write xd://<tool>` (execute), gated by the `tools.xdev` setting (default on) and inlined into the system prompt.
- Merged the `irc`, `job`, and `launch` tools into a single `hub` tool (`tools/hub/`, `async/job-manager.ts`): messaging keeps `send`/`inbox`/`list`, job control maps to `wait`/`cancel`/`jobs`, process supervision keeps `start`/`logs`/`stop`/`restart`/`describe` with `ps`, and the unified `wait` races background jobs against peer messages; SDK `IrcTool`/`JobTool`/`LaunchTool` are replaced by `HubTool`.
- Removed the hidden `resolve` tool in favor of the `xd://resolve`/`xd://reject`/`xd://propose` resolution devices, auto-including `write` whenever a deferrable tool or plan mode is present.
- Removed the BM25 tool-discovery system: the `search_tool_bm25` tool, the `tool-discovery` module, the `tools.discoveryMode`/`mcp.discoveryMode`/`mcp.discoveryDefaultServers`/`tools.essentialOverride` settings, per-tool MCP selection, and the `mcp_tool_selection` message type.
- Unified tool presentation on `ToolLoadMode` (`essential`|`discoverable`), replacing the custom-tool `xdev?: boolean` opt-out; custom, extension, MCP, RPC host, image-generation, and TTS tools now default to `discoverable`, and added a `satisfies` predicate to `SoftToolRequirement`.
- Removed the standalone `ssh` command tool and `ssh/ssh-executor` (the `ssh://` read/write/search protocol stays), and made `--tools` address hidden built-ins.
- Updated collab-web to render `xd://` dispatches and `hub` op families, dropped the `search_tool_bm25`/`ssh`/`report-finding` renderers, refreshed tool docs and prompts, and migrated the affected tests and changelogs.
2026-07-15 15:16:29 +02:00

104 lines
3.6 KiB
TypeScript

/**
* Tool card chrome + per-tool dispatch. Works in the collab-web app and inside
* the `<omp-tool-view>` web component embedded in HTML session exports.
*/
import { INTENT_FIELD } from "@oh-my-pi/pi-wire";
import type { ReactNode } from "react";
import { useState } from "react";
import { resolveToolRenderer } from "./registry";
import type { ToolRenderHost, ToolRenderProps, ToolResultLike } from "./types";
import { isRecord, replaceTabs, stripAnsi } from "./util";
import "./tool-render.css";
export interface ToolViewProps {
name: string;
args?: unknown;
result?: ToolResultLike;
/** Tool is still executing (live collab view). */
running?: boolean;
/** Model-provided intent (`i`), shown atop the body. */
intent?: string;
/** Streaming partial output tail while running. */
partial?: string;
defaultOpen?: boolean;
/** Host capabilities (sub-session drill-down, …). */
host?: ToolRenderHost;
}
function normalizeArgs(raw: unknown): { args: Record<string, unknown>; intent: string | undefined } {
if (!isRecord(raw)) return { args: {}, intent: undefined };
const intent = typeof raw[INTENT_FIELD] === "string" ? (raw[INTENT_FIELD] as string).trim() : undefined;
if (!(INTENT_FIELD in raw)) return { args: raw, intent };
const args: Record<string, unknown> = {};
for (const k in raw) {
if (k !== INTENT_FIELD) args[k] = raw[k];
}
return { args, intent };
}
interface XdevDispatch {
tool: string;
args: Record<string, unknown>;
inner: unknown;
}
function executeXdevDispatch(props: ToolViewProps): XdevDispatch | null {
if (props.name !== "write" || props.result?.isError === true || !isRecord(props.result?.details)) return null;
const xdev = props.result.details.xdev;
if (!isRecord(xdev) || xdev.mode !== "execute" || typeof xdev.tool !== "string") return null;
return { tool: xdev.tool, args: isRecord(xdev.args) ? xdev.args : {}, inner: xdev.inner };
}
export function ToolView(props: ToolViewProps): ReactNode {
const [open, setOpen] = useState(props.defaultOpen ?? false);
const xdev = executeXdevDispatch(props);
const { args, intent: argIntent } = normalizeArgs(props.args);
const intent = props.intent?.trim() || argIntent;
const name = xdev?.tool ?? props.name;
const result = xdev
? { content: props.result!.content, details: xdev.inner, isError: props.result!.isError }
: props.result;
const renderer = resolveToolRenderer(name);
const renderProps: ToolRenderProps = {
name,
args: xdev?.args ?? args,
result,
running: props.running,
host: props.host,
};
const isError = props.result?.isError === true;
const status = props.running ? "run" : isError ? "err" : props.result ? "ok" : "pending";
const partial = props.running && !props.result && props.partial ? stripAnsi(replaceTabs(props.partial)) : "";
return (
<div className={`tv-card${isError ? " tv-card--error" : ""}`}>
<button
type="button"
className="tv-head"
aria-expanded={open}
onClick={() => setOpen(v => !v)}
title={intent || undefined}
>
{status === "run" ? (
<span className="tv-spin" aria-label="running" />
) : (
<span className={`tv-status tv-status--${status}`} aria-hidden="true" />
)}
<span className="tv-name">{xdev ? `xd://${name}` : name}</span>
<span className="tv-sum">
<renderer.Summary {...renderProps} />
</span>
<span className="tv-chev" aria-hidden="true" />
</button>
{open && (
<div className="tv-body">
{intent && <div className="tv-intent">{intent}</div>}
{renderer.Body ? <renderer.Body {...renderProps} /> : null}
</div>
)}
{partial && <pre className="tv-partial">{partial.length > 2048 ? `…${partial.slice(-2048)}` : partial}</pre>}
</div>
);
}