/** * @fileoverview TaskShowPlugin — the roy-agent plugin entry point. * * Lifecycle (mirrors `ReminderPlugin` / `TaskTagPlugin`): * 1. Constructor accepts a config (defaults merged in). * 2. `init(env)` registers hooks on the global hook manager: * - `tool:before.execute` → record per-call start timestamp * - `tool:after.execute` → record each tool call * - `task:before.create` → open a new TaskSession * - `task:after.create` → mark session ready * - `task:after.update` → freeze + broadcast status update * - `task:after.complete` → freeze + broadcast terminal status * 3. `dispose()` stops the HTTP service & releases listeners. * * The plugin is intentionally lean: all the heavy lifting lives in * `collector.ts`, `server.ts`, and `event-bus.ts`. That keeps this file * readable and makes the surface area easy to test. * * ## Visualization reach-out (v0.5.0+) * * v0.5.0 replaces the previous `env.notify({type:"visualization_ready"})` * mechanism with a Server-Sent Events stream served by the local HTTP * service. The frontend subscribes to `GET /api/events` and receives a * real-time push every time: * * - a new task is created (`task.created`) * - a tool call is recorded (`tool.recorded`) * - a task status changes (`task.updated`) * - a task transitions to terminal status (`task.completed`) * * This eliminates the host-side NotificationChannel plumbing and works * with any roy-agent host — including ones that pre-date the * NotificationChannel abstraction. */ import type { PluginEnvLike, TaskShowConfig } from "./types.js"; import { ToolCallCollector } from "./collector.js"; import { EventBus } from "./event-bus.js"; /** * Optional logger-like sink for {@link printStartBanner}. Tests inject a * stub with `info` (and optionally `log`) to capture calls; production * callers rely on the default plugin logger. * * v2.5.12 (Task #2961): the default sink is the plugin unified logger, * NOT `console`. The banner writes to the log file by default and only * surfaces on stdout/stderr when `setQuietMode(false)` is called. */ export type BannerLogger = { log?: (msg: string) => void; info?: (msg: string) => void; }; /** * Print the "🚀 task-show running at …" URL. * * v2.5.12 (Task #2953): was gated behind `TASK_SHOW_DEBUG=1` to keep * the banner out of stdout (the chat subprocess was capturing stdout * and surfacing the URL as a chat-panel banner prefix). * * v2.5.12 (Task #2961): the gate is removed — the URL is emitted * unconditionally through the plugin unified logger (default sink), * which is quiet-by-default and writes to a per-category log file * under `${XDG_DATA_HOME:-~/.local/share}/roy-agent/logs/`. The * banner never touches `console.log` / `console.warn` / `console.error` * unless an operator explicitly calls `setQuietMode(false)`. This is * the user's "走日志系统,不污染 stdout/stderr" requirement. * * Exported (and parameterised on a logger sink) so tests can spy on * the output without monkey-patching the global `console.log`. */ export declare function printStartBanner(lanHost: string, port: number, logger?: BannerLogger): void; /** * v0.6.2 — emit a multi-line ⚠️ warning banner when the HTTP service * could not be started (EADDRINUSE / EACCES / port exhaustion / ...). * * Operators get to see: * 1. The configured host:port they should free / use. * 2. An actionable alternate-port hint when one was suggested by the * server (e.g. server.start() returned `suggestedPort`). * 3. The reason for the failure (raw error string). * 4. The fact that hook registration proceeds in fail-open mode * (visualizations are unavailable, but the agent keeps working). */ export declare function printStartBannerWithWarning(opts: { lanHost: string; port: number; reason: string; suggestedPort?: number; logger?: BannerLogger; }): void; /** * Minimal logger shape required by `resolveToolName`. Production callers * pass `console`; tests pass a stub with `warn` only. */ export type ResolveToolNameLogger = { warn?: (msg: string) => void; }; /** * Resolve a tool name from any of the field shapes the host hook manager * has been observed to use across roy-agent versions. * * Precedence (first non-empty wins): * 1. `ctx.tool?.name` * 2. `ctx.toolDef?.name` * 3. `ctx.toolName` (top-level) * 4. `ctx.toolDef?.toolName` * 5. `ctx.name` (last resort) * 6. `"unknown"` → emits a one-time WARN so the operator sees which * payload shape to fix (deduped globally). * * Why so many keys: `tool:after.execute` events arrive with the tool * identifier nested under different fields depending on host version. * Bundled test harness sets `tool.name`; the interactive host sometimes * sets only `toolName` or wraps the tool under `toolDef`. We try them all. */ export declare function resolveToolName(ctx: any, logger?: ResolveToolNameLogger): string; /** * Reset the dedup flag for `resolveToolName`'s "unknown" warn. Test-only — * exported via a name that signals intent so production callers don't * reach for it. */ export declare function __resetResolveToolNameWarnDedupForTests(): void; /** * Public, friendly type used in README + tests. * * It deliberately matches `BasePlugin` from `@ai-setting/roy-agent-core` * (init/dispose/hooks-style), so this plugin can be loaded both by the * official loader and by hand-rolled test harnesses. */ export interface TaskShowPluginInterface { readonly name: string; readonly version: string; readonly description: string; init(env: PluginEnvLike): void | Promise; dispose(): void | Promise; /** accessor for tests / health checks */ getServerPort(): number | null; /** accessor for tests / health checks */ getCollector(): ToolCallCollector; /** accessor for tests / health checks */ getEventBus(): EventBus; /** accessor for tests that want to bypass the hook manager entirely */ getConfig(): TaskShowConfig; /** * Compute the URL the page would be served at. Helpful in tests where * `init()` was skipped. */ getVisualizationUrl(taskId: number): string; } /** * Main plugin class. Decorated with the static `roy-agent` metadata block * via a side-table so we don't have to import the host's BasePlugin. */ export declare class TaskShowPlugin implements TaskShowPluginInterface { readonly name = "roy-plugin-task-show"; readonly version = "1.1.0"; readonly description = "Visually trace a task's tool-call chain on a local HTTP service. v1.1.0 adds full SSE realtime subscription (task.created / operation.updated / tool.called) on both the home and per-task pages, plus a 5-state SSE-aware pipeline header badge (stale / connecting / live / reconnecting / error)."; private readonly cfg; private readonly collector; private readonly server; private readonly eventBus; /** v0.7.0+: cache for parsed task operations envelopes. */ private readonly operationsCache; /** v0.8.0+: cache for parsed task tree envelopes (used by /api/tasks/tree). */ private readonly tasksTreeCache; /** * v0.9.0+: session-scoped task forest store. When set, GET / renders * the session forest (only tasks created after `sessionStartedAt` * plus their external ancestors) instead of the legacy cross-host * tree view. The plugin entry point always wires this so users get * the v0.9.0 behavior automatically when the plugin is loaded. */ private readonly sessionStore; private env; private disposed; /** * `tool:before.execute` → `tool:after.execute` start-time buffer. * Keyed by a fingerprint derived from (taskId, toolName, argsHash) so * that interleaved tool calls in the same task don't collide. The * matching `after` consume + delete the entry. * * This buffer lets the plugin compute `durationMs` end-to-end itself * (without requiring the host to propagate a `start_ts` through the * tool's result envelope). */ private pendingStarts; /** * Per-instance dedup set — guards against the same task reaching this * handler from both `task:after.complete` AND `task:after.update` * (which can happen on newer hosts where both hook points fire on * completion). Cleared on dispose(). */ private finalizedTaskIds; /** Cached list of hook points we registered, so dispose() can be defensive even if the env lacks unregister. */ private registered; constructor(config?: Partial); /** * Thin alias to the module-level `resolveRoyAgentCliPath` in * `./cli-path-resolver.js`. Kept as a method so existing callers * (and subclasses) keep working. See the cli-path-resolver module * for the full Task #2895 PATH-first algorithm. */ private resolveRoyAgentCliPath; /** * Called by the loader (or by tests) with the host's `PluginEnv`. We use * the host's `registerHook` if available, otherwise fall back to the * global hook manager from `@ai-setting/roy-agent-core`. */ init(env: PluginEnvLike): Promise; /** * Best-effort dispose. We tolerate the absence of `dispose` on the env. */ dispose(): Promise; getServerPort(): number | null; getCollector(): ToolCallCollector; getEventBus(): EventBus; getConfig(): TaskShowConfig; getVisualizationUrl(taskId: number): string; /** * Public function exposed so unit tests can simulate the * `tool:before.execute` payload (without going through the global hook * manager). Used to record a per-call start timestamp so the matching * `tool:after.execute` can compute a real `durationMs`. * * Host contracts vary: some pass an explicit `ctx.start_ts` or a * `metadata.start_ts`, others do not. We accept all three. */ onToolBeforeExecute(ctx: any, metadata?: Record): Promise; /** * Drop pendingStarts entries older than . * Called from onToolBeforeExecute (sweep-before-insert) and onToolAfterExecute * (to opportunistically prune leftovers from a previous burst). */ private sweepPendingStarts; /** * Public function exposed so unit tests can simulate the * `tool:after.execute` payload without going through the global hook * manager. */ onToolAfterExecute(ctx: any, metadata?: Record): Promise; /** * Public function exposed for tests; mirrors the `task:before.create` * payload. We use it to mint a fresh TaskSession before any tool call * is recorded (gives us a stable taskId/title even if no tool fires). */ onTaskBeforeCreate(ctx: any): Promise; /** * Public function exposed for tests; mirrors the `task:after.create` * payload. Right now this is mostly a no-op (session was already opened * in `onTaskBeforeCreate`); the hook is wired so the frontend can * distinguish "task opened" from "task started executing". * * Hosts that only emit `task:after.create` (no before) are also * supported — in that case the session is opened here. */ onTaskAfterCreate(ctx: any): Promise; /** * Public function exposed for tests; mirrors BOTH the preferred * `task:after.complete` payload AND the legacy `task:after.update` * payload. * * - `task:after.complete` (preferred, terminal): * - `data.terminalStatus` is explicit; no filtering needed. * - `task:after.update` (legacy): * - `data.task.status` carries the new status. We treat it as a * generic status update — terminal vs non-terminal decides which * event type we broadcast. * * An internal dedup set guarantees that when both hook points fire * (newer hosts), we broadcast the terminal event exactly once. */ onTaskAfterComplete(ctx: any): Promise; /** * @deprecated Use {@link onTaskAfterComplete}. Kept as a thin alias for * backward compatibility with the previous `task:after.update` API * contract used in v0.1.0 unit tests. Will be removed in v2.0. */ onTaskAfterUpdate(ctx: any): Promise; /** * Construct a TaskEvent and push it to every connected SSE client. * * The broadcast is fire-and-forget; SSE write failures are absorbed by * the bus itself, so this method never throws. Centralizing the event * construction here keeps every hook handler symmetric. */ private broadcast; /** * Subscribe to the hook points we care about. We try to use the env's * `registerHook` first (the modern, type-safe plugin API), then fall back * to the global hook manager if available. */ private registerHooks; private registerViaGlobalManager; /** * Extract the resolved task id from a tool-hook context. Mirrors the * resolution order used in `onToolAfterExecute` so the `before` and * `after` handlers key into `pendingStarts` identically. */ /** * v0.6.6+: returns `null` when no env-context task id is present * (e.g. interactive session with no task_create). Callers must * filter out tool calls that lack a task id rather than synthesise * one — the fallback to `synthCounter` historically created one * synthetic "task" per tool call with no relation to the roy-agent * task system. */ private extractTaskIdFromToolCtx; /** * Compute a deterministic fingerprint for a tool invocation, used to key * the pendingStarts buffer. We deliberately use ONLY (taskId + toolName) * — collisions from two consecutive identical calls in the same task are * acceptable in practice (we over-write), and the buffer is wiped on * matching `after` so long-lived ghost entries cannot accumulate. */ private fingerprintToolCall; private lookupPendingStart; } /** * Convenience factory — used when loaded as a bare ES module (the typical * loader pattern in `coder-harness` / `task-tag`). */ export declare function createTaskShowPlugin(config?: Partial): TaskShowPlugin; /** * Default export. The loader imports `default` or `TaskShowPlugin` from this * module — both routes are wired here. */ export default TaskShowPlugin; //# sourceMappingURL=plugin.d.ts.map