@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.
@@ -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 buildAgentDetailMarkdown(agent) {
119
- const lines = [
120
- `# Agent: ${agent.agent_id}`,
121
- "",
122
- "| Field | Value |",
123
- "|---|---|",
124
- markdownRow("Agent", code(agent.agent_id)),
125
- markdownRow("Session", code(agent.relay_session_id)),
126
- markdownRow("Status", agent.status),
127
- markdownRow("Backend", agent.backend ?? "-"),
128
- markdownRow("Role", agent.role ?? "-"),
129
- markdownRow("Task", formatTaskReference(agent.task_title, agent.task_id)),
130
- markdownRow("Started", code(agent.started_at)),
131
- markdownRow("Running", formatDuration(agent.running_for_seconds)),
132
- markdownRow("Parent", agent.parent_relay_session_id
133
- ? code(agent.parent_relay_session_id)
134
- : "-"),
135
- ];
136
- return `${lines.join("\n")}\n`;
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 buildTaskDetailMarkdown(node) {
139
- const lines = [
140
- `# Task: ${node.title}`,
141
- "",
142
- "| Field | Value |",
143
- "|---|---|",
144
- markdownRow("Task ID", code(node.task_id)),
145
- markdownRow("Status", node.status),
146
- markdownRow("Agent", node.agent_id ? code(node.agent_id) : "-"),
147
- markdownRow("Current Session", node.current_relay_session_id ? code(node.current_relay_session_id) : "-"),
148
- markdownRow("Dependencies", node.depends_on.length > 0
149
- ? node.depends_on.map((dependency) => code(dependency)).join(", ")
150
- : "-"),
151
- markdownRow("Blocked", node.blocked_reason
152
- ? `${node.blocked_kind ?? "unknown"}: ${escapeMarkdownCell(node.blocked_reason)}`
153
- : "-"),
154
- markdownRow("Warnings", node.warnings.length > 0
155
- ? node.warnings.map((warning) => escapeMarkdownCell(warning)).join("<br/>")
156
- : "-"),
157
- ];
158
- if (node.children.length > 0) {
159
- lines.push("", "## Children", "", ...node.children.map((child) => `- ${escapeMarkdownCell(child.title)} (${code(child.task_id)})`));
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
- return `${lines.join("\n")}\n`;
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 `${escapeMarkdownCell(taskTitle)} (${code(taskId)})`;
614
+ return `${escapeHtml(taskTitle)} (${renderCode(taskId)})`;
203
615
  }
204
616
  if (taskTitle) {
205
- return escapeMarkdownCell(taskTitle);
617
+ return escapeHtml(taskTitle);
206
618
  }
207
619
  if (taskId) {
208
- return code(taskId);
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 code(value) {
213
- return `\`${value.replace(/`/g, "\\`")}\``;
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 escapeMarkdownCell(value) {
216
- return value.replace(/\|/g, "\\|").replace(/\n/g, "<br/>");
948
+ function escapeHtml(value) {
949
+ return value
950
+ .replace(/&/g, "&amp;")
951
+ .replace(/</g, "&lt;")
952
+ .replace(/>/g, "&gt;")
953
+ .replace(/"/g, "&quot;")
954
+ .replace(/'/g, "&#39;");
217
955
  }
218
956
  //# sourceMappingURL=view-model.js.map