pmx-canvas 0.6.4 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/extensions/pmx-canvas/extension.mjs +67 -3
- package/CHANGELOG.md +73 -0
- package/Readme.md +35 -5
- package/dist/canvas/global.css +498 -28
- package/dist/canvas/index.js +110 -95
- package/dist/canvas/mermaid-entry.js +258 -258
- package/dist/types/cli/agent.d.ts +4 -0
- package/dist/types/cli/commands/backup.d.ts +1 -0
- package/dist/types/cli/commands/boards.d.ts +1 -0
- package/dist/types/cli/commands/export.d.ts +1 -0
- package/dist/types/cli/commands/record.d.ts +12 -0
- package/dist/types/client/canvas/BoardCopyDialog.d.ts +5 -0
- package/dist/types/client/canvas/BoardFolderDialog.d.ts +7 -0
- package/dist/types/client/canvas/BoardLinkDialog.d.ts +3 -0
- package/dist/types/client/canvas/BoardSwitcher.d.ts +8 -0
- package/dist/types/client/canvas/ExportDialog.d.ts +7 -0
- package/dist/types/client/canvas/HomeView.d.ts +9 -0
- package/dist/types/client/canvas/Presentation.d.ts +13 -0
- package/dist/types/client/canvas/use-node-drag.d.ts +2 -1
- package/dist/types/client/nodes/AttachmentNode.d.ts +6 -0
- package/dist/types/client/nodes/BoardNode.d.ts +5 -0
- package/dist/types/client/nodes/McpAppNode.d.ts +24 -1
- package/dist/types/client/state/boards-store.d.ts +79 -0
- package/dist/types/client/state/canvas-store.d.ts +2 -0
- package/dist/types/client/state/intent-bridge.d.ts +1 -0
- package/dist/types/client/state/presentation.d.ts +3 -0
- package/dist/types/client/state/session-store.d.ts +7 -0
- package/dist/types/client/types.d.ts +2 -2
- package/dist/types/json-render/catalog.d.ts +3 -3
- package/dist/types/json-render/charts/definitions.d.ts +1 -1
- package/dist/types/json-render/server.d.ts +8 -0
- package/dist/types/mcp/canvas-access.d.ts +10 -3
- package/dist/types/mcp/context-uri-template.d.ts +6 -0
- package/dist/types/server/agent-presence.d.ts +1 -1
- package/dist/types/server/attribution.d.ts +11 -0
- package/dist/types/server/ax-context.d.ts +3 -1
- package/dist/types/server/ax-interaction.d.ts +2 -2
- package/dist/types/server/ax-state.d.ts +13 -3
- package/dist/types/server/backup.d.ts +36 -0
- package/dist/types/server/board-export.d.ts +45 -0
- package/dist/types/server/canvas-db.d.ts +65 -9
- package/dist/types/server/canvas-operations.d.ts +45 -0
- package/dist/types/server/canvas-provenance.d.ts +1 -1
- package/dist/types/server/canvas-schema.d.ts +1 -0
- package/dist/types/server/canvas-state.d.ts +146 -3
- package/dist/types/server/canvas-validation.d.ts +3 -0
- package/dist/types/server/context-brief.d.ts +111 -0
- package/dist/types/server/context-reads.d.ts +55 -0
- package/dist/types/server/document-import.d.ts +66 -0
- package/dist/types/server/html-surface.d.ts +1 -1
- package/dist/types/server/index.d.ts +107 -4
- package/dist/types/server/mutation-history.d.ts +1 -0
- package/dist/types/server/operations/invoker.d.ts +2 -0
- package/dist/types/server/operations/mcp.d.ts +7 -1
- package/dist/types/server/operations/ops/app.d.ts +2 -0
- package/dist/types/server/operations/ops/ax-shared.d.ts +2 -2
- package/dist/types/server/operations/ops/backup.d.ts +2 -0
- package/dist/types/server/operations/ops/boards.d.ts +10 -0
- package/dist/types/server/operations/ops/export.d.ts +2 -0
- package/dist/types/server/operations/ops/imports.d.ts +2 -0
- package/dist/types/server/operations/ops/json-render.d.ts +2 -0
- package/dist/types/server/operations/ops/nodes.d.ts +4 -2
- package/dist/types/server/operations/ops/query.d.ts +2 -0
- package/dist/types/server/operations/registry.d.ts +9 -1
- package/dist/types/server/operations/types.d.ts +1 -1
- package/dist/types/server/server.d.ts +1 -0
- package/dist/types/server/trace-manager.d.ts +7 -0
- package/dist/types/server/web-artifacts.d.ts +3 -0
- package/dist/types/server/webpage-node.d.ts +22 -1
- package/dist/types/server/workbench-auth.d.ts +3 -0
- package/dist/types/shared/boards.d.ts +6 -0
- package/dist/types/shared/tour.d.ts +72 -0
- package/docs/RELEASE.md +5 -0
- package/docs/cli.md +157 -0
- package/docs/http-api.md +277 -6
- package/docs/mcp.md +96 -7
- package/docs/node-types.md +106 -0
- package/docs/plans/plan-010-attributed-ask-answers.md +39 -0
- package/docs/plans/plan-011-context-read-instrumentation.md +54 -0
- package/docs/plans/plan-012-many-boards-stage-1.md +98 -0
- package/docs/plans/plan-013-static-board-export.md +47 -0
- package/docs/plans/plan-014-connected-board-memory.md +140 -0
- package/docs/product-vision-2026-09.md +118 -18
- package/docs/sdk.md +50 -0
- package/package.json +1 -1
- package/skills/pmx-canvas/SKILL.md +62 -5
- package/skills/pmx-canvas/references/full-reference.md +44 -8
- package/skills/pmx-canvas/references/github-copilot-app-adapter.md +3 -0
- package/skills/pmx-canvas/references/installing-pmx-canvas.md +4 -4
- package/skills/pmx-canvas-orchestration/SKILL.md +5 -0
- package/skills/pmx-canvas-testing/SKILL.md +4 -0
- package/src/cli/agent.ts +9 -0
- package/src/cli/commands/ax.ts +13 -0
- package/src/cli/commands/backup.ts +66 -0
- package/src/cli/commands/boards.ts +71 -0
- package/src/cli/commands/export.ts +30 -0
- package/src/cli/commands/nodes.ts +14 -1
- package/src/cli/commands/pins.ts +6 -2
- package/src/cli/commands/pump.ts +3 -0
- package/src/cli/commands/query.ts +33 -7
- package/src/cli/commands/record.ts +218 -0
- package/src/cli/commands/view.ts +7 -5
- package/src/cli/index.ts +21 -9
- package/src/client/App.tsx +28 -4
- package/src/client/canvas/AgentPresenceLayer.tsx +1 -1
- package/src/client/canvas/BoardCopyDialog.tsx +153 -0
- package/src/client/canvas/BoardFolderDialog.tsx +120 -0
- package/src/client/canvas/BoardLinkDialog.tsx +93 -0
- package/src/client/canvas/BoardSwitcher.tsx +149 -0
- package/src/client/canvas/CanvasNode.tsx +8 -0
- package/src/client/canvas/CanvasViewport.tsx +3 -0
- package/src/client/canvas/CommandPalette.tsx +107 -4
- package/src/client/canvas/ContextMenu.tsx +4 -0
- package/src/client/canvas/ExpandedNodeOverlay.tsx +3 -0
- package/src/client/canvas/ExportDialog.tsx +197 -0
- package/src/client/canvas/HomeView.tsx +277 -0
- package/src/client/canvas/IntentLayer.tsx +9 -2
- package/src/client/canvas/Presentation.tsx +108 -0
- package/src/client/canvas/SessionPanel.tsx +28 -1
- package/src/client/canvas/ToolRail.tsx +38 -3
- package/src/client/canvas/TopBar.tsx +49 -7
- package/src/client/canvas/import-files.ts +19 -1
- package/src/client/canvas/kind-colors.ts +1 -0
- package/src/client/canvas/use-node-drag.ts +8 -3
- package/src/client/icons.tsx +2 -0
- package/src/client/mermaid-entry.ts +39 -0
- package/src/client/nodes/AttachmentNode.tsx +148 -0
- package/src/client/nodes/BoardNode.tsx +35 -0
- package/src/client/nodes/FileNode.tsx +6 -0
- package/src/client/nodes/HtmlNode.tsx +12 -13
- package/src/client/nodes/MarkdownNode.tsx +47 -19
- package/src/client/nodes/McpAppNode.tsx +105 -13
- package/src/client/nodes/MermaidNode.tsx +3 -2
- package/src/client/nodes/ax-node-actions.ts +2 -5
- package/src/client/nodes/use-ax-surface-bridge.ts +1 -1
- package/src/client/state/boards-store.ts +167 -0
- package/src/client/state/canvas-store.ts +22 -0
- package/src/client/state/intent-bridge.ts +30 -2
- package/src/client/state/intent-store.ts +5 -1
- package/src/client/state/node-factory.ts +1 -0
- package/src/client/state/presentation.ts +7 -0
- package/src/client/state/session-store.ts +8 -0
- package/src/client/state/sse-bridge.ts +52 -5
- package/src/client/theme/global.css +498 -28
- package/src/client/types.ts +2 -0
- package/src/json-render/server.ts +45 -9
- package/src/mcp/canvas-access.ts +59 -22
- package/src/mcp/context-uri-template.ts +28 -0
- package/src/mcp/server.ts +122 -15
- package/src/server/agent-context.ts +116 -29
- package/src/server/agent-presence.ts +15 -9
- package/src/server/attribution.ts +29 -0
- package/src/server/ax-context.ts +4 -2
- package/src/server/ax-interaction.ts +1 -0
- package/src/server/ax-state-manager.ts +72 -3
- package/src/server/ax-state.ts +41 -0
- package/src/server/backup.ts +178 -0
- package/src/server/board-export.ts +686 -0
- package/src/server/canvas-db.ts +612 -105
- package/src/server/canvas-operations.ts +183 -31
- package/src/server/canvas-provenance.ts +1 -0
- package/src/server/canvas-schema.ts +22 -0
- package/src/server/canvas-state.ts +812 -48
- package/src/server/canvas-validation.ts +9 -1
- package/src/server/context-brief.ts +372 -0
- package/src/server/context-reads.ts +238 -0
- package/src/server/demo-state.json +670 -9
- package/src/server/document-import.ts +223 -0
- package/src/server/html-primitives.ts +199 -91
- package/src/server/html-surface.ts +8 -3
- package/src/server/index.ts +243 -5
- package/src/server/mutation-history.ts +10 -13
- package/src/server/operations/composites.ts +29 -5
- package/src/server/operations/http.ts +9 -3
- package/src/server/operations/index.ts +8 -0
- package/src/server/operations/invoker.ts +14 -3
- package/src/server/operations/mcp.ts +50 -11
- package/src/server/operations/ops/app.ts +15 -0
- package/src/server/operations/ops/ax-read.ts +10 -6
- package/src/server/operations/ops/ax-timeline.ts +75 -0
- package/src/server/operations/ops/backup.ts +134 -0
- package/src/server/operations/ops/batch.ts +20 -4
- package/src/server/operations/ops/boards.ts +351 -0
- package/src/server/operations/ops/export.ts +74 -0
- package/src/server/operations/ops/ext-app.ts +4 -0
- package/src/server/operations/ops/groups.ts +14 -7
- package/src/server/operations/ops/imports.ts +220 -0
- package/src/server/operations/ops/json-render.ts +37 -5
- package/src/server/operations/ops/nodes.ts +99 -21
- package/src/server/operations/ops/query.ts +206 -9
- package/src/server/operations/ops/viewport.ts +32 -0
- package/src/server/operations/registry.ts +129 -31
- package/src/server/operations/types.ts +1 -1
- package/src/server/scope-fence.ts +1 -1
- package/src/server/server.ts +155 -8
- package/src/server/spatial-analysis.ts +9 -17
- package/src/server/trace-manager.ts +19 -0
- package/src/server/web-artifacts.ts +11 -3
- package/src/server/webpage-node.ts +173 -13
- package/src/server/workbench-auth.ts +11 -0
- package/src/server/workboard.ts +3 -3
- package/src/shared/boards.ts +18 -0
- package/src/shared/node-sizes.ts +1 -0
- package/src/shared/tour.ts +87 -0
|
@@ -497,7 +497,7 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
497
497
|
function text(value: unknown, fallback = ''): string {
|
|
498
498
|
if (typeof value === 'string') return value;
|
|
499
499
|
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
|
|
500
|
-
return fallback;
|
|
500
|
+
return value === undefined ? fallback : '';
|
|
501
501
|
}
|
|
502
502
|
|
|
503
503
|
function records(value: unknown): Record<string, unknown>[] {
|
|
@@ -514,8 +514,9 @@ function fieldRecords(
|
|
|
514
514
|
key: string,
|
|
515
515
|
fallback: Record<string, unknown>[],
|
|
516
516
|
): Record<string, unknown>[] {
|
|
517
|
-
|
|
518
|
-
|
|
517
|
+
// Document rule: examples fill an omitted field only. An explicitly present
|
|
518
|
+
// empty (or invalid) value is caller intent and must not resurrect examples.
|
|
519
|
+
return Object.hasOwn(data, key) ? records(data[key]) : fallback;
|
|
519
520
|
}
|
|
520
521
|
|
|
521
522
|
const DEFAULT_DECK_SLIDES: Record<string, unknown>[] = [
|
|
@@ -573,8 +574,22 @@ function enrichPresentationData(kind: HtmlPrimitiveKind, data: Record<string, un
|
|
|
573
574
|
}
|
|
574
575
|
|
|
575
576
|
function fieldStrings(data: Record<string, unknown>, key: string, fallback: string[]): string[] {
|
|
576
|
-
|
|
577
|
-
|
|
577
|
+
return Object.hasOwn(data, key) ? strings(data[key]) : fallback;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/** Omitted fields may use examples; explicitly empty fields omit their entire UI section. */
|
|
581
|
+
function fieldSection(data: Record<string, unknown>, keys: string | string[], markup: string): string {
|
|
582
|
+
const empty = (key: string) => {
|
|
583
|
+
if (!Object.hasOwn(data, key)) return false;
|
|
584
|
+
const value = data[key];
|
|
585
|
+
return (
|
|
586
|
+
value == null ||
|
|
587
|
+
(typeof value === 'string' && !value.trim()) ||
|
|
588
|
+
(Array.isArray(value) && value.length === 0) ||
|
|
589
|
+
(isRecord(value) && Object.keys(value).length === 0)
|
|
590
|
+
);
|
|
591
|
+
};
|
|
592
|
+
return (Array.isArray(keys) ? keys : [keys]).every(empty) ? '' : markup;
|
|
578
593
|
}
|
|
579
594
|
|
|
580
595
|
function escapeHtml(value: string): string {
|
|
@@ -910,19 +925,22 @@ function renderChoiceGrid({ title, data, descriptor }: Parameters<PrimitiveRende
|
|
|
910
925
|
cons: ['More maintenance'],
|
|
911
926
|
},
|
|
912
927
|
]);
|
|
913
|
-
const body =
|
|
914
|
-
.
|
|
915
|
-
|
|
928
|
+
const body =
|
|
929
|
+
items.length > 0
|
|
930
|
+
? `<section class="grid">${items
|
|
931
|
+
.map(
|
|
932
|
+
(item, index) => `
|
|
916
933
|
<article class="card ${index === 0 ? 'emphasis' : ''}">
|
|
917
934
|
<div class="small">Choice ${index + 1}</div>
|
|
918
935
|
<h2>${escapeHtml(itemTitle(item, `Option ${index + 1}`))}</h2>
|
|
919
|
-
|
|
936
|
+
${fieldSection(item, 'summary', `<p>${escapeHtml(text(item.summary, 'Summarize the approach here.'))}</p>`)}
|
|
920
937
|
${text(item.tradeoff) ? `<p><strong>Tradeoff:</strong> ${escapeHtml(text(item.tradeoff))}</p>` : ''}
|
|
921
|
-
|
|
938
|
+
${fieldSection(item, ['pros', 'cons'], `<div class="two">${fieldSection(item, 'pros', `<div><h3>Pros</h3>${list(strings(item.pros))}</div>`)}${fieldSection(item, 'cons', `<div><h3>Cons</h3>${list(strings(item.cons))}</div>`)}</div>`)}
|
|
922
939
|
${codeBlock(item.code)}
|
|
923
940
|
</article>`,
|
|
924
|
-
|
|
925
|
-
|
|
941
|
+
)
|
|
942
|
+
.join('')}</section>`
|
|
943
|
+
: '';
|
|
926
944
|
return page({ title, kind: 'choice-grid', summary: descriptor.description, data, body });
|
|
927
945
|
}
|
|
928
946
|
|
|
@@ -942,13 +960,20 @@ function renderPlanTimeline({ title, data, descriptor }: Parameters<PrimitiveRen
|
|
|
942
960
|
{ risk: 'Overly generic output', mitigation: 'Use named primitives with clear use cases.' },
|
|
943
961
|
]);
|
|
944
962
|
const snippets = fieldRecords(data, 'snippets', []);
|
|
945
|
-
const
|
|
946
|
-
.
|
|
947
|
-
|
|
963
|
+
const milestonesSection =
|
|
964
|
+
milestones.length > 0
|
|
965
|
+
? `<div class="panel"><h2>Milestones</h2><div class="timeline">${milestones
|
|
966
|
+
.map(
|
|
967
|
+
(item, index) => `
|
|
948
968
|
<div class="step"><div class="dot">${index + 1}</div><div class="card"><h3>${escapeHtml(itemTitle(item, `Milestone ${index + 1}`))} ${badge(text(item.status, 'planned'))}</h3><p>${escapeHtml(text(item.detail, 'Add implementation detail.'))}</p></div></div>`,
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
969
|
+
)
|
|
970
|
+
.join('')}</div></div>`
|
|
971
|
+
: '';
|
|
972
|
+
const flowAndRisksSection =
|
|
973
|
+
flow.length > 0 || risks.length > 0
|
|
974
|
+
? `<div class="panel">${flow.length > 0 ? `<h2>Data Flow</h2><div class="flow">${flow.map((item, index) => `<div class="flow-node"><h3>${escapeHtml(text(item.from, `Step ${index + 1}`))}</h3><p>${escapeHtml(text(item.label, 'flows to'))}</p><strong>${escapeHtml(text(item.to, 'Next'))}</strong></div>${index < flow.length - 1 ? '<div class="arrow">-></div>' : ''}`).join('')}</div>` : ''}${risks.length > 0 ? `<h2 style="margin-top:18px">Risks</h2>${risks.map((item) => `<div class="card"><strong>${escapeHtml(text(item.risk, 'Risk'))}</strong><p>${escapeHtml(text(item.mitigation, 'Mitigation'))}</p></div>`).join('')}` : ''}</div>`
|
|
975
|
+
: '';
|
|
976
|
+
const body = `${milestonesSection || flowAndRisksSection ? `<section class="two">${milestonesSection}${flowAndRisksSection}</section>` : ''}
|
|
952
977
|
${snippets.length > 0 ? `<section class="panel" style="margin-top:14px"><h2>Code Checkpoints</h2>${snippets.map((item) => `<h3>${escapeHtml(itemTitle(item, 'Snippet'))}</h3>${codeBlock(item.code)}`).join('')}</section>` : ''}`;
|
|
953
978
|
return page({ title, kind: 'plan-timeline', summary: descriptor.description, data, body });
|
|
954
979
|
}
|
|
@@ -965,13 +990,21 @@ function renderReviewSheet({ title, data, descriptor }: Parameters<PrimitiveRend
|
|
|
965
990
|
]);
|
|
966
991
|
const files = fieldRecords(data, 'files', []);
|
|
967
992
|
const diff = text(data.diff);
|
|
968
|
-
const body =
|
|
969
|
-
|
|
970
|
-
|
|
993
|
+
const body = `${fieldSection(
|
|
994
|
+
data,
|
|
995
|
+
['findings', 'files'],
|
|
996
|
+
`<section class="two">${fieldSection(
|
|
997
|
+
data,
|
|
998
|
+
'findings',
|
|
999
|
+
`<div class="panel"><h2>Findings</h2>${findings
|
|
1000
|
+
.map(
|
|
1001
|
+
(item) => `
|
|
971
1002
|
<article class="card"><h3>${badge(text(item.severity, 'info'))} ${escapeHtml(itemTitle(item, 'Finding'))}</h3><p class="small">${escapeHtml([text(item.file), text(item.line)].filter(Boolean).join(':'))}</p><p>${escapeHtml(text(item.detail, 'Add review note.'))}</p></article>`,
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
1003
|
+
)
|
|
1004
|
+
.join('')}</div>`,
|
|
1005
|
+
)}
|
|
1006
|
+
${fieldSection(data, 'files', `<div class="panel"><h2>Review Tour</h2>${files.length > 0 ? files.map((item) => `<article class="card"><h3>${escapeHtml(text(item.path, 'File'))}</h3><p>${escapeHtml(text(item.why, 'Why this file matters.'))}</p></article>`).join('') : '<p class="muted">Add files with path and why fields for a guided review.</p>'}</div>`)}</section>`,
|
|
1007
|
+
)}
|
|
975
1008
|
${diff ? `<section class="panel" style="margin-top:14px"><h2>Diff Excerpt</h2>${codeBlock(diff)}</section>` : ''}`;
|
|
976
1009
|
return page({ title, kind: 'review-sheet', summary: descriptor.description, data, body });
|
|
977
1010
|
}
|
|
@@ -980,11 +1013,15 @@ function renderPrWriteup({ title, data, descriptor }: Parameters<PrimitiveRender
|
|
|
980
1013
|
const files = fieldRecords(data, 'files', [
|
|
981
1014
|
{ path: 'src/example.ts', why: 'Core behavior changed here.', focus: 'Review edge cases and tests.' },
|
|
982
1015
|
]);
|
|
983
|
-
const body = `<section class="panel"
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
1016
|
+
const body = `${fieldSection(data, ['summary', 'why'], `<section class="panel">${fieldSection(data, 'summary', `<h2>Summary</h2><p>${escapeHtml(text(data.summary, 'Summarize the change in one reviewer-friendly paragraph.'))}</p>`)}${fieldSection(data, 'why', `<p>${escapeHtml(text(data.why, 'Explain why this change matters now.'))}</p>`)}</section>`)}
|
|
1017
|
+
${fieldSection(data, ['before', 'after'], `<section class="two" style="margin-top:14px">${fieldSection(data, 'before', `<div class="card"><h2>Before</h2>${list(fieldStrings(data, 'before', ['Current behavior or pain point.']))}</div>`)}${fieldSection(data, 'after', `<div class="card emphasis"><h2>After</h2>${list(fieldStrings(data, 'after', ['New behavior or reviewer-visible outcome.']))}</div>`)}</section>`)}
|
|
1018
|
+
${fieldSection(
|
|
1019
|
+
data,
|
|
1020
|
+
['files', 'reviewFocus', 'tests', 'rollout'],
|
|
1021
|
+
`<section class="three" style="margin-top:14px">${fieldSection(data, 'files', `<div class="panel"><h2>File Tour</h2>${files.map((file) => `<details open><summary><strong>${escapeHtml(text(file.path, 'File'))}</strong></summary><p>${escapeHtml(text(file.why, 'Why this file matters.'))}</p>${fieldSection(file, 'focus', `<p class="small">Focus: ${escapeHtml(text(file.focus, 'Review behavior and tests.'))}</p>`)}</details>`).join('')}</div>`)}
|
|
1022
|
+
${fieldSection(data, ['reviewFocus', 'tests'], `<div class="panel">${fieldSection(data, 'reviewFocus', `<h2>Review Focus</h2>${list(fieldStrings(data, 'reviewFocus', ['Correctness of changed behavior.', 'Missing regression coverage.']))}`)}${fieldSection(data, 'tests', `<h2 style="margin-top:18px">Tests</h2>${list(fieldStrings(data, 'tests', ['Add or run targeted tests.']))}`)}</div>`)}
|
|
1023
|
+
${fieldSection(data, 'rollout', `<div class="panel sticky"><h2>Rollout</h2>${list(fieldStrings(data, 'rollout', ['Merge behind normal release flow.']))}<button type="button" data-copy-markdown style="margin-top:12px">Copy PR markdown</button></div>`)}</section>`,
|
|
1024
|
+
)}`;
|
|
988
1025
|
return page({
|
|
989
1026
|
title,
|
|
990
1027
|
kind: 'pr-writeup',
|
|
@@ -1020,9 +1057,9 @@ function renderSystemMap({ title, data, descriptor }: Parameters<PrimitiveRender
|
|
|
1020
1057
|
{ from: 'state', to: 'browser', label: 'SSE update' },
|
|
1021
1058
|
]);
|
|
1022
1059
|
const entryPoints = fieldStrings(data, 'entryPoints', ['MCP tools', 'CLI commands', 'HTTP API']);
|
|
1023
|
-
const body = `<section class="panel"><h2>Entry Points</h2><div class="swatches">${entryPoints.map((entry) => badge(entry)).join('')}</div></section
|
|
1024
|
-
|
|
1025
|
-
|
|
1060
|
+
const body = `${fieldSection(data, 'entryPoints', `<section class="panel"><h2>Entry Points</h2><div class="swatches">${entryPoints.map((entry) => badge(entry)).join('')}</div></section>`)}
|
|
1061
|
+
${fieldSection(data, 'modules', `<section class="grid" style="margin-top:14px">${modules.map((item) => `<article class="card"><div class="small">${escapeHtml(text(item.role, text(item.id, 'module')))}</div><h2>${escapeHtml(itemTitle(item, 'Module'))}</h2><p>${escapeHtml(text(item.detail, 'Describe this module.'))}</p></article>`).join('')}</section>`)}
|
|
1062
|
+
${fieldSection(data, 'edges', `<section class="panel" style="margin-top:14px"><h2>Relationships</h2><div class="flow">${edges.map((item) => `<div class="flow-node"><strong>${escapeHtml(text(item.from, 'from'))}</strong><p>${escapeHtml(text(item.label, 'connects'))}</p><strong>${escapeHtml(text(item.to, 'to'))}</strong></div>`).join('<div class="arrow">+</div>')}</div></section>`)}`;
|
|
1026
1063
|
return page({ title, kind: 'system-map', summary: descriptor.description, data, body });
|
|
1027
1064
|
}
|
|
1028
1065
|
|
|
@@ -1037,10 +1074,14 @@ function renderCodeWalkthrough({ title, data, descriptor }: Parameters<Primitive
|
|
|
1037
1074
|
]);
|
|
1038
1075
|
const keyFiles = fieldRecords(data, 'keyFiles', []);
|
|
1039
1076
|
const edges = fieldRecords(data, 'edges', []);
|
|
1040
|
-
const body = `<section class="panel"
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1077
|
+
const body = `${fieldSection(data, ['summary', 'modules', 'edges'], `<section class="panel">${fieldSection(data, 'summary', `<h2>Path Summary</h2><p>${escapeHtml(text(data.summary, 'Explain the code path this walkthrough covers.'))}</p>`)}${fieldSection(data, 'modules', `<div class="flow" style="margin-top:12px">${modules.map((module) => `<div class="flow-node"><div class="small">${escapeHtml(text(module.role, text(module.id, 'module')))}</div><h3>${escapeHtml(itemTitle(module, 'Module'))}</h3><p>${escapeHtml(text(module.detail, ''))}</p></div>`).join('<div class="arrow">-></div>')}</div>`)}${edges.length > 0 ? `<p class="small" style="margin-top:10px">Edges: ${escapeHtml(edges.map((edge) => `${text(edge.from)} -> ${text(edge.to)}${text(edge.label) ? ` (${text(edge.label)})` : ''}`).join(', '))}</p>` : ''}</section>`)}
|
|
1078
|
+
${fieldSection(
|
|
1079
|
+
data,
|
|
1080
|
+
['steps', 'keyFiles', 'gotchas'],
|
|
1081
|
+
`<section class="three" style="margin-top:14px">${fieldSection(data, 'steps', `<div class="panel"><h2>Walkthrough</h2>${steps.map((step, index) => `<details ${index === 0 ? 'open' : ''}><summary><strong>${index + 1}. ${escapeHtml(itemTitle(step, 'Step'))}</strong></summary><p class="small">${escapeHtml(text(step.file, ''))}</p><p>${escapeHtml(text(step.detail, ''))}</p>${codeBlock(step.code)}</details>`).join('')}</div>`)}
|
|
1082
|
+
${fieldSection(data, 'keyFiles', `<div class="panel"><h2>Key Files</h2>${keyFiles.length > 0 ? keyFiles.map((file) => `<article class="card"><h3>${escapeHtml(text(file.path, 'File'))}</h3><p>${escapeHtml(text(file.description, text(file.why, '')))}</p></article>`).join('') : '<p class="muted">No key files listed.</p>'}</div>`)}
|
|
1083
|
+
${fieldSection(data, 'gotchas', `<div class="panel sticky"><h2>Gotchas</h2>${list(fieldStrings(data, 'gotchas', ['Watch for hidden state, async boundaries, and validation gaps.']))}</div>`)}</section>`,
|
|
1084
|
+
)}`;
|
|
1044
1085
|
return page({ title, kind: 'code-walkthrough', summary: descriptor.description, data, body });
|
|
1045
1086
|
}
|
|
1046
1087
|
|
|
@@ -1060,18 +1101,22 @@ function renderDesignSheet({ title, data, descriptor }: Parameters<PrimitiveRend
|
|
|
1060
1101
|
},
|
|
1061
1102
|
]);
|
|
1062
1103
|
const tokens = fieldRecords(data, 'tokens', []);
|
|
1063
|
-
const body =
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1104
|
+
const body = `${fieldSection(
|
|
1105
|
+
data,
|
|
1106
|
+
'directions',
|
|
1107
|
+
`<section class="grid">${directions
|
|
1108
|
+
.map((item) => {
|
|
1109
|
+
const palette = strings(item.palette);
|
|
1110
|
+
return `<article class="card"><h2>${escapeHtml(itemTitle(item, 'Direction'))}</h2><p>${escapeHtml(text(item.tone, 'Tone'))}</p><div class="swatches">${palette.map((color) => `<span class="swatch" title="${escapeHtml(color)}" style="background:${safeCssColor(color)}"></span>`).join('')}</div><p>${escapeHtml(text(item.rationale, 'Rationale'))}</p></article>`;
|
|
1111
|
+
})
|
|
1112
|
+
.join('')}</section>`,
|
|
1113
|
+
)}
|
|
1069
1114
|
${tokens.length > 0 ? `<section class="panel" style="margin-top:14px"><h2>Tokens</h2><div class="grid">${tokens.map((item) => `<div class="card"><strong>${escapeHtml(itemTitle(item, 'Token'))}</strong><p>${escapeHtml(text(item.value, ''))}</p></div>`).join('')}</div></section>` : ''}`;
|
|
1070
1115
|
return page({ title, kind: 'design-sheet', summary: descriptor.description, data, body });
|
|
1071
1116
|
}
|
|
1072
1117
|
|
|
1073
1118
|
function renderComponentGallery({ title, data, descriptor }: Parameters<PrimitiveRenderer>[0]): string {
|
|
1074
|
-
const component = text(data.component, 'Component');
|
|
1119
|
+
const component = text(data.component, 'Component').trim();
|
|
1075
1120
|
const variants = fieldRecords(data, 'variants', [
|
|
1076
1121
|
{ label: 'Primary', state: 'default', intent: 'Main action', example: 'Continue', note: 'High emphasis.' },
|
|
1077
1122
|
{ label: 'Secondary', state: 'hover', intent: 'Alternative action', example: 'Back', note: 'Lower emphasis.' },
|
|
@@ -1083,8 +1128,8 @@ function renderComponentGallery({ title, data, descriptor }: Parameters<Primitiv
|
|
|
1083
1128
|
note: 'Requires confirmation.',
|
|
1084
1129
|
},
|
|
1085
1130
|
]);
|
|
1086
|
-
const body = `<section class="panel"><h2>${escapeHtml(component)}</h2><p>Variant contact sheet for fast visual review.</p></section
|
|
1087
|
-
|
|
1131
|
+
const body = `${component ? `<section class="panel"><h2>${escapeHtml(component)}</h2><p>Variant contact sheet for fast visual review.</p></section>` : ''}
|
|
1132
|
+
${variants.length > 0 ? `<section class="grid" style="margin-top:14px">${variants.map((item) => `<article class="card"><div class="small">${escapeHtml(text(item.state, 'state'))} / ${escapeHtml(text(item.intent, 'intent'))}</div><h2>${escapeHtml(itemTitle(item, 'Variant'))}</h2><button type="button">${escapeHtml(text(item.example, itemTitle(item, 'Example')))}</button><p>${escapeHtml(text(item.note, ''))}</p></article>`).join('')}</section>` : ''}`;
|
|
1088
1133
|
return page({ title, kind: 'component-gallery', summary: descriptor.description, data, body });
|
|
1089
1134
|
}
|
|
1090
1135
|
|
|
@@ -1100,9 +1145,13 @@ function renderInteractionPrototype({ title, data, descriptor }: Parameters<Prim
|
|
|
1100
1145
|
const annotations = fieldRecords(data, 'annotations', [
|
|
1101
1146
|
{ title: 'Decision', detail: 'Tune the values until this feels right.' },
|
|
1102
1147
|
]);
|
|
1103
|
-
const body =
|
|
1104
|
-
|
|
1105
|
-
|
|
1148
|
+
const body = fieldSection(
|
|
1149
|
+
data,
|
|
1150
|
+
['scenario', 'screens', 'controls', 'annotations', 'questions', 'snippet'],
|
|
1151
|
+
`<section class="three">${fieldSection(data, ['scenario', 'screens'], `<div class="panel">${fieldSection(data, 'scenario', `<h2>Stage</h2><p>${escapeHtml(text(data.scenario, 'Describe the interaction this prototype evaluates.'))}</p>`)}${fieldSection(data, 'screens', `<div class="flow" style="margin-top:16px">${screens.map((screen, index) => `<button class="flow-node" type="button" data-screen="${index}"><h3>${escapeHtml(itemTitle(screen, 'Screen'))}</h3><p>${escapeHtml(text(screen.detail, ''))}</p></button>`).join('<div class="arrow">-></div>')}</div><div class="card emphasis" id="prototype-readout" style="margin-top:14px">Select a screen to inspect it.</div>`)}</div>`)}
|
|
1152
|
+
${fieldSection(data, 'controls', `<div class="panel"><h2>Controls</h2>${controls.map((control, index) => `<label class="card"><span class="small">${escapeHtml(text(control.key, `control${index}`))}</span><h3>${escapeHtml(text(control.label, 'Control'))}: <span data-control-value="${index}">${escapeHtml(text(control.value, '0'))}</span>${escapeHtml(text(control.unit, ''))}</h3><input type="range" data-control="${index}" min="${number(control.min, 0)}" max="${number(control.max, 1000)}" value="${number(control.value, 0)}"></label>`).join('')}</div>`)}
|
|
1153
|
+
${fieldSection(data, ['annotations', 'questions', 'snippet'], `<div class="panel sticky">${fieldSection(data, 'annotations', `<h2>Notes</h2>${annotations.map((item) => `<article class="card"><h3>${escapeHtml(itemTitle(item, 'Note'))}</h3><p>${escapeHtml(text(item.detail, ''))}</p></article>`).join('')}`)}${fieldSection(data, 'questions', `<h2 style="margin-top:18px">Questions</h2>${list(fieldStrings(data, 'questions', ['Does the timing feel responsive?', 'What should persist after completion?']))}`)}${codeBlock(data.snippet)}</div>`)}</section>`,
|
|
1154
|
+
);
|
|
1106
1155
|
return page({
|
|
1107
1156
|
title,
|
|
1108
1157
|
kind: 'interaction-prototype',
|
|
@@ -1129,8 +1178,12 @@ function renderFlowchart({ title, data, descriptor }: Parameters<PrimitiveRender
|
|
|
1129
1178
|
{ title: 'Emit event', detail: 'Notify browser and agents through SSE/resources.', status: 'ok', duration: '5ms' },
|
|
1130
1179
|
]);
|
|
1131
1180
|
const failurePaths = fieldRecords(data, 'failurePaths', []);
|
|
1132
|
-
const body =
|
|
1133
|
-
|
|
1181
|
+
const body = `${
|
|
1182
|
+
steps.length > 0
|
|
1183
|
+
? `<section class="flow">${steps.map((item, index) => `<button class="flow-node" type="button" data-step="${index}"><h3>${escapeHtml(itemTitle(item, `Step ${index + 1}`))}</h3><p>${badge(text(item.status, 'step'))} ${escapeHtml(text(item.duration))}</p></button>${index < steps.length - 1 ? '<div class="arrow">-></div>' : ''}`).join('')}</section>
|
|
1184
|
+
<section class="panel" style="margin-top:14px"><h2 id="step-title">${escapeHtml(itemTitle(steps[0] ?? {}, 'Step'))}</h2><p id="step-detail">${escapeHtml(text(steps[0]?.detail, 'Select a step.'))}</p></section>`
|
|
1185
|
+
: ''
|
|
1186
|
+
}
|
|
1134
1187
|
${failurePaths.length > 0 ? `<section class="panel" style="margin-top:14px"><h2>Failure Paths</h2>${failurePaths.map((item) => `<article class="card"><h3>${escapeHtml(text(item.from, 'Step'))}: ${escapeHtml(text(item.label, 'failure'))}</h3><p>${escapeHtml(text(item.detail, ''))}</p></article>`).join('')}</section>` : ''}`;
|
|
1135
1188
|
return page({
|
|
1136
1189
|
title,
|
|
@@ -1160,12 +1213,16 @@ function renderIllustrationSet({ title, data, descriptor }: Parameters<Primitive
|
|
|
1160
1213
|
],
|
|
1161
1214
|
},
|
|
1162
1215
|
]);
|
|
1163
|
-
const body =
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1216
|
+
const body = fieldSection(
|
|
1217
|
+
data,
|
|
1218
|
+
'figures',
|
|
1219
|
+
`<section class="grid">${figures
|
|
1220
|
+
.map((figure, index) => {
|
|
1221
|
+
const shapes = records(figure.shapes);
|
|
1222
|
+
return `<article class="card figure"><h2>${escapeHtml(itemTitle(figure, `Figure ${index + 1}`))}</h2><svg viewBox="0 0 560 260" role="img" aria-label="${escapeHtml(itemTitle(figure, `Figure ${index + 1}`))}" data-figure="${index}"><defs><marker id="arrow" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto" markerUnits="strokeWidth"><path d="M0,0 L0,6 L9,3 z" fill="currentColor"></path></marker></defs>${shapes.map(inlineSvgShape).join('')}</svg><p>${escapeHtml(text(figure.caption, ''))}</p><button type="button" data-copy-svg="${index}">Copy SVG</button></article>`;
|
|
1223
|
+
})
|
|
1224
|
+
.join('')}</section>`,
|
|
1225
|
+
);
|
|
1169
1226
|
return page({
|
|
1170
1227
|
title,
|
|
1171
1228
|
kind: 'illustration-set',
|
|
@@ -1182,7 +1239,11 @@ document.querySelectorAll('[data-copy-svg]').forEach((button) => button.addEvent
|
|
|
1182
1239
|
|
|
1183
1240
|
function renderDeck({ title, data, descriptor }: Parameters<PrimitiveRenderer>[0]): string {
|
|
1184
1241
|
const slides = presentationSlides(data, DEFAULT_DECK_SLIDES);
|
|
1185
|
-
const body =
|
|
1242
|
+
const body = fieldSection(
|
|
1243
|
+
data,
|
|
1244
|
+
'slides',
|
|
1245
|
+
`<section class="panel"><div class="small"><span id="slide-count">1</span> / ${slides.length} - use left/right arrows</div>${slides.map((item, index) => `<article class="slide ${index === 0 ? 'active' : ''}" data-slide="${index}"><div><div class="kicker">${escapeHtml(text(item.kicker, `Slide ${index + 1}`))}</div><h2>${escapeHtml(itemTitle(item, 'Slide'))}</h2><p>${escapeHtml(text(item.body, ''))}</p>${list(strings(item.bullets))}<p class="small">${escapeHtml(text(item.note, ''))}</p></div></article>`).join('')}</section>`,
|
|
1246
|
+
);
|
|
1186
1247
|
return page({
|
|
1187
1248
|
title,
|
|
1188
1249
|
kind: 'deck',
|
|
@@ -1193,6 +1254,7 @@ function renderDeck({ title, data, descriptor }: Parameters<PrimitiveRenderer>[0
|
|
|
1193
1254
|
let currentSlide = 0;
|
|
1194
1255
|
const slides = Array.from(document.querySelectorAll('[data-slide]'));
|
|
1195
1256
|
function showSlide(index) {
|
|
1257
|
+
if (!slides.length) return;
|
|
1196
1258
|
currentSlide = Math.max(0, Math.min(slides.length - 1, index));
|
|
1197
1259
|
slides.forEach((slide, i) => slide.classList.toggle('active', i === currentSlide));
|
|
1198
1260
|
document.getElementById('slide-count').textContent = String(currentSlide + 1);
|
|
@@ -1281,14 +1343,18 @@ function renderPresentation({ title, data, descriptor }: Parameters<PrimitiveRen
|
|
|
1281
1343
|
<body>
|
|
1282
1344
|
<main class="deck">
|
|
1283
1345
|
<header class="topbar">
|
|
1284
|
-
<div class="brand"><div class="eyebrow">PMX presentation</div><div class="title">${escapeHtml(title)}</div
|
|
1346
|
+
<div class="brand"><div class="eyebrow">PMX presentation</div><div class="title">${escapeHtml(title)}</div>${fieldSection(data, 'subtitle', `<p>${escapeHtml(subtitle)}</p>`)}</div>
|
|
1285
1347
|
</header>
|
|
1286
|
-
|
|
1348
|
+
${fieldSection(
|
|
1349
|
+
data,
|
|
1350
|
+
'slides',
|
|
1351
|
+
`<section class="slides">${slideMarkup}</section>
|
|
1287
1352
|
<footer class="bottombar">
|
|
1288
1353
|
<div class="dots">${slides.map((_, index) => `<button class="dot ${index === 0 ? 'active' : ''}" type="button" data-dot="${index}" aria-label="Go to slide ${index + 1}"></button>`).join('')}</div>
|
|
1289
1354
|
<div class="hint"><span id="slide-current">1</span> / ${slides.length} - Arrow keys, Space, Page Up/Down</div>
|
|
1290
1355
|
<div class="progress" aria-hidden="true"><span id="slide-progress"></span></div>
|
|
1291
|
-
</footer
|
|
1356
|
+
</footer>`,
|
|
1357
|
+
)}
|
|
1292
1358
|
</main>
|
|
1293
1359
|
<script type="application/json" id="pmx-data">${safeJson(data)}</script>
|
|
1294
1360
|
<script>
|
|
@@ -1296,6 +1362,7 @@ let currentSlide = 0;
|
|
|
1296
1362
|
const slides = Array.from(document.querySelectorAll('[data-slide]'));
|
|
1297
1363
|
const dots = Array.from(document.querySelectorAll('[data-dot]'));
|
|
1298
1364
|
function showSlide(index) {
|
|
1365
|
+
if (!slides.length) return;
|
|
1299
1366
|
currentSlide = Math.max(0, Math.min(slides.length - 1, index));
|
|
1300
1367
|
slides.forEach((slide, i) => slide.classList.toggle('active', i === currentSlide));
|
|
1301
1368
|
dots.forEach((dot, i) => dot.classList.toggle('active', i === currentSlide));
|
|
@@ -1333,10 +1400,10 @@ function renderExplainer({ title, data, descriptor }: Parameters<PrimitiveRender
|
|
|
1333
1400
|
const snippets = fieldRecords(data, 'snippets', []);
|
|
1334
1401
|
const faq = fieldRecords(data, 'faq', []);
|
|
1335
1402
|
const glossary = fieldRecords(data, 'glossary', []);
|
|
1336
|
-
const body = `<section class="panel"><h2>TLDR</h2><p>${escapeHtml(text(data.summary, 'Add the one-paragraph explanation a reader should remember.'))}</p></section
|
|
1337
|
-
|
|
1403
|
+
const body = `${fieldSection(data, 'summary', `<section class="panel"><h2>TLDR</h2><p>${escapeHtml(text(data.summary, 'Add the one-paragraph explanation a reader should remember.'))}</p></section>`)}
|
|
1404
|
+
${fieldSection(data, 'steps', `<section class="grid" style="margin-top:14px">${steps.map((item, index) => `<details open><summary><strong>${index + 1}. ${escapeHtml(itemTitle(item, 'Step'))}</strong></summary><p>${escapeHtml(text(item.detail, ''))}</p></details>`).join('')}</section>`)}
|
|
1338
1405
|
${snippets.length > 0 ? `<section class="panel" style="margin-top:14px"><h2>Annotated Snippets</h2>${snippets.map((item) => `<h3>${escapeHtml(itemTitle(item, 'Snippet'))}</h3><p>${escapeHtml(text(item.note, ''))}</p>${codeBlock(item.code)}`).join('')}</section>` : ''}
|
|
1339
|
-
|
|
1406
|
+
${fieldSection(data, ['faq', 'glossary'], `<section class="two" style="margin-top:14px">${fieldSection(data, 'faq', `<div class="panel"><h2>FAQ</h2>${faq.map((item) => `<details><summary>${escapeHtml(text(item.q, 'Question'))}</summary><p>${escapeHtml(text(item.a, 'Answer'))}</p></details>`).join('') || '<p class="muted">No FAQ entries yet.</p>'}</div>`)}${fieldSection(data, 'glossary', `<div class="panel"><h2>Glossary</h2>${glossary.map((item) => `<div class="card"><strong>${escapeHtml(text(item.term, 'Term'))}</strong><p>${escapeHtml(text(item.definition, 'Definition'))}</p></div>`).join('') || '<p class="muted">No glossary entries yet.</p>'}</div>`)}</section>`)}`;
|
|
1340
1407
|
return page({ title, kind: 'explainer', summary: descriptor.description, data, body });
|
|
1341
1408
|
}
|
|
1342
1409
|
|
|
@@ -1345,8 +1412,8 @@ function renderStatusReport({ title, data, descriptor }: Parameters<PrimitiveRen
|
|
|
1345
1412
|
{ label: 'Health', value: 'on track', tone: 'ok' },
|
|
1346
1413
|
{ label: 'Risk', value: 'medium', tone: 'warn' },
|
|
1347
1414
|
]);
|
|
1348
|
-
const body = `<section class="grid">${metrics.map((item) => `<article class="card"><div class="small">${escapeHtml(text(item.label, 'Metric'))}</div><h2>${escapeHtml(text(item.value, 'Value'))}</h2>${badge(text(item.tone, 'info'))}</article>`).join('')}</section
|
|
1349
|
-
|
|
1415
|
+
const body = `${fieldSection(data, 'metrics', `<section class="grid">${metrics.map((item) => `<article class="card"><div class="small">${escapeHtml(text(item.label, 'Metric'))}</div><h2>${escapeHtml(text(item.value, 'Value'))}</h2>${badge(text(item.tone, 'info'))}</article>`).join('')}</section>`)}
|
|
1416
|
+
${fieldSection(data, ['shipped', 'slipped', 'risks', 'next'], `<section class="grid" style="margin-top:14px">${fieldSection(data, 'shipped', `<article class="card"><h2>Shipped</h2>${list(fieldStrings(data, 'shipped', ['Add shipped items.']))}</article>`)}${fieldSection(data, 'slipped', `<article class="card"><h2>Slipped</h2>${list(fieldStrings(data, 'slipped', ['Add slipped items.']))}</article>`)}${fieldSection(data, 'risks', `<article class="card"><h2>Risks</h2>${list(fieldStrings(data, 'risks', ['Add risks.']))}</article>`)}${fieldSection(data, 'next', `<article class="card"><h2>Next</h2>${list(fieldStrings(data, 'next', ['Add next actions.']))}</article>`)}</section>`)}`;
|
|
1350
1417
|
return page({ title, kind: 'status-report', summary: descriptor.description, data, body });
|
|
1351
1418
|
}
|
|
1352
1419
|
|
|
@@ -1362,11 +1429,25 @@ function renderIncidentReport({ title, data, descriptor }: Parameters<PrimitiveR
|
|
|
1362
1429
|
const actions = fieldRecords(data, 'actions', [
|
|
1363
1430
|
{ done: false, owner: 'Unassigned', description: 'Add follow-up action.', due: 'TBD' },
|
|
1364
1431
|
]);
|
|
1365
|
-
const body =
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1432
|
+
const body = `${fieldSection(
|
|
1433
|
+
data,
|
|
1434
|
+
'impact',
|
|
1435
|
+
`<section class="grid">${impact
|
|
1436
|
+
.filter((item) => text(item.value).trim())
|
|
1437
|
+
.map(
|
|
1438
|
+
(item) =>
|
|
1439
|
+
`<article class="card metric"><div class="small">${escapeHtml(text(item.label, 'Metric'))}</div><strong>${escapeHtml(text(item.value, 'Value'))}</strong>${badge(text(item.tone, 'info'))}</article>`,
|
|
1440
|
+
)
|
|
1441
|
+
.join('')}</section>`,
|
|
1442
|
+
)}
|
|
1443
|
+
${fieldSection(data, 'summary', `<section class="panel" style="margin-top:14px"><h2>Executive Summary</h2><p>${escapeHtml(text(data.summary, 'Summarize user impact, detection, and resolution.'))}</p></section>`)}
|
|
1444
|
+
${fieldSection(
|
|
1445
|
+
data,
|
|
1446
|
+
['timeline', 'rootCause', 'logs', 'actions'],
|
|
1447
|
+
`<section class="three" style="margin-top:14px">${fieldSection(data, 'timeline', `<div class="panel"><h2>Timeline</h2><div class="timeline">${timeline.map((item, index) => `<div class="step"><div class="dot">${escapeHtml(text(item.time, String(index + 1)))}</div><div class="card"><h3>${escapeHtml(text(item.event, 'Event'))} ${badge(text(item.tone, 'info'))}</h3><p>${escapeHtml(text(item.detail, ''))}</p></div></div>`).join('')}</div></div>`)}
|
|
1448
|
+
${fieldSection(data, ['rootCause', 'logs'], `<div class="panel">${fieldSection(data, 'rootCause', `<h2>Root Cause</h2><p>${escapeHtml(text(data.rootCause, 'Add confirmed or suspected root cause.'))}</p>`)}${codeBlock(data.logs)}</div>`)}
|
|
1449
|
+
${fieldSection(data, 'actions', `<div class="panel sticky"><h2>Actions</h2>${actions.map((action) => `<label class="card"><h3><input type="checkbox" data-action ${action.done === true ? 'checked' : ''}> ${escapeHtml(text(action.description, 'Action'))}</h3><p class="small">${escapeHtml(text(action.owner, 'Owner'))} / ${escapeHtml(text(action.due, 'Due'))}</p></label>`).join('')}<button type="button" data-copy-actions>Copy actions</button></div>`)}</section>`,
|
|
1450
|
+
)}`;
|
|
1370
1451
|
return page({
|
|
1371
1452
|
title,
|
|
1372
1453
|
kind: 'incident-report',
|
|
@@ -1398,20 +1479,28 @@ function renderTriageBoard({ title, data, descriptor }: Parameters<PrimitiveRend
|
|
|
1398
1479
|
rationale: 'Useful but not blocking.',
|
|
1399
1480
|
},
|
|
1400
1481
|
]);
|
|
1401
|
-
const body =
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
)
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1482
|
+
const body = fieldSection(
|
|
1483
|
+
data,
|
|
1484
|
+
'columns',
|
|
1485
|
+
fieldSection(
|
|
1486
|
+
data,
|
|
1487
|
+
'items',
|
|
1488
|
+
`<section class="columns">${columns
|
|
1489
|
+
.map(
|
|
1490
|
+
(column) =>
|
|
1491
|
+
`<div class="column" data-column="${escapeHtml(column)}"><h2>${escapeHtml(column)}</h2>${items
|
|
1492
|
+
.filter((item) => text(item.column, columns[0]) === column)
|
|
1493
|
+
.map(
|
|
1494
|
+
(item, index) =>
|
|
1495
|
+
`<article class="card ticket" draggable="true" data-ticket="${index}"><h3>${escapeHtml(itemTitle(item, 'Item'))}</h3><p>${escapeHtml(text(item.detail, ''))}</p><p class="small">${escapeHtml(text(item.rationale, ''))}</p></article>`,
|
|
1496
|
+
)
|
|
1497
|
+
.join('')}</div>`,
|
|
1498
|
+
)
|
|
1499
|
+
.join(
|
|
1500
|
+
'',
|
|
1501
|
+
)}</section><p class="small" style="margin-top:12px">Drag cards between columns, then copy JSON or markdown.</p><button type="button" data-copy-markdown>Copy markdown</button>`,
|
|
1502
|
+
),
|
|
1503
|
+
);
|
|
1415
1504
|
return page({
|
|
1416
1505
|
title,
|
|
1417
1506
|
kind: 'triage-board',
|
|
@@ -1465,12 +1554,16 @@ function renderConfigEditor({ title, data, descriptor }: Parameters<PrimitiveRen
|
|
|
1465
1554
|
description: 'Routes users through the new flow.',
|
|
1466
1555
|
},
|
|
1467
1556
|
]);
|
|
1468
|
-
const body =
|
|
1557
|
+
const body = fieldSection(
|
|
1558
|
+
data,
|
|
1559
|
+
'flags',
|
|
1560
|
+
`<section class="grid">${flags.map((flag, index) => `<label class="card"><div class="small">${escapeHtml(text(flag.area, 'General'))}</div><h3><input type="checkbox" data-flag="${index}" ${flag.enabled === true ? 'checked' : ''}> ${escapeHtml(text(flag.label, text(flag.key, `Flag ${index + 1}`)))}</h3><p>${escapeHtml(text(flag.description, ''))}</p><p class="small">Key: ${escapeHtml(text(flag.key, 'unknown'))}${strings(flag.requires).length > 0 ? ` / requires: ${escapeHtml(strings(flag.requires).join(', '))}` : ''}</p><p class="small" data-warning="${index}"></p></label>`).join('')}</section><button type="button" data-copy-diff style="margin-top:12px">Copy diff</button>`,
|
|
1561
|
+
);
|
|
1469
1562
|
return page({
|
|
1470
1563
|
title,
|
|
1471
1564
|
kind: 'config-editor',
|
|
1472
1565
|
summary: descriptor.description,
|
|
1473
|
-
data,
|
|
1566
|
+
data: { ...data, flags },
|
|
1474
1567
|
body,
|
|
1475
1568
|
script: `
|
|
1476
1569
|
const originalFlags = PMX_DATA.flags || [];
|
|
@@ -1503,7 +1596,11 @@ function renderPromptTuner({ title, data, descriptor }: Parameters<PrimitiveRend
|
|
|
1503
1596
|
const samples = fieldRecords(data, 'samples', [
|
|
1504
1597
|
{ name: 'Default', variables: { feature: 'PMX Canvas pins', audience: 'coding agents' } },
|
|
1505
1598
|
]);
|
|
1506
|
-
const body =
|
|
1599
|
+
const body = fieldSection(
|
|
1600
|
+
data,
|
|
1601
|
+
'template',
|
|
1602
|
+
`<section class="two"><div class="panel"><h2>Template</h2><textarea id="template">${escapeHtml(template)}</textarea><p class="small"><span id="char-count">0</span> characters</p></div>${fieldSection(data, 'samples', `<div class="panel"><h2>Live Samples</h2><div id="previews"></div><button type="button" data-copy-template>Copy template</button></div>`)}</section>`,
|
|
1603
|
+
);
|
|
1507
1604
|
return page({
|
|
1508
1605
|
title,
|
|
1509
1606
|
kind: 'prompt-tuner',
|
|
@@ -1518,12 +1615,14 @@ function fill(template, vars) {
|
|
|
1518
1615
|
return template.replace(/{{\\s*([\\w.-]+)\\s*}}/g, (_, key) => vars?.[key] ?? '{{' + key + '}}');
|
|
1519
1616
|
}
|
|
1520
1617
|
function renderPreviews() {
|
|
1618
|
+
if (!templateEl) return;
|
|
1521
1619
|
const value = templateEl.value;
|
|
1522
1620
|
document.getElementById('char-count').textContent = String(value.length);
|
|
1621
|
+
if (!previewsEl) return;
|
|
1523
1622
|
previewsEl.innerHTML = samples.map((sample) => '<div class="card"><h3>' + (sample.name || 'Sample') + '</h3><div class="preview">' + fill(value, sample.variables || {}).replace(/[&<>]/g, (c) => ({'&':'&','<':'<','>':'>'}[c])) + '</div></div>').join('');
|
|
1524
1623
|
}
|
|
1525
|
-
templateEl
|
|
1526
|
-
window.__pmxGetCopyJson = () => ({ ...PMX_DATA, template: templateEl
|
|
1624
|
+
templateEl?.addEventListener('input', renderPreviews);
|
|
1625
|
+
window.__pmxGetCopyJson = () => ({ ...PMX_DATA, template: templateEl?.value ?? '' });
|
|
1527
1626
|
document.querySelector('[data-copy-template]')?.addEventListener('click', () => copyText(templateEl.value));
|
|
1528
1627
|
renderPreviews();`,
|
|
1529
1628
|
});
|
|
@@ -1742,7 +1841,7 @@ function renderAxBoard({ title, data, descriptor }: Parameters<PrimitiveRenderer
|
|
|
1742
1841
|
<section class="two">
|
|
1743
1842
|
<div class="panel">
|
|
1744
1843
|
<h2>Give the agent a task</h2>
|
|
1745
|
-
|
|
1844
|
+
${fieldSection(data, 'note', `<p class="small">${escapeHtml(note)}</p>`)}
|
|
1746
1845
|
<div class="ax-field"><input type="text" id="ax-task-title" placeholder="What should the agent do?"></div>
|
|
1747
1846
|
<div class="ax-field"><textarea id="ax-task-detail" placeholder="Optional detail" style="min-height:64px"></textarea></div>
|
|
1748
1847
|
<div class="ax-row"><button type="button" id="ax-add-task">Add task</button><span class="small" id="ax-task-status"></span></div>
|
|
@@ -1882,6 +1981,15 @@ function renderAxFlow({ title, data, descriptor }: Parameters<PrimitiveRenderer>
|
|
|
1882
1981
|
const detail = text(step.detail).trim().slice(0, 2000);
|
|
1883
1982
|
return detail ? { title: stepTitle, detail } : { title: stepTitle };
|
|
1884
1983
|
});
|
|
1984
|
+
if (steps.length === 0) {
|
|
1985
|
+
return page({
|
|
1986
|
+
title,
|
|
1987
|
+
kind: 'ax-flow',
|
|
1988
|
+
summary: descriptor.description,
|
|
1989
|
+
data,
|
|
1990
|
+
body: fieldSection(data, 'note', `<p class="small">${escapeHtml(note)}</p>`),
|
|
1991
|
+
});
|
|
1992
|
+
}
|
|
1885
1993
|
const loopRecord = isRecord(data.loop) ? data.loop : {};
|
|
1886
1994
|
const loopEnabled = loopRecord.enabled === true;
|
|
1887
1995
|
const maxRuns = Math.min(20, Math.max(1, Math.round(number(loopRecord.maxRuns, 3))));
|
|
@@ -1918,7 +2026,7 @@ function renderAxFlow({ title, data, descriptor }: Parameters<PrimitiveRenderer>
|
|
|
1918
2026
|
<section class="panel" id="ax-offline" hidden><h2>AX bridge unavailable</h2><p class="small">This flow is not running as an AX-enabled canvas node, so its controls are inert.</p></section>
|
|
1919
2027
|
<section class="panel">
|
|
1920
2028
|
<div class="ax-work-head"><h2>${escapeHtml(title)}</h2><span class="small" id="ax-flow-counts"></span></div>
|
|
1921
|
-
|
|
2029
|
+
${fieldSection(data, 'note', `<p class="small">${escapeHtml(note)}</p>`)}
|
|
1922
2030
|
<div class="ax-flow-scroll">
|
|
1923
2031
|
<div class="ax-flow-wrap${loopEnabled ? ' looping' : ''}" id="ax-flow-wrap">
|
|
1924
2032
|
<div class="ax-flow-diagram" id="ax-flow-diagram"></div>
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
AX_SURFACE_EMIT_SOURCE,
|
|
21
21
|
HTML_SURFACE_PUSH_SOURCE,
|
|
22
22
|
} from '../shared/ax-surface-protocol.js';
|
|
23
|
-
import { contentHeightReporterTag } from '../shared/content-height-reporter.js';
|
|
23
|
+
import { contentHeightReporterTag, sanitizeFrameToken } from '../shared/content-height-reporter.js';
|
|
24
24
|
import { type CanvasThemeName, normalizeCanvasThemeName } from '../shared/themes.js';
|
|
25
25
|
|
|
26
26
|
export type SurfaceTheme = CanvasThemeName;
|
|
@@ -218,7 +218,12 @@ function escapeSurfaceHtml(value: string): string {
|
|
|
218
218
|
* of the main SPA bundle). Escaping is mandatory: the source is arbitrary node
|
|
219
219
|
* data and must not be parsed as markup.
|
|
220
220
|
*/
|
|
221
|
-
export function buildMermaidSurfaceHtml(
|
|
221
|
+
export function buildMermaidSurfaceHtml(
|
|
222
|
+
source: string,
|
|
223
|
+
inlineEntry?: string | null,
|
|
224
|
+
fit: 'contain' | 'none' = 'contain',
|
|
225
|
+
frameToken = '',
|
|
226
|
+
): string {
|
|
222
227
|
// srcdoc-rendered surfaces in hosts that block sub-frame subresources (the
|
|
223
228
|
// Claude Code desktop browser blocks the script request too, not just the
|
|
224
229
|
// frame document) get the renderer embedded as a data: URL — no network
|
|
@@ -228,7 +233,7 @@ export function buildMermaidSurfaceHtml(source: string, inlineEntry?: string | n
|
|
|
228
233
|
const entry = inlineEntry
|
|
229
234
|
? `<script src="data:text/javascript;charset=utf-8;base64,${Buffer.from(inlineEntry, 'utf-8').toString('base64')}"></script>`
|
|
230
235
|
: '<script src="/canvas/mermaid-entry.js"></script>';
|
|
231
|
-
return `<pre class="mermaid-source" style="display:none">${escapeSurfaceHtml(source)}</pre>${entry}`;
|
|
236
|
+
return `<pre class="mermaid-source" data-fit="${fit}" data-frame-token="${sanitizeFrameToken(frameToken)}" style="display:none">${escapeSurfaceHtml(source)}</pre>${entry}`;
|
|
232
237
|
}
|
|
233
238
|
|
|
234
239
|
export interface HtmlSurfaceOptions {
|