5ff277349c
- 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.
104 lines
3.6 KiB
TypeScript
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>
|
|
);
|
|
}
|