@ai-setting/roy-plugin-task-show 0.8.10 → 0.9.5
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/dist/cli-tasks-adapter.d.ts.map +1 -1
- package/dist/cli-tasks-adapter.js +4 -0
- package/dist/cli-tasks-adapter.js.map +1 -1
- package/dist/plugin.d.ts +8 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +97 -0
- package/dist/plugin.js.map +1 -1
- package/dist/server.d.ts +15 -2
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +33 -4
- package/dist/server.js.map +1 -1
- package/dist/task-detail-mermaid.d.ts +9 -0
- package/dist/task-detail-mermaid.d.ts.map +1 -1
- package/dist/task-detail-mermaid.js +43 -3
- package/dist/task-detail-mermaid.js.map +1 -1
- package/dist/task-session-store.d.ts +69 -0
- package/dist/task-session-store.d.ts.map +1 -0
- package/dist/task-session-store.js +153 -0
- package/dist/task-session-store.js.map +1 -0
- package/dist/tracing/decorator.d.ts +48 -0
- package/dist/tracing/decorator.d.ts.map +1 -0
- package/dist/tracing/decorator.js +310 -0
- package/dist/tracing/decorator.js.map +1 -0
- package/package.json +1 -1
- package/plugin.json +2 -2
- package/public/app.js +45 -1
- package/public/index.html +3 -0
- package/public/session-forest.js +97 -0
- package/public/task-operations.js +7 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview @TracedAs decorator stub for the roy-plugin-task-show v0.9.0.
|
|
3
|
+
*
|
|
4
|
+
* The plugin is a standalone npm package with `dependencies: {}`, so it
|
|
5
|
+
* cannot import the host's `roy-agent-core` tracing module statically.
|
|
6
|
+
* This module provides a minimal local `@TracedAs(name, options)`
|
|
7
|
+
* decorator that:
|
|
8
|
+
*
|
|
9
|
+
* 1. At module load, locates a writable `spans.db` (SQLite) using:
|
|
10
|
+
* a. `process.env.ROY_TRACE_DB` if set
|
|
11
|
+
* b. `~/.config/roy-agent/spans.db` (host default)
|
|
12
|
+
* If none of these resolve to a writable file, the decorator
|
|
13
|
+
* degrades to a no-op that just calls the wrapped function.
|
|
14
|
+
*
|
|
15
|
+
* 2. Wraps the target method so that every invocation writes a
|
|
16
|
+
* single row to the `spans` table mirroring the host's
|
|
17
|
+
* OpenTelemetry-compatible schema:
|
|
18
|
+
*
|
|
19
|
+
* trace_id (16 bytes hex)
|
|
20
|
+
* span_id (8 bytes hex, unique per row)
|
|
21
|
+
* parent_span_id (always NULL for top-level spans)
|
|
22
|
+
* name (the span_name passed to @TracedAs)
|
|
23
|
+
* kind (always "internal")
|
|
24
|
+
* status ("ok" or "error")
|
|
25
|
+
* start_time (Unix epoch ms)
|
|
26
|
+
* end_time (Unix epoch ms)
|
|
27
|
+
* duration_ms (end - start)
|
|
28
|
+
* created_at (now)
|
|
29
|
+
*
|
|
30
|
+
* If the DB write throws (locked, missing schema, permission
|
|
31
|
+
* denied), the error is swallowed — the decorator must never
|
|
32
|
+
* break the wrapped function. This is critical for tests,
|
|
33
|
+
* CI, and any environment where the host spans.db is absent.
|
|
34
|
+
*
|
|
35
|
+
* 3. Generates a process-lifetime `trace_id` (16 bytes hex) at
|
|
36
|
+
* module init. The host's spans.db queries by `name`, so a
|
|
37
|
+
* shared trace_id is sufficient.
|
|
38
|
+
*
|
|
39
|
+
* The plugin runs in the Bun runtime (declared in `package.json
|
|
40
|
+
* engines.bun`), so we use `bun:sqlite` for the writer. If Bun is
|
|
41
|
+
* not available, the decorator degrades to no-op (tests don't
|
|
42
|
+
* need to write spans to pass).
|
|
43
|
+
*
|
|
44
|
+
* Why not import from `@ai-setting/roy-agent-core`? See the
|
|
45
|
+
* plan doc §3.1 / review op #20182: keeping the plugin zero-dep
|
|
46
|
+
* is a hard contract. A 90-line local stub is cheaper than
|
|
47
|
+
* carrying a runtime dep that may not exist in every install
|
|
48
|
+
* target (e.g. the plugin's own test suite).
|
|
49
|
+
*/
|
|
50
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
51
|
+
import { dirname, join } from "node:path";
|
|
52
|
+
import { homedir } from "node:os";
|
|
53
|
+
let cachedDb = null;
|
|
54
|
+
let resolvedSpansDbPath = null;
|
|
55
|
+
/**
|
|
56
|
+
* Process-lifetime trace_id (16 random bytes hex). The host's
|
|
57
|
+
* trace_check queries spans by `name`, so a single trace_id per
|
|
58
|
+
* process is sufficient and matches the host's per-process model.
|
|
59
|
+
*/
|
|
60
|
+
const PROCESS_TRACE_ID = (() => {
|
|
61
|
+
const bytes = new Uint8Array(16);
|
|
62
|
+
const c = globalThis.crypto;
|
|
63
|
+
if (c && typeof c.getRandomValues === "function") {
|
|
64
|
+
c.getRandomValues(bytes);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
for (let i = 0; i < bytes.length; i += 1)
|
|
68
|
+
bytes[i] = Math.floor(Math.random() * 256);
|
|
69
|
+
}
|
|
70
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
71
|
+
})();
|
|
72
|
+
/**
|
|
73
|
+
* Generate a unique 8-byte hex span_id per call.
|
|
74
|
+
*/
|
|
75
|
+
function newSpanId() {
|
|
76
|
+
const bytes = new Uint8Array(8);
|
|
77
|
+
const c = globalThis.crypto;
|
|
78
|
+
if (c && typeof c.getRandomValues === "function") {
|
|
79
|
+
c.getRandomValues(bytes);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
for (let i = 0; i < bytes.length; i += 1)
|
|
83
|
+
bytes[i] = Math.floor(Math.random() * 256);
|
|
84
|
+
}
|
|
85
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Locate the spans.db path. Returns null if no candidate exists
|
|
89
|
+
* or is writable. The lookup is cached after the first successful
|
|
90
|
+
* resolution.
|
|
91
|
+
*/
|
|
92
|
+
function resolveSpansDbPath() {
|
|
93
|
+
if (resolvedSpansDbPath !== null)
|
|
94
|
+
return resolvedSpansDbPath;
|
|
95
|
+
const candidates = [];
|
|
96
|
+
if (process.env.ROY_TRACE_DB)
|
|
97
|
+
candidates.push(process.env.ROY_TRACE_DB);
|
|
98
|
+
if (process.env.ROY_AGENT_DATA) {
|
|
99
|
+
candidates.push(join(process.env.ROY_AGENT_DATA, "spans.db"));
|
|
100
|
+
}
|
|
101
|
+
candidates.push(join(homedir(), ".config", "roy-agent", "spans.db"));
|
|
102
|
+
for (const c of candidates) {
|
|
103
|
+
if (c && existsSync(c)) {
|
|
104
|
+
resolvedSpansDbPath = c;
|
|
105
|
+
return c;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Lazily open the spans.db. Returns null if Bun is not available,
|
|
112
|
+
* the file cannot be opened, or the `spans` table does not exist.
|
|
113
|
+
*/
|
|
114
|
+
function getDb() {
|
|
115
|
+
if (cachedDb)
|
|
116
|
+
return cachedDb;
|
|
117
|
+
if (typeof globalThis.Bun === "undefined")
|
|
118
|
+
return null;
|
|
119
|
+
const path = resolveSpansDbPath();
|
|
120
|
+
if (!path)
|
|
121
|
+
return null;
|
|
122
|
+
try {
|
|
123
|
+
const { Database } = require("bun:sqlite");
|
|
124
|
+
const db = new Database(path);
|
|
125
|
+
try {
|
|
126
|
+
db.exec("SELECT 1 FROM span LIMIT 0");
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
db.close();
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
cachedDb = db;
|
|
133
|
+
return cachedDb;
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Write a single span row. Never throws — decorator must not
|
|
141
|
+
* break the wrapped function.
|
|
142
|
+
*/
|
|
143
|
+
function writeSpan(row) {
|
|
144
|
+
const db = getDb();
|
|
145
|
+
if (!db)
|
|
146
|
+
return;
|
|
147
|
+
const spanId = newSpanId();
|
|
148
|
+
const durationMs = row.endTime - row.startTime;
|
|
149
|
+
try {
|
|
150
|
+
db.prepare(`INSERT INTO span (trace_id, span_id, parent_span_id, name, kind, status, start_time, end_time, attributes, time_created)
|
|
151
|
+
VALUES (?, ?, NULL, ?, 'internal', ?, ?, ?, ?, ?)`).run(PROCESS_TRACE_ID, spanId, row.name, row.status, row.startTime, row.endTime, JSON.stringify({ duration_ms: durationMs, ...(row.errorMessage ? { error: row.errorMessage } : {}) }), Date.now());
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
// Swallow — see file header.
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* @TracedAs(name, options) decorator factory.
|
|
159
|
+
*
|
|
160
|
+
* Usage:
|
|
161
|
+
* @TracedAs("server.home.render", { recordParams: false, recordResult: false })
|
|
162
|
+
* private async sendIndex(res: http.ServerResponse): Promise<void> { ... }
|
|
163
|
+
*
|
|
164
|
+
* If the spans.db is unreachable (no host, no env, schema mismatch),
|
|
165
|
+
* the decorator becomes a transparent passthrough — the wrapped
|
|
166
|
+
* function is called with its original `this` and arguments.
|
|
167
|
+
*/
|
|
168
|
+
export function TracedAs(name, _options) {
|
|
169
|
+
return function (_target, propertyKey, descriptor) {
|
|
170
|
+
const originalFn = descriptor.value;
|
|
171
|
+
if (!originalFn)
|
|
172
|
+
return descriptor;
|
|
173
|
+
const spanName = name || propertyKey;
|
|
174
|
+
const wrapped = function (...args) {
|
|
175
|
+
const startTime = Date.now();
|
|
176
|
+
let result;
|
|
177
|
+
try {
|
|
178
|
+
result = originalFn.apply(this, args);
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
const endTime = Date.now();
|
|
182
|
+
writeSpan({
|
|
183
|
+
name: spanName,
|
|
184
|
+
status: "error",
|
|
185
|
+
startTime,
|
|
186
|
+
endTime,
|
|
187
|
+
errorMessage: err instanceof Error ? err.message : String(err),
|
|
188
|
+
});
|
|
189
|
+
throw err;
|
|
190
|
+
}
|
|
191
|
+
if (result && typeof result.then === "function") {
|
|
192
|
+
return Promise.resolve(result).then((value) => {
|
|
193
|
+
writeSpan({
|
|
194
|
+
name: spanName,
|
|
195
|
+
status: "ok",
|
|
196
|
+
startTime,
|
|
197
|
+
endTime: Date.now(),
|
|
198
|
+
});
|
|
199
|
+
return value;
|
|
200
|
+
}, (err) => {
|
|
201
|
+
writeSpan({
|
|
202
|
+
name: spanName,
|
|
203
|
+
status: "error",
|
|
204
|
+
startTime,
|
|
205
|
+
endTime: Date.now(),
|
|
206
|
+
errorMessage: err instanceof Error ? err.message : String(err),
|
|
207
|
+
});
|
|
208
|
+
throw err;
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
writeSpan({
|
|
212
|
+
name: spanName,
|
|
213
|
+
status: "ok",
|
|
214
|
+
startTime,
|
|
215
|
+
endTime: Date.now(),
|
|
216
|
+
});
|
|
217
|
+
return result;
|
|
218
|
+
};
|
|
219
|
+
wrapped.__tracedAsName = spanName;
|
|
220
|
+
return {
|
|
221
|
+
...descriptor,
|
|
222
|
+
value: wrapped,
|
|
223
|
+
};
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* For tests: reset the module-level db cache so test fixtures
|
|
228
|
+
* can simulate a fresh host install. Not used in production.
|
|
229
|
+
*/
|
|
230
|
+
export function __resetTracedAsCacheForTests() {
|
|
231
|
+
if (cachedDb) {
|
|
232
|
+
try {
|
|
233
|
+
cachedDb.close();
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
// ignore
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
cachedDb = null;
|
|
240
|
+
resolvedSpansDbPath = null;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* For tests: returns the resolved spans.db path or null. Allows
|
|
244
|
+
* tests to assert the decorator located the host DB.
|
|
245
|
+
*/
|
|
246
|
+
export function __getResolvedSpansDbPathForTests() {
|
|
247
|
+
return resolveSpansDbPath();
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Ensure the parent dir of a candidate spans.db path exists
|
|
251
|
+
* (used by tests that point at a temp file). Best-effort.
|
|
252
|
+
*/
|
|
253
|
+
export function __ensureSpansDbDirForTests(path) {
|
|
254
|
+
try {
|
|
255
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
// ignore
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* `withTrace(name, fn)` — manual instrumentation helper for free
|
|
263
|
+
* functions (which TS decorators cannot decorate). Runs `fn` and
|
|
264
|
+
* records a span around the invocation. Mirrors the @TracedAs
|
|
265
|
+
* contract: never throws, no-ops when spans.db is unavailable.
|
|
266
|
+
*
|
|
267
|
+
* Used by `renderSessionForestPage` and `encodeMermaidLabelText`
|
|
268
|
+
* (top-level exports that cannot carry a class-method decorator).
|
|
269
|
+
*/
|
|
270
|
+
export async function withTrace(name, fn) {
|
|
271
|
+
const startTime = Date.now();
|
|
272
|
+
try {
|
|
273
|
+
const result = await fn();
|
|
274
|
+
writeSpan({ name, status: "ok", startTime, endTime: Date.now() });
|
|
275
|
+
return result;
|
|
276
|
+
}
|
|
277
|
+
catch (err) {
|
|
278
|
+
writeSpan({
|
|
279
|
+
name,
|
|
280
|
+
status: "error",
|
|
281
|
+
startTime,
|
|
282
|
+
endTime: Date.now(),
|
|
283
|
+
errorMessage: err instanceof Error ? err.message : String(err),
|
|
284
|
+
});
|
|
285
|
+
throw err;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Synchronous variant of `withTrace`. Used for top-level
|
|
290
|
+
* non-async exports.
|
|
291
|
+
*/
|
|
292
|
+
export function withTraceSync(name, fn) {
|
|
293
|
+
const startTime = Date.now();
|
|
294
|
+
try {
|
|
295
|
+
const result = fn();
|
|
296
|
+
writeSpan({ name, status: "ok", startTime, endTime: Date.now() });
|
|
297
|
+
return result;
|
|
298
|
+
}
|
|
299
|
+
catch (err) {
|
|
300
|
+
writeSpan({
|
|
301
|
+
name,
|
|
302
|
+
status: "error",
|
|
303
|
+
startTime,
|
|
304
|
+
endTime: Date.now(),
|
|
305
|
+
errorMessage: err instanceof Error ? err.message : String(err),
|
|
306
|
+
});
|
|
307
|
+
throw err;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
//# sourceMappingURL=decorator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decorator.js","sourceRoot":"","sources":["../../src/tracing/decorator.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/plugin.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-setting/roy-plugin-task-show",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.5",
|
|
4
4
|
"type": "tool-plugin",
|
|
5
|
-
"description": "Visualize the tool call chain of a task on a local web service with real-time SSE updates. v0.5.0+: page refreshes stream over GET /api/events (Server-Sent Events). Subscribes to tool:before.execute, tool:after.execute, task:before.create, task:after.create, task:after.complete (preferred, 2026-07-10+), and task:after.update (legacy fallback). v0.6.11: Mermaid re-rendering is delegated to a self-contained controller (public/mermaid-renderer.js) that prevents the SVG→raw-source regression on async updates and surfaces recoverable .mermaid-error states. v0.6.12: Task lifecycle pipeline (operations timeline) server now emits data-task-id on the pipeline section; client preserves it on swap, so the page actually fetches /api/tasks/<id>/operations and renders the 7-op timeline (previously silently bailed). v0.7.0: Home page redesigned as a hierarchical task tree (driven by `roy-agent tasks tree --json`); new /api/tasks/tree endpoint with status / priority / type / root-id filters, expand/collapse UI, search, and live 30s polling. v0.8.0: per-task page Mermaid area now renders the hierarchical 'Task lifecycle + tools' view — each operation record owns a subgraph that nests its tool calls, with click callbacks (`window.__toolClick`) that scroll-into-view + highlight + auto-expand the matching row in the tool-call table below. Operation record descriptions (`description` + `processDescription`) are now always rendered inline (no `<details>` collapse) so the user sees the lifecycle state at a glance; a fallback `<details>` kicks in only for descriptions longer than 600 chars. v0.8.1: hotfix for two pre-existing bugs in v0.8.0 (browser smoke test surfaced after merge). (a) Mermaid click directives were emitted as `click t1 __toolClick(1)` (missing `call` keyword) — Mermaid 10's parser rejects this with `got 'PS'`. Fixed to `click t1 call __toolClick(1)` (the v10 grammar requires `call` to invoke a callback with arguments). (b) `buildMermaidSource` lived inside the `attachTaskPageTimeline` IIFE but was also called from a listener in the `attachToolClickBridge` IIFE — sibling IIFEs cannot see each other's locals, so the listener threw `ReferenceError: buildMermaidSource is not defined` and the Mermaid diagram silently failed to re-render after `task-show:lifecycle-ops-loaded`. Fixed by hoisting the function (and its three helpers) to script top-level so both IIFEs can see it via the script-wide closure; the function is also exposed on `window.buildMermaidSource` for tests + tooling. v0.8.3: tree-display fix (Task #2426). The home page used to look like a flat list of root tasks because `autoExpandFirstLevels(..., 2)` only opened the first 2 levels — 30/47 roots were leaf nodes and the remaining 17 collapsed to one level so grandchildren were never visible. Default expand depth is now 3 (root + child + grandchild + great-grandchild are visible on first paint), the summary line now shows per-depth count pills (root / child / grandchild / great-grandchild / level-N), each `tree-row` carries a `data-depth` attribute so CSS can paint coloured left rails per level, and the duplicated 'Live tool-call sessions (legacy view)' panel that made the page look like both a flat table AND a tree is now hidden behind `#legacy-sessions[hidden]` (kept for future debug-toggle restoration). v0.8.10: bug-fix release (Task #2537 + Task #2534). (a) Heap-bounded plugin caches: OperationsCache and TasksTreeCache now enforce a hard maxEntries cap (default 256 / 64). Oldest stale entries are evicted before inserting a new one, so long-lived roy-agent sessions (BackgroundTaskManager + MemorySessionStore) no longer leak Map entries through the plugin's per-task caches — see Task #2537 for the heap-unbounded-state RED→GREEN repro. (b) Mermaid CJK font-family: server.ts renderTaskPage now configures mermaid.initialize({ themeVariables: { fontFamily: '\"PingFang SC\", \"Microsoft YaHei\", \"Noto Sans CJK SC\", \"Source Han Sans CN\", \"WenQuanYi Micro Hei\", sans-serif' } }) so Chinese node labels render correctly in browsers that have at least one of those fonts installed (see Task #2534).",
|
|
5
|
+
"description": "Visualize the tool call chain of a task on a local web service with real-time SSE updates. v0.9.0: Session-scoped home page (only show tasks created after plugin load + their external ancestors), with per-row 「显示全部栏位」 toggle and lazy-loaded operations timeline; per-task Mermaid labels now correctly render CJK / mixed-Latin / emoji text (encoded as \\uXXXX before emission, decoded by the browser); detail page layout reordered to lifecycle → pipeline → stats → toolcalls → rawjson. v0.5.0+: page refreshes stream over GET /api/events (Server-Sent Events). Subscribes to tool:before.execute, tool:after.execute, task:before.create, task:after.create, task:after.complete (preferred, 2026-07-10+), and task:after.update (legacy fallback). v0.6.11: Mermaid re-rendering is delegated to a self-contained controller (public/mermaid-renderer.js) that prevents the SVG→raw-source regression on async updates and surfaces recoverable .mermaid-error states. v0.6.12: Task lifecycle pipeline (operations timeline) server now emits data-task-id on the pipeline section; client preserves it on swap, so the page actually fetches /api/tasks/<id>/operations and renders the 7-op timeline (previously silently bailed). v0.7.0: Home page redesigned as a hierarchical task tree (driven by `roy-agent tasks tree --json`); new /api/tasks/tree endpoint with status / priority / type / root-id filters, expand/collapse UI, search, and live 30s polling. v0.8.0: per-task page Mermaid area now renders the hierarchical 'Task lifecycle + tools' view — each operation record owns a subgraph that nests its tool calls, with click callbacks (`window.__toolClick`) that scroll-into-view + highlight + auto-expand the matching row in the tool-call table below. Operation record descriptions (`description` + `processDescription`) are now always rendered inline (no `<details>` collapse) so the user sees the lifecycle state at a glance; a fallback `<details>` kicks in only for descriptions longer than 600 chars. v0.8.1: hotfix for two pre-existing bugs in v0.8.0 (browser smoke test surfaced after merge). (a) Mermaid click directives were emitted as `click t1 __toolClick(1)` (missing `call` keyword) — Mermaid 10's parser rejects this with `got 'PS'`. Fixed to `click t1 call __toolClick(1)` (the v10 grammar requires `call` to invoke a callback with arguments). (b) `buildMermaidSource` lived inside the `attachTaskPageTimeline` IIFE but was also called from a listener in the `attachToolClickBridge` IIFE — sibling IIFEs cannot see each other's locals, so the listener threw `ReferenceError: buildMermaidSource is not defined` and the Mermaid diagram silently failed to re-render after `task-show:lifecycle-ops-loaded`. Fixed by hoisting the function (and its three helpers) to script top-level so both IIFEs can see it via the script-wide closure; the function is also exposed on `window.buildMermaidSource` for tests + tooling. v0.8.3: tree-display fix (Task #2426). The home page used to look like a flat list of root tasks because `autoExpandFirstLevels(..., 2)` only opened the first 2 levels — 30/47 roots were leaf nodes and the remaining 17 collapsed to one level so grandchildren were never visible. Default expand depth is now 3 (root + child + grandchild + great-grandchild are visible on first paint), the summary line now shows per-depth count pills (root / child / grandchild / great-grandchild / level-N), each `tree-row` carries a `data-depth` attribute so CSS can paint coloured left rails per level, and the duplicated 'Live tool-call sessions (legacy view)' panel that made the page look like both a flat table AND a tree is now hidden behind `#legacy-sessions[hidden]` (kept for future debug-toggle restoration). v0.8.10: bug-fix release (Task #2537 + Task #2534). (a) Heap-bounded plugin caches: OperationsCache and TasksTreeCache now enforce a hard maxEntries cap (default 256 / 64). Oldest stale entries are evicted before inserting a new one, so long-lived roy-agent sessions (BackgroundTaskManager + MemorySessionStore) no longer leak Map entries through the plugin's per-task caches — see Task #2537 for the heap-unbounded-state RED→GREEN repro. (b) Mermaid CJK font-family: server.ts renderTaskPage now configures mermaid.initialize({ themeVariables: { fontFamily: '\"PingFang SC\", \"Microsoft YaHei\", \"Noto Sans CJK SC\", \"Source Han Sans CN\", \"WenQuanYi Micro Hei\", sans-serif' } }) so Chinese node labels render correctly in browsers that have at least one of those fonts installed (see Task #2534).",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"hooks": [
|
|
8
8
|
{
|
package/public/app.js
CHANGED
|
@@ -247,7 +247,7 @@ function buildMermaidSource(session, operations) {
|
|
|
247
247
|
// Top-level helpers used by buildMermaidSource. Kept here so both
|
|
248
248
|
// IIFEs can call them via the script-wide closure.
|
|
249
249
|
function toolLabel(t) {
|
|
250
|
-
const head =
|
|
250
|
+
const head = encodeMermaidLabelTextClient(`${t.sequence}. ${t.toolName}${t.hasAttachment ? " \ud83d\udcce" : ""}`);
|
|
251
251
|
return `${head}<br/><small>${t.success ? "ok" : "FAIL"} \u00b7 ${t.durationMs}ms</small>`;
|
|
252
252
|
}
|
|
253
253
|
|
|
@@ -263,6 +263,35 @@ function escapeMermaid(s) {
|
|
|
263
263
|
return String(s).replace(/[<>"#]/g, "").replace(/[^A-Za-z0-9_.\-]/g, "_");
|
|
264
264
|
}
|
|
265
265
|
|
|
266
|
+
/**
|
|
267
|
+
* v0.9.0 client-side mirror of `encodeMermaidLabelText` from
|
|
268
|
+
* `src/task-detail-mermaid.ts`. MUST stay byte-for-byte identical
|
|
269
|
+
* to the server implementation; the parity test in
|
|
270
|
+
* `test/mermaid-cjk-encoding-client.test.ts` enforces this.
|
|
271
|
+
*/
|
|
272
|
+
function encodeMermaidLabelTextClient(s) {
|
|
273
|
+
var cleaned = String(s == null ? "" : s)
|
|
274
|
+
.replace(/["\r\n\t]+/g, " ")
|
|
275
|
+
.replace(/\s+/g, " ")
|
|
276
|
+
.trim();
|
|
277
|
+
var out = "";
|
|
278
|
+
for (var i = 0; i < cleaned.length; i++) {
|
|
279
|
+
var ch = cleaned[i];
|
|
280
|
+
var code = ch.codePointAt ? ch.codePointAt(0) : (ch.charCodeAt ? ch.charCodeAt(0) : undefined);
|
|
281
|
+
if (code === undefined) continue;
|
|
282
|
+
if (code < 0x80) { out += ch; continue; }
|
|
283
|
+
if (code <= 0xFFFF) {
|
|
284
|
+
out += "\\u" + code.toString(16).padStart(4, "0");
|
|
285
|
+
} else {
|
|
286
|
+
var v = code - 0x10000;
|
|
287
|
+
var hi = 0xD800 + (v >> 10);
|
|
288
|
+
var lo = 0xDC00 + (v & 0x3FF);
|
|
289
|
+
out += "\\u" + hi.toString(16).padStart(4, "0") + "\\u" + lo.toString(16).padStart(4, "0");
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return out;
|
|
293
|
+
}
|
|
294
|
+
|
|
266
295
|
// ---------------------------------------------------------------------------
|
|
267
296
|
// v0.8.2 (fix/mermaid-readable-labels) — human-readable Mermaid
|
|
268
297
|
// phase labels. This block mirrors `src/task-detail-mermaid.ts` so
|
|
@@ -481,6 +510,21 @@ function getPhaseLabel(op) {
|
|
|
481
510
|
if (typeof window !== "undefined") {
|
|
482
511
|
window.buildMermaidSource = buildMermaidSource;
|
|
483
512
|
window.getPhaseLabel = getPhaseLabel;
|
|
513
|
+
window.encodeMermaidLabelTextClient = encodeMermaidLabelTextClient;
|
|
514
|
+
window.toolLabelClient = function (t) { return toolLabel(t); };
|
|
515
|
+
// v0.9.0 invariant: when invoked with a server encoder, the client
|
|
516
|
+
// encoder must produce byte-for-byte identical output. Drift is
|
|
517
|
+
// surfaced as a hard throw so SSR HTML and dynamic re-render stay
|
|
518
|
+
// in sync.
|
|
519
|
+
window.__cjkParityCheck = function (serverEncode) {
|
|
520
|
+
var cases = ["Phase 3: Plan", "\u4efb\u52a1\u7ba1\u7406", "Phase 1: \u4efb\u52a1\u7ba1\u7406"];
|
|
521
|
+
for (var i = 0; i < cases.length; i++) {
|
|
522
|
+
if (encodeMermaidLabelTextClient(cases[i]) !== serverEncode(cases[i])) {
|
|
523
|
+
throw new Error("CJK parity broken: " + JSON.stringify(cases[i]));
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
return true;
|
|
527
|
+
};
|
|
484
528
|
}
|
|
485
529
|
/**
|
|
486
530
|
* v0.8.0+ Mermaid ↔ tool-table bridge.
|
package/public/index.html
CHANGED
|
@@ -80,6 +80,9 @@
|
|
|
80
80
|
</section>
|
|
81
81
|
|
|
82
82
|
<script src="/static/app.js"></script>
|
|
83
|
+
<script src="/static/mermaid-renderer.js"></script>
|
|
83
84
|
<script src="/static/tasks-tree.js"></script>
|
|
85
|
+
<script src="/static/task-operations.js"></script>
|
|
86
|
+
<script src="/static/session-forest.js"></script>
|
|
84
87
|
</body>
|
|
85
88
|
</html>
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Client-side controller for the v0.9.0 session-scoped forest.
|
|
3
|
+
*
|
|
4
|
+
* - Wires each `[data-toggle="<id>"]` button to its matching
|
|
5
|
+
* `details[data-details="<id>"]` block (the "显示全部栏位" toggle).
|
|
6
|
+
* - Lazy-loads `/api/tasks/<id>/operations` into each
|
|
7
|
+
* `ul.session-operations[data-task-operations="<id>"]` placeholder,
|
|
8
|
+
* reusing `renderPipelineHtml` from `task-operations.js`.
|
|
9
|
+
*
|
|
10
|
+
* Vanilla JS, no framework dependency. Bootstraps on DOMContentLoaded.
|
|
11
|
+
*/
|
|
12
|
+
(function () {
|
|
13
|
+
"use strict";
|
|
14
|
+
|
|
15
|
+
function esc(s) {
|
|
16
|
+
return String(s == null ? "" : s)
|
|
17
|
+
.replace(/&/g, "&")
|
|
18
|
+
.replace(/</g, "<")
|
|
19
|
+
.replace(/>/g, ">")
|
|
20
|
+
.replace(/"/g, """)
|
|
21
|
+
.replace(/'/g, "'");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function wireToggle(button) {
|
|
25
|
+
var id = button.getAttribute("data-toggle");
|
|
26
|
+
if (!id) return;
|
|
27
|
+
var target = document.querySelector('[data-details="' + cssEscape(id) + '"]');
|
|
28
|
+
if (!target) return;
|
|
29
|
+
button.addEventListener("click", function () {
|
|
30
|
+
target.open = !target.open;
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function cssEscape(s) {
|
|
35
|
+
if (window.CSS && typeof window.CSS.escape === "function") {
|
|
36
|
+
return window.CSS.escape(s);
|
|
37
|
+
}
|
|
38
|
+
return String(s).replace(/[^a-zA-Z0-9_-]/g, function (c) {
|
|
39
|
+
return "\\" + c;
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function loadOperationsFor(placeholder) {
|
|
44
|
+
var id = placeholder.getAttribute("data-task-operations");
|
|
45
|
+
if (!id) return;
|
|
46
|
+
placeholder.innerHTML = '<p class="empty">Loading operations…</p>';
|
|
47
|
+
fetch("/api/tasks/" + encodeURIComponent(id) + "/operations")
|
|
48
|
+
.then(function (r) {
|
|
49
|
+
if (!r.ok) throw new Error("HTTP " + r.status);
|
|
50
|
+
return r.json();
|
|
51
|
+
})
|
|
52
|
+
.then(function (envelope) {
|
|
53
|
+
if (typeof window.renderPipelineHtml === "function") {
|
|
54
|
+
placeholder.innerHTML = window.renderPipelineHtml(envelope.operations || [], envelope.stale, Number(id));
|
|
55
|
+
} else {
|
|
56
|
+
// Fallback: render a minimal list
|
|
57
|
+
var html = "<ol class=\"ops-fallback\">";
|
|
58
|
+
(envelope.operations || []).forEach(function (op) {
|
|
59
|
+
html += "<li>" + esc(op.title || op.milestoneType || "op") + "</li>";
|
|
60
|
+
});
|
|
61
|
+
html += "</ol>";
|
|
62
|
+
placeholder.innerHTML = html;
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
.catch(function (err) {
|
|
66
|
+
placeholder.innerHTML = '<p class="empty error">Failed to load operations: ' + esc(err && err.message || err) + '</p>';
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function boot() {
|
|
71
|
+
var section = document.querySelector('[data-session-forest="1"]');
|
|
72
|
+
if (!section) return;
|
|
73
|
+
var buttons = section.querySelectorAll('[data-toggle]');
|
|
74
|
+
for (var i = 0; i < buttons.length; i++) wireToggle(buttons[i]);
|
|
75
|
+
var placeholders = section.querySelectorAll('[data-task-operations]');
|
|
76
|
+
for (var j = 0; j < placeholders.length; j++) loadOperationsFor(placeholders[j]);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (typeof window !== "undefined") {
|
|
80
|
+
window.__sessionForestBoot = boot;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (typeof document !== "undefined") {
|
|
84
|
+
if (document.readyState === "loading") {
|
|
85
|
+
document.addEventListener("DOMContentLoaded", boot);
|
|
86
|
+
} else {
|
|
87
|
+
boot();
|
|
88
|
+
}
|
|
89
|
+
} else if (typeof window !== "undefined" && window.document) {
|
|
90
|
+
// jsdom eval fallback: window may exist but document may not be in scope
|
|
91
|
+
if (window.document.readyState === "loading") {
|
|
92
|
+
window.document.addEventListener("DOMContentLoaded", boot);
|
|
93
|
+
} else {
|
|
94
|
+
boot();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
})();
|
|
@@ -282,6 +282,13 @@
|
|
|
282
282
|
window.__pipelineController = new PipelineController(root);
|
|
283
283
|
}
|
|
284
284
|
|
|
285
|
+
// v0.9.0: expose renderPipelineHtml to the global scope so other
|
|
286
|
+
// modules (session-forest.js) can re-use the same timeline rendering
|
|
287
|
+
// for the v0.9.0 session-forest view.
|
|
288
|
+
if (typeof window !== "undefined") {
|
|
289
|
+
window.renderPipelineHtml = renderPipelineHtml;
|
|
290
|
+
}
|
|
291
|
+
|
|
285
292
|
if (document.readyState === "loading") {
|
|
286
293
|
document.addEventListener("DOMContentLoaded", boot);
|
|
287
294
|
} else {
|