@ai-setting/roy-plugin-task-show 0.6.11 → 0.8.4
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 +121 -1
- package/dist/cli-tasks-adapter.d.ts +142 -0
- package/dist/cli-tasks-adapter.d.ts.map +1 -0
- package/dist/cli-tasks-adapter.js +379 -0
- package/dist/cli-tasks-adapter.js.map +1 -0
- package/dist/cli-tasks-tree-adapter.d.ts +143 -0
- package/dist/cli-tasks-tree-adapter.d.ts.map +1 -0
- package/dist/cli-tasks-tree-adapter.js +400 -0
- package/dist/cli-tasks-tree-adapter.js.map +1 -0
- package/dist/operations-cache.d.ts +44 -0
- package/dist/operations-cache.d.ts.map +1 -0
- package/dist/operations-cache.js +103 -0
- package/dist/operations-cache.js.map +1 -0
- package/dist/plugin.d.ts +14 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +82 -0
- package/dist/plugin.js.map +1 -1
- package/dist/server.d.ts +30 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +289 -89
- package/dist/server.js.map +1 -1
- package/dist/task-detail-mermaid.d.ts +171 -0
- package/dist/task-detail-mermaid.d.ts.map +1 -0
- package/dist/task-detail-mermaid.js +697 -0
- package/dist/task-detail-mermaid.js.map +1 -0
- package/dist/task-operations-html.d.ts +50 -0
- package/dist/task-operations-html.d.ts.map +1 -0
- package/dist/task-operations-html.js +187 -0
- package/dist/task-operations-html.js.map +1 -0
- package/dist/tasks-tree-cache.d.ts +44 -0
- package/dist/tasks-tree-cache.d.ts.map +1 -0
- package/dist/tasks-tree-cache.js +111 -0
- package/dist/tasks-tree-cache.js.map +1 -0
- package/dist/types.d.ts +16 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +6 -1
- package/plugin.json +2 -2
- package/public/app.js +534 -34
- package/public/index.html +54 -21
- package/public/style.css +697 -1
- package/public/task-operations.js +290 -0
- package/public/tasks-tree.js +449 -0
package/public/app.js
CHANGED
|
@@ -104,6 +104,510 @@
|
|
|
104
104
|
}
|
|
105
105
|
})();
|
|
106
106
|
|
|
107
|
+
/* ------------------------------------------------------------------------- */
|
|
108
|
+
/* v0.8.1+ Mermaid source builder (top-level — shared by both IIFEs) */
|
|
109
|
+
/* ------------------------------------------------------------------------- */
|
|
110
|
+
/**
|
|
111
|
+
* Build a Mermaid `flowchart TD` source string for the per-task page.
|
|
112
|
+
*
|
|
113
|
+
* v0.8.0 kept this function inside the `attachTaskPageTimeline` IIFE,
|
|
114
|
+
* but the `attachToolClickBridge` IIFE's `lifecycle-ops-loaded`
|
|
115
|
+
* listener also calls it. IIFE-local bindings are NOT visible across
|
|
116
|
+
* sibling IIFEs, so the listener threw `ReferenceError:
|
|
117
|
+
* buildMermaidSource is not defined` and the Mermaid diagram silently
|
|
118
|
+
* failed to re-render. The fix is to hoist the function (and the three
|
|
119
|
+
* helpers it uses) to script top-level so both IIFEs can see them via
|
|
120
|
+
* the script-wide closure. We also attach the function to `window` so
|
|
121
|
+
* tests + any future tooling can call it directly.
|
|
122
|
+
*
|
|
123
|
+
* Behaviour unchanged from v0.8.0: when `operations` is empty we
|
|
124
|
+
* emit a linear tool chain (matching the server's initial render);
|
|
125
|
+
* when operations are present we nest each tool under its owning
|
|
126
|
+
* operation subgraph + attach click callbacks.
|
|
127
|
+
*/
|
|
128
|
+
function buildMermaidSource(session, operations) {
|
|
129
|
+
const lines = [`flowchart TD`];
|
|
130
|
+
lines.push(` classDef ok fill:#dcfce7,stroke:#16a34a,color:#064e3b`);
|
|
131
|
+
lines.push(` classDef fail fill:#fee2e2,stroke:#dc2626,color:#7f1d1d`);
|
|
132
|
+
lines.push(` classDef op fill:#dbeafe,stroke:#2563eb,color:#1e3a8a`);
|
|
133
|
+
lines.push(` classDef runtime fill:#ede9fe,stroke:#6d28d9,color:#3b0764`);
|
|
134
|
+
lines.push(` classDef bookend fill:#f1f5f9,stroke:#475569,color:#0f172a`);
|
|
135
|
+
lines.push(` task_start([Task #${session.taskId} start]):::runtime`);
|
|
136
|
+
|
|
137
|
+
const ops = Array.isArray(operations) ? operations.slice().sort((a, b) => (a.sequence || 0) - (b.sequence || 0)) : [];
|
|
138
|
+
const toolCalls = session.toolCalls || [];
|
|
139
|
+
|
|
140
|
+
if (ops.length === 0) {
|
|
141
|
+
// No operation data yet — fall back to a single linear chain
|
|
142
|
+
// matching the server's initial render.
|
|
143
|
+
let prev = "task_start";
|
|
144
|
+
if (toolCalls.length === 0) {
|
|
145
|
+
lines.push(` task_end([end]):::runtime`);
|
|
146
|
+
lines.push(` ${prev} --> task_end`);
|
|
147
|
+
return lines.join("\n");
|
|
148
|
+
}
|
|
149
|
+
toolCalls.forEach((call) => {
|
|
150
|
+
const nodeId = `t${call.sequence}`;
|
|
151
|
+
const head = escapeMermaid(`${call.sequence}. ${call.toolName}${call.hasAttachment ? " \ud83d\udcce" : ""}`);
|
|
152
|
+
const status = call.success ? "ok" : "FAIL";
|
|
153
|
+
lines.push(` ${nodeId}["${head}<br/><small>${call.durationMs}ms \u00b7 ${status}</small>"]`);
|
|
154
|
+
lines.push(` class ${nodeId} ${call.success ? "ok" : "fail"}`);
|
|
155
|
+
lines.push(` click ${nodeId} call __toolClick("${call.sequence}")`);
|
|
156
|
+
lines.push(` ${prev} --> ${nodeId}`);
|
|
157
|
+
prev = nodeId;
|
|
158
|
+
});
|
|
159
|
+
const endLabel = session.status === "failed" ? `\u274c Task ${session.status}` : (session.status === "completed" ? `\u2705 Task ${session.status}` : "end");
|
|
160
|
+
lines.push(` task_end([${endLabel}]):::runtime`);
|
|
161
|
+
lines.push(` ${prev} --> task_end`);
|
|
162
|
+
return lines.join("\n");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// With operations: bucket tools by op and render subgraphs.
|
|
166
|
+
// (Same heuristic as `assignToolsToOps` in the server module.)
|
|
167
|
+
const sortedOps = ops.slice().sort((a, b) => String(a.timestamp).localeCompare(String(b.timestamp)));
|
|
168
|
+
const opMs = sortedOps.map((o) => ({ op: o, ms: parseIsoToMs(o.timestamp) }));
|
|
169
|
+
const buckets = { pre: [], post: [], in: {} };
|
|
170
|
+
for (const t of toolCalls) {
|
|
171
|
+
if (opMs.length === 0 || t.timestamp < opMs[0].ms) {
|
|
172
|
+
buckets.pre.push(t);
|
|
173
|
+
} else if (t.timestamp >= opMs[opMs.length - 1].ms) {
|
|
174
|
+
buckets.post.push(t);
|
|
175
|
+
} else {
|
|
176
|
+
let owner = null;
|
|
177
|
+
for (const o of opMs) {
|
|
178
|
+
if (o.ms <= t.timestamp) owner = o.op;
|
|
179
|
+
else break;
|
|
180
|
+
}
|
|
181
|
+
const key = owner ? owner.sequence : "_";
|
|
182
|
+
if (!buckets.in[key]) buckets.in[key] = [];
|
|
183
|
+
buckets.in[key].push(t);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
let prev = "task_start";
|
|
187
|
+
if (buckets.pre.length > 0) {
|
|
188
|
+
lines.push(` subgraph pre_task["pre-task"]`);
|
|
189
|
+
for (const t of buckets.pre) {
|
|
190
|
+
const id = `t${t.sequence}`;
|
|
191
|
+
const label = toolLabel(t);
|
|
192
|
+
lines.push(` ${id}["${label}"]`);
|
|
193
|
+
lines.push(` class ${id} ${t.success ? "ok" : "fail"}`);
|
|
194
|
+
lines.push(` click ${id} call __toolClick("${t.sequence}")`);
|
|
195
|
+
}
|
|
196
|
+
lines.push(` end`);
|
|
197
|
+
lines.push(` ${prev} --> pre_task`);
|
|
198
|
+
prev = "pre_task";
|
|
199
|
+
}
|
|
200
|
+
for (const o of opMs) {
|
|
201
|
+
const sub = `op${o.op.sequence}`;
|
|
202
|
+
// v0.8.2 (fix/mermaid-readable-labels): replace the raw
|
|
203
|
+
// `<seq>. <title>` label (which leaked technical details like
|
|
204
|
+
// `plan_doc: docs/.../plan.md - 485 - worktree: HEAD <hash> from
|
|
205
|
+
// main`) with the concise emoji + friendly-name + short-desc
|
|
206
|
+
// label produced by getPhaseLabel(). The same logic is in
|
|
207
|
+
// `src/task-detail-mermaid.ts` (server side) so the initial
|
|
208
|
+
// server-rendered label and the client-side re-rendered label
|
|
209
|
+
// stay in sync.
|
|
210
|
+
lines.push(` subgraph ${sub}["${getPhaseLabel(o.op)}"]`);
|
|
211
|
+
lines.push(` direction TB`);
|
|
212
|
+
const own = buckets.in[o.op.sequence] || [];
|
|
213
|
+
if (own.length === 0) {
|
|
214
|
+
lines.push(` ${sub}_body["\uff08\u65e0\u5de5\u5177\u8c03\u7528\uff09"]:::bookend`);
|
|
215
|
+
} else {
|
|
216
|
+
for (const t of own) {
|
|
217
|
+
const id = `t${t.sequence}`;
|
|
218
|
+
const label = toolLabel(t);
|
|
219
|
+
lines.push(` ${id}["${label}"]`);
|
|
220
|
+
lines.push(` class ${id} ${t.success ? "ok" : "fail"}`);
|
|
221
|
+
lines.push(` click ${id} call __toolClick("${t.sequence}")`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
lines.push(` end`);
|
|
225
|
+
lines.push(` ${prev} --> ${sub}`);
|
|
226
|
+
prev = sub;
|
|
227
|
+
}
|
|
228
|
+
if (buckets.post.length > 0) {
|
|
229
|
+
lines.push(` subgraph post_task["post-task"]`);
|
|
230
|
+
for (const t of buckets.post) {
|
|
231
|
+
const id = `t${t.sequence}`;
|
|
232
|
+
const label = toolLabel(t);
|
|
233
|
+
lines.push(` ${id}["${label}"]`);
|
|
234
|
+
lines.push(` class ${id} ${t.success ? "ok" : "fail"}`);
|
|
235
|
+
lines.push(` click ${id} call __toolClick("${t.sequence}")`);
|
|
236
|
+
}
|
|
237
|
+
lines.push(` end`);
|
|
238
|
+
lines.push(` ${prev} --> post_task`);
|
|
239
|
+
prev = "post_task";
|
|
240
|
+
}
|
|
241
|
+
const endLabel = session.status === "failed" ? `\u274c Task ${session.status}` : (session.status === "completed" ? `\u2705 Task ${session.status}` : "end");
|
|
242
|
+
lines.push(` task_end([${endLabel}]):::runtime`);
|
|
243
|
+
lines.push(` ${prev} --> task_end`);
|
|
244
|
+
return lines.join("\n");
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Top-level helpers used by buildMermaidSource. Kept here so both
|
|
248
|
+
// IIFEs can call them via the script-wide closure.
|
|
249
|
+
function toolLabel(t) {
|
|
250
|
+
const head = escapeMermaid(`${t.sequence}. ${t.toolName}${t.hasAttachment ? " \ud83d\udcce" : ""}`);
|
|
251
|
+
return `${head}<br/><small>${t.success ? "ok" : "FAIL"} \u00b7 ${t.durationMs}ms</small>`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function parseIsoToMs(s) {
|
|
255
|
+
if (!s) return 0;
|
|
256
|
+
const d = new Date(s);
|
|
257
|
+
if (!isNaN(d.getTime())) return d.getTime();
|
|
258
|
+
const n = Number(s);
|
|
259
|
+
return Number.isFinite(n) ? n : 0;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function escapeMermaid(s) {
|
|
263
|
+
return String(s).replace(/[<>"#]/g, "").replace(/[^A-Za-z0-9_.\-]/g, "_");
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ---------------------------------------------------------------------------
|
|
267
|
+
// v0.8.2 (fix/mermaid-readable-labels) — human-readable Mermaid
|
|
268
|
+
// phase labels. This block mirrors `src/task-detail-mermaid.ts` so
|
|
269
|
+
// the client-side Mermaid re-render emits the same labels the
|
|
270
|
+
// server-rendered HTML initially shows.
|
|
271
|
+
//
|
|
272
|
+
// When the two implementations drift, the diagram will visibly
|
|
273
|
+
// "snap" between the two label formats on the first re-render. To
|
|
274
|
+
// detect this in tests, we expose `getPhaseLabel` on `window` and
|
|
275
|
+
// also assert label consistency in `test/mermaid-phase-labels.test.ts`.
|
|
276
|
+
// ---------------------------------------------------------------------------
|
|
277
|
+
|
|
278
|
+
const PHASE_LABELS_CLIENT = {
|
|
279
|
+
"init-task": { emoji: "\ud83d\udccb", name: "\u4efb\u52a1\u7ba1\u7406", description: "task_search + task_create" },
|
|
280
|
+
"setup-worktree": { emoji: "\ud83c\udf3f", name: "Worktree \u9694\u79bb", description: "git worktree add" },
|
|
281
|
+
"plan": { emoji: "\ud83d\udcdd", name: "Plan", description: "brainstorming + writing-plans" },
|
|
282
|
+
"tdd-implement": { emoji: "\ud83e\uddea", name: "TDD \u5faa\u73af", description: "RED \u2192 GREEN \u2192 REFACTOR" },
|
|
283
|
+
"code-review": { emoji: "\ud83d\udd0d", name: "Code Review", description: "BLOCKING/IMPORTANT/MINOR/NIT" },
|
|
284
|
+
"cli-e2e-test": { emoji: "\u2699\ufe0f", name: "CLI E2E", description: "build + test + cli smoke" },
|
|
285
|
+
"final-verify": { emoji: "\u2705", name: "\u6700\u7ec8\u9a8c\u8bc1", description: "8 \u9879 checklist" },
|
|
286
|
+
"merge-to-main": { emoji: "\ud83d\udd00", name: "Merge", description: "auto_merge \u94c1\u5f8b" },
|
|
287
|
+
"summary-report": { emoji: "\ud83d\udcca", name: "\u603b\u7ed3\u62a5\u544a", description: "reports/ summary" },
|
|
288
|
+
"task-complete": { emoji: "\ud83c\udfc1", name: "\u4efb\u52a1\u95ed\u73af", description: "task_complete" },
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const PHASE_ALIASES_CLIENT = {
|
|
292
|
+
// init-task
|
|
293
|
+
"init-task": "init-task", "init_task": "init-task", "inittask": "init-task",
|
|
294
|
+
"\u4efb\u52a1\u7ba1\u7406": "init-task", "\u4efb\u52a1\u521b\u5efa": "init-task",
|
|
295
|
+
"task create": "init-task", "task-create": "init-task", "create task": "init-task",
|
|
296
|
+
// setup-worktree
|
|
297
|
+
"setup-worktree": "setup-worktree", "setup_worktree": "setup-worktree",
|
|
298
|
+
"setupworktree": "setup-worktree", "worktree": "setup-worktree",
|
|
299
|
+
"worktree \u9694\u79bb": "setup-worktree", "worktree\u9694\u79bb": "setup-worktree",
|
|
300
|
+
"worktree isolation": "setup-worktree",
|
|
301
|
+
// plan
|
|
302
|
+
"plan": "plan", "planning": "plan", "\u8ba1\u5212": "plan",
|
|
303
|
+
"\u8bbe\u8ba1\u4e0e\u8ba1\u5212": "plan", "plan/design": "plan",
|
|
304
|
+
"plan doc": "plan", "planner": "plan",
|
|
305
|
+
// tdd-implement
|
|
306
|
+
"tdd": "tdd-implement", "tdd-implement": "tdd-implement",
|
|
307
|
+
"tdd_implement": "tdd-implement", "tddimplement": "tdd-implement",
|
|
308
|
+
"tdd \u5faa\u73af": "tdd-implement", "tdd cycle": "tdd-implement",
|
|
309
|
+
"red \u2192 green \u2192 refactor": "tdd-implement",
|
|
310
|
+
"red green refactor": "tdd-implement",
|
|
311
|
+
"red\u2192green\u2192refactor": "tdd-implement",
|
|
312
|
+
"implement": "tdd-implement",
|
|
313
|
+
// code-review
|
|
314
|
+
"code-review": "code-review", "code_review": "code-review",
|
|
315
|
+
"codereview": "code-review", "code review": "code-review",
|
|
316
|
+
"review": "code-review", "\u5ba1\u67e5": "code-review",
|
|
317
|
+
// cli-e2e-test
|
|
318
|
+
"cli-e2e-test": "cli-e2e-test", "cli_e2e_test": "cli-e2e-test",
|
|
319
|
+
"clie2etest": "cli-e2e-test", "cli e2e": "cli-e2e-test",
|
|
320
|
+
"cli e2e test": "cli-e2e-test", "e2e": "cli-e2e-test",
|
|
321
|
+
"e2e test": "cli-e2e-test", "smoke": "cli-e2e-test",
|
|
322
|
+
// final-verify
|
|
323
|
+
"final-verify": "final-verify", "final_verify": "final-verify",
|
|
324
|
+
"finalverify": "final-verify", "verify": "final-verify",
|
|
325
|
+
"\u6700\u7ec8\u9a8c\u8bc1": "final-verify", "verification": "final-verify",
|
|
326
|
+
// merge-to-main
|
|
327
|
+
"merge-to-main": "merge-to-main", "merge_to_main": "merge-to-main",
|
|
328
|
+
"mergetomain": "merge-to-main", "merge": "merge-to-main",
|
|
329
|
+
"merge to main": "merge-to-main", "merge-to-main.": "merge-to-main",
|
|
330
|
+
// summary-report
|
|
331
|
+
"summary-report": "summary-report", "summary_report": "summary-report",
|
|
332
|
+
"summaryreport": "summary-report", "summary": "summary-report",
|
|
333
|
+
"\u603b\u7ed3": "summary-report", "\u603b\u7ed3\u62a5\u544a": "summary-report",
|
|
334
|
+
"report": "summary-report",
|
|
335
|
+
// task-complete
|
|
336
|
+
"task-complete": "task-complete", "task_complete": "task-complete",
|
|
337
|
+
"taskcomplete": "task-complete", "\u5b8c\u6210": "task-complete",
|
|
338
|
+
"\u95ed\u73af": "task-complete", "done": "task-complete",
|
|
339
|
+
"completed": "task-complete",
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
const NOISE_FIELD_MARKERS_CLIENT = [
|
|
343
|
+
"plan_doc", "plan_doc:", "plan_doc ", "worktree:", "worktree ", "worktree \u5df2",
|
|
344
|
+
"commits:", "commits_reviewed", "commits reviewed", "commit ", "duration",
|
|
345
|
+
"files_changed", "files changed", "tests_passed", "tests_pass",
|
|
346
|
+
"tests/cli/", "tests/", "test_hello_world", "baseline",
|
|
347
|
+
"modified:", "modified ", "new:",
|
|
348
|
+
"tdd-implement", "tdd_implement", "tdd ", "tdd-",
|
|
349
|
+
"code-review", "code review", "code-review:",
|
|
350
|
+
"(test_", "scripts/", "verify-installed", "verify-installed:",
|
|
351
|
+
"HEAD=", "HEAD:",
|
|
352
|
+
];
|
|
353
|
+
|
|
354
|
+
/** Normalise a phase-name string for alias lookup. */
|
|
355
|
+
function normaliseAliasKeyClient(raw) {
|
|
356
|
+
return String(raw || "")
|
|
357
|
+
.toLowerCase()
|
|
358
|
+
.replace(/[\u2014\u2013_]+/g, "-")
|
|
359
|
+
.replace(/[()\uFF08\uFF09\u3010\u3011\u3010\u3010\u300C\u300D\u3001,,\.\u3002::;;\uFF1B]/g, " ")
|
|
360
|
+
.replace(/\s+/g, " ")
|
|
361
|
+
.trim();
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** Extract `Phase N` + name from a title (matches anywhere in the string). */
|
|
365
|
+
function extractPhaseInfoClient(title) {
|
|
366
|
+
if (!title) return null;
|
|
367
|
+
const m = String(title).match(/phase\s+(\d+)\s*[:\uFF1A(\s]*\s*([^\n:\uFF1A(\)\u3001,\uFF0C]+?)(?=$|\s*(?:[:\uFF1A(\)\u3001,\uFF0C]|\u5b8c\u6210|pass|fail|running|pending|[\r\n]))/i);
|
|
368
|
+
if (!m) return null;
|
|
369
|
+
let rawName = (m[2] || "").trim();
|
|
370
|
+
if (!rawName) return null;
|
|
371
|
+
// Defensive trailing-stop strip.
|
|
372
|
+
for (const stop of ["\u5b8c\u6210", "pass", "fail", "pending", "running", ":", "\uFF1A", "\u2014", "-", "\uFF0C", ",", "\u3002"]) {
|
|
373
|
+
const i = rawName.toLowerCase().lastIndexOf(stop.toLowerCase());
|
|
374
|
+
if (i >= 2 && i >= rawName.length - stop.length - 1) {
|
|
375
|
+
rawName = rawName.substring(0, i).trim();
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
if (!rawName) return null;
|
|
379
|
+
const cleaned = rawName.replace(/\s*(\u5b8c\u6210|pass|fail|pending|running)\s*$/i, "").trim();
|
|
380
|
+
return { num: m[1], name: cleaned || rawName };
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/** Resolve a phase-name string to a canonical phaseId. */
|
|
384
|
+
function resolvePhaseIdClient(phaseName) {
|
|
385
|
+
if (!phaseName) return null;
|
|
386
|
+
if (PHASE_ALIASES_CLIENT[phaseName]) return PHASE_ALIASES_CLIENT[phaseName];
|
|
387
|
+
const norm = normaliseAliasKeyClient(phaseName);
|
|
388
|
+
if (!norm) return null;
|
|
389
|
+
if (PHASE_ALIASES_CLIENT[norm]) return PHASE_ALIASES_CLIENT[norm];
|
|
390
|
+
for (const key of Object.keys(PHASE_ALIASES_CLIENT)) {
|
|
391
|
+
if (normaliseAliasKeyClient(key) === norm) return PHASE_ALIASES_CLIENT[key];
|
|
392
|
+
}
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** Sanitise a noisy title by cutting at the first known noise marker. */
|
|
397
|
+
function sanitiseTitleForLabelClient(title) {
|
|
398
|
+
if (!title) return "";
|
|
399
|
+
let s = String(title);
|
|
400
|
+
const limit = Math.min(s.length, 80);
|
|
401
|
+
let cutAt = -1;
|
|
402
|
+
// 1. Cut at ": commit " / " commit " (common milestone suffix).
|
|
403
|
+
for (const m of [": commit ", " commit "]) {
|
|
404
|
+
const idx = s.indexOf(m);
|
|
405
|
+
if (idx > 0 && idx < limit && (cutAt < 0 || idx < cutAt)) cutAt = idx;
|
|
406
|
+
}
|
|
407
|
+
// 2. Cut at the first noise-marker after a known separator.
|
|
408
|
+
const seps = [" - ", " \u2014 ", " | ", " (", ": "];
|
|
409
|
+
for (const marker of NOISE_FIELD_MARKERS_CLIENT) {
|
|
410
|
+
for (const sep of seps) {
|
|
411
|
+
const idx = s.indexOf(sep + marker, 0);
|
|
412
|
+
if (idx > 0 && idx < limit && (cutAt < 0 || idx < cutAt)) cutAt = idx;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
// 3. Cut at the first " <commit-hash>" or ": <commit-hash>".
|
|
416
|
+
const hashMatch = s.match(/[:\s]\s*[a-f0-9]{7,}\b/i);
|
|
417
|
+
if (hashMatch && hashMatch.index != null && hashMatch.index < limit && (cutAt < 0 || hashMatch.index < cutAt)) {
|
|
418
|
+
cutAt = hashMatch.index + (hashMatch[0].startsWith(":") ? 1 : 0);
|
|
419
|
+
}
|
|
420
|
+
if (cutAt > 0) s = s.substring(0, cutAt);
|
|
421
|
+
return escapeMermaid(s);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Build a human-readable Mermaid label for one operation. Mirrors
|
|
426
|
+
* `getPhaseLabel()` in `src/task-detail-mermaid.ts`. The two
|
|
427
|
+
* implementations must stay in sync — drift is detected by
|
|
428
|
+
* `test/mermaid-phase-labels.test.ts`'s renderAndParse round-trip.
|
|
429
|
+
*/
|
|
430
|
+
function getPhaseLabel(op) {
|
|
431
|
+
const title = (op && op.title != null ? String(op.title) : "");
|
|
432
|
+
const milestoneType = (op && op.milestoneType != null ? String(op.milestoneType) : "op");
|
|
433
|
+
// 1. Structured "Phase N: <name>" path.
|
|
434
|
+
const info = extractPhaseInfoClient(title);
|
|
435
|
+
if (info) {
|
|
436
|
+
const phaseId = resolvePhaseIdClient(info.name);
|
|
437
|
+
if (phaseId && PHASE_LABELS_CLIENT[phaseId]) {
|
|
438
|
+
const meta = PHASE_LABELS_CLIENT[phaseId];
|
|
439
|
+
return `${meta.emoji} Phase ${info.num}: ${meta.name}<br/>${meta.description}`;
|
|
440
|
+
}
|
|
441
|
+
return `\u2699\ufe0f Phase ${info.num}: ${escapeMermaid(info.name)}`;
|
|
442
|
+
}
|
|
443
|
+
// 2. Try matching the entire (short) title as a phase alias.
|
|
444
|
+
if (title.length <= 60) {
|
|
445
|
+
const fullId = resolvePhaseIdClient(title);
|
|
446
|
+
if (fullId && PHASE_LABELS_CLIENT[fullId]) {
|
|
447
|
+
const meta = PHASE_LABELS_CLIENT[fullId];
|
|
448
|
+
return `${meta.emoji} ${meta.name}<br/>${meta.description}`;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
// 3. Substring scan: only when the title STARTS with a phase keyword.
|
|
452
|
+
const lowerStart = title.toLowerCase();
|
|
453
|
+
const startsWithPhaseHits = [
|
|
454
|
+
["init-task", "init-task"], ["\u4efb\u52a1\u7ba1\u7406", "init-task"],
|
|
455
|
+
["setup-worktree", "setup-worktree"], ["worktree", "setup-worktree"],
|
|
456
|
+
["plan", "plan"], ["\u8ba1\u5212", "plan"], ["\u8bbe\u8ba1\u4e0e\u8ba1\u5212", "plan"],
|
|
457
|
+
["tdd", "tdd-implement"], ["tdd-implement", "tdd-implement"],
|
|
458
|
+
["review", "code-review"], ["code review", "code-review"], ["code-review", "code-review"],
|
|
459
|
+
["cli e2e", "cli-e2e-test"], ["cli-e2e-test", "cli-e2e-test"], ["smoke", "cli-e2e-test"],
|
|
460
|
+
["verify", "final-verify"], ["\u6700\u7ec8\u9a8c\u8bc1", "final-verify"],
|
|
461
|
+
["merge-to-main", "merge-to-main"], ["merge to main", "merge-to-main"], ["merge", "merge-to-main"],
|
|
462
|
+
["summary", "summary-report"], ["\u603b\u7ed3\u62a5\u544a", "summary-report"],
|
|
463
|
+
["\u603b\u7ed3", "summary-report"], ["\u62a5\u544a", "summary-report"],
|
|
464
|
+
["\u5b8c\u6210", "task-complete"], ["\u95ed\u73af", "task-complete"],
|
|
465
|
+
];
|
|
466
|
+
for (const [needle, phaseId] of startsWithPhaseHits) {
|
|
467
|
+
if (lowerStart.startsWith(needle.toLowerCase())) {
|
|
468
|
+
const meta = PHASE_LABELS_CLIENT[phaseId];
|
|
469
|
+
return `${meta.emoji} ${meta.name}<br/>${meta.description}`;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
// 4. Last resort: sanitise + neutral ⚙️.
|
|
473
|
+
const sanitised = sanitiseTitleForLabelClient(title);
|
|
474
|
+
if (sanitised) return `\u2699\ufe0f ${sanitised}`;
|
|
475
|
+
// 5. Absolute fallback: milestoneType.
|
|
476
|
+
return `\u2699\ufe0f ${escapeMermaid(milestoneType || "op")}`;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Expose for tests + any tooling that wants to render the Mermaid
|
|
480
|
+
// source on demand.
|
|
481
|
+
if (typeof window !== "undefined") {
|
|
482
|
+
window.buildMermaidSource = buildMermaidSource;
|
|
483
|
+
window.getPhaseLabel = getPhaseLabel;
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* v0.8.0+ Mermaid ↔ tool-table bridge.
|
|
487
|
+
*
|
|
488
|
+
* The Mermaid diagram emits `click tN __toolClick("N")` callbacks for
|
|
489
|
+
* every tool node. We expose a single global `window.__toolClick` that:
|
|
490
|
+
* 1. Locates the row in `table.toolcalls` with `data-tool-id="N"`.
|
|
491
|
+
* 2. If the row is collapsed, opens the input/result <details>.
|
|
492
|
+
* 3. Scrolls the row into view (smooth, centered).
|
|
493
|
+
* 4. Adds the `highlight` class for 2 seconds so the user sees a
|
|
494
|
+
* yellow flash pointing at the row.
|
|
495
|
+
*
|
|
496
|
+
* If the page has no `table.toolcalls` (e.g. the index page), the
|
|
497
|
+
* function is a no-op. We also defensively no-op on invalid IDs so
|
|
498
|
+
* Mermaid can never break the page by firing a stale callback.
|
|
499
|
+
*/
|
|
500
|
+
(function attachToolClickBridge() {
|
|
501
|
+
function scrollAndHighlight(toolId) {
|
|
502
|
+
const id = String(toolId ?? "").trim();
|
|
503
|
+
if (!id) return;
|
|
504
|
+
const row = document.querySelector(`table.toolcalls tr[data-tool-id="${CSS.escape(id)}"]`);
|
|
505
|
+
if (!row) return;
|
|
506
|
+
// Expand the row's input/result <details> so the user can see the
|
|
507
|
+
// payload without an extra click.
|
|
508
|
+
row.querySelectorAll("details.tool-detail").forEach((d) => {
|
|
509
|
+
d.open = true;
|
|
510
|
+
});
|
|
511
|
+
// Scroll into view (smooth, centered). Use block:'center' so the
|
|
512
|
+
// row is roughly in the middle of the viewport.
|
|
513
|
+
try {
|
|
514
|
+
row.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
515
|
+
} catch {
|
|
516
|
+
// Old browsers — try the synchronous variant.
|
|
517
|
+
try { row.scrollIntoView(); } catch (_) { /* noop */ }
|
|
518
|
+
}
|
|
519
|
+
// Flash highlight.
|
|
520
|
+
row.classList.add("highlight");
|
|
521
|
+
setTimeout(() => {
|
|
522
|
+
row.classList.remove("highlight");
|
|
523
|
+
}, 2000);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Expose the global callback. Mermaid emits `click t1 __toolClick("1")`,
|
|
527
|
+
// so the function name is fixed and the string is the tool sequence id.
|
|
528
|
+
window.__toolClick = function (toolId) {
|
|
529
|
+
try {
|
|
530
|
+
scrollAndHighlight(toolId);
|
|
531
|
+
} catch (err) {
|
|
532
|
+
// Never let a click callback throw — Mermaid would log the error
|
|
533
|
+
// and the user would see a broken interaction.
|
|
534
|
+
// eslint-disable-next-line no-console
|
|
535
|
+
console.warn("[task-show] __toolClick failed:", err && err.message || err);
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* v0.8.0+ Mermaid re-render hook. When `task-operations.js` finishes
|
|
541
|
+
* a fetch it fires `task-show:lifecycle-ops-loaded`. We re-build the
|
|
542
|
+
* Mermaid source with the freshly-loaded operations so the diagram
|
|
543
|
+
* goes from the linear placeholder to the hierarchical view.
|
|
544
|
+
*
|
|
545
|
+
* We don't have direct access to the live `session` here (the SSE
|
|
546
|
+
* controller owns that), so we trigger a re-render via the same code
|
|
547
|
+
* path the SSE handler uses. If the SSE handler hasn't run yet (no
|
|
548
|
+
* tool calls), we still update the diagram from the operations-only
|
|
549
|
+
* path so the user immediately sees the hierarchy.
|
|
550
|
+
*/
|
|
551
|
+
window.addEventListener("task-show:lifecycle-ops-loaded", () => {
|
|
552
|
+
const mermaidDiv = document.querySelector(".mermaid");
|
|
553
|
+
if (!mermaidDiv) return;
|
|
554
|
+
const ops = (window.__lifecycleOps && window.__lifecycleOps.operations) || [];
|
|
555
|
+
// Build a minimal session stub from the operations cache (we don't
|
|
556
|
+
// have the live tool calls here, but for a freshly-loaded page the
|
|
557
|
+
// server-rendered Mermaid is already in the DOM, so the SSE path
|
|
558
|
+
// will reconcile later). For the "operations-only" rebuild we
|
|
559
|
+
// synthesise a tiny session so buildMermaidSource can be called.
|
|
560
|
+
const banner = document.querySelector("[data-live-refresh][data-task-id]");
|
|
561
|
+
const taskId = banner ? Number(banner.getAttribute("data-task-id")) : 0;
|
|
562
|
+
if (!Number.isFinite(taskId) || taskId <= 0) return;
|
|
563
|
+
// Trigger SSE's applyTaskUpdate by dispatching a fake "snapshot"
|
|
564
|
+
// event with the operations (we don't have one, so just re-render
|
|
565
|
+
// directly with an empty session — the SSE handler will replace
|
|
566
|
+
// this on the first real event).
|
|
567
|
+
// Simplest: directly call the controller's update path.
|
|
568
|
+
const session = (window.__pipelineController && window.__pipelineController.lastSession) || {
|
|
569
|
+
taskId,
|
|
570
|
+
toolCalls: [],
|
|
571
|
+
status: "running",
|
|
572
|
+
};
|
|
573
|
+
if (!session.toolCalls || session.toolCalls.length === 0) {
|
|
574
|
+
// No SSE has fired yet — fetch /api/sessions/:taskId so we know
|
|
575
|
+
// what tools belong to the diagram.
|
|
576
|
+
fetch(`/api/sessions/${taskId}`, { headers: { Accept: "application/json" } })
|
|
577
|
+
.then((r) => (r.ok ? r.json() : null))
|
|
578
|
+
.then((s) => {
|
|
579
|
+
if (!s) return;
|
|
580
|
+
const source = buildMermaidSource(s, ops);
|
|
581
|
+
if (window.MermaidRenderer && window.MermaidRenderer.createMermaidController) {
|
|
582
|
+
if (!window.__mermaidController) {
|
|
583
|
+
window.__mermaidController = window.MermaidRenderer.createMermaidController({
|
|
584
|
+
idPrefix: "lifecycle-" + taskId,
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
window.__mermaidController.update(mermaidDiv, source);
|
|
588
|
+
} else {
|
|
589
|
+
mermaidDiv.textContent = source;
|
|
590
|
+
}
|
|
591
|
+
})
|
|
592
|
+
.catch(() => {
|
|
593
|
+
// Network down — leave the current diagram in place.
|
|
594
|
+
});
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
const source = buildMermaidSource(session, ops);
|
|
598
|
+
if (window.MermaidRenderer && window.MermaidRenderer.createMermaidController) {
|
|
599
|
+
if (!window.__mermaidController) {
|
|
600
|
+
window.__mermaidController = window.MermaidRenderer.createMermaidController({
|
|
601
|
+
idPrefix: "lifecycle-" + taskId,
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
window.__mermaidController.update(mermaidDiv, source);
|
|
605
|
+
} else {
|
|
606
|
+
mermaidDiv.textContent = source;
|
|
607
|
+
}
|
|
608
|
+
});
|
|
609
|
+
})();
|
|
610
|
+
|
|
107
611
|
/**
|
|
108
612
|
* Initialize a live timeline on the per-task page (when `data-task-id` is
|
|
109
613
|
* present). We re-render the timeline on every `tool.recorded` event.
|
|
@@ -142,6 +646,15 @@
|
|
|
142
646
|
}
|
|
143
647
|
|
|
144
648
|
function applyTaskUpdate(session) {
|
|
649
|
+
// Expose the latest session for the Mermaid re-render hook
|
|
650
|
+
// (task-show:lifecycle-ops-loaded). The hook can use this to
|
|
651
|
+
// re-build the diagram with both the latest tool calls and the
|
|
652
|
+
// latest operations.
|
|
653
|
+
if (window.__pipelineController) {
|
|
654
|
+
window.__pipelineController.lastSession = session;
|
|
655
|
+
} else {
|
|
656
|
+
window.__pipelineController = { lastSession: session };
|
|
657
|
+
}
|
|
145
658
|
// Update status badge.
|
|
146
659
|
const badge = document.querySelector(".badge");
|
|
147
660
|
if (badge) {
|
|
@@ -187,7 +700,12 @@
|
|
|
187
700
|
// Re-render mermaid diagram.
|
|
188
701
|
const mermaidDiv = document.querySelector(".mermaid");
|
|
189
702
|
if (mermaidDiv) {
|
|
190
|
-
|
|
703
|
+
// v0.8.0+: prefer the latest operations fetched by
|
|
704
|
+
// task-operations.js (window.__lifecycleOps) so the Mermaid shows
|
|
705
|
+
// the hierarchical view. Falls back to the linear chain when no
|
|
706
|
+
// operations are loaded yet.
|
|
707
|
+
const ops = (window.__lifecycleOps && window.__lifecycleOps.operations) || null;
|
|
708
|
+
const source = buildMermaidSource(session, ops);
|
|
191
709
|
// The controller handles mermaid availability, async race protection,
|
|
192
710
|
// error fallback, and idempotency. If Mermaid hasn't loaded yet
|
|
193
711
|
// (window.MermaidRenderer is undefined), we fall back to writing the
|
|
@@ -250,15 +768,27 @@
|
|
|
250
768
|
: (call.outputPreview.slice(0, 240) + (call.outputPreview.length > 240 ? "…" : ""));
|
|
251
769
|
const status = call.success ? "ok" : "fail";
|
|
252
770
|
const attach = call.hasAttachment ? ' <span class="attach">📎</span>' : "";
|
|
771
|
+
// v0.8.0: `data-tool-id` lets the Mermaid `__toolClick` callback
|
|
772
|
+
// scroll-into-view + highlight the right row. Default state is
|
|
773
|
+
// `collapsed` (input/output `<details>` are closed) so long-running
|
|
774
|
+
// tasks don't drown the page.
|
|
253
775
|
return (
|
|
254
|
-
`<tr class="row-${status}" data-args="${escapeHtmlAttr(argsJson)}">` +
|
|
776
|
+
`<tr class="tool-row row-${status} collapsed" data-tool-id="${call.sequence}" data-args="${escapeHtmlAttr(argsJson)}">` +
|
|
255
777
|
`<td class="seq">${call.sequence}</td>` +
|
|
256
778
|
`<td><code>${escapeHtml(call.toolName)}</code>${attach}</td>` +
|
|
257
779
|
`<td class="status status-${status}">${status}</td>` +
|
|
258
780
|
`<td>${call.durationMs}</td>` +
|
|
259
781
|
`<td>${escapeHtml(formatTs(call.timestamp))}</td>` +
|
|
260
|
-
`<td class="args"
|
|
261
|
-
|
|
782
|
+
`<td class="args-cell">` +
|
|
783
|
+
`<details class="tool-detail"><summary>show args</summary>` +
|
|
784
|
+
`<pre class="json">${escapeHtml(argsJson)}</pre>` +
|
|
785
|
+
`</details>` +
|
|
786
|
+
`</td>` +
|
|
787
|
+
`<td class="result-cell">` +
|
|
788
|
+
`<details class="tool-detail"><summary>show result</summary>` +
|
|
789
|
+
`<pre class="json">${escapeHtml(preview)}</pre>` +
|
|
790
|
+
`</details>` +
|
|
791
|
+
`</td>` +
|
|
262
792
|
`</tr>`
|
|
263
793
|
);
|
|
264
794
|
}
|
|
@@ -276,32 +806,6 @@
|
|
|
276
806
|
return out;
|
|
277
807
|
}
|
|
278
808
|
|
|
279
|
-
function buildMermaidSource(session) {
|
|
280
|
-
const lines = [`flowchart TD`];
|
|
281
|
-
lines.push(` classDef ok fill:#dcfce7,stroke:#16a34a,color:#064e3b`);
|
|
282
|
-
lines.push(` classDef fail fill:#fee2e2,stroke:#dc2626,color:#7f1d1d`);
|
|
283
|
-
lines.push(` classDef runtime fill:#dbeafe,stroke:#2563eb,color:#1e3a8a`);
|
|
284
|
-
lines.push(` Start([Task #${session.taskId} start]):::runtime`);
|
|
285
|
-
let prev = "Start";
|
|
286
|
-
if (session.toolCalls.length === 0) {
|
|
287
|
-
lines.push(` End([end]):::runtime`);
|
|
288
|
-
lines.push(` Start --> End`);
|
|
289
|
-
return lines.join("\n");
|
|
290
|
-
}
|
|
291
|
-
session.toolCalls.forEach((call) => {
|
|
292
|
-
const nodeId = `n${call.sequence}`;
|
|
293
|
-
const label = `${call.sequence}. ${escapeMermaid(call.toolName)}${call.hasAttachment ? " 📎" : ""}`;
|
|
294
|
-
lines.push(` ${nodeId}["${label}<br/><small>${call.durationMs}ms · ${call.success ? "ok" : "FAIL"}</small>"]`);
|
|
295
|
-
lines.push(` class ${nodeId} ${call.success ? "ok" : "fail"}`);
|
|
296
|
-
lines.push(` ${prev} --> ${nodeId}`);
|
|
297
|
-
prev = nodeId;
|
|
298
|
-
});
|
|
299
|
-
const endLabel = session.status === "failed" ? `❌ Task ${session.status}` : `✅ Task ${session.status}`;
|
|
300
|
-
lines.push(` End([${endLabel}]):::runtime`);
|
|
301
|
-
lines.push(` ${prev} --> End`);
|
|
302
|
-
return lines.join("\n");
|
|
303
|
-
}
|
|
304
|
-
|
|
305
809
|
function formatTs(ms) {
|
|
306
810
|
try {
|
|
307
811
|
return new Date(ms).toISOString().replace("T", " ").slice(0, 19);
|
|
@@ -323,10 +827,6 @@
|
|
|
323
827
|
return clone;
|
|
324
828
|
}
|
|
325
829
|
|
|
326
|
-
function escapeMermaid(s) {
|
|
327
|
-
return String(s).replace(/[<>"#]/g, "").replace(/[^A-Za-z0-9_.\-]/g, "_");
|
|
328
|
-
}
|
|
329
|
-
|
|
330
830
|
function escapeHtml(s) {
|
|
331
831
|
return String(s)
|
|
332
832
|
.replace(/&/g, "&")
|