@rk0429/agentic-relay 19.9.0 → 19.9.2
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/interfaces/mcp/relay-mcp-server.d.ts +10 -1
- package/dist/interfaces/mcp/relay-mcp-server.js +119 -5
- package/dist/interfaces/mcp/relay-mcp-server.js.map +1 -1
- package/dist/interfaces/vscode/extension.js +59 -34
- package/dist/interfaces/vscode/extension.js.map +1 -1
- package/dist/interfaces/vscode/view-model.d.ts +9 -2
- package/dist/interfaces/vscode/view-model.js +792 -54
- package/dist/interfaces/vscode/view-model.js.map +1 -1
- package/package.json +1 -1
|
@@ -90,7 +90,7 @@ export function taskTooltip(node) {
|
|
|
90
90
|
return lines.join("\n");
|
|
91
91
|
}
|
|
92
92
|
export function activeAgentLabel(agent) {
|
|
93
|
-
return agent.agent_id;
|
|
93
|
+
return agent.task_title ?? agent.role ?? agent.agent_id;
|
|
94
94
|
}
|
|
95
95
|
export function describeActiveAgent(agent) {
|
|
96
96
|
return [
|
|
@@ -115,50 +115,465 @@ export function activeAgentTooltip(agent) {
|
|
|
115
115
|
`started_at: ${agent.started_at}`,
|
|
116
116
|
].join("\n");
|
|
117
117
|
}
|
|
118
|
-
export function
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
"",
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
118
|
+
export function buildAgentDetailHtml(agent) {
|
|
119
|
+
const label = activeAgentLabel(agent);
|
|
120
|
+
return buildDetailHtml({
|
|
121
|
+
kind: "Agent",
|
|
122
|
+
title: label,
|
|
123
|
+
subtitle: label === agent.agent_id ? undefined : agent.agent_id,
|
|
124
|
+
status: agent.status,
|
|
125
|
+
sections: [
|
|
126
|
+
{
|
|
127
|
+
title: "Metadata",
|
|
128
|
+
rows: [
|
|
129
|
+
detailRow("Agent ID", renderCode(agent.agent_id)),
|
|
130
|
+
detailRow("Session ID", renderCode(agent.relay_session_id)),
|
|
131
|
+
detailRow("Backend", renderText(agent.backend ?? "-")),
|
|
132
|
+
detailRow("Role", renderText(agent.role ?? "-")),
|
|
133
|
+
detailRow("Task", formatTaskReference(agent.task_title, agent.task_id)),
|
|
134
|
+
detailRow("Started", renderCode(agent.started_at)),
|
|
135
|
+
detailRow("Duration", renderText(formatDuration(agent.running_for_seconds))),
|
|
136
|
+
detailRow("Parent", agent.parent_relay_session_id
|
|
137
|
+
? renderCode(agent.parent_relay_session_id)
|
|
138
|
+
: renderMuted("-")),
|
|
139
|
+
],
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
});
|
|
137
143
|
}
|
|
138
|
-
export function
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
144
|
+
export function buildTaskDetailHtml(node) {
|
|
145
|
+
return buildDetailHtml({
|
|
146
|
+
kind: "Task",
|
|
147
|
+
title: node.title,
|
|
148
|
+
status: node.status,
|
|
149
|
+
sections: [
|
|
150
|
+
{
|
|
151
|
+
title: "Metadata",
|
|
152
|
+
rows: [
|
|
153
|
+
detailRow("Task ID", renderCode(node.task_id)),
|
|
154
|
+
detailRow("Status", renderText(node.status)),
|
|
155
|
+
detailRow("Agent", node.agent_id ? renderCode(node.agent_id) : renderMuted("-")),
|
|
156
|
+
detailRow("Session", node.current_relay_session_id
|
|
157
|
+
? renderCode(node.current_relay_session_id)
|
|
158
|
+
: renderMuted("-")),
|
|
159
|
+
detailRow("Dependencies", renderCodeList(node.depends_on)),
|
|
160
|
+
detailRow("Blocked", node.blocked_reason
|
|
161
|
+
? renderText(`${node.blocked_kind ?? "unknown"}: ${node.blocked_reason}`)
|
|
162
|
+
: renderMuted("-")),
|
|
163
|
+
detailRow("Warnings", renderTextList(node.warnings)),
|
|
164
|
+
],
|
|
165
|
+
},
|
|
166
|
+
...(node.children.length > 0
|
|
167
|
+
? [
|
|
168
|
+
{
|
|
169
|
+
title: "Children",
|
|
170
|
+
rows: node.children.map((child) => detailRow(child.title, `${renderStatusBadge(child.status)} ${renderCode(child.task_id)}`)),
|
|
171
|
+
},
|
|
172
|
+
]
|
|
173
|
+
: []),
|
|
174
|
+
],
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
export function buildRelayWebviewHtml(data) {
|
|
178
|
+
const nodeWidth = 220;
|
|
179
|
+
const nodeHeight = 76;
|
|
180
|
+
const layerGap = 140;
|
|
181
|
+
const columnGap = 260;
|
|
182
|
+
const paddingX = 48;
|
|
183
|
+
const paddingY = 40;
|
|
184
|
+
const graphNodes = data.selectedKind === "agent"
|
|
185
|
+
? [...data.snapshot.active_agents]
|
|
186
|
+
.sort((left, right) => left.started_at.localeCompare(right.started_at))
|
|
187
|
+
.map((agent) => ({
|
|
188
|
+
id: agent.relay_session_id,
|
|
189
|
+
label: activeAgentLabel(agent),
|
|
190
|
+
status: agent.status,
|
|
191
|
+
detailHtml: extractBodyContent(buildAgentDetailHtml(agent)),
|
|
192
|
+
sortKey: `${agent.started_at}:${activeAgentLabel(agent)}`,
|
|
193
|
+
}))
|
|
194
|
+
: flattenTaskTree(data.snapshot.task_tree).map((node) => ({
|
|
195
|
+
id: node.task_id,
|
|
196
|
+
label: node.title,
|
|
197
|
+
status: node.status,
|
|
198
|
+
detailHtml: extractBodyContent(buildTaskDetailHtml(node)),
|
|
199
|
+
sortKey: node.title,
|
|
200
|
+
}));
|
|
201
|
+
const nodeIds = new Set(graphNodes.map((node) => node.id));
|
|
202
|
+
const graphEdges = data.selectedKind === "agent"
|
|
203
|
+
? data.snapshot.active_agents
|
|
204
|
+
.filter((agent) => agent.parent_relay_session_id &&
|
|
205
|
+
nodeIds.has(agent.parent_relay_session_id))
|
|
206
|
+
.map((agent) => ({
|
|
207
|
+
from: agent.parent_relay_session_id,
|
|
208
|
+
to: agent.relay_session_id,
|
|
209
|
+
kind: "hierarchy",
|
|
210
|
+
}))
|
|
211
|
+
: flattenTaskTree(data.snapshot.task_tree).flatMap((node) => {
|
|
212
|
+
const edges = [];
|
|
213
|
+
if (node.parent_id && nodeIds.has(node.parent_id)) {
|
|
214
|
+
edges.push({
|
|
215
|
+
from: node.parent_id,
|
|
216
|
+
to: node.task_id,
|
|
217
|
+
kind: "hierarchy",
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
for (const dependencyId of node.depends_on) {
|
|
221
|
+
if (!nodeIds.has(dependencyId)) {
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
edges.push({
|
|
225
|
+
from: dependencyId,
|
|
226
|
+
to: node.task_id,
|
|
227
|
+
kind: "dependency",
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
return edges;
|
|
231
|
+
});
|
|
232
|
+
const incomingCounts = new Map(graphNodes.map((node) => [node.id, 0]));
|
|
233
|
+
const outgoing = new Map();
|
|
234
|
+
for (const edge of graphEdges) {
|
|
235
|
+
incomingCounts.set(edge.to, (incomingCounts.get(edge.to) ?? 0) + 1);
|
|
236
|
+
const siblings = outgoing.get(edge.from) ?? [];
|
|
237
|
+
siblings.push(edge.to);
|
|
238
|
+
outgoing.set(edge.from, siblings);
|
|
239
|
+
}
|
|
240
|
+
const layerById = new Map(graphNodes.map((node) => [node.id, 0]));
|
|
241
|
+
const queue = graphNodes
|
|
242
|
+
.filter((node) => (incomingCounts.get(node.id) ?? 0) === 0)
|
|
243
|
+
.sort((left, right) => left.sortKey.localeCompare(right.sortKey))
|
|
244
|
+
.map((node) => node.id);
|
|
245
|
+
const processed = new Set();
|
|
246
|
+
const remainingIncoming = new Map(incomingCounts);
|
|
247
|
+
while (queue.length > 0) {
|
|
248
|
+
const currentId = queue.shift();
|
|
249
|
+
if (!currentId || processed.has(currentId)) {
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
processed.add(currentId);
|
|
253
|
+
const nextLayer = (layerById.get(currentId) ?? 0) + 1;
|
|
254
|
+
for (const targetId of outgoing.get(currentId) ?? []) {
|
|
255
|
+
layerById.set(targetId, Math.max(layerById.get(targetId) ?? 0, nextLayer));
|
|
256
|
+
remainingIncoming.set(targetId, (remainingIncoming.get(targetId) ?? 1) - 1);
|
|
257
|
+
if ((remainingIncoming.get(targetId) ?? 0) <= 0) {
|
|
258
|
+
queue.push(targetId);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
for (const node of graphNodes) {
|
|
263
|
+
if (processed.has(node.id)) {
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
layerById.set(node.id, layerById.get(node.id) ?? 0);
|
|
160
267
|
}
|
|
161
|
-
|
|
268
|
+
const layers = new Map();
|
|
269
|
+
for (const node of graphNodes) {
|
|
270
|
+
const layer = layerById.get(node.id) ?? 0;
|
|
271
|
+
const siblings = layers.get(layer) ?? [];
|
|
272
|
+
siblings.push(node);
|
|
273
|
+
layers.set(layer, siblings);
|
|
274
|
+
}
|
|
275
|
+
const sortedLayers = [...layers.entries()]
|
|
276
|
+
.sort(([left], [right]) => left - right)
|
|
277
|
+
.map(([layer, nodes]) => [
|
|
278
|
+
layer,
|
|
279
|
+
[...nodes].sort((left, right) => left.sortKey.localeCompare(right.sortKey)),
|
|
280
|
+
]);
|
|
281
|
+
const maxLayerNodeCount = Math.max(1, ...sortedLayers.map(([, nodes]) => nodes.length));
|
|
282
|
+
const svgWidth = paddingX * 2 + nodeWidth + columnGap * (maxLayerNodeCount - 1);
|
|
283
|
+
const svgHeight = paddingY * 2 +
|
|
284
|
+
nodeHeight +
|
|
285
|
+
layerGap * Math.max(sortedLayers.length - 1, 0);
|
|
286
|
+
const positionedNodes = sortedLayers.flatMap(([layer, nodes]) => {
|
|
287
|
+
const layerWidth = nodeWidth + columnGap * Math.max(nodes.length - 1, 0);
|
|
288
|
+
const layerOffsetX = (svgWidth - layerWidth) / 2;
|
|
289
|
+
return nodes.map((node, index) => ({
|
|
290
|
+
...node,
|
|
291
|
+
layer,
|
|
292
|
+
x: layerOffsetX + columnGap * index,
|
|
293
|
+
y: paddingY + layerGap * layer,
|
|
294
|
+
}));
|
|
295
|
+
});
|
|
296
|
+
const nodePositions = new Map(positionedNodes.map((node) => [node.id, node]));
|
|
297
|
+
const selectedNode = graphNodes.find((node) => node.id === data.selectedId);
|
|
298
|
+
const initialDetailHtml = selectedNode?.detailHtml ?? extractBodyContent(data.selectedDetailHtml);
|
|
299
|
+
const edgeMarkup = graphEdges
|
|
300
|
+
.map((edge) => {
|
|
301
|
+
const source = nodePositions.get(edge.from);
|
|
302
|
+
const target = nodePositions.get(edge.to);
|
|
303
|
+
if (!source || !target) {
|
|
304
|
+
return "";
|
|
305
|
+
}
|
|
306
|
+
const startX = source.x + nodeWidth / 2;
|
|
307
|
+
const startY = source.y + nodeHeight;
|
|
308
|
+
const endX = target.x + nodeWidth / 2;
|
|
309
|
+
const endY = target.y;
|
|
310
|
+
const midY = startY + (endY - startY) / 2;
|
|
311
|
+
return `
|
|
312
|
+
<path
|
|
313
|
+
class="graph-edge ${edge.kind === "dependency" ? "graph-edge-dependency" : "graph-edge-hierarchy"}"
|
|
314
|
+
d="M ${startX} ${startY} C ${startX} ${midY}, ${endX} ${midY}, ${endX} ${endY}"
|
|
315
|
+
marker-end="url(#relay-arrowhead)"
|
|
316
|
+
/>`;
|
|
317
|
+
})
|
|
318
|
+
.join("");
|
|
319
|
+
const nodeMarkup = positionedNodes
|
|
320
|
+
.map((node) => {
|
|
321
|
+
const palette = graphNodePalette(node.status);
|
|
322
|
+
const lines = wrapGraphLabel(node.label, 22, 3);
|
|
323
|
+
return `
|
|
324
|
+
<g
|
|
325
|
+
class="graph-node${node.id === data.selectedId ? " is-selected" : ""}"
|
|
326
|
+
data-node-id="${escapeHtml(node.id)}"
|
|
327
|
+
data-status="${escapeHtml(node.status)}"
|
|
328
|
+
data-detail-html="${escapeHtml(node.detailHtml)}"
|
|
329
|
+
transform="translate(${node.x}, ${node.y})"
|
|
330
|
+
tabindex="0"
|
|
331
|
+
role="button"
|
|
332
|
+
aria-label="${escapeHtml(node.label)}"
|
|
333
|
+
>
|
|
334
|
+
<rect
|
|
335
|
+
width="${nodeWidth}"
|
|
336
|
+
height="${nodeHeight}"
|
|
337
|
+
rx="8"
|
|
338
|
+
fill="${palette.fill}"
|
|
339
|
+
fill-opacity="0.18"
|
|
340
|
+
stroke="${palette.stroke}"
|
|
341
|
+
/>
|
|
342
|
+
<text x="${nodeWidth / 2}" y="28" text-anchor="middle" fill="${palette.text}">
|
|
343
|
+
${lines
|
|
344
|
+
.map((line, index) => `<tspan x="${nodeWidth / 2}" dy="${index === 0 ? 0 : 18}">${escapeHtml(line)}</tspan>`)
|
|
345
|
+
.join("")}
|
|
346
|
+
</text>
|
|
347
|
+
</g>`;
|
|
348
|
+
})
|
|
349
|
+
.join("");
|
|
350
|
+
const emptyStateMarkup = graphNodes.length === 0
|
|
351
|
+
? `<div class="graph-empty">${data.selectedKind === "agent"
|
|
352
|
+
? "No active agents available."
|
|
353
|
+
: "No tasks available."}</div>`
|
|
354
|
+
: `<svg
|
|
355
|
+
id="relay-graph"
|
|
356
|
+
viewBox="0 0 ${svgWidth} ${svgHeight}"
|
|
357
|
+
role="img"
|
|
358
|
+
aria-label="${data.selectedKind === "agent" ? "Active agent graph" : "Task DAG graph"}"
|
|
359
|
+
preserveAspectRatio="xMidYMin meet"
|
|
360
|
+
>
|
|
361
|
+
<defs>
|
|
362
|
+
<marker
|
|
363
|
+
id="relay-arrowhead"
|
|
364
|
+
markerWidth="12"
|
|
365
|
+
markerHeight="12"
|
|
366
|
+
refX="10"
|
|
367
|
+
refY="6"
|
|
368
|
+
orient="auto"
|
|
369
|
+
markerUnits="strokeWidth"
|
|
370
|
+
>
|
|
371
|
+
<path d="M 0 0 L 12 6 L 0 12 z" fill="var(--vscode-panel-border)" />
|
|
372
|
+
</marker>
|
|
373
|
+
</defs>
|
|
374
|
+
${edgeMarkup}
|
|
375
|
+
${nodeMarkup}
|
|
376
|
+
</svg>`;
|
|
377
|
+
return `<!DOCTYPE html>
|
|
378
|
+
<html lang="en">
|
|
379
|
+
<head>
|
|
380
|
+
<meta charset="UTF-8" />
|
|
381
|
+
<meta
|
|
382
|
+
http-equiv="Content-Security-Policy"
|
|
383
|
+
content="default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline';"
|
|
384
|
+
/>
|
|
385
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
386
|
+
<style>
|
|
387
|
+
:root {
|
|
388
|
+
color-scheme: light dark;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
* {
|
|
392
|
+
box-sizing: border-box;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
html,
|
|
396
|
+
body {
|
|
397
|
+
height: 100%;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
body {
|
|
401
|
+
margin: 0;
|
|
402
|
+
background: var(--vscode-editor-background);
|
|
403
|
+
color: var(--vscode-editor-foreground);
|
|
404
|
+
font-family: var(--vscode-font-family);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
.relay-layout {
|
|
408
|
+
display: grid;
|
|
409
|
+
grid-template-rows: minmax(280px, 1fr) minmax(260px, 1fr);
|
|
410
|
+
height: 100vh;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
.graph-pane,
|
|
414
|
+
.detail-pane {
|
|
415
|
+
min-height: 0;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
.graph-pane {
|
|
419
|
+
display: flex;
|
|
420
|
+
flex-direction: column;
|
|
421
|
+
gap: 12px;
|
|
422
|
+
padding: 18px 20px 12px;
|
|
423
|
+
border-bottom: 1px solid var(--vscode-panel-border);
|
|
424
|
+
background:
|
|
425
|
+
linear-gradient(
|
|
426
|
+
180deg,
|
|
427
|
+
color-mix(in srgb, var(--vscode-editor-background) 82%, var(--vscode-sideBar-background)),
|
|
428
|
+
var(--vscode-editor-background)
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
.graph-header {
|
|
433
|
+
display: flex;
|
|
434
|
+
align-items: baseline;
|
|
435
|
+
justify-content: space-between;
|
|
436
|
+
gap: 12px;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
.graph-title {
|
|
440
|
+
margin: 0;
|
|
441
|
+
font-size: 1em;
|
|
442
|
+
font-weight: 700;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
.graph-subtitle {
|
|
446
|
+
color: var(--vscode-descriptionForeground);
|
|
447
|
+
font-size: 0.9em;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
.graph-surface {
|
|
451
|
+
min-height: 0;
|
|
452
|
+
flex: 1;
|
|
453
|
+
overflow: auto;
|
|
454
|
+
border: 1px solid var(--vscode-panel-border);
|
|
455
|
+
border-radius: 16px;
|
|
456
|
+
background:
|
|
457
|
+
radial-gradient(circle at top, rgba(127, 127, 127, 0.1), transparent 55%),
|
|
458
|
+
var(--vscode-sideBar-background);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
#relay-graph {
|
|
462
|
+
display: block;
|
|
463
|
+
min-width: 100%;
|
|
464
|
+
height: auto;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
.graph-edge {
|
|
468
|
+
fill: none;
|
|
469
|
+
stroke: var(--vscode-panel-border);
|
|
470
|
+
stroke-width: 1.7;
|
|
471
|
+
opacity: 0.92;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
.graph-edge-dependency {
|
|
475
|
+
stroke-dasharray: 6 6;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
.graph-node {
|
|
479
|
+
cursor: pointer;
|
|
480
|
+
outline: none;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
.graph-node rect {
|
|
484
|
+
stroke-width: 1.8;
|
|
485
|
+
transition: stroke-width 120ms ease, fill-opacity 120ms ease;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
.graph-node text {
|
|
489
|
+
font-size: 13px;
|
|
490
|
+
font-weight: 600;
|
|
491
|
+
pointer-events: none;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
.graph-node:hover rect,
|
|
495
|
+
.graph-node:focus rect {
|
|
496
|
+
fill-opacity: 0.24;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
.graph-node.is-selected rect {
|
|
500
|
+
stroke-width: 3.5;
|
|
501
|
+
fill-opacity: 0.28;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
.graph-empty {
|
|
505
|
+
display: grid;
|
|
506
|
+
place-items: center;
|
|
507
|
+
height: 100%;
|
|
508
|
+
color: var(--vscode-descriptionForeground);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
.detail-pane {
|
|
512
|
+
overflow: auto;
|
|
513
|
+
background: var(--vscode-editor-background);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
@media (max-width: 720px) {
|
|
517
|
+
.relay-layout {
|
|
518
|
+
grid-template-rows: minmax(240px, 320px) minmax(320px, 1fr);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
.graph-pane {
|
|
522
|
+
padding: 14px;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
</style>
|
|
526
|
+
</head>
|
|
527
|
+
<body>
|
|
528
|
+
<div class="relay-layout">
|
|
529
|
+
<section class="graph-pane">
|
|
530
|
+
<div class="graph-header">
|
|
531
|
+
<h1 class="graph-title">${data.selectedKind === "agent" ? "Active Agent Graph" : "Task DAG"}</h1>
|
|
532
|
+
<div class="graph-subtitle">Click a node to update the detail panel.</div>
|
|
533
|
+
</div>
|
|
534
|
+
<div class="graph-surface">${emptyStateMarkup}</div>
|
|
535
|
+
</section>
|
|
536
|
+
<section class="detail-pane">
|
|
537
|
+
<div id="relay-detail-host"></div>
|
|
538
|
+
</section>
|
|
539
|
+
</div>
|
|
540
|
+
<script>
|
|
541
|
+
const detailHost = document.getElementById("relay-detail-host");
|
|
542
|
+
const detailStyles = ${JSON.stringify(detailDocumentStyles(":host"))};
|
|
543
|
+
const initialDetailHtml = ${JSON.stringify(initialDetailHtml)};
|
|
544
|
+
const graphNodes = Array.from(document.querySelectorAll(".graph-node"));
|
|
545
|
+
const detailRoot = detailHost ? detailHost.attachShadow({ mode: "open" }) : undefined;
|
|
546
|
+
|
|
547
|
+
function renderDetail(detailHtml) {
|
|
548
|
+
if (!detailRoot) {
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
detailRoot.innerHTML = "<style>" + detailStyles + "</style>" + detailHtml;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function selectNode(node) {
|
|
556
|
+
for (const candidate of graphNodes) {
|
|
557
|
+
candidate.classList.toggle("is-selected", candidate === node);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
renderDetail(node.dataset.detailHtml || initialDetailHtml);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
for (const node of graphNodes) {
|
|
564
|
+
node.addEventListener("click", () => selectNode(node));
|
|
565
|
+
node.addEventListener("keydown", (event) => {
|
|
566
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
567
|
+
event.preventDefault();
|
|
568
|
+
selectNode(node);
|
|
569
|
+
}
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
renderDetail(initialDetailHtml);
|
|
574
|
+
</script>
|
|
575
|
+
</body>
|
|
576
|
+
</html>`;
|
|
162
577
|
}
|
|
163
578
|
export function formatDuration(totalSeconds) {
|
|
164
579
|
const hours = Math.floor(totalSeconds / 3600);
|
|
@@ -194,25 +609,348 @@ function truncate(value, maxLength) {
|
|
|
194
609
|
}
|
|
195
610
|
return `${value.slice(0, maxLength - 1)}…`;
|
|
196
611
|
}
|
|
197
|
-
function markdownRow(label, value) {
|
|
198
|
-
return `| ${escapeMarkdownCell(label)} | ${value} |`;
|
|
199
|
-
}
|
|
200
612
|
function formatTaskReference(taskTitle, taskId) {
|
|
201
613
|
if (taskTitle && taskId) {
|
|
202
|
-
return `${
|
|
614
|
+
return `${escapeHtml(taskTitle)} (${renderCode(taskId)})`;
|
|
203
615
|
}
|
|
204
616
|
if (taskTitle) {
|
|
205
|
-
return
|
|
617
|
+
return escapeHtml(taskTitle);
|
|
206
618
|
}
|
|
207
619
|
if (taskId) {
|
|
208
|
-
return
|
|
620
|
+
return renderCode(taskId);
|
|
621
|
+
}
|
|
622
|
+
return renderMuted("-");
|
|
623
|
+
}
|
|
624
|
+
function buildDetailHtml({ kind, title, subtitle, status, sections, }) {
|
|
625
|
+
const escapedTitle = escapeHtml(title);
|
|
626
|
+
const escapedSubtitle = subtitle ? escapeHtml(subtitle) : "";
|
|
627
|
+
const statusClass = statusToClass(status);
|
|
628
|
+
const cards = sections
|
|
629
|
+
.map((section) => `
|
|
630
|
+
<section class="card">
|
|
631
|
+
<h2>${escapeHtml(section.title)}</h2>
|
|
632
|
+
<div class="rows">
|
|
633
|
+
${section.rows
|
|
634
|
+
.map((row) => `
|
|
635
|
+
<div class="row">
|
|
636
|
+
<span class="label">${escapeHtml(row.label)}</span>
|
|
637
|
+
<span class="value">${row.value}</span>
|
|
638
|
+
</div>`)
|
|
639
|
+
.join("")}
|
|
640
|
+
</div>
|
|
641
|
+
</section>`)
|
|
642
|
+
.join("");
|
|
643
|
+
return `<!DOCTYPE html>
|
|
644
|
+
<html lang="en">
|
|
645
|
+
<head>
|
|
646
|
+
<meta charset="UTF-8" />
|
|
647
|
+
<meta
|
|
648
|
+
http-equiv="Content-Security-Policy"
|
|
649
|
+
content="default-src 'none'; style-src 'unsafe-inline';"
|
|
650
|
+
/>
|
|
651
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
652
|
+
<style>
|
|
653
|
+
${detailDocumentStyles()}
|
|
654
|
+
</style>
|
|
655
|
+
</head>
|
|
656
|
+
<body>
|
|
657
|
+
<div class="page">
|
|
658
|
+
<header class="header">
|
|
659
|
+
<div>
|
|
660
|
+
<div class="eyebrow">${kind}</div>
|
|
661
|
+
<h1>${escapedTitle}</h1>
|
|
662
|
+
${subtitle ? `<div class="subtitle">${escapedSubtitle}</div>` : ""}
|
|
663
|
+
</div>
|
|
664
|
+
${renderStatusBadge(status, statusClass)}
|
|
665
|
+
</header>
|
|
666
|
+
${cards}
|
|
667
|
+
</div>
|
|
668
|
+
</body>
|
|
669
|
+
</html>`;
|
|
670
|
+
}
|
|
671
|
+
function detailRow(label, value) {
|
|
672
|
+
return { label, value };
|
|
673
|
+
}
|
|
674
|
+
function renderStatusBadge(status, className = statusToClass(status)) {
|
|
675
|
+
return `<span class="badge badge-${className}">${escapeHtml(status)}</span>`;
|
|
676
|
+
}
|
|
677
|
+
function renderCode(value) {
|
|
678
|
+
return `<code>${escapeHtml(value)}</code>`;
|
|
679
|
+
}
|
|
680
|
+
function renderText(value) {
|
|
681
|
+
return escapeHtml(value);
|
|
682
|
+
}
|
|
683
|
+
function renderMuted(value) {
|
|
684
|
+
return `<span class="muted">${escapeHtml(value)}</span>`;
|
|
685
|
+
}
|
|
686
|
+
function renderCodeList(values) {
|
|
687
|
+
if (values.length === 0) {
|
|
688
|
+
return renderMuted("-");
|
|
209
689
|
}
|
|
210
|
-
return "-"
|
|
690
|
+
return `<span class="inline-list">${values.map((value) => renderCode(value)).join("")}</span>`;
|
|
211
691
|
}
|
|
212
|
-
function
|
|
213
|
-
|
|
692
|
+
function renderTextList(values) {
|
|
693
|
+
if (values.length === 0) {
|
|
694
|
+
return renderMuted("-");
|
|
695
|
+
}
|
|
696
|
+
return `<span class="inline-list">${values
|
|
697
|
+
.map((value) => `<span>${escapeHtml(value)}</span>`)
|
|
698
|
+
.join("")}</span>`;
|
|
699
|
+
}
|
|
700
|
+
function statusToClass(status) {
|
|
701
|
+
return status.toLowerCase().replace(/[^a-z0-9]+/g, "-");
|
|
702
|
+
}
|
|
703
|
+
function detailDocumentStyles(bodySelector = "body") {
|
|
704
|
+
return `
|
|
705
|
+
:root {
|
|
706
|
+
color-scheme: light dark;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
* {
|
|
710
|
+
box-sizing: border-box;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
${bodySelector} {
|
|
714
|
+
margin: 0;
|
|
715
|
+
padding: 24px;
|
|
716
|
+
background: var(--vscode-editor-background);
|
|
717
|
+
color: var(--vscode-editor-foreground);
|
|
718
|
+
font-family: var(--vscode-font-family);
|
|
719
|
+
font-size: var(--vscode-font-size);
|
|
720
|
+
line-height: 1.5;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
.page {
|
|
724
|
+
max-width: 920px;
|
|
725
|
+
margin: 0 auto;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
.header {
|
|
729
|
+
display: flex;
|
|
730
|
+
align-items: flex-start;
|
|
731
|
+
justify-content: space-between;
|
|
732
|
+
gap: 16px;
|
|
733
|
+
margin-bottom: 20px;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
.eyebrow {
|
|
737
|
+
margin-bottom: 6px;
|
|
738
|
+
color: var(--vscode-descriptionForeground);
|
|
739
|
+
font-size: 0.85em;
|
|
740
|
+
font-weight: 600;
|
|
741
|
+
letter-spacing: 0.08em;
|
|
742
|
+
text-transform: uppercase;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
h1 {
|
|
746
|
+
margin: 0;
|
|
747
|
+
font-size: 1.8em;
|
|
748
|
+
line-height: 1.2;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
.subtitle {
|
|
752
|
+
margin-top: 6px;
|
|
753
|
+
color: var(--vscode-descriptionForeground);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
.badge {
|
|
757
|
+
display: inline-flex;
|
|
758
|
+
align-items: center;
|
|
759
|
+
justify-content: center;
|
|
760
|
+
min-width: 96px;
|
|
761
|
+
padding: 6px 12px;
|
|
762
|
+
border-radius: 999px;
|
|
763
|
+
color: var(--vscode-editor-background);
|
|
764
|
+
font-size: 0.85em;
|
|
765
|
+
font-weight: 700;
|
|
766
|
+
text-transform: lowercase;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
.badge-running {
|
|
770
|
+
background: var(--vscode-terminal-ansiGreen);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
.badge-stale {
|
|
774
|
+
background: var(--vscode-terminal-ansiYellow);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
.badge-backlog {
|
|
778
|
+
background: var(--vscode-terminal-ansiMagenta);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
.badge-ready {
|
|
782
|
+
background: var(--vscode-terminal-ansiCyan);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
.badge-in-progress,
|
|
786
|
+
.badge-review {
|
|
787
|
+
background: var(--vscode-terminal-ansiBlue);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
.badge-blocked {
|
|
791
|
+
background: var(--vscode-terminal-ansiRed);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
.badge-done,
|
|
795
|
+
.badge-superseded,
|
|
796
|
+
.badge-archived {
|
|
797
|
+
background: var(--vscode-terminal-ansiBrightBlack);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
.card {
|
|
801
|
+
margin-top: 16px;
|
|
802
|
+
padding: 18px;
|
|
803
|
+
border: 1px solid var(--vscode-panel-border);
|
|
804
|
+
border-radius: 14px;
|
|
805
|
+
background: var(--vscode-sideBar-background);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
h2 {
|
|
809
|
+
margin: 0 0 12px;
|
|
810
|
+
font-size: 1.05em;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
.rows {
|
|
814
|
+
display: flex;
|
|
815
|
+
flex-direction: column;
|
|
816
|
+
gap: 10px;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
.row {
|
|
820
|
+
display: grid;
|
|
821
|
+
grid-template-columns: minmax(120px, 180px) minmax(0, 1fr);
|
|
822
|
+
gap: 12px;
|
|
823
|
+
align-items: start;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
.label {
|
|
827
|
+
color: var(--vscode-descriptionForeground);
|
|
828
|
+
font-weight: 600;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
.value {
|
|
832
|
+
min-width: 0;
|
|
833
|
+
white-space: pre-wrap;
|
|
834
|
+
word-break: break-word;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
.muted {
|
|
838
|
+
color: var(--vscode-descriptionForeground);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
code {
|
|
842
|
+
padding: 0.1em 0.35em;
|
|
843
|
+
border-radius: 6px;
|
|
844
|
+
background: var(--vscode-textCodeBlock-background);
|
|
845
|
+
font-family: var(--vscode-editor-font-family, var(--vscode-font-family));
|
|
846
|
+
font-size: 0.95em;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
.inline-list {
|
|
850
|
+
display: inline-flex;
|
|
851
|
+
flex-wrap: wrap;
|
|
852
|
+
gap: 8px;
|
|
853
|
+
align-items: center;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
.child-entry {
|
|
857
|
+
display: inline-flex;
|
|
858
|
+
flex-wrap: wrap;
|
|
859
|
+
gap: 8px;
|
|
860
|
+
align-items: center;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
@media (max-width: 640px) {
|
|
864
|
+
${bodySelector} {
|
|
865
|
+
padding: 16px;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
.header {
|
|
869
|
+
flex-direction: column;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
.row {
|
|
873
|
+
grid-template-columns: 1fr;
|
|
874
|
+
gap: 4px;
|
|
875
|
+
}
|
|
876
|
+
}`;
|
|
877
|
+
}
|
|
878
|
+
function graphNodePalette(status) {
|
|
879
|
+
switch (status) {
|
|
880
|
+
case "running":
|
|
881
|
+
return palette("var(--vscode-terminal-ansiGreen)");
|
|
882
|
+
case "stale":
|
|
883
|
+
return palette("var(--vscode-terminal-ansiYellow)");
|
|
884
|
+
case "blocked":
|
|
885
|
+
return palette("var(--vscode-terminal-ansiRed)");
|
|
886
|
+
case "in_progress":
|
|
887
|
+
case "review":
|
|
888
|
+
return palette("var(--vscode-terminal-ansiBlue)");
|
|
889
|
+
case "ready":
|
|
890
|
+
return palette("var(--vscode-terminal-ansiCyan)");
|
|
891
|
+
case "backlog":
|
|
892
|
+
return palette("var(--vscode-terminal-ansiMagenta)");
|
|
893
|
+
case "done":
|
|
894
|
+
case "superseded":
|
|
895
|
+
case "archived":
|
|
896
|
+
return palette("var(--vscode-terminal-ansiBrightBlack)");
|
|
897
|
+
default:
|
|
898
|
+
return palette("var(--vscode-panel-border)");
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
function palette(color) {
|
|
902
|
+
return {
|
|
903
|
+
fill: color,
|
|
904
|
+
stroke: color,
|
|
905
|
+
text: "var(--vscode-editor-foreground)",
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
function flattenTaskTree(nodes) {
|
|
909
|
+
return nodes.flatMap((node) => [node, ...flattenTaskTree(node.children)]);
|
|
910
|
+
}
|
|
911
|
+
function extractBodyContent(html) {
|
|
912
|
+
const match = html.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
|
|
913
|
+
return match?.[1]?.trim() ?? html.trim();
|
|
914
|
+
}
|
|
915
|
+
function wrapGraphLabel(label, maxLineLength, maxLines) {
|
|
916
|
+
const normalized = label.trim().replace(/\s+/g, " ");
|
|
917
|
+
if (normalized.length === 0) {
|
|
918
|
+
return [label];
|
|
919
|
+
}
|
|
920
|
+
const words = normalized.split(" ");
|
|
921
|
+
const lines = [];
|
|
922
|
+
let index = 0;
|
|
923
|
+
while (index < words.length && lines.length < maxLines) {
|
|
924
|
+
let line = "";
|
|
925
|
+
while (index < words.length) {
|
|
926
|
+
const word = words[index];
|
|
927
|
+
if (!word) {
|
|
928
|
+
break;
|
|
929
|
+
}
|
|
930
|
+
const candidate = line ? `${line} ${word}` : word;
|
|
931
|
+
if (candidate.length > maxLineLength && line) {
|
|
932
|
+
break;
|
|
933
|
+
}
|
|
934
|
+
line = candidate;
|
|
935
|
+
index += 1;
|
|
936
|
+
if (line.length >= maxLineLength) {
|
|
937
|
+
break;
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
lines.push(line);
|
|
941
|
+
}
|
|
942
|
+
if (index < words.length && lines.length > 0) {
|
|
943
|
+
const remaining = words.slice(index).join(" ");
|
|
944
|
+
lines[lines.length - 1] = `${lines[lines.length - 1]} ${remaining}`.trim();
|
|
945
|
+
}
|
|
946
|
+
return lines.map((line) => truncate(line, maxLineLength));
|
|
214
947
|
}
|
|
215
|
-
function
|
|
216
|
-
return value
|
|
948
|
+
function escapeHtml(value) {
|
|
949
|
+
return value
|
|
950
|
+
.replace(/&/g, "&")
|
|
951
|
+
.replace(/</g, "<")
|
|
952
|
+
.replace(/>/g, ">")
|
|
953
|
+
.replace(/"/g, """)
|
|
954
|
+
.replace(/'/g, "'");
|
|
217
955
|
}
|
|
218
956
|
//# sourceMappingURL=view-model.js.map
|