@ai-setting/roy-plugin-task-show 0.1.0

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/README.md ADDED
@@ -0,0 +1,263 @@
1
+ # roy-plugin-task-show
2
+
3
+ > A [roy-agent](https://example/roy-agent) plugin that visualizes the tool-call
4
+ > chain of every task on a local web service and injects the visualization URL
5
+ > back into the task's tool output.
6
+
7
+ ## What it does
8
+
9
+ When loaded into a `roy-agent` host, this plugin:
10
+
11
+ 1. Listens to the `tool:after.execute` and `task:after.update` hook points.
12
+ 2. Records every tool invocation (tool name, args, result preview, duration,
13
+ success flag, timestamp) into an in-memory collector keyed by task id.
14
+ 3. When a task transitions to a terminal status (`completed` / `failed` /
15
+ `cancelled`), mutates the **last** tool result to append a markdown link to
16
+ the visualization page.
17
+ 4. Serves the visualization on a local HTTP server (default
18
+ `http://127.0.0.1:7788/`).
19
+
20
+ The result: every task in the chat gets a 🔗 link the user can click to open
21
+ a mermaid-powered flow chart showing exactly how the agent solved it.
22
+
23
+ ## Repository layout
24
+
25
+ ```
26
+ roy-plugin-task-show/
27
+ ├── plugin.json ← roy-agent plugin manifest
28
+ ├── package.json
29
+ ├── tsconfig.json
30
+ ├── README.md
31
+ ├── public/ ← static frontend (HTML/CSS/JS, served by Node http)
32
+ │ ├── index.html
33
+ │ ├── style.css
34
+ │ └── app.js
35
+ ├── src/
36
+ │ ├── index.ts ← public API (re-exports)
37
+ │ ├── types.ts ← shared types & default config
38
+ │ ├── collector.ts ← in-memory tool-call collector
39
+ │ ├── server.ts ← tiny standalone Node http server (no deps)
40
+ │ ├── url-injector.ts ← tool-result mutation helpers
41
+ │ ├── plugin.ts ← the TaskShowPlugin class (BasePlugin-compatible)
42
+ │ └── core-stub.d.ts ← type stub for the optional core peer dep
43
+ ├── test/
44
+ │ ├── collector.test.ts
45
+ │ ├── url-injector.test.ts
46
+ │ ├── server.test.ts
47
+ │ └── plugin.test.ts
48
+ └── scripts/
49
+ ├── verify-service.ts ← end-to-end check (port 7788 + curl pages)
50
+ └── run-demo.ts ← demo that simulates a real task lifecycle
51
+ ```
52
+
53
+ ## Installation
54
+
55
+ This plugin lives in its own package. Inside a `roy-agent` workspace:
56
+
57
+ ```bash
58
+ # (already wired in monorepo workspaces — otherwise `pnpm add ./roy-plugin-task-show`)
59
+ pnpm install
60
+ ```
61
+
62
+ Or run it standalone for demo purposes:
63
+
64
+ ```bash
65
+ cd roy-plugin-task-show
66
+ bun install
67
+ bun run build
68
+ ```
69
+
70
+ ## Loading into roy-agent
71
+
72
+ `roy-agent`'s plugin loader (`EnvironmentService.loadPlugins()`) supports
73
+ two plugin forms:
74
+
75
+ | form | how it works |
76
+ | --------------------------------- | --------------------------------------------------------- |
77
+ | `import('@scope/plugin')` | Dynamically imports a module and instantiates the plugin. |
78
+ | file path (`/abs/path/to/plug.js`)| Treated as an external `ToolPlugin` script. |
79
+
80
+ For this plugin, we ship a **named export** so it slots into the same
81
+ mechanism used by `task-tag`, `lsp`, etc.
82
+
83
+ ### Option A — register via the loader (recommended)
84
+
85
+ In `roy-agent`'s CLI args:
86
+
87
+ ```bash
88
+ roy-agent interactive --plugin task-show
89
+ ```
90
+
91
+ This works out of the box once `@roy-agent/task-show` is added to the
92
+ workspaces' plugin allow-list. To register it manually, add a case to the
93
+ `loadPlugins()` switch in `packages/cli/src/services/environment.service.ts`:
94
+
95
+ ```ts
96
+ case "task-show": {
97
+ const { createTaskShowPlugin } = await import("@roy-agent/task-show");
98
+ plugin = createTaskShowPlugin({ port: 7788 });
99
+ break;
100
+ }
101
+ ```
102
+
103
+ ### Option B — wire directly in interactive / act mode
104
+
105
+ If you only need it locally, drop the plugin init into the
106
+ `interactive-shutdown.ts` (or `act.ts`) flow next to the
107
+ `pluginAdapterDispose` plumbing. The plugin's lifecycle is:
108
+
109
+ ```ts
110
+ import { createTaskShowPlugin } from "@roy-agent/task-show";
111
+
112
+ const taskShow = createTaskShowPlugin({ port: 7788 });
113
+ await taskShow.init({
114
+ registerHook: (def) => myPluginComponent.register(def),
115
+ getComponent: (name) => env.getComponent(name),
116
+ getConfig: (key) => configComponent?.get(key),
117
+ });
118
+
119
+ // Later, on shutdown:
120
+ await taskShow.dispose();
121
+ ```
122
+
123
+ The plugin registers hooks for:
124
+
125
+ - `tool:after.execute` — collects every tool call (priority 50).
126
+ - `task:after.update` — freezes the session and (optionally) injects the
127
+ visualization URL into the last tool result (priority 60).
128
+
129
+ ## Configuration
130
+
131
+ All config lives in `plugin.json` (and is overridable at construction time):
132
+
133
+ | key | type | default | description |
134
+ | ------------------- | -------- | ----------- | -------------------------------------------------------- |
135
+ | `port` | number | `7788` | HTTP port for the visualization service. |
136
+ | `host` | string | `127.0.0.1` | HTTP host. Use `0.0.0.0` to allow LAN access. |
137
+ | `autoStart` | boolean | `true` | Start the HTTP server when `init()` is called. |
138
+ | `maxStoredTasks` | number | `50` | Max sessions kept in memory. Older ones are evicted. |
139
+ | `urlInjectEnabled` | boolean | `true` | Append the visualization URL to the last tool result. |
140
+ | `publicDir` | string | `public` | Directory holding the static frontend. |
141
+
142
+ Override at construction time:
143
+
144
+ ```ts
145
+ createTaskShowPlugin({
146
+ port: 9000,
147
+ urlInjectEnabled: false,
148
+ });
149
+ ```
150
+
151
+ ## HTTP API
152
+
153
+ | route | description |
154
+ | --------------------------- | ------------------------------------------------------ |
155
+ | `GET /` | Index of recent task sessions. |
156
+ | `GET /task/:taskId` | Per-task page with mermaid flow + tool-call table. |
157
+ | `GET /api/sessions` | JSON list of all sessions (newest first). |
158
+ | `GET /api/sessions/:taskId` | JSON detail of one session (full payload). |
159
+ | `GET /static/*` | Static frontend assets (`style.css`, `app.js`). |
160
+
161
+ If the configured `port` is already in use, the service falls back to an OS
162
+ assigned port and logs the actual URL to stderr.
163
+
164
+ ## Tool result injection format
165
+
166
+ When a task reaches a terminal status, the plugin appends a banner to the
167
+ **last** tool result that was recorded for that task:
168
+
169
+ ```text
170
+ …original tool output…
171
+
172
+ ---
173
+ 📊 **可视化工具调用链路**: <http://127.0.0.1:7788/task/1234>
174
+ (浏览器访问即可查看 mermaid 流程图:每个工具调用、参数、结果摘要、耗时)
175
+ ```
176
+
177
+ It also sets `result.metadata.task_visualization_url` so chat clients that
178
+ prefer structured fields can render the link from there.
179
+
180
+ The mutation is **non-destructive** — the original output is preserved, the
181
+ banner is appended after a `---` divider, and LLM agents can still parse
182
+ the response.
183
+
184
+ ## Development
185
+
186
+ ```bash
187
+ # Type-check + build
188
+ bun run build
189
+
190
+ # Run unit tests (collector, url-injector, server, plugin)
191
+ bun test
192
+
193
+ # End-to-end check (boots the service, hits it, asserts response)
194
+ bun run verify
195
+
196
+ # Manual demo (boots the service + simulates a real task lifecycle)
197
+ bun run start:demo
198
+ ```
199
+
200
+ ### Debug logging
201
+
202
+ Set `TASK_SHOW_DEBUG=1` to enable verbose logging from the collector:
203
+
204
+ ```bash
205
+ TASK_SHOW_DEBUG=1 bun run start:demo
206
+ ```
207
+
208
+ ## Hook payload reference
209
+
210
+ ### `tool:after.execute` (collected)
211
+
212
+ The plugin accepts the standard payload the `globalHookManager` emits for
213
+ this hook:
214
+
215
+ ```ts
216
+ {
217
+ tool: { name: string, ... },
218
+ args: Record<string, unknown>,
219
+ context: { taskId?: number, ... },
220
+ result: { success: boolean, output: string, error?: string, metadata?: any },
221
+ }
222
+ ```
223
+
224
+ It also recognizes (in priority order):
225
+ - `ctx.currentTaskId`
226
+ - `metadata.current_task_id` (top-level)
227
+ - `result.metadata.current_task_id`
228
+
229
+ If no task id can be recovered, the call lands in a synthetic session so
230
+ the visualization still works.
231
+
232
+ ### `task:after.update` (consumed)
233
+
234
+ The plugin accepts the standard `TaskUpdateContext`:
235
+
236
+ ```ts
237
+ { data: { task: { id: number, status: string, title?: string } } }
238
+ ```
239
+
240
+ as well as duck-typed variants like `{ taskId, status, title }`. The plugin
241
+ only injects the URL when `status` is one of `completed`, `failed`,
242
+ `cancelled` — intermediate statuses are ignored.
243
+
244
+ ## Limitations & follow-ups
245
+
246
+ - **Memory-only**: the collector keeps data in memory only. Restarting the
247
+ host process drops everything. Persisting to a file would be a small
248
+ follow-up.
249
+ - **Single host**: if you launch multiple `roy-agent` instances on the same
250
+ machine, change the `port` to avoid clashes (the service falls back to a
251
+ random port automatically, but the URL won't be predictable).
252
+ - **Mermaid CDN**: the frontend loads `mermaid@10` from jsDelivr. Air-gapped
253
+ deployments should vendor a copy locally.
254
+ - **No agent-level hooks**: the plugin currently does not subscribe to
255
+ `agent:after.execute` because the agent tool loop already fires
256
+ `tool:after.execute` per tool. If you want a "task finished" view that
257
+ includes the final agent message, listen to `agent:after.execute` too.
258
+ - **No file persistence**: data lives only in memory. A future improvement
259
+ would be a tiny SQLite sink so a restart still shows recent tasks.
260
+
261
+ ## License
262
+
263
+ MIT
@@ -0,0 +1,104 @@
1
+ /**
2
+ * @fileoverview In-memory collector for tool-call traces.
3
+ *
4
+ * Each task has its own append-only log of `ToolCallRecord`s. The collector
5
+ * exposes simple getters used by the HTTP layer; eviction keeps the memory
6
+ * footprint bounded (oldest tasks are dropped past `maxStoredTasks`).
7
+ */
8
+ import type { TaskSession, TaskShowConfig } from "./types.js";
9
+ /**
10
+ * ToolCallCollector
11
+ *
12
+ * Owns the `Map<taskId, TaskSession>` and exposes:
13
+ * - recordToolCall(): append a single tool call record
14
+ * - getSession(): read-only snapshot
15
+ * - listSessions(): sorted snapshot for the index page
16
+ * - finalizeOnTaskUpdate(): mark session as completed/failed and stamp the
17
+ * visualization URL (called from the `task:after.update` hook)
18
+ *
19
+ * The collector is intentionally synchronous apart from the explicit async
20
+ * hook handlers — that keeps the mental model simple.
21
+ */
22
+ export declare class ToolCallCollector {
23
+ private readonly sessions;
24
+ private readonly cfg;
25
+ private readonly logger;
26
+ /** callback used by the server to know when data has changed (no-op in tests). */
27
+ private readonly onChange?;
28
+ constructor(cfg: TaskShowConfig, options?: {
29
+ onChange?: () => void;
30
+ logPrefix?: string;
31
+ });
32
+ /**
33
+ * Get a defensive snapshot of one task's session (or undefined).
34
+ */
35
+ getSession(taskId: number): TaskSession | undefined;
36
+ /**
37
+ * List every session, sorted by `startedAt` descending (newest first).
38
+ */
39
+ listSessions(): TaskSession[];
40
+ /**
41
+ * Convenience accessor used by the server to build per-task URLs.
42
+ */
43
+ size(): number;
44
+ /**
45
+ * Append a `tool:after.execute` invocation into the right session.
46
+ *
47
+ * - If `explicitTaskId` is supplied (or recovered from the hook metadata),
48
+ * the record lands under that task.
49
+ * - Otherwise we open a synthetic session so demo runs without a
50
+ * TaskComponent still produce a nice flowchart.
51
+ */
52
+ recordToolCall(opts: {
53
+ toolName: string;
54
+ args: Record<string, unknown>;
55
+ success: boolean;
56
+ outputPreview: string;
57
+ error?: string;
58
+ durationMs: number;
59
+ timestamp: number;
60
+ iteration?: number;
61
+ metadata?: Record<string, unknown>;
62
+ ctx?: any;
63
+ explicitTaskId?: number;
64
+ }): number;
65
+ /**
66
+ * Mark a task as completed/failed and freeze its `endedAt`. Used by the
67
+ * `task:after.update` hook.
68
+ *
69
+ * Returns the (possibly updated) session, or undefined if no session
70
+ * existed. The visualization URL is computed by the server (no hard
71
+ * coupling), so this method only persists the timestamp.
72
+ */
73
+ finalizeOnTaskUpdate(opts: {
74
+ taskId: number;
75
+ newStatus: TaskSession["status"];
76
+ title?: string;
77
+ timestamp?: number;
78
+ }): TaskSession | undefined;
79
+ /**
80
+ * Stamp or refresh a session's title (used by `recordTaskTitle` from any
81
+ * context that happens to know the title, e.g. task:before.create).
82
+ */
83
+ setTaskTitle(taskId: number, title: string): void;
84
+ /**
85
+ * Clear all data. Useful in tests and when the plugin is disposed.
86
+ */
87
+ clear(): void;
88
+ /**
89
+ * Drop the oldest non-running sessions once we exceed `maxStoredTasks`.
90
+ *
91
+ * "Running" sessions are never evicted because the user may still be
92
+ * interacting with the page; we simply remove the oldest `completed`
93
+ * / `failed` / `cancelled` sessions.
94
+ */
95
+ private evictIfNeeded;
96
+ /**
97
+ * Defensive deep clone so external code never mutates internal state.
98
+ * We use JSON because all payloads must already be JSON-serializable
99
+ * (they come from the tool hook / TaskComponent and include args/output
100
+ * previews).
101
+ */
102
+ private cloneSession;
103
+ }
104
+ //# sourceMappingURL=collector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collector.d.ts","sourceRoot":"","sources":["../src/collector.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EACV,WAAW,EAEX,cAAc,EACf,MAAM,YAAY,CAAC;AA6DpB;;;;;;;;;;;;GAYG;AACH,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAkC;IAC3D,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAiB;IACrC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAa;IACpC,kFAAkF;IAClF,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAa;gBAGrC,GAAG,EAAE,cAAc,EACnB,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE;IAOzD;;OAEG;IACH,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS;IAMnD;;OAEG;IACH,YAAY,IAAI,WAAW,EAAE;IAM7B;;OAEG;IACH,IAAI,IAAI,MAAM;IAId;;;;;;;OAOG;IACH,cAAc,CAAC,IAAI,EAAE;QACnB,QAAQ,EAAE,MAAM,CAAC;QACjB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAC9B,OAAO,EAAE,OAAO,CAAC;QACjB,aAAa,EAAE,MAAM,CAAC;QACtB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,UAAU,EAAE,MAAM,CAAC;QACnB,SAAS,EAAE,MAAM,CAAC;QAClB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACnC,GAAG,CAAC,EAAE,GAAG,CAAC;QACV,cAAc,CAAC,EAAE,MAAM,CAAC;KACzB,GAAG,MAAM;IA6CV;;;;;;;OAOG;IACH,oBAAoB,CAAC,IAAI,EAAE;QACzB,MAAM,EAAE,MAAM,CAAC;QACf,SAAS,EAAE,WAAW,CAAC,QAAQ,CAAC,CAAC;QACjC,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,GAAG,WAAW,GAAG,SAAS;IAe3B;;;OAGG;IACH,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAOjD;;OAEG;IACH,KAAK,IAAI,IAAI;IASb;;;;;;OAMG;IACH,OAAO,CAAC,aAAa;IAoBrB;;;;;OAKG;IACH,OAAO,CAAC,YAAY;CAGrB"}
@@ -0,0 +1,247 @@
1
+ /**
2
+ * @fileoverview In-memory collector for tool-call traces.
3
+ *
4
+ * Each task has its own append-only log of `ToolCallRecord`s. The collector
5
+ * exposes simple getters used by the HTTP layer; eviction keeps the memory
6
+ * footprint bounded (oldest tasks are dropped past `maxStoredTasks`).
7
+ */
8
+ /**
9
+ * Tiny logger helper that respects a configurable prefix. We avoid pulling in
10
+ * the host `createLogger` because the plugin should remain self-contained.
11
+ */
12
+ class TinyLogger {
13
+ prefix;
14
+ constructor(prefix) {
15
+ this.prefix = prefix;
16
+ }
17
+ info(msg) {
18
+ if (typeof process !== "undefined" && process.env?.TASK_SHOW_DEBUG) {
19
+ // eslint-disable-next-line no-console
20
+ console.log(`[${this.prefix}] ${msg}`);
21
+ }
22
+ }
23
+ warn(msg) {
24
+ // eslint-disable-next-line no-console
25
+ console.warn(`[${this.prefix}] WARN ${msg}`);
26
+ }
27
+ error(msg) {
28
+ // eslint-disable-next-line no-console
29
+ console.error(`[${this.prefix}] ERROR ${msg}`);
30
+ }
31
+ }
32
+ /**
33
+ * Resolve the "current" task id from a ToolHookContext.
34
+ *
35
+ * The roy-agent tool hook metadata typically carries the `current_task_id`
36
+ * (see `getCurrentTaskId()` in tool-component.ts). When absent — for example
37
+ * in tests — we accept a caller-supplied override.
38
+ */
39
+ function extractTaskId(ctx, metadata) {
40
+ if (typeof ctx?.currentTaskId === "number")
41
+ return ctx.currentTaskId;
42
+ if (typeof metadata?.current_task_id === "number") {
43
+ return metadata.current_task_id;
44
+ }
45
+ if (metadata?.current_task_id !== undefined &&
46
+ typeof metadata.current_task_id === "number") {
47
+ return metadata.current_task_id;
48
+ }
49
+ // Common field name set by the tool-component (matches result.metadata)
50
+ if (ctx?.result?.metadata?.current_task_id !== undefined &&
51
+ typeof ctx.result.metadata.current_task_id === "number") {
52
+ return ctx.result.metadata.current_task_id;
53
+ }
54
+ return null;
55
+ }
56
+ /**
57
+ * Run-level fallback task id. We mint a synthetic id when the tool hook does
58
+ * not carry one — useful for the bundled demo, where no TaskComponent exists.
59
+ */
60
+ let synthCounter = 1_900_000_000;
61
+ /**
62
+ * ToolCallCollector
63
+ *
64
+ * Owns the `Map<taskId, TaskSession>` and exposes:
65
+ * - recordToolCall(): append a single tool call record
66
+ * - getSession(): read-only snapshot
67
+ * - listSessions(): sorted snapshot for the index page
68
+ * - finalizeOnTaskUpdate(): mark session as completed/failed and stamp the
69
+ * visualization URL (called from the `task:after.update` hook)
70
+ *
71
+ * The collector is intentionally synchronous apart from the explicit async
72
+ * hook handlers — that keeps the mental model simple.
73
+ */
74
+ export class ToolCallCollector {
75
+ sessions = new Map();
76
+ cfg;
77
+ logger;
78
+ /** callback used by the server to know when data has changed (no-op in tests). */
79
+ onChange;
80
+ constructor(cfg, options) {
81
+ this.cfg = cfg;
82
+ this.onChange = options?.onChange;
83
+ this.logger = new TinyLogger(options?.logPrefix ?? "task-show:collector");
84
+ }
85
+ /**
86
+ * Get a defensive snapshot of one task's session (or undefined).
87
+ */
88
+ getSession(taskId) {
89
+ const s = this.sessions.get(taskId);
90
+ if (!s)
91
+ return undefined;
92
+ return this.cloneSession(s);
93
+ }
94
+ /**
95
+ * List every session, sorted by `startedAt` descending (newest first).
96
+ */
97
+ listSessions() {
98
+ return Array.from(this.sessions.values())
99
+ .map((s) => this.cloneSession(s))
100
+ .sort((a, b) => b.startedAt - a.startedAt);
101
+ }
102
+ /**
103
+ * Convenience accessor used by the server to build per-task URLs.
104
+ */
105
+ size() {
106
+ return this.sessions.size;
107
+ }
108
+ /**
109
+ * Append a `tool:after.execute` invocation into the right session.
110
+ *
111
+ * - If `explicitTaskId` is supplied (or recovered from the hook metadata),
112
+ * the record lands under that task.
113
+ * - Otherwise we open a synthetic session so demo runs without a
114
+ * TaskComponent still produce a nice flowchart.
115
+ */
116
+ recordToolCall(opts) {
117
+ const taskId = opts.explicitTaskId ??
118
+ extractTaskId(opts.ctx, opts.metadata) ??
119
+ synthCounter++;
120
+ let session = this.sessions.get(taskId);
121
+ if (!session) {
122
+ session = {
123
+ taskId,
124
+ title: "", // populated later by finalizeOnTaskUpdate or setTaskTitle
125
+ startedAt: opts.timestamp,
126
+ status: "running",
127
+ toolCalls: [],
128
+ };
129
+ this.sessions.set(taskId, session);
130
+ }
131
+ const sequence = session.toolCalls.length + 1;
132
+ const hasAttachment = detectAttachment(opts.args);
133
+ const record = {
134
+ sequence,
135
+ toolName: opts.toolName,
136
+ args: opts.args,
137
+ success: opts.success,
138
+ outputPreview: opts.outputPreview,
139
+ error: opts.error,
140
+ durationMs: opts.durationMs,
141
+ timestamp: opts.timestamp,
142
+ iteration: opts.iteration,
143
+ metadata: opts.metadata,
144
+ hasAttachment,
145
+ };
146
+ session.toolCalls.push(record);
147
+ this.logger.info(`Recorded #${sequence} ${opts.toolName} (${opts.success ? "ok" : "fail"}) → task=${taskId}`);
148
+ this.evictIfNeeded();
149
+ this.onChange?.();
150
+ return taskId;
151
+ }
152
+ /**
153
+ * Mark a task as completed/failed and freeze its `endedAt`. Used by the
154
+ * `task:after.update` hook.
155
+ *
156
+ * Returns the (possibly updated) session, or undefined if no session
157
+ * existed. The visualization URL is computed by the server (no hard
158
+ * coupling), so this method only persists the timestamp.
159
+ */
160
+ finalizeOnTaskUpdate(opts) {
161
+ const session = this.sessions.get(opts.taskId);
162
+ if (!session)
163
+ return undefined;
164
+ session.status = opts.newStatus;
165
+ session.endedAt = opts.timestamp ?? Date.now();
166
+ if (opts.title)
167
+ session.title = opts.title;
168
+ this.logger.info(`Task ${opts.taskId} → ${opts.newStatus} (${session.toolCalls.length} tool calls)`);
169
+ this.onChange?.();
170
+ return this.cloneSession(session);
171
+ }
172
+ /**
173
+ * Stamp or refresh a session's title (used by `recordTaskTitle` from any
174
+ * context that happens to know the title, e.g. task:before.create).
175
+ */
176
+ setTaskTitle(taskId, title) {
177
+ const session = this.sessions.get(taskId);
178
+ if (!session)
179
+ return;
180
+ session.title = title;
181
+ this.onChange?.();
182
+ }
183
+ /**
184
+ * Clear all data. Useful in tests and when the plugin is disposed.
185
+ */
186
+ clear() {
187
+ this.sessions.clear();
188
+ this.onChange?.();
189
+ }
190
+ // -----------------------------------------------------------------------
191
+ // helpers
192
+ // -----------------------------------------------------------------------
193
+ /**
194
+ * Drop the oldest non-running sessions once we exceed `maxStoredTasks`.
195
+ *
196
+ * "Running" sessions are never evicted because the user may still be
197
+ * interacting with the page; we simply remove the oldest `completed`
198
+ * / `failed` / `cancelled` sessions.
199
+ */
200
+ evictIfNeeded() {
201
+ const limit = this.cfg.maxStoredTasks;
202
+ if (this.sessions.size <= limit)
203
+ return;
204
+ const evictable = Array.from(this.sessions.values())
205
+ .filter((s) => s.status !== "running")
206
+ .sort((a, b) => a.startedAt - b.startedAt);
207
+ const overflow = this.sessions.size - limit;
208
+ let removed = 0;
209
+ for (const s of evictable) {
210
+ if (removed >= overflow)
211
+ break;
212
+ this.sessions.delete(s.taskId);
213
+ removed += 1;
214
+ }
215
+ if (removed > 0) {
216
+ this.logger.warn(`Evicted ${removed} sessions (limit ${limit})`);
217
+ }
218
+ }
219
+ /**
220
+ * Defensive deep clone so external code never mutates internal state.
221
+ * We use JSON because all payloads must already be JSON-serializable
222
+ * (they come from the tool hook / TaskComponent and include args/output
223
+ * previews).
224
+ */
225
+ cloneSession(s) {
226
+ return JSON.parse(JSON.stringify(s));
227
+ }
228
+ }
229
+ /**
230
+ * Detect whether the tool args suggest an attachment (image/pdf/etc.).
231
+ *
232
+ * The hook payload is free-form, but we look at common field names used by
233
+ * `read_file`, `attach`, and similar tools. This is a soft heuristic — false
234
+ * positives are fine, the frontend just renders an "📎" badge.
235
+ */
236
+ function detectAttachment(args) {
237
+ if (!args)
238
+ return false;
239
+ const target = args.file_path ??
240
+ args.path ??
241
+ args.file ??
242
+ args.attachmentPath;
243
+ if (typeof target !== "string")
244
+ return false;
245
+ return /\.(png|jpe?g|webp|gif|bmp|pdf)$/i.test(target);
246
+ }
247
+ //# sourceMappingURL=collector.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collector.js","sourceRoot":"","sources":["../src/collector.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * @fileoverview Public API for the roy-plugin-task-show package.
3
+ *
4
+ * The host loader typically does:
5
+ * import TaskShowPlugin, { createTaskShowPlugin } from "@.../roy-plugin-task-show";
6
+ * const plugin = createTaskShowPlugin({ port: 7788 });
7
+ * plugin.init(env);
8
+ */
9
+ export { TaskShowPlugin, createTaskShowPlugin, } from "./plugin.js";
10
+ export type { TaskShowPluginInterface } from "./plugin.js";
11
+ export { DEFAULT_CONFIG, } from "./types.js";
12
+ export type { TaskShowConfig, PluginEnvLike, TaskSession, ToolCallRecord, } from "./types.js";
13
+ export { ToolCallCollector, } from "./collector.js";
14
+ export { TaskShowServer, } from "./server.js";
15
+ export type { ServerInfo } from "./server.js";
16
+ export { buildVisualizationUrl, buildIndexUrl, injectVisualizationUrl, appendVisualizationBanner, } from "./url-injector.js";
17
+ export type { InjectionResult } from "./url-injector.js";
18
+ import TaskShowPlugin from "./plugin.js";
19
+ export default TaskShowPlugin;
20
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EACL,cAAc,EACd,oBAAoB,GACrB,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AAE3D,OAAO,EACL,cAAc,GACf,MAAM,YAAY,CAAC;AACpB,YAAY,EACV,cAAc,EACd,aAAa,EACb,WAAW,EACX,cAAc,GACf,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,iBAAiB,GAClB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,cAAc,GACf,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C,OAAO,EACL,qBAAqB,EACrB,aAAa,EACb,sBAAsB,EACtB,yBAAyB,GAC1B,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEzD,OAAO,cAAc,MAAM,aAAa,CAAC;AACzC,eAAe,cAAc,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,16 @@
1
+ /**
2
+ * @fileoverview Public API for the roy-plugin-task-show package.
3
+ *
4
+ * The host loader typically does:
5
+ * import TaskShowPlugin, { createTaskShowPlugin } from "@.../roy-plugin-task-show";
6
+ * const plugin = createTaskShowPlugin({ port: 7788 });
7
+ * plugin.init(env);
8
+ */
9
+ export { TaskShowPlugin, createTaskShowPlugin, } from "./plugin.js";
10
+ export { DEFAULT_CONFIG, } from "./types.js";
11
+ export { ToolCallCollector, } from "./collector.js";
12
+ export { TaskShowServer, } from "./server.js";
13
+ export { buildVisualizationUrl, buildIndexUrl, injectVisualizationUrl, appendVisualizationBanner, } from "./url-injector.js";
14
+ import TaskShowPlugin from "./plugin.js";
15
+ export default TaskShowPlugin;
16
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EACL,cAAc,EACd,oBAAoB,GACrB,MAAM,aAAa,CAAC;AAGrB,OAAO,EACL,cAAc,GACf,MAAM,YAAY,CAAC;AAQpB,OAAO,EACL,iBAAiB,GAClB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,cAAc,GACf,MAAM,aAAa,CAAC;AAGrB,OAAO,EACL,qBAAqB,EACrB,aAAa,EACb,sBAAsB,EACtB,yBAAyB,GAC1B,MAAM,mBAAmB,CAAC;AAG3B,OAAO,cAAc,MAAM,aAAa,CAAC;AACzC,eAAe,cAAc,CAAC"}