@imgly/codesign-mcp 0.1.7 → 0.1.8

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.
Files changed (104) hide show
  1. package/assets/docs/api/index.d.ts +9 -3
  2. package/assets/docs/guide/SKILL.md +2 -2
  3. package/assets/docs/guide/api/node/classes/creativeengine.md +13 -13
  4. package/assets/docs/guide/edit-image/transform/scale.md +1 -3
  5. package/assets/docs/guide/filters-and-effects/duotone.md +1 -1
  6. package/assets/docs/guide/get-started/bun.md +0 -2
  7. package/assets/docs/guide/get-started/deno.md +0 -1
  8. package/assets/docs/guide/get-started/vanilla.md +0 -2
  9. package/assets/docs/guide/import-media/default-assets.md +20 -24
  10. package/assets/docs/guide/plugins/print-ready-pdf.md +0 -2
  11. package/assets/docs/guide/serve-assets.md +80 -23
  12. package/assets/docs/handbook/SKILL.md +6 -7
  13. package/dist/assets/inline.generated.js +12 -12
  14. package/dist/assets/inline.generated.js.map +1 -1
  15. package/dist/cli/http.d.ts.map +1 -1
  16. package/dist/cli/http.js +17 -3
  17. package/dist/cli/http.js.map +1 -1
  18. package/dist/cli/stdio.d.ts.map +1 -1
  19. package/dist/cli/stdio.js +44 -15
  20. package/dist/cli/stdio.js.map +1 -1
  21. package/dist/cli/trial-license.generated.d.ts +1 -1
  22. package/dist/cli/trial-license.generated.js +1 -1
  23. package/dist/cli/trial-license.generated.js.map +1 -1
  24. package/dist/config.d.ts +6 -0
  25. package/dist/config.d.ts.map +1 -1
  26. package/dist/config.js +8 -0
  27. package/dist/config.js.map +1 -1
  28. package/dist/diagnostics/bundle.d.ts +3 -0
  29. package/dist/diagnostics/bundle.d.ts.map +1 -1
  30. package/dist/diagnostics/bundle.js +25 -22
  31. package/dist/diagnostics/bundle.js.map +1 -1
  32. package/dist/diagnostics/prompt.d.ts +3 -0
  33. package/dist/diagnostics/prompt.d.ts.map +1 -0
  34. package/dist/diagnostics/prompt.js +21 -0
  35. package/dist/diagnostics/prompt.js.map +1 -0
  36. package/dist/diagnostics/router.d.ts +12 -0
  37. package/dist/diagnostics/router.d.ts.map +1 -0
  38. package/dist/diagnostics/router.js +64 -0
  39. package/dist/diagnostics/router.js.map +1 -0
  40. package/dist/diagnostics/spa-assets.generated.d.ts +2 -0
  41. package/dist/diagnostics/spa-assets.generated.d.ts.map +1 -0
  42. package/dist/diagnostics/spa-assets.generated.js +1231 -0
  43. package/dist/diagnostics/spa-assets.generated.js.map +1 -0
  44. package/dist/diagnostics/status.d.ts +38 -0
  45. package/dist/diagnostics/status.d.ts.map +1 -0
  46. package/dist/diagnostics/status.js +53 -0
  47. package/dist/diagnostics/status.js.map +1 -0
  48. package/dist/diagnostics/summary.d.ts +48 -0
  49. package/dist/diagnostics/summary.d.ts.map +1 -0
  50. package/dist/diagnostics/summary.js +116 -0
  51. package/dist/diagnostics/summary.js.map +1 -0
  52. package/dist/diagnostics/tool.d.ts +16 -0
  53. package/dist/diagnostics/tool.d.ts.map +1 -0
  54. package/dist/diagnostics/tool.js +90 -0
  55. package/dist/diagnostics/tool.js.map +1 -0
  56. package/dist/engine/asset-sources.d.ts +8 -0
  57. package/dist/engine/asset-sources.d.ts.map +1 -1
  58. package/dist/engine/asset-sources.js +9 -2
  59. package/dist/engine/asset-sources.js.map +1 -1
  60. package/dist/engine/uri-resolver.js.map +1 -1
  61. package/dist/server.d.ts +25 -0
  62. package/dist/server.d.ts.map +1 -1
  63. package/dist/server.js +24 -4
  64. package/dist/server.js.map +1 -1
  65. package/dist/tools/engine.d.ts +79 -14
  66. package/dist/tools/engine.d.ts.map +1 -1
  67. package/dist/tools/engine.js +119 -20
  68. package/dist/tools/engine.js.map +1 -1
  69. package/dist/tools/export.d.ts.map +1 -1
  70. package/dist/tools/export.js +34 -4
  71. package/dist/tools/export.js.map +1 -1
  72. package/dist/tools/index.js +2 -2
  73. package/dist/tools/index.js.map +1 -1
  74. package/dist/tools/load.d.ts.map +1 -1
  75. package/dist/tools/load.js +26 -19
  76. package/dist/tools/load.js.map +1 -1
  77. package/dist/tools/preview.d.ts.map +1 -1
  78. package/dist/tools/preview.js +60 -40
  79. package/dist/tools/preview.js.map +1 -1
  80. package/dist/tools/{open-editor.d.ts → view.d.ts} +3 -3
  81. package/dist/tools/view.d.ts.map +1 -0
  82. package/dist/tools/{open-editor.js → view.js} +20 -18
  83. package/dist/tools/view.js.map +1 -0
  84. package/dist/viewer/listener.d.ts +2 -0
  85. package/dist/viewer/listener.d.ts.map +1 -1
  86. package/dist/viewer/listener.js +2 -0
  87. package/dist/viewer/listener.js.map +1 -1
  88. package/dist/viewer/presigned-r2-url-builder.d.ts +1 -1
  89. package/dist/viewer/presigned-r2-url-builder.js +1 -1
  90. package/dist/viewer/presigned-r2-url-builder.js.map +1 -1
  91. package/dist/viewer/router.d.ts.map +1 -1
  92. package/dist/viewer/router.js +8 -17
  93. package/dist/viewer/router.js.map +1 -1
  94. package/dist/viewer/session-mount.js.map +1 -1
  95. package/dist/viewer/spa-assets.generated.d.ts.map +1 -1
  96. package/dist/viewer/spa-assets.generated.js +750 -869
  97. package/dist/viewer/spa-assets.generated.js.map +1 -1
  98. package/dist/viewer/url-builder.d.ts +4 -3
  99. package/dist/viewer/url-builder.d.ts.map +1 -1
  100. package/dist/viewer/url-builder.js +4 -4
  101. package/dist/viewer/url-builder.js.map +1 -1
  102. package/package.json +6 -5
  103. package/dist/tools/open-editor.d.ts.map +0 -1
  104. package/dist/tools/open-editor.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"spa-assets.generated.js","sourceRoot":"","sources":["../../src/viewer/spa-assets.generated.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,MAAM,CAAC,MAAM,UAAU,GAA2B;IACjD,YAAY,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2Td;IACA,mBAAmB,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwMrB;IACA,aAAa,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmIf;IACA,WAAW,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoFb;IACA,WAAW,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyPb;CACA,CAAC","sourcesContent":["// AUTO-GENERATED by scripts/generate-spa-assets.mjs. Do not edit by hand.\nexport const SPA_ASSETS: Record<string, string> = {\n\t\"index.html\": `<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <title>codesign viewer</title>\n <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\" />\n <!-- Mount prefix injected by the viewer router (router.ts). \"\" on the stdio\n listener, \"/viewer\" on the HTTP transport. Read by viewer.js. -->\n <script>\n window.__VIEWER_BASE__ = '{{VIEWER_BASE}}';\n </script>\n <style>\n /* -------- reset & canvas -------- */\n html,\n body {\n margin: 0;\n padding: 0;\n height: 100%;\n width: 100%;\n background: #1d1d1f;\n color: #eee;\n font-family: -apple-system, system-ui, sans-serif;\n }\n #cesdk {\n position: fixed;\n inset: 0;\n }\n\n /* -------- pill base (spec §7) -------- */\n .pill {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n padding: 6px 11px;\n border-radius: 999px;\n font:\n 500 12px -apple-system,\n system-ui,\n sans-serif;\n color: #e8e8e8;\n background: rgba(20, 20, 22, 0.72);\n backdrop-filter: blur(12px);\n -webkit-backdrop-filter: blur(12px);\n border: 1px solid rgba(255, 255, 255, 0.08);\n box-shadow: 0 4px 14px rgba(0, 0, 0, 0.25);\n text-decoration: none;\n cursor: pointer;\n white-space: nowrap;\n }\n .pill.icon {\n padding: 6px 9px;\n }\n .pill:hover {\n background: rgba(30, 30, 33, 0.82);\n }\n\n /* -------- cluster anchors -------- */\n #bar-left,\n #bar-right {\n position: fixed;\n top: 14px;\n z-index: 10;\n display: flex;\n gap: 8px;\n }\n #bar-left {\n left: 14px;\n }\n #bar-right {\n right: 14px;\n }\n\n /* -------- title pill --------\n\t\t\t Non-interactive (no click handler) — override .pill's cursor + hover so\n\t\t\t the title visually matches its actual behavior. For a local MCP the URL\n\t\t\t isn't shareable, so there's nothing useful to copy. Revisit when a\n\t\t\t remote MCP ships — copy-share-URL is the natural fit there. */\n #title {\n font-weight: 600;\n max-width: 240px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n cursor: default;\n }\n #title:hover {\n background: rgba(20, 20, 22, 0.72);\n }\n\n /* -------- LIVE pill (read-only) -------- */\n .pill.live {\n color: #c8e6c9;\n cursor: default;\n }\n .pill.live:hover {\n background: rgba(20, 20, 22, 0.72);\n }\n .pill.live::before {\n content: '';\n width: 6px;\n height: 6px;\n border-radius: 999px;\n background: #34c759;\n box-shadow: 0 0 5px rgba(52, 199, 89, 0.55);\n }\n\n /* -------- hash chip -------- */\n .pill.hash {\n font-family: ui-monospace, monospace;\n font-size: 11px;\n color: #b8b8b8;\n }\n\n /* -------- Go Live pill + count badge -------- */\n .pill.golive {\n color: #9ec8ff;\n }\n .pill.golive .count {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n min-width: 16px;\n height: 16px;\n padding: 0 4px;\n border-radius: 999px;\n background: #2b6cb0;\n color: #fff;\n font-size: 10px;\n font-weight: 600;\n margin-left: 4px;\n }\n\n /* -------- History pill -------- */\n .pill.history::before {\n content: '⎇';\n opacity: 0.8;\n }\n .pill.history.active,\n .pill.history.active:hover {\n background: rgba(43, 108, 176, 0.85);\n border-color: rgba(43, 108, 176, 0.95);\n color: #fff;\n }\n\n /* -------- floating history panel (spec §6) -------- */\n #revision-panel {\n position: fixed;\n top: 62px;\n right: 14px;\n bottom: 14px;\n width: 280px;\n background: rgba(20, 20, 22, 0.88);\n backdrop-filter: blur(16px);\n -webkit-backdrop-filter: blur(16px);\n border: 1px solid rgba(255, 255, 255, 0.08);\n border-radius: 12px;\n box-shadow: 0 14px 36px rgba(0, 0, 0, 0.42);\n box-sizing: border-box;\n overflow-y: auto;\n display: none;\n opacity: 0;\n transform: translateX(8px);\n z-index: 10;\n }\n body.panel-open #revision-panel {\n display: block;\n }\n @media (prefers-reduced-motion: no-preference) {\n #revision-panel {\n transition:\n opacity 160ms ease-out,\n transform 160ms ease-out;\n }\n body.panel-open #revision-panel {\n opacity: 1;\n transform: translateX(0);\n }\n }\n @media (prefers-reduced-motion: reduce) {\n body.panel-open #revision-panel {\n opacity: 1;\n transform: none;\n }\n }\n\n /* -------- panel internals (polish pass) -------- */\n .rp-header {\n padding: 12px 14px 10px;\n font-size: 10px;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n color: #888;\n border-bottom: 1px solid rgba(255, 255, 255, 0.06);\n }\n .rp-tree {\n padding: 8px 6px;\n }\n .rp-empty {\n padding: 16px 14px;\n font-size: 12px;\n color: #888;\n }\n\n .rp-node {\n display: block;\n text-decoration: none;\n color: inherit;\n padding: 6px 8px;\n border-radius: 6px;\n }\n .rp-node:hover {\n background: rgba(255, 255, 255, 0.04);\n }\n .rp-node.rp-current {\n background: rgba(30, 47, 68, 0.6);\n box-shadow: inset 2px 0 0 #4a9eff;\n }\n .rp-node-row {\n display: flex;\n gap: 8px;\n align-items: baseline;\n }\n .rp-elbow {\n color: #555;\n flex: none;\n }\n .rp-dot {\n flex: none;\n width: 12px;\n text-align: center;\n font-size: 10px;\n color: #888;\n }\n .rp-node.rp-head .rp-dot {\n color: #4a9eff;\n }\n .rp-node.rp-branch .rp-dot {\n color: #d6973f;\n }\n .rp-body {\n flex: 1;\n min-width: 0;\n }\n .rp-id {\n display: flex;\n align-items: center;\n gap: 6px;\n font-family: ui-monospace, monospace;\n font-size: 10px;\n color: #888;\n }\n .rp-tag {\n border: 1px solid rgba(255, 255, 255, 0.1);\n border-radius: 4px;\n padding: 0 4px;\n font-size: 9px;\n }\n .rp-tag.rp-tag-head {\n background: rgba(43, 108, 176, 0.85);\n border-color: rgba(43, 108, 176, 0.95);\n color: #fff;\n }\n .rp-tag.rp-tag-fork {\n color: #d6973f;\n border-color: rgba(214, 151, 63, 0.35);\n }\n .rp-note {\n font-size: 12px;\n color: #eee;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n }\n .rp-time {\n font-size: 10px;\n color: #777;\n }\n .rp-node.rp-current .rp-time::after {\n content: ' · on canvas';\n color: #4a9eff;\n }\n .rp-copy {\n font: inherit;\n font-size: 9px;\n color: #ccc;\n background: rgba(20, 20, 22, 0.72);\n border: 1px solid rgba(255, 255, 255, 0.08);\n border-radius: 999px;\n padding: 2px 7px;\n cursor: pointer;\n opacity: 0;\n margin-left: auto;\n transition:\n opacity 120ms ease-out,\n background 120ms ease-out;\n }\n .rp-node:hover .rp-copy {\n opacity: 1;\n }\n .rp-copy:hover {\n background: rgba(30, 30, 33, 0.82);\n }\n </style>\n </head>\n <body>\n <div id=\"cesdk\"></div>\n <div id=\"bar-left\">\n <a id=\"back\" class=\"pill icon\" href=\"{{VIEWER_BASE}}/\">←</a>\n <span id=\"title\" class=\"pill\">loading…</span>\n </div>\n <div id=\"bar-right\"></div>\n <aside id=\"revision-panel\"></aside>\n <script type=\"module\" src=\"{{VIEWER_BASE}}/static/viewer.js\"></script>\n </body>\n</html>\n`,\n\t\"revision-panel.js\": `// revision-panel.js — the in-viewer revision history panel. Fetches a design's\n// full revision tree from /api/design/<root>/revisions and renders it; clicking\n// a node asks the viewer to navigate there. The viewer drives setCurrent()\n// (highlight) and refresh() (re-fetch on SSE).\n//\n// All DOM is built with createElement + textContent: revision notes are\n// agent-authored text and must never reach innerHTML.\n\n// Mount prefix set by index.html (\"\" on stdio, \"/viewer\" on HTTP); node hrefs\n// and the revisions fetch are built relative to it. See router.ts.\nconst BASE = window.__VIEWER_BASE__ ?? '';\n\nexport function createRevisionPanel({ container, root, onNavigate, onCount }) {\n let currentRev = null;\n\n function relTime(ms) {\n const s = Math.max(0, Math.round((Date.now() - ms) / 1000));\n if (s < 60) return \\`\\${s}s ago\\`;\n const m = Math.round(s / 60);\n if (m < 60) return \\`\\${m}m ago\\`;\n const h = Math.round(m / 60);\n if (h < 24) return \\`\\${h}h ago\\`;\n return \\`\\${Math.round(h / 24)}d ago\\`;\n }\n\n // Depth-first walk. At each node the spine child (or, off-spine, the latest\n // child) continues at the same depth; other children are branches at\n // depth + 1, emitted before the continuation so the spine reads top-to-bottom.\n function order(nodes) {\n const byId = new Map(nodes.map((n) => [n.id, n]));\n const children = new Map();\n for (const n of nodes) {\n if (n.parent != null) {\n const sibs = children.get(n.parent) ?? [];\n sibs.push(n);\n children.set(n.parent, sibs);\n }\n }\n for (const sibs of children.values())\n sibs.sort((a, b) => a.createdAt - b.createdAt);\n const rootNode = nodes.find((n) => n.parent === null);\n const head = nodes.reduce((a, b) => (b.createdAt > a.createdAt ? b : a));\n const spine = new Set();\n for (let n = head; n; n = n.parent != null ? byId.get(n.parent) : null)\n spine.add(n.id);\n const out = [];\n function walk(node, depth) {\n out.push({\n node,\n depth,\n isHead: node.id === head.id,\n isRoot: node.parent === null\n });\n const kids = children.get(node.id) ?? [];\n if (kids.length === 0) return;\n const primary =\n kids.find((k) => spine.has(k.id)) ?? kids[kids.length - 1];\n for (const k of kids) if (k !== primary) walk(k, depth + 1);\n walk(primary, depth);\n }\n if (rootNode) walk(rootNode, 0);\n return out;\n }\n\n function tag(text, cls) {\n const el = document.createElement('span');\n el.className = cls ? \\`rp-tag \\${cls}\\` : 'rp-tag';\n el.textContent = text;\n return el;\n }\n\n function makeNode({ node, depth, isHead, isRoot }) {\n const a = document.createElement('a');\n a.className = \\`rp-node\\${depth > 0 ? ' rp-branch' : ''}\\${isHead ? ' rp-head' : ''}\\`;\n if (isRoot) a.dataset.root = '';\n a.dataset.rev = node.id;\n a.href = \\`\\${BASE}/d/\\${root}/\\${node.id}\\`;\n a.style.marginLeft = \\`\\${depth * 16}px\\`;\n if (node.id === currentRev) a.classList.add('rp-current');\n\n const row = document.createElement('div');\n row.className = 'rp-node-row';\n if (depth > 0) {\n const elbow = document.createElement('span');\n elbow.className = 'rp-elbow';\n elbow.textContent = '└';\n row.appendChild(elbow);\n }\n const dot = document.createElement('span');\n dot.className = 'rp-dot';\n dot.textContent = '●';\n row.appendChild(dot);\n\n const body = document.createElement('div');\n body.className = 'rp-body';\n\n const idLine = document.createElement('div');\n idLine.className = 'rp-id';\n const idText = document.createElement('span');\n idText.textContent = node.id.slice(0, 7);\n idLine.appendChild(idText);\n if (isRoot) idLine.appendChild(tag('root', ''));\n if (isHead) idLine.appendChild(tag('head', 'rp-tag-head'));\n if (depth > 0) idLine.appendChild(tag('⑂ fork', 'rp-tag-fork'));\n\n const copy = document.createElement('button');\n copy.type = 'button';\n copy.className = 'rp-copy';\n copy.textContent = '⧉ copy id';\n copy.addEventListener('click', async (e) => {\n e.preventDefault();\n e.stopPropagation();\n try {\n await navigator.clipboard?.writeText(node.id);\n copy.textContent = 'copied';\n setTimeout(() => {\n copy.textContent = '⧉ copy id';\n }, 1200);\n } catch {\n /* clipboard unavailable — fail quietly (spec §6) */\n }\n });\n idLine.appendChild(copy);\n body.appendChild(idLine);\n\n const note = document.createElement('div');\n note.className = 'rp-note';\n note.textContent = node.note || '(no note)';\n note.title = node.note || '';\n body.appendChild(note);\n\n const time = document.createElement('div');\n time.className = 'rp-time';\n time.textContent = relTime(node.createdAt);\n body.appendChild(time);\n\n row.appendChild(body);\n a.appendChild(row);\n\n a.addEventListener('click', (e) => {\n // Modified clicks (⌘/Ctrl/Shift/middle) fall through to the browser,\n // which opens the pinned /d/<root>/<rev> href in a new tab.\n if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey)\n return;\n e.preventDefault();\n onNavigate(node.id);\n });\n return a;\n }\n\n function paint(nodes) {\n container.replaceChildren();\n const header = document.createElement('div');\n header.className = 'rp-header';\n header.textContent = 'Revisions';\n container.appendChild(header);\n if (nodes.length === 0) {\n const empty = document.createElement('div');\n empty.className = 'rp-empty';\n empty.textContent = 'no revisions yet';\n container.appendChild(empty);\n return;\n }\n const tree = document.createElement('div');\n tree.className = 'rp-tree';\n for (const item of order(nodes)) tree.appendChild(makeNode(item));\n container.appendChild(tree);\n }\n\n function showError(message) {\n container.replaceChildren();\n const el = document.createElement('div');\n el.className = 'rp-empty';\n el.textContent = message;\n container.appendChild(el);\n }\n\n async function refresh() {\n try {\n const res = await fetch(\\`\\${BASE}/api/design/\\${root}/revisions\\`);\n if (!res.ok) throw new Error(String(res.status));\n const nodes = await res.json();\n paint(nodes);\n onCount(nodes.length);\n } catch {\n showError(\"couldn't load history\");\n onCount(0);\n }\n }\n\n function setCurrent(rev) {\n currentRev = rev;\n for (const el of container.querySelectorAll('.rp-node')) {\n el.classList.toggle('rp-current', el.dataset.rev === rev);\n }\n }\n\n refresh();\n return { setCurrent, refresh };\n}\n`,\n\t\"studio.html\": `<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <title>IMG.LY CODESIGN MCP Workspace</title>\n <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\" />\n <!-- Mount prefix injected by the viewer router (router.ts). \"\" on the stdio\n listener, \"/viewer\" on the HTTP transport. Read by studio.js. -->\n <script>\n window.__VIEWER_BASE__ = '{{VIEWER_BASE}}';\n </script>\n <style>\n /* -------- reset --------\n\t\t\t Same palette + typography as the viewer SPA so the overview reads\n\t\t\t as part of the same product. See src/viewer/spa/index.html. */\n html,\n body {\n margin: 0;\n padding: 0;\n min-height: 100%;\n background: #1d1d1f;\n color: #eee;\n font-family: -apple-system, system-ui, sans-serif;\n }\n\n /* -------- sticky frosted strip --------\n\t\t\t Same frosted-glass material as the viewer pills + history panel:\n\t\t\t translucent dark + backdrop-blur + 1px hairline border. Sticky so\n\t\t\t the title stays visible while scrolling. */\n #bar {\n position: sticky;\n top: 0;\n z-index: 10;\n padding: 14px 24px;\n background: rgba(20, 20, 22, 0.72);\n backdrop-filter: blur(12px);\n -webkit-backdrop-filter: blur(12px);\n border-bottom: 1px solid rgba(255, 255, 255, 0.08);\n }\n #bar .title {\n font:\n 600 14px -apple-system,\n system-ui,\n sans-serif;\n letter-spacing: 0.02em;\n color: #e8e8e8;\n }\n\n /* -------- design grid -------- */\n .grid {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));\n gap: 16px;\n padding: 24px;\n }\n\n .empty {\n padding: 24px 4px;\n font-size: 13px;\n color: #888;\n }\n .empty code {\n font-family: ui-monospace, monospace;\n background: rgba(255, 255, 255, 0.05);\n border: 1px solid rgba(255, 255, 255, 0.08);\n border-radius: 4px;\n padding: 1px 6px;\n font-size: 12px;\n color: #cfcfcf;\n }\n\n /* -------- card --------\n\t\t\t Frosted-glass card, same materials as the history panel rows.\n\t\t\t Hover lifts the card and brightens the border + shadow to match the\n\t\t\t pill-hover pattern in the viewer chrome. */\n .card {\n display: block;\n text-decoration: none;\n color: inherit;\n background: rgba(20, 20, 22, 0.72);\n backdrop-filter: blur(12px);\n -webkit-backdrop-filter: blur(12px);\n border: 1px solid rgba(255, 255, 255, 0.08);\n border-radius: 12px;\n box-shadow: 0 4px 14px rgba(0, 0, 0, 0.25);\n overflow: hidden;\n transition:\n transform 120ms ease-out,\n border-color 120ms ease-out,\n box-shadow 120ms ease-out;\n }\n .card:hover {\n transform: translateY(-2px);\n border-color: rgba(255, 255, 255, 0.15);\n box-shadow: 0 8px 22px rgba(0, 0, 0, 0.35);\n }\n .card img {\n display: block;\n width: 100%;\n aspect-ratio: 4/3;\n object-fit: cover;\n background: #000;\n }\n .card .meta {\n padding: 12px 14px;\n }\n .card .title {\n font:\n 600 13px -apple-system,\n system-ui,\n sans-serif;\n color: #eee;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n .card .updated {\n margin-top: 4px;\n font-size: 11px;\n color: #888;\n }\n </style>\n </head>\n <body>\n <header id=\"bar\">\n <span class=\"title\">IMG.LY CODESIGN MCP Workspace</span>\n </header>\n <div id=\"grid\" class=\"grid\"></div>\n <script type=\"module\" src=\"{{VIEWER_BASE}}/static/studio.js\"></script>\n </body>\n</html>\n`,\n\t\"studio.js\": `// Workspace overview landing page. Fetches /api/designs and renders one card\n// per design (keyed by root revision). Re-renders on tab focus and a 10s poll\n// so card links and thumbnails never go stale after the agent edits.\n//\n// DOM is built with createElement + textContent (never innerHTML for\n// design-supplied strings) so an author-controlled title can't escape into\n// HTML. Same pattern as viewer.js.\n\n// Mount prefix set by studio.html (\"\" on stdio, \"/viewer\" on HTTP); card\n// links, thumbnails, and the designs fetch are built relative to it. See\n// router.ts.\nconst BASE = window.__VIEWER_BASE__ ?? '';\n\nconst grid = document.getElementById('grid');\n\nfunction buildCard(d) {\n const card = document.createElement('a');\n card.className = 'card';\n card.href = \\`\\${BASE}/d/\\${d.rootRevision}\\`;\n\n const img = document.createElement('img');\n img.src = \\`\\${BASE}/snapshot/\\${d.latestLeaf}/thumbnail\\`;\n img.alt = '';\n img.addEventListener('error', () => {\n img.style.display = 'none';\n });\n card.appendChild(img);\n\n const meta = document.createElement('div');\n meta.className = 'meta';\n\n const title = document.createElement('div');\n title.className = 'title';\n title.textContent = d.title || 'Untitled';\n meta.appendChild(title);\n\n const updated = document.createElement('div');\n updated.className = 'updated';\n updated.textContent = \\`updated \\${new Date(d.updatedAt).toLocaleString()}\\`;\n meta.appendChild(updated);\n\n card.appendChild(meta);\n return card;\n}\n\nfunction renderEmptyText(text) {\n const div = document.createElement('div');\n div.className = 'empty';\n div.textContent = text;\n grid.replaceChildren(div);\n}\n\nfunction renderFirstRunEmpty() {\n const div = document.createElement('div');\n div.className = 'empty';\n div.appendChild(document.createTextNode('No designs yet — your first '));\n const code = document.createElement('code');\n code.textContent = 'edit({ parent: null, ... })';\n div.appendChild(code);\n div.appendChild(document.createTextNode(' will land here.'));\n grid.replaceChildren(div);\n}\n\nasync function render() {\n const res = await fetch(\\`\\${BASE}/api/designs\\`);\n if (!res.ok) {\n renderEmptyText(\\`failed to load designs: \\${res.status}\\`);\n return;\n }\n const designs = await res.json();\n if (designs.length === 0) {\n renderFirstRunEmpty();\n return;\n }\n grid.replaceChildren(...designs.map(buildCard));\n}\n\n// \\`render()\\` rejects on a network error; every caller logs it rather than\n// leaving an unhandled promise rejection.\nawait render().catch(console.error);\ndocument.addEventListener('visibilitychange', () => {\n if (!document.hidden) render().catch(console.error);\n});\nsetInterval(() => render().catch(console.error), 10000);\n`,\n\t\"viewer.js\": `// Read-only CE.SDK viewer with a revision-history side panel.\n// /d/<root> LIVE — opens at the design head, follows new revisions\n// /d/<root>/<rev> PINNED — opens frozen at <rev>\n//\n// Chrome is floating pill clusters in the corners; the history panel is a\n// detached card on the right.\n// See: docs/superpowers/specs/2026-05-24-viewer-ui-refresh-design.md\n//\n// Navigation is client-side: clicking a panel node (or the \"Go Live →\"\n// affordance) swaps the canvas and rewrites the URL via the History API — one\n// CE.SDK instance for the life of the tab. The SSE connection is held open\n// in BOTH modes so the panel's tree always stays fresh; it moves the canvas\n// only while \\`following\\`.\n\n// Mount prefix set by index.html (\"\" on stdio, \"/viewer\" on HTTP). Every\n// API/asset/history URL below is built relative to it so the SPA works under\n// any mount path. See router.ts.\nconst BASE = window.__VIEWER_BASE__ ?? '';\n\n// Path of the current page with the mount prefix stripped, so PATH_RE (which\n// is written relative to the SPA root) matches under \"/viewer\" too.\nconst relPath = () =>\n location.pathname.startsWith(BASE)\n ? location.pathname.slice(BASE.length)\n : location.pathname;\n\nconst { createRevisionPanel } = await import(\n \\`\\${BASE}/static/revision-panel.js\\`\n);\n\nconst PATH_RE = /^\\\\/d\\\\/([0-9a-f]{12})(?:\\\\/([0-9a-f]{12}))?$/;\nconst titleEl = document.getElementById('title');\nconst barRightEl = document.getElementById('bar-right');\nconst panelEl = document.getElementById('revision-panel');\n\nconst match = relPath().match(PATH_RE);\n\nif (!match) {\n titleEl.textContent = 'invalid URL';\n} else {\n const root = match[1];\n\n // Server-supplied license + resource base, fetched before bootstrapping\n // CE.SDK. CE.SDK validates the license regardless of role; \\`resourceBase\\`\n // is where workspace:// asset URIs resolve.\n const cfgRes = await fetch(\\`\\${BASE}/api/config\\`);\n if (!cfgRes.ok) throw new Error(\\`/api/config failed: \\${cfgRes.status}\\`);\n const { license, resourceBase, cesdkEngineUrl } = await cfgRes.json();\n\n const cesdkMod = await import(cesdkEngineUrl);\n const CreativeEditorSDK = cesdkMod.default ?? cesdkMod.CreativeEditorSDK;\n const instance = await CreativeEditorSDK.create(\n document.getElementById('cesdk'),\n {\n license,\n role: 'Viewer',\n theme: 'dark',\n // The refresh moved all chrome out of CE.SDK and into our own floating\n // pills + history panel — back / title / LIVE / Go Live / hash / History.\n // CE.SDK's own nav + dock would render in the same physical space and,\n // because #cesdk has no stacking context, their internal z-indexes\n // outrank our z:10 pills, hiding them. \\`ui.hide: true\\` disables the\n // CE.SDK chrome entirely; users keep mousewheel zoom + drag-to-pan via\n // the engine, which is unaffected.\n ui: { hide: true }\n }\n );\n\n // Resolve workspace:// asset URIs to the /r/ HTTP route.\n const absResourceBase = location.origin + resourceBase;\n instance.engine.editor.setURIResolver((uri, defaultResolver) =>\n uri.startsWith('workspace://')\n ? absResourceBase + uri.slice('workspace://'.length)\n : defaultResolver(uri)\n );\n\n // --- runtime state ----------------------------------------------------\n let head = null; // the design's latest leaf\n let currentRev = null; // the revision currently on canvas\n let following = false; // true → the canvas tracks head as SSE arrives\n let newCount = 0; // revisions arrived since following stopped\n let panelOpen = false; // whether the history panel is visible\n\n async function loadRevision(rev) {\n const res = await fetch(\\`\\${BASE}/snapshot/\\${rev}\\`);\n if (!res.ok) throw new Error(\\`snapshot \\${rev} \\${res.status}\\`);\n const snap = await res.json();\n await instance.engine.scene.loadFromString(snap.scene);\n titleEl.textContent = snap.title ?? 'Untitled';\n }\n\n // Builds the right-side cluster from current state. Idempotent — called on\n // every mode flip, panel toggle, and newCount change. Uses createElement +\n // textContent everywhere (never innerHTML) so any author-supplied strings\n // can't escape into HTML.\n function renderRightCluster() {\n const nodes = [];\n\n if (following) {\n // LIVE — read-only green-dot pill.\n const live = document.createElement('span');\n live.className = 'pill live';\n live.textContent = 'LIVE';\n nodes.push(live);\n } else {\n // PINNED — hash chip (click-to-copy) + Go Live (+ count badge).\n const hash = document.createElement('button');\n hash.type = 'button';\n hash.className = 'pill hash';\n hash.title = 'copy revision id';\n hash.textContent = currentRev ? currentRev.slice(0, 7) : '';\n hash.addEventListener('click', async () => {\n if (!currentRev) return;\n try {\n await navigator.clipboard?.writeText(currentRev);\n } catch {\n /* clipboard unavailable — fail quietly (matches .rp-copy) */\n }\n });\n nodes.push(hash);\n\n const golive = document.createElement('button');\n golive.type = 'button';\n golive.className = 'pill golive';\n golive.appendChild(document.createTextNode('Go Live →'));\n if (newCount > 0) {\n const count = document.createElement('span');\n count.className = 'count';\n count.textContent = String(newCount);\n golive.appendChild(count);\n }\n golive.addEventListener('click', (e) => {\n if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey)\n return;\n e.preventDefault();\n if (head) show(head, { following: true, historyMode: 'push' });\n });\n nodes.push(golive);\n }\n\n const history = document.createElement('button');\n history.type = 'button';\n history.className = \\`pill history\\${panelOpen ? ' active' : ''}\\`;\n history.textContent = 'History';\n history.addEventListener('click', () => setPanelOpen(!panelOpen));\n nodes.push(history);\n\n barRightEl.replaceChildren(...nodes);\n }\n\n function setPanelOpen(open) {\n panelOpen = open;\n document.body.classList.toggle('panel-open', open);\n renderRightCluster();\n }\n\n // The single navigation primitive. Loads \\`rev\\` onto the canvas, sets the\n // LIVE/PINNED state, and writes the URL. \\`historyMode\\`: \"push\" adds a\n // history entry, \"replace\" rewrites in place, \"none\" leaves history alone\n // (used by popstate, where the browser already changed the URL).\n async function show(rev, { following: nextFollowing, historyMode }) {\n try {\n await loadRevision(rev);\n currentRev = rev;\n following = nextFollowing;\n if (following) newCount = 0;\n const url = following ? \\`\\${BASE}/d/\\${root}\\` : \\`\\${BASE}/d/\\${root}/\\${rev}\\`;\n if (historyMode === 'push') window.history.pushState({ rev }, '', url);\n else if (historyMode === 'replace')\n window.history.replaceState({ rev }, '', url);\n panel.setCurrent(rev);\n } catch {\n titleEl.textContent = 'unknown revision';\n }\n renderRightCluster();\n }\n\n // --- revision panel ---------------------------------------------------\n const panel = createRevisionPanel({\n container: panelEl,\n root,\n // Clicking a node pins it — unless it is the head, which is LIVE.\n onNavigate: (rev) =>\n show(rev, { following: rev === head, historyMode: 'push' }),\n onCount: () => {\n /* The History pill no longer carries the total count; the unseen-since-\n\t\t\t pin count lives on Go Live and is driven by \\`newCount\\` directly. */\n }\n });\n\n // --- keyboard ---------------------------------------------------------\n window.addEventListener('keydown', (e) => {\n if (e.key === 'Escape' && panelOpen) setPanelOpen(false);\n });\n\n // --- bootstrap --------------------------------------------------------\n let latestLeaf;\n try {\n const res = await fetch(\\`\\${BASE}/api/design/\\${root}\\`);\n if (!res.ok) throw new Error(String(res.status));\n latestLeaf = (await res.json()).latestLeaf;\n } catch {\n titleEl.textContent = 'unknown design';\n }\n\n if (latestLeaf !== undefined) {\n head = latestLeaf;\n const pinned = match[2]; // undefined → LIVE\n // Mode is the URL shape: a pinned URL stays pinned even if <rev> is head.\n await show(pinned ?? head, {\n following: pinned === undefined,\n historyMode: 'replace'\n });\n\n // SSE — held open for the life of the tab, in both modes. Every event\n // refreshes the panel's tree; it moves the canvas only while following.\n const es = new EventSource(\\`\\${BASE}/sse/\\${root}\\`);\n es.onmessage = (ev) => {\n let payload;\n try {\n payload = JSON.parse(ev.data);\n } catch (err) {\n console.error('sse parse failed', err);\n return;\n }\n if (!payload || typeof payload.revision !== 'string') return;\n head = payload.revision;\n panel.refresh();\n if (following) {\n show(head, { following: true, historyMode: 'replace' });\n } else {\n newCount += 1;\n renderRightCluster();\n }\n };\n }\n\n // Browser back/forward: re-resolve the URL shape and sync, without writing\n // history (the entry already changed).\n window.addEventListener('popstate', () => {\n const m = relPath().match(PATH_RE);\n if (!m || m[1] !== root) return;\n const pinned = m[2];\n show(pinned ?? head, {\n following: pinned === undefined,\n historyMode: 'none'\n });\n });\n}\n`,\n};\n"]}
1
+ {"version":3,"file":"spa-assets.generated.js","sourceRoot":"","sources":["../../src/viewer/spa-assets.generated.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,MAAM,CAAC,MAAM,UAAU,GAA2B;IACjD,WAAW,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsvBb;IACA,YAAY,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0Cd;IACA,kBAAkB,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8DpB;CACA,CAAC","sourcesContent":["// AUTO-GENERATED by scripts/generate-spa-assets.mjs. Do not edit by hand.\nexport const SPA_ASSETS: Record<string, string> = {\n\t\"editor.js\": `// The CoDesign editor — the single browser surface for a design (CODE-74).\n// /d/<root> LIVE — opens at the design head, follows new revisions\n// /d/<root>/<rev> PINNED — opens frozen at <rev>\n//\n// The canvas is FULLY EDITABLE from the start; the read-only viewer and the\n// detached \"Edit a copy\" tab are gone. Edits are LOCAL TO THE TAB: nothing\n// writes back to the workspace or notifies the agent — the export actions in\n// the navigation bar (archive / image / PDF) are the only way work leaves\n// this tab. While the tab is untouched, /d/<root> live-follows the head over\n// SSE; the FIRST local edit pauses following, pins the URL, and shows an\n// infinite \"not saved back\" warning (see the dirty tracking below).\n// See docs/specs/2026-07-10-merged-editor-design.md.\n//\n// CE.SDK ships NO default UI since v1.69: \\`create()\\` renders only the bare\n// canvas and every UI area starts EMPTY. The canonical design-editor UI is the\n// \\`DesignEditorConfig\\` plugin from \\`@cesdk/core-configs-web/design-editor\\`,\n// but that package is NOT published to npm or the CDN, so it can't be imported\n// in this bundler-less browser module that loads CE.SDK straight from the CDN.\n// \\`configureDesignEditorUI()\\` below therefore mirrors what \\`DesignEditorConfig\\`\n// does — the exact features, actions and component orders from IMG.LY's Design\n// Editor starter kit — via the public Order API, trimmed for this surface: the\n// asset-library dock IS present, with Text / Shapes / Stickers fed by CE.SDK's\n// default asset sources (no demo/stock media, no uploads); video/audio\n// features stay off (so a static design never shows a timeline).\n// See docs/specs/2026-07-07-codesign-detached-editor-design.md.\n\n// Mount prefix set by index.html (\"\" on stdio, \"/viewer\" on HTTP). See router.ts.\nconst BASE = window.__VIEWER_BASE__ ?? '';\n\nconst { order, relTime, fetchRevisions } = await import(\n \\`\\${BASE}/static/revision-data.js\\`\n);\n\nconst relPath = () =>\n location.pathname.startsWith(BASE)\n ? location.pathname.slice(BASE.length)\n : location.pathname;\n\nconst PATH_RE = /^\\\\/d\\\\/([0-9a-f]{12})(?:\\\\/([0-9a-f]{12}))?$/;\nconst statusEl = document.getElementById('status');\n\nconst HISTORY_PANEL = 'codesign.revisionHistory';\n\nconst match = relPath().match(PATH_RE);\n\nif (!match) {\n statusEl.textContent = 'invalid URL';\n} else {\n const root = match[1];\n\n // Server-supplied license + resource base + engine URL.\n const cfgRes = await fetch(\\`\\${BASE}/api/config\\`);\n if (!cfgRes.ok) throw new Error(\\`/api/config failed: \\${cfgRes.status}\\`);\n const { license, resourceBase, cesdkEngineUrl, defaultAssetsBaseUrl } =\n await cfgRes.json();\n\n const cesdkMod = await import(cesdkEngineUrl);\n const CreativeEditorSDK = cesdkMod.default ?? cesdkMod.CreativeEditorSDK;\n\n // Engine WASM/data (\\`baseURL\\`) and UI icons/CSS (\\`ui.baseURL\\`) both load from\n // the CDN we imported the SDK from (\\`…/index.js\\` → \\`…/assets[/ui/]\\`), so they\n // track the engine version and the release-vs-nightly path shape.\n const assetBase = cesdkEngineUrl.replace(/\\\\/index\\\\.js$/, '/assets');\n const cesdk = await CreativeEditorSDK.create(\n document.getElementById('cesdk'),\n {\n license,\n baseURL: assetBase,\n ui: { baseURL: assetBase + '/ui/' },\n theme: 'dark'\n }\n );\n const engine = cesdk.engine;\n\n configureDesignEditorUI(cesdk);\n\n // Register CE.SDK's DEFAULT asset sources — shapes, stickers, typefaces,\n // color palettes, LUT filters/effects, page & crop presets — never the\n // demo ones (no stock images/videos/audio). The manifest base URL comes\n // from /api/config so the browser and the headless engine resolve the\n // same manifests (IMGLY_DEFAULT_ASSETS_BASE_URL-aware). \\`ly.img.text\\` is\n // excluded exactly like the node side (its manifest is missing from the\n // CDN manifest root and one 404 rejects the whole call). Page presets\n // ride along here — the former page-presets plugin registered the same\n // source id and the two cannot coexist (colliding registrations reject\n // the whole call), so the plugin is gone; the \\`ly.img.pagePresets\\` LIBRARY\n // ENTRY the Resize Page panel reads is wired in configureDesignEditorUI.\n // Best-effort and isolated in its own try/catch: a down/misconfigured\n // IMGLY_DEFAULT_ASSETS_BASE_URL leaves the editor usable with an empty\n // shapes/stickers/typefaces/LUT-filters/presets library, and must NOT take\n // down the engine-CDN sources registered below (different host, see that\n // block's comment).\n try {\n await cesdk.addDefaultAssetSources({\n baseURL: defaultAssetsBaseUrl,\n excludeAssetSourceIds: ['ly.img.text']\n });\n } catch (err) {\n console.error(\n 'codesign: asset-source registration failed — asset library will be empty',\n err\n );\n }\n\n // The engine-assets manifests below cover what the default manifest root\n // does NOT carry (each 404s under <defaultAssetsBaseUrl>/<id>/content.json\n // but is published under <engine>/assets/<id>/content.json, where the CDN\n // plugins bundle used to load them from):\n // - the stock \\`ly.img.text\\` library entry queries the four text sources —\n // with none registered the Text dock panel shows \"No Elements\";\n // - the appearance Filter panel hard-queries the combined \\`ly.img.filter\\`\n // source (not filter.lut/filter.duotone) and shows an infinite spinner\n // while that id is unregistered;\n // - the stock Shapes entry titles its group headers with\n // \\`libraries.ly.img.vector.shape.<group>.label\\`, and the UI bundle only\n // ships translations for THIS source's groups (filled/outline/gradient/\n // image/abstract-*) — the default root's vectorpath source has verbose\n // category group ids that render as raw i18n keys.\n // These six are registered in parallel via Promise.allSettled, each\n // isolated from the others' failures — one 404 must not take out the rest\n // of the library. This block runs UNCONDITIONALLY, regardless of whether\n // addDefaultAssetSources above succeeded: the two target different hosts —\n // a down/misconfigured IMGLY_DEFAULT_ASSETS_BASE_URL must not blank the\n // Text/Filter panels whose manifests come from the (independent) engine\n // CDN. No try/catch needed here: Promise.allSettled never rejects.\n const engineAssetSourceIds = [\n 'ly.img.text',\n 'ly.img.text.styles',\n 'ly.img.text.components',\n 'ly.img.text.curves',\n 'ly.img.filter',\n 'ly.img.vector.shape'\n ];\n const results = await Promise.allSettled(\n engineAssetSourceIds.map((id) =>\n engine.asset.addLocalAssetSourceFromJSONURI(\n \\`\\${assetBase}/\\${id}/content.json\\`\n )\n )\n );\n results.forEach((result, index) => {\n if (result.status === 'rejected') {\n console.error(\n 'codesign: failed to register asset source',\n engineAssetSourceIds[index],\n result.reason\n );\n }\n });\n\n // Resolve workspace:// asset URIs to the /r/ HTTP route.\n const absResourceBase = location.origin + resourceBase;\n engine.editor.setURIResolver((uri, defaultResolver) =>\n uri.startsWith('workspace://')\n ? absResourceBase + uri.slice('workspace://'.length)\n : defaultResolver(uri)\n );\n\n // --- runtime state ----------------------------------------------------\n let head = null; // the design's latest leaf\n let currentRev = null; // the revision currently on canvas\n let following = false; // true → the canvas tracks head as SSE arrives\n let dirty = false; // true → a local edit happened since the last load\n let loading = false; // true while show() swaps the scene programmatically\n let warnNotification = null; // id of the \"not saved back\" notification\n let newCount = 0; // revisions arrived since following stopped\n let orderedRevisions = []; // order(fetchRevisions(...)) — drives the panel\n\n // --- CE.SDK re-render bridge ------------------------------------------\n // Builder components re-render when a state() value they read changes. Our\n // editor state lives in the plain variables above, which CE.SDK's reactor\n // can't see, so each reactive component reads a monotonic \\`uiVersion\\` from\n // state() purely to subscribe, and invalidateUI() bumps every mounted\n // component to force a coherent re-read. The title, too, is bridged — CE.SDK's\n // builder components don't re-render on engine name changes alone (see\n // codesign.title below). Calls are wrapped because a component that has\n // since unmounted (e.g. a closed panel) throws on setValue.\n let uiVersion = 0;\n const bump = new Map(); // componentKey -> setValue, refreshed on each render\n function invalidateUI() {\n uiVersion += 1;\n for (const setVersion of bump.values()) {\n try {\n setVersion(uiVersion);\n } catch {\n /* component unmounted — ignore */\n }\n }\n }\n\n // Subscribe a component to invalidateUI(). Reading \\`version.value\\` (not just\n // grabbing setValue) is what registers the reactive dependency — CE.SDK only\n // re-renders a component when a state value it *read* during render changes,\n // so invalidateUI()'s setValue bumps are no-ops unless the value was read.\n function subscribeToUI(state, key) {\n const version = state('v', 0);\n bump.set(key, version.setValue);\n return version.value;\n }\n\n async function loadRevision(rev) {\n const res = await fetch(\\`\\${BASE}/snapshot/\\${rev}\\`);\n if (!res.ok) throw new Error(\\`snapshot \\${rev} \\${res.status}\\`);\n const snap = await res.json();\n await engine.scene.loadFromString(snap.scene);\n // Feed the nav-bar title component via engine reactivity.\n const scene = engine.scene.get();\n if (scene != null) engine.block.setName(scene, snap.title ?? 'Untitled');\n // Fit the page in the viewport and keep it fitted on resize.\n const page = engine.block.findByType('page')[0];\n if (page != null)\n engine.scene.enableZoomAutoFit(page, 'Both', 40, 40, 40, 40);\n }\n\n // The single navigation primitive. Loads \\`rev\\`, sets LIVE/PINNED, writes the\n // URL. historyMode: \"push\" adds a history entry, \"replace\" rewrites in place,\n // \"none\" leaves history alone (popstate already changed the URL).\n // Every arrival here is a fresh scene: it clears \\`dirty\\` and dismisses the\n // warning. Callers navigating away from local edits must confirmDiscard()\n // FIRST — show() itself never asks.\n async function show(rev, { following: nextFollowing, historyMode }) {\n try {\n // Clear any prior error banner so a later successful load self-heals it.\n statusEl.textContent = '';\n loading = true;\n try {\n await loadRevision(rev);\n } finally {\n loading = false;\n }\n if (dirty) {\n dirty = false;\n dismissEditWarning();\n }\n currentRev = rev;\n following = nextFollowing;\n if (following) newCount = 0;\n const url = following ? \\`\\${BASE}/d/\\${root}\\` : \\`\\${BASE}/d/\\${root}/\\${rev}\\`;\n if (historyMode === 'push') window.history.pushState({ rev }, '', url);\n else if (historyMode === 'replace')\n window.history.replaceState({ rev }, '', url);\n } catch {\n statusEl.textContent = 'unknown revision';\n }\n invalidateUI();\n }\n\n async function refreshRevisions() {\n try {\n orderedRevisions = order(await fetchRevisions(BASE, root));\n } catch {\n orderedRevisions = [];\n }\n invalidateUI();\n }\n\n // --- dirty tracking -----------------------------------------------------\n // The first REAL local edit detaches the tab from live-follow: edits are\n // local-only (no write path back to the workspace), so the canvas must stop\n // being swapped out underneath them. 'Updated' events during a programmatic\n // load (loading === true) and the post-load baseline (canUndo() === false)\n // are not edits. Undoing back to the baseline does NOT clear \\`dirty\\` — once\n // touched, the tab stays detached until explicit navigation via show().\n function dismissEditWarning() {\n if (warnNotification == null) return;\n try {\n cesdk.ui.dismissNotification?.(warnNotification);\n } catch {\n /* notification already gone — ignore */\n }\n warnNotification = null;\n }\n\n engine.editor.onHistoryUpdatedWithKind((kind) => {\n if (kind !== 'Updated' || loading || dirty) return;\n if (!engine.editor.canUndo()) return;\n dirty = true;\n following = false;\n // The URL mirrors following-state everywhere else (LIVE = /d/<root>,\n // PINNED = /d/<root>/<rev>); a detached-by-edit tab is pinned.\n if (currentRev != null)\n window.history.replaceState(\n { rev: currentRev },\n '',\n \\`\\${BASE}/d/\\${root}/\\${currentRev}\\`\n );\n warnNotification = cesdk.ui.showNotification({\n type: 'warning',\n duration: 'infinite',\n message:\n \"Your edits aren't saved back or seen by the assistant — export to keep them.\"\n });\n invalidateUI();\n });\n\n // Local edits are throwaway: every navigation that would replace the scene\n // asks before discarding them.\n function confirmDiscard() {\n return (\n !dirty ||\n window.confirm(\n 'Discard your local edits? They are not saved back to the workspace.'\n )\n );\n }\n\n // Closing the tab with unexported local edits prompts (browsers show their\n // own generic message; preventDefault is what arms it).\n window.addEventListener('beforeunload', (ev) => {\n if (!dirty) return;\n ev.preventDefault();\n ev.returnValue = '';\n });\n\n // --- nav bar components ------------------------------------------------\n installCodesignChrome();\n\n function installCodesignChrome() {\n // Panel title (registered here so the toggle's target exists and the i18n\n // title is set once; the toggle itself is hidden for now).\n cesdk.i18n.setTranslations({\n en: { [\\`panel.\\${HISTORY_PANEL}\\`]: 'Revisions' }\n });\n\n // Live-follow status: ● LIVE while following; Go Live (+ unseen count) when\n // pinned or detached by a local edit. Reads module state → bridged via the\n // version bump.\n cesdk.ui.registerComponent('codesign.liveStatus', ({ builder, state }) => {\n subscribeToUI(state, 'liveStatus');\n if (following) {\n builder.Button('codesign.liveStatus.live', {\n label: '● LIVE',\n variant: 'plain',\n isDisabled: true\n });\n } else {\n builder.Button('codesign.liveStatus.goLive', {\n label: newCount > 0 ? \\`Go Live (\\${newCount})\\` : 'Go Live',\n variant: 'plain',\n onClick: () => {\n if (head && confirmDiscard())\n show(head, { following: true, historyMode: 'push' });\n }\n });\n }\n });\n\n // Document title. The built-in \\`ly.img.title.navigationBar\\` does NOT read\n // engine.block.getName() reactively — it only renders a static string\n // passed via \\`ui.elements.navigation.title\\` at create() time or a\n // per-instance \\`payload.title\\` in the order array (neither of which we\n // set), so it never renders anything here. Read the scene name ourselves\n // instead, bridged through the same state()/invalidateUI() mechanism as\n // codesign.liveStatus so it refreshes on every show(). Refreshes on\n // invalidateUI() bumps; a rename via document settings while the tab is\n // already dirty stays stale until the next bump — accepted cosmetic\n // staleness.\n cesdk.ui.registerComponent('codesign.title', ({ builder, state }) => {\n subscribeToUI(state, 'title');\n const scene = engine.scene.get();\n const title = scene != null ? engine.block.getName(scene) : '';\n if (title) builder.Heading('codesign.title.heading', { content: title });\n });\n\n // History toggle. isActive rides CE.SDK's own reactive panel-open state.\n cesdk.ui.registerComponent('codesign.historyToggle', ({ builder }) => {\n const open = cesdk.ui.isPanelOpen(HISTORY_PANEL);\n builder.Button('codesign.historyToggle.button', {\n label: 'History',\n isActive: open,\n onClick: () =>\n open\n ? cesdk.ui.closePanel(HISTORY_PANEL)\n : cesdk.ui.openPanel(HISTORY_PANEL, {\n position: 'right',\n floating: true\n })\n });\n });\n\n // Revision history — native builder panel. Reuses revision-data.js's order()\n // output; reads module state → bridged via the version bump. Rows are\n // buttons: indent = depth, label = short id · note · time (+ head/root/fork).\n cesdk.ui.registerPanel(HISTORY_PANEL, ({ builder, state }) => {\n subscribeToUI(state, 'historyPanel');\n builder.Section('codesign.history.section', {\n title: following ? 'Revisions · LIVE' : 'Revisions · PINNED',\n children: () => {\n if (orderedRevisions.length === 0) {\n builder.Text('codesign.history.empty', {\n content: 'No revisions yet'\n });\n return;\n }\n for (const { node, depth, isHead, isRoot } of orderedRevisions) {\n const indent = depth > 0 ? ' '.repeat(depth) + '↳ ' : '';\n const marker = isHead\n ? ' · head'\n : isRoot\n ? ' · root'\n : depth > 0\n ? ' · fork'\n : '';\n const note = node.note ? \\` · \\${node.note}\\` : '';\n builder.Button(\\`codesign.history.\\${node.id}\\`, {\n label: \\`\\${indent}\\${node.id.slice(0, 7)}\\${note}\\${marker} · \\${relTime(node.createdAt)}\\`,\n variant: 'plain',\n isActive: node.id === currentRev,\n onClick: () => {\n if (!confirmDiscard()) return;\n show(node.id, {\n following: node.id === head,\n historyMode: 'push'\n });\n }\n });\n }\n }\n });\n });\n\n // Final navigation bar: the design-editor chrome (document settings,\n // undo/redo, title, zoom, preview) plus the live-follow status. This\n // REPLACES the order configureDesignEditorUI set, so the export dropdown\n // must be (re-)inserted after it.\n cesdk.ui.setComponentOrder({ in: 'ly.img.navigation.bar' }, [\n 'ly.img.documentSettings.navigationBar',\n 'ly.img.undoRedo.navigationBar',\n 'ly.img.spacer',\n 'codesign.title',\n 'ly.img.spacer',\n 'codesign.liveStatus',\n 'ly.img.zoom.navigationBar',\n 'ly.img.preview.navigationBar'\n // 'codesign.historyToggle' — hidden for now; the panel + refreshRevisions\n // machinery stays wired, so re-add this line to bring History back.\n ]);\n\n // Export dropdown. The scene references workspace:// assets, so exporting\n // the bare scene (or \"Save\") would produce broken links — the self-contained\n // archive (.zip, assets embedded) is the default/prominent action, with\n // image and PDF in the overflow. Each child carries an explicit handler\n // that ends in a browser download — the only way work leaves this tab.\n cesdk.ui.insertOrderComponent(\n { in: 'ly.img.navigation.bar', position: 'end' },\n {\n id: 'ly.img.actions.navigationBar',\n children: [\n {\n id: 'ly.img.exportArchive.navigationBar',\n onClick: async () => {\n const archive = await engine.scene.saveToArchive();\n await cesdk.utils.downloadFile(archive, 'application/zip');\n }\n },\n {\n id: 'ly.img.exportImage.navigationBar',\n onClick: async () => {\n const { blobs, options } = await cesdk.utils.export({\n mimeType: 'image/png'\n });\n await cesdk.utils.downloadFile(blobs[0], options.mimeType);\n }\n },\n {\n id: 'ly.img.exportPDF.navigationBar',\n onClick: async () => {\n const { blobs, options } = await cesdk.utils.export({\n mimeType: 'application/pdf'\n });\n await cesdk.utils.downloadFile(blobs[0], options.mimeType);\n }\n }\n ]\n }\n );\n }\n\n // --- bootstrap --------------------------------------------------------\n let latestLeaf;\n try {\n const res = await fetch(\\`\\${BASE}/api/design/\\${root}\\`);\n if (!res.ok) throw new Error(String(res.status));\n latestLeaf = (await res.json()).latestLeaf;\n } catch {\n statusEl.textContent = 'unknown design';\n }\n\n if (latestLeaf !== undefined) {\n head = latestLeaf;\n const pinned = match[2]; // undefined → LIVE\n await show(pinned ?? head, {\n following: pinned === undefined,\n historyMode: 'replace'\n });\n await refreshRevisions();\n\n // SSE — held open for the tab's life, in every state. It moves the canvas\n // only while following (a dirty tab is never following).\n const es = new EventSource(\\`\\${BASE}/sse/\\${root}\\`);\n es.onmessage = (ev) => {\n let payload;\n try {\n payload = JSON.parse(ev.data);\n } catch (err) {\n console.error('sse parse failed', err);\n return;\n }\n if (!payload || typeof payload.revision !== 'string') return;\n head = payload.revision;\n refreshRevisions();\n if (following) {\n show(head, { following: true, historyMode: 'replace' });\n } else {\n newCount += 1;\n invalidateUI();\n }\n };\n }\n\n // Browser back/forward: re-resolve the URL shape and sync, without writing\n // history (the entry already changed). With unexported local edits, ask\n // first — on cancel, restore the URL of the revision still on canvas.\n window.addEventListener('popstate', () => {\n const m = relPath().match(PATH_RE);\n if (!m || m[1] !== root) return;\n if (!confirmDiscard()) {\n const url = following\n ? \\`\\${BASE}/d/\\${root}\\`\n : \\`\\${BASE}/d/\\${root}/\\${currentRev}\\`;\n window.history.pushState({ rev: currentRev }, '', url);\n return;\n }\n const pinned = m[2];\n show(pinned ?? head, {\n following: pinned === undefined,\n historyMode: 'none'\n });\n });\n}\n\n// Install the design-editor UI. This mirrors IMG.LY's \\`DesignEditorConfig\\`\n// plugin (features → actions → component orders), which we can't import here\n// (see the file header), reproduced verbatim from the Design Editor starter kit\n// and trimmed for this surface:\n// - the dock / asset library IS enabled (features \\`ly.img.dock\\`,\n// \\`ly.img.library.panel\\`) with a Text / Shapes / Stickers dock order set\n// below, fed by the default asset sources registered in the boot code;\n// - no video/audio features enabled (so a static design has no timeline);\n// - the Trim and Vector inspector edit modes are omitted because their\n// features (video trimming, vector-path editing) aren't enabled here, so\n// those modes are unreachable;\n// - the Document inspector's \"Resize Page\" panel is fed by the\n// \\`ly.img.pagePresets\\` entry wired below to the page-presets source that\n// rides along with the default asset sources (see the boot code);\n// - the navigation-bar order + export dropdown live in\n// installCodesignChrome() (they include codesign components registered\n// after the resetEditor() below).\nfunction configureDesignEditorUI(cesdk) {\n // Clear the empty default configuration, then apply ours. \\`resetEditor()\\` can\n // drop the theme passed to \\`create()\\`, so re-apply it.\n cesdk.resetEditor();\n cesdk.ui.setTheme('dark');\n\n // --- Features (starter-kit design set, plus the dock/library) -------------\n cesdk.feature.enable([\n 'ly.img.navigation',\n 'ly.img.dock',\n 'ly.img.library.panel',\n 'ly.img.text',\n 'ly.img.crop',\n 'ly.img.transform',\n 'ly.img.filter',\n 'ly.img.adjustment',\n 'ly.img.effect',\n 'ly.img.blur',\n 'ly.img.shadow',\n 'ly.img.cutout',\n 'ly.img.canvas',\n 'ly.img.inspector',\n 'ly.img.inspector.bar',\n 'ly.img.inspector.toggle',\n 'ly.img.delete',\n 'ly.img.duplicate',\n 'ly.img.group',\n 'ly.img.replace',\n 'ly.img.page',\n 'ly.img.scene.layout',\n 'ly.img.fill.color',\n 'ly.img.fill.image',\n 'ly.img.stroke',\n 'ly.img.opacity',\n 'ly.img.blendMode',\n 'ly.img.shape.options',\n 'ly.img.combine',\n 'ly.img.position',\n 'ly.img.trim',\n 'ly.img.notifications',\n 'ly.img.keyboard.shortcuts'\n ]);\n\n // --- Panels: inspector docked on the left, pushing the canvas -------------\n cesdk.ui.setPanelPosition('//ly.img.panel/inspector', 'left');\n cesdk.ui.setPanelFloating('//ly.img.panel/inspector', false);\n\n // --- Dock: asset library with Text / Shapes / Stickers --------------------\n // The stock \\`ly.img.vector.shape\\` and \\`ly.img.sticker\\` entries ship with\n // EMPTY sourceIds — wire them explicitly (no reliance on hidden\n // auto-population). Shapes points at the engine-CDN \\`ly.img.vector.shape\\`\n // source registered in the boot code — NOT the default root's vectorpath\n // source, whose group ids have no shipped translations and render as raw\n // i18n keys in the panel's section headers. Stickers uses the source\n // addDefaultAssetSources registers; the \\`ly.img.text\\` entry keeps its stock\n // sources. No image/video/audio/upload entries: the library carries only\n // the neutral defaults, no demo content.\n cesdk.ui.updateAssetLibraryEntry('ly.img.vector.shape', {\n sourceIds: ['ly.img.vector.shape']\n });\n cesdk.ui.updateAssetLibraryEntry('ly.img.sticker', {\n sourceIds: ['ly.img.sticker']\n });\n // The Document inspector's \"Resize Page\" panel lists the formats of the\n // sources named by the \\`ly.img.pagePresets\\` entry (note: entry id, NOT the\n // \\`ly.img.page.presets\\` source id) — it also ships with EMPTY sourceIds,\n // which left the panel body blank. The source itself is registered by\n // addDefaultAssetSources in the boot code.\n cesdk.ui.updateAssetLibraryEntry('ly.img.pagePresets', {\n sourceIds: ['ly.img.page.presets']\n });\n cesdk.ui.setComponentOrder({ in: 'ly.img.dock' }, [\n {\n id: 'ly.img.assetLibrary.dock',\n key: 'text',\n label: 'libraries.ly.img.text.label',\n icon: '@imgly/Text',\n entries: ['ly.img.text']\n },\n {\n id: 'ly.img.assetLibrary.dock',\n key: 'shapes',\n label: 'libraries.ly.img.vector.shape.label',\n icon: '@imgly/Shapes',\n entries: ['ly.img.vector.shape']\n },\n {\n id: 'ly.img.assetLibrary.dock',\n key: 'stickers',\n label: 'libraries.ly.img.sticker.label',\n icon: '@imgly/Sticker',\n entries: ['ly.img.sticker']\n }\n ]);\n\n // --- Inspector bar — contextual controls for the selected block -----------\n cesdk.ui.setComponentOrder(\n { in: 'ly.img.inspector.bar', when: { editMode: 'Transform' } },\n [\n 'ly.img.spacer',\n 'ly.img.pageResize.inspectorBar',\n 'ly.img.separator',\n 'ly.img.video.caption.inspectorBar',\n 'ly.img.shape.options.inspectorBar',\n 'ly.img.cutout.type.inspectorBar',\n 'ly.img.cutout.offset.inspectorBar',\n 'ly.img.cutout.smoothing.inspectorBar',\n 'ly.img.group.create.inspectorBar',\n 'ly.img.group.ungroup.inspectorBar',\n 'ly.img.audio.replace.inspectorBar',\n 'ly.img.separator',\n 'ly.img.text.typeFace.inspectorBar',\n 'ly.img.text.bold.inspectorBar',\n 'ly.img.text.italic.inspectorBar',\n 'ly.img.text.fontSize.inspectorBar',\n 'ly.img.text.alignHorizontal.inspectorBar',\n 'ly.img.text.advanced.inspectorBar',\n 'ly.img.combine.inspectorBar',\n 'ly.img.separator',\n 'ly.img.fill.inspectorBar',\n 'ly.img.trim.inspectorBar',\n 'ly.img.volume.inspectorBar',\n 'ly.img.crop.inspectorBar',\n 'ly.img.separator',\n 'ly.img.stroke.inspectorBar',\n 'ly.img.separator',\n 'ly.img.text.background.inspectorBar',\n 'ly.img.separator',\n 'ly.img.animations.inspectorBar',\n 'ly.img.separator',\n {\n id: 'ly.img.appearance.inspectorBar',\n children: [\n 'ly.img.adjustment.inspectorBar',\n 'ly.img.filter.inspectorBar',\n 'ly.img.effect.inspectorBar',\n 'ly.img.blur.inspectorBar'\n ]\n },\n 'ly.img.separator',\n 'ly.img.shadow.inspectorBar',\n 'ly.img.separator',\n 'ly.img.opacityOptions.inspectorBar',\n 'ly.img.separator',\n 'ly.img.position.inspectorBar',\n 'ly.img.spacer',\n 'ly.img.separator',\n 'ly.img.inspectorToggle.inspectorBar'\n ]\n );\n cesdk.ui.setComponentOrder(\n { in: 'ly.img.inspector.bar', when: { editMode: 'Crop' } },\n ['ly.img.cropControls.inspectorBar']\n );\n\n // --- Canvas bar (bottom) and the selection context menu -------------------\n cesdk.ui.setComponentOrder({ in: 'ly.img.canvas.bar', at: 'bottom' }, [\n 'ly.img.settings.canvasBar',\n 'ly.img.spacer',\n 'ly.img.page.add.canvasBar',\n 'ly.img.spacer'\n ]);\n cesdk.ui.setComponentOrder(\n { in: 'ly.img.canvas.menu', when: { editMode: 'Transform' } },\n [\n 'ly.img.group.enter.canvasMenu',\n 'ly.img.group.select.canvasMenu',\n 'ly.img.page.moveUp.canvasMenu',\n 'ly.img.page.moveDown.canvasMenu',\n 'ly.img.separator',\n 'ly.img.text.edit.canvasMenu',\n 'ly.img.text.styles.canvasMenu',\n 'ly.img.replace.canvasMenu',\n 'ly.img.separator',\n 'ly.img.bringForward.canvasMenu',\n 'ly.img.sendBackward.canvasMenu',\n 'ly.img.separator',\n 'ly.img.duplicate.canvasMenu',\n 'ly.img.delete.canvasMenu',\n 'ly.img.separator',\n 'ly.img.options.canvasMenu'\n ]\n );\n cesdk.ui.setComponentOrder(\n { in: 'ly.img.canvas.menu', when: { editMode: 'Text' } },\n [\n 'ly.img.text.color.canvasMenu',\n 'ly.img.separator',\n 'ly.img.text.bold.canvasMenu',\n 'ly.img.text.italic.canvasMenu',\n 'ly.img.text.underline.canvasMenu',\n 'ly.img.text.strikethrough.canvasMenu',\n 'ly.img.separator',\n 'ly.img.text.list.unordered.canvasMenu',\n 'ly.img.text.list.ordered.canvasMenu',\n 'ly.img.separator',\n 'ly.img.text.variables.canvasMenu'\n ]\n );\n}\n`,\n\t\"index.html\": `<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <title>CoDesign</title>\n <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\" />\n <!-- Mount prefix injected by the viewer router (router.ts). \"\" on the stdio\n listener, \"/viewer\" on the HTTP transport. Read by editor.js. -->\n <script>\n window.__VIEWER_BASE__ = '{{VIEWER_BASE}}';\n </script>\n <style>\n html,\n body {\n margin: 0;\n height: 100%;\n background: #1d1d1f;\n color: #eee;\n font-family: -apple-system, system-ui, sans-serif;\n }\n #cesdk {\n position: fixed;\n inset: 0;\n }\n #status {\n position: fixed;\n top: 14px;\n left: 14px;\n z-index: 10;\n font:\n 500 12px -apple-system,\n system-ui,\n sans-serif;\n }\n </style>\n </head>\n <body>\n <div id=\"cesdk\"></div>\n <div id=\"status\"></div>\n <script type=\"module\" src=\"{{VIEWER_BASE}}/static/editor.js\"></script>\n </body>\n</html>\n`,\n\t\"revision-data.js\": `// revision-data.js — framework-free data layer for the editor's revision\n// history. No DOM and no top-level \\`window\\` access, so order()/relTime() are\n// unit-testable under node. Consumed by editor.js's CE.SDK history panel.\n\n// Relative time label (\"5m ago\"). Uses Date.now() at call time (browser + node).\nexport function relTime(ms) {\n const s = Math.max(0, Math.round((Date.now() - ms) / 1000));\n if (s < 60) return \\`\\${s}s ago\\`;\n const m = Math.round(s / 60);\n if (m < 60) return \\`\\${m}m ago\\`;\n const h = Math.round(m / 60);\n if (h < 24) return \\`\\${h}h ago\\`;\n return \\`\\${Math.round(h / 24)}d ago\\`;\n}\n\n// Depth-first walk. At each node the spine child (or, off-spine, the latest\n// child) continues at the same depth; other children are branches at depth + 1,\n// emitted before the continuation so the spine reads top-to-bottom. Returns\n// [{ node, depth, isHead, isRoot }]. Pure — no DOM, no globals.\nexport function order(nodes) {\n if (nodes.length === 0) return [];\n const byId = new Map(nodes.map((n) => [n.id, n]));\n const children = new Map();\n for (const n of nodes) {\n if (n.parent != null) {\n const sibs = children.get(n.parent) ?? [];\n sibs.push(n);\n children.set(n.parent, sibs);\n }\n }\n for (const sibs of children.values())\n sibs.sort((a, b) => a.createdAt - b.createdAt);\n const rootNode = nodes.find((n) => n.parent === null);\n const head = nodes.reduce((a, b) => (b.createdAt > a.createdAt ? b : a));\n const spine = new Set();\n for (let n = head; n; n = n.parent != null ? byId.get(n.parent) : null)\n spine.add(n.id);\n const out = [];\n function walk(node, depth) {\n out.push({\n node,\n depth,\n isHead: node.id === head.id,\n isRoot: node.parent === null\n });\n const kids = children.get(node.id) ?? [];\n if (kids.length === 0) return;\n const primary = kids.find((k) => spine.has(k.id)) ?? kids[kids.length - 1];\n for (const k of kids) if (k !== primary) walk(k, depth + 1);\n walk(primary, depth);\n }\n if (rootNode) walk(rootNode, 0);\n return out;\n}\n\n// Fetch a design's full revision tree. \\`base\\` is the mount prefix (\"\" on stdio,\n// \"/viewer\" on HTTP). Throws on a non-2xx response so the caller can degrade.\nexport async function fetchRevisions(base, root) {\n const res = await fetch(\\`\\${base}/api/design/\\${root}/revisions\\`);\n if (!res.ok) throw new Error(\\`revisions \\${root} \\${res.status}\\`);\n return res.json();\n}\n`,\n};\n"]}
@@ -5,7 +5,8 @@ export interface ViewerUrlBuilder {
5
5
  /** Permalink to one exact revision of a design. The viewer renders it
6
6
  * frozen — no follow. */
7
7
  forPinnedRevision(root: string, revision: string): string;
8
- forStudio(): string;
8
+ /** Loopback URL of the served diagnostics viewer (Download-ZIP button). Local only. */
9
+ forDiagnostics(): string;
9
10
  /** Absolute URL for a workspace resource (assets/, exports/) served by the `/r/` route.
10
11
  * MUST stay synchronous — the CE.SDK `workspace://` URI resolver callback is sync. */
11
12
  forResource(relpath: string): string;
@@ -23,7 +24,7 @@ export declare class LocalViewerUrlBuilder implements ViewerUrlBuilder {
23
24
  constructor(opts: LocalUrlBuilderOptions);
24
25
  forDesign(root: string): string;
25
26
  forPinnedRevision(root: string, revision: string): string;
26
- forStudio(): string;
27
+ forDiagnostics(): string;
27
28
  forResource(relpath: string): string;
28
29
  forDownload(relpath: string): Promise<string>;
29
30
  }
@@ -39,7 +40,7 @@ export interface ViewerBaseOptions {
39
40
  sessionId: string;
40
41
  }
41
42
  /**
42
- * Where a session's viewer lives. This is the single rule both `open_editor`
43
+ * Where a session's viewer lives. This is the single rule both `view`
43
44
  * URL construction (via the builders' `base`) and the docs rely on:
44
45
  * session → <origin>/v/<sessionId> (multi-tenant mount, CF + local)
45
46
  * other → <origin>/viewer (fixed-workspace mount)
@@ -1 +1 @@
1
- {"version":3,"file":"url-builder.d.ts","sourceRoot":"","sources":["../../src/viewer/url-builder.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,gBAAgB;IAC/B;0EACsE;IACtE,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAChC;8BAC0B;IAC1B,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;IAC1D,SAAS,IAAI,MAAM,CAAC;IACpB;2FACuF;IACvF,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC;IACrC;;gGAE4F;IAC5F,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CAC/C;AAED,MAAM,WAAW,sBAAsB;IACrC,0EAA0E;IAC1E,IAAI,EAAE,MAAM,CAAC;CACd;AAED,qBAAa,qBAAsB,YAAW,gBAAgB;IAChD,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAAJ,IAAI,EAAE,sBAAsB;IACzD,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;IAG/B,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM;IAGzD,SAAS,IAAI,MAAM;IAGnB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM;IAG9B,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;CAIpD;AAED,MAAM,WAAW,iBAAiB;IAChC,sEAAsE;IACtE,aAAa,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC;8EAC0E;IAC1E,IAAI,EAAE,MAAM,CAAC;IACb;iCAC6B;IAC7B,IAAI,EAAE,QAAQ,GAAG,SAAS,GAAG,QAAQ,CAAC;IACtC,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,iBAAiB,GAAG,MAAM,CAK7D"}
1
+ {"version":3,"file":"url-builder.d.ts","sourceRoot":"","sources":["../../src/viewer/url-builder.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,gBAAgB;IAC/B;0EACsE;IACtE,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAChC;8BAC0B;IAC1B,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;IAC1D,uFAAuF;IACvF,cAAc,IAAI,MAAM,CAAC;IACzB;2FACuF;IACvF,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC;IACrC;;gGAE4F;IAC5F,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CAC/C;AAED,MAAM,WAAW,sBAAsB;IACrC,0EAA0E;IAC1E,IAAI,EAAE,MAAM,CAAC;CACd;AAED,qBAAa,qBAAsB,YAAW,gBAAgB;IAChD,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAAJ,IAAI,EAAE,sBAAsB;IACzD,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;IAG/B,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM;IAGzD,cAAc,IAAI,MAAM;IAGxB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM;IAG9B,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;CAIpD;AAED,MAAM,WAAW,iBAAiB;IAChC,sEAAsE;IACtE,aAAa,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC;8EAC0E;IAC1E,IAAI,EAAE,MAAM,CAAC;IACb;iCAC6B;IAC7B,IAAI,EAAE,QAAQ,GAAG,SAAS,GAAG,QAAQ,CAAC;IACtC,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,iBAAiB,GAAG,MAAM,CAK7D"}
@@ -1,6 +1,6 @@
1
1
  //
2
2
  // Builds viewer URLs. Local: a single base + path concat. Hosted (Phase 2):
3
- // signed-URL minting with account scope. Split out so the open_editor tool
3
+ // signed-URL minting with account scope. Split out so the view tool
4
4
  // and the viewer router share construction logic.
5
5
  export class LocalViewerUrlBuilder {
6
6
  constructor(opts) {
@@ -12,8 +12,8 @@ export class LocalViewerUrlBuilder {
12
12
  forPinnedRevision(root, revision) {
13
13
  return `${this.opts.base}/d/${root}/${revision}`;
14
14
  }
15
- forStudio() {
16
- return `${this.opts.base}/`;
15
+ forDiagnostics() {
16
+ return `${this.opts.base}/diagnostics`;
17
17
  }
18
18
  forResource(relpath) {
19
19
  return `${this.opts.base}/r/${relpath}`;
@@ -24,7 +24,7 @@ export class LocalViewerUrlBuilder {
24
24
  }
25
25
  }
26
26
  /**
27
- * Where a session's viewer lives. This is the single rule both `open_editor`
27
+ * Where a session's viewer lives. This is the single rule both `view`
28
28
  * URL construction (via the builders' `base`) and the docs rely on:
29
29
  * session → <origin>/v/<sessionId> (multi-tenant mount, CF + local)
30
30
  * other → <origin>/viewer (fixed-workspace mount)
@@ -1 +1 @@
1
- {"version":3,"file":"url-builder.js","sourceRoot":"","sources":["../../src/viewer/url-builder.ts"],"names":[],"mappings":"AAAA,EAAE;AACF,4EAA4E;AAC5E,2EAA2E;AAC3E,kDAAkD;AAwBlD,MAAM,OAAO,qBAAqB;IAChC,YAA6B,IAA4B;QAA5B,SAAI,GAAJ,IAAI,CAAwB;IAAG,CAAC;IAC7D,SAAS,CAAC,IAAY;QACpB,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,MAAM,IAAI,EAAE,CAAC;IACvC,CAAC;IACD,iBAAiB,CAAC,IAAY,EAAE,QAAgB;QAC9C,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,MAAM,IAAI,IAAI,QAAQ,EAAE,CAAC;IACnD,CAAC;IACD,SAAS;QACP,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC;IAC9B,CAAC;IACD,WAAW,CAAC,OAAe;QACzB,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,MAAM,OAAO,EAAE,CAAC;IAC1C,CAAC;IACD,KAAK,CAAC,WAAW,CAAC,OAAe;QAC/B,sFAAsF;QACtF,OAAO,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;IACnC,CAAC;CACF;AAcD;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAAC,IAAuB;IACnD,MAAM,MAAM,GAAG,IAAI,CAAC,aAAa,IAAI,oBAAoB,IAAI,CAAC,IAAI,EAAE,CAAC;IACrE,OAAO,IAAI,CAAC,IAAI,KAAK,SAAS;QAC5B,CAAC,CAAC,GAAG,MAAM,MAAM,IAAI,CAAC,SAAS,EAAE;QACjC,CAAC,CAAC,GAAG,MAAM,SAAS,CAAC;AACzB,CAAC","sourcesContent":["//\n// Builds viewer URLs. Local: a single base + path concat. Hosted (Phase 2):\n// signed-URL minting with account scope. Split out so the open_editor tool\n// and the viewer router share construction logic.\n\nexport interface ViewerUrlBuilder {\n /** Stable URL for a design, keyed by its root revision id. The viewer opens\n * at the head and live-follows — the URL to print once and reuse. */\n forDesign(root: string): string;\n /** Permalink to one exact revision of a design. The viewer renders it\n * frozen — no follow. */\n forPinnedRevision(root: string, revision: string): string;\n forStudio(): string;\n /** Absolute URL for a workspace resource (assets/, exports/) served by the `/r/` route.\n * MUST stay synchronous — the CE.SDK `workspace://` URI resolver callback is sync. */\n forResource(relpath: string): string;\n /** Download URL for a workspace artifact.\n * Local: identical to `forResource` (the `/r/` route works locally).\n * Hosted (S3/R2): a presigned GET URL so the client can download directly from storage. */\n forDownload(relpath: string): Promise<string>;\n}\n\nexport interface LocalUrlBuilderOptions {\n /** Listener base URL, e.g. \"http://127.0.0.1:8745\". No trailing slash. */\n base: string;\n}\n\nexport class LocalViewerUrlBuilder implements ViewerUrlBuilder {\n constructor(private readonly opts: LocalUrlBuilderOptions) {}\n forDesign(root: string): string {\n return `${this.opts.base}/d/${root}`;\n }\n forPinnedRevision(root: string, revision: string): string {\n return `${this.opts.base}/d/${root}/${revision}`;\n }\n forStudio(): string {\n return `${this.opts.base}/`;\n }\n forResource(relpath: string): string {\n return `${this.opts.base}/r/${relpath}`;\n }\n async forDownload(relpath: string): Promise<string> {\n // Local: the /r/ route is reachable, so the resource URL doubles as the download URL.\n return this.forResource(relpath);\n }\n}\n\nexport interface ViewerBaseOptions {\n /** Public origin (no trailing slash); undefined → loopback origin. */\n publicBaseUrl: string | undefined;\n /** Listener port — used only for the loopback origin. The loopback host is\n * 127.0.0.1 (a connect address; the server binds 0.0.0.0 separately). */\n port: number;\n /** `session` uses the per-session `/v/<sid>` mount; `single`/`header` the\n * fixed `/viewer` mount. */\n mode: 'header' | 'session' | 'single';\n sessionId: string;\n}\n\n/**\n * Where a session's viewer lives. This is the single rule both `open_editor`\n * URL construction (via the builders' `base`) and the docs rely on:\n * session → <origin>/v/<sessionId> (multi-tenant mount, CF + local)\n * other → <origin>/viewer (fixed-workspace mount)\n */\nexport function viewerBaseFor(opts: ViewerBaseOptions): string {\n const origin = opts.publicBaseUrl ?? `http://127.0.0.1:${opts.port}`;\n return opts.mode === 'session'\n ? `${origin}/v/${opts.sessionId}`\n : `${origin}/viewer`;\n}\n"]}
1
+ {"version":3,"file":"url-builder.js","sourceRoot":"","sources":["../../src/viewer/url-builder.ts"],"names":[],"mappings":"AAAA,EAAE;AACF,4EAA4E;AAC5E,oEAAoE;AACpE,kDAAkD;AAyBlD,MAAM,OAAO,qBAAqB;IAChC,YAA6B,IAA4B;QAA5B,SAAI,GAAJ,IAAI,CAAwB;IAAG,CAAC;IAC7D,SAAS,CAAC,IAAY;QACpB,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,MAAM,IAAI,EAAE,CAAC;IACvC,CAAC;IACD,iBAAiB,CAAC,IAAY,EAAE,QAAgB;QAC9C,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,MAAM,IAAI,IAAI,QAAQ,EAAE,CAAC;IACnD,CAAC;IACD,cAAc;QACZ,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,cAAc,CAAC;IACzC,CAAC;IACD,WAAW,CAAC,OAAe;QACzB,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,MAAM,OAAO,EAAE,CAAC;IAC1C,CAAC;IACD,KAAK,CAAC,WAAW,CAAC,OAAe;QAC/B,sFAAsF;QACtF,OAAO,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;IACnC,CAAC;CACF;AAcD;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAAC,IAAuB;IACnD,MAAM,MAAM,GAAG,IAAI,CAAC,aAAa,IAAI,oBAAoB,IAAI,CAAC,IAAI,EAAE,CAAC;IACrE,OAAO,IAAI,CAAC,IAAI,KAAK,SAAS;QAC5B,CAAC,CAAC,GAAG,MAAM,MAAM,IAAI,CAAC,SAAS,EAAE;QACjC,CAAC,CAAC,GAAG,MAAM,SAAS,CAAC;AACzB,CAAC","sourcesContent":["//\n// Builds viewer URLs. Local: a single base + path concat. Hosted (Phase 2):\n// signed-URL minting with account scope. Split out so the view tool\n// and the viewer router share construction logic.\n\nexport interface ViewerUrlBuilder {\n /** Stable URL for a design, keyed by its root revision id. The viewer opens\n * at the head and live-follows — the URL to print once and reuse. */\n forDesign(root: string): string;\n /** Permalink to one exact revision of a design. The viewer renders it\n * frozen — no follow. */\n forPinnedRevision(root: string, revision: string): string;\n /** Loopback URL of the served diagnostics viewer (Download-ZIP button). Local only. */\n forDiagnostics(): string;\n /** Absolute URL for a workspace resource (assets/, exports/) served by the `/r/` route.\n * MUST stay synchronous — the CE.SDK `workspace://` URI resolver callback is sync. */\n forResource(relpath: string): string;\n /** Download URL for a workspace artifact.\n * Local: identical to `forResource` (the `/r/` route works locally).\n * Hosted (S3/R2): a presigned GET URL so the client can download directly from storage. */\n forDownload(relpath: string): Promise<string>;\n}\n\nexport interface LocalUrlBuilderOptions {\n /** Listener base URL, e.g. \"http://127.0.0.1:8745\". No trailing slash. */\n base: string;\n}\n\nexport class LocalViewerUrlBuilder implements ViewerUrlBuilder {\n constructor(private readonly opts: LocalUrlBuilderOptions) {}\n forDesign(root: string): string {\n return `${this.opts.base}/d/${root}`;\n }\n forPinnedRevision(root: string, revision: string): string {\n return `${this.opts.base}/d/${root}/${revision}`;\n }\n forDiagnostics(): string {\n return `${this.opts.base}/diagnostics`;\n }\n forResource(relpath: string): string {\n return `${this.opts.base}/r/${relpath}`;\n }\n async forDownload(relpath: string): Promise<string> {\n // Local: the /r/ route is reachable, so the resource URL doubles as the download URL.\n return this.forResource(relpath);\n }\n}\n\nexport interface ViewerBaseOptions {\n /** Public origin (no trailing slash); undefined → loopback origin. */\n publicBaseUrl: string | undefined;\n /** Listener port — used only for the loopback origin. The loopback host is\n * 127.0.0.1 (a connect address; the server binds 0.0.0.0 separately). */\n port: number;\n /** `session` uses the per-session `/v/<sid>` mount; `single`/`header` the\n * fixed `/viewer` mount. */\n mode: 'header' | 'session' | 'single';\n sessionId: string;\n}\n\n/**\n * Where a session's viewer lives. This is the single rule both `view`\n * URL construction (via the builders' `base`) and the docs rely on:\n * session → <origin>/v/<sessionId> (multi-tenant mount, CF + local)\n * other → <origin>/viewer (fixed-workspace mount)\n */\nexport function viewerBaseFor(opts: ViewerBaseOptions): string {\n const origin = opts.publicBaseUrl ?? `http://127.0.0.1:${opts.port}`;\n return opts.mode === 'session'\n ? `${origin}/v/${opts.sessionId}`\n : `${origin}/viewer`;\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@imgly/codesign-mcp",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "Codesign MCP server: SYSTEM prompt, prompts, skills, design tools as MCP resources/prompts/tools.",
5
5
  "license": "SEE LICENSE IN LICENSE.md",
6
6
  "homepage": "https://github.com/imgly/imgly-codesign/tree/main/packages/codesign-mcp",
@@ -53,8 +53,9 @@
53
53
  "sync:cesdk-skills": "node --experimental-strip-types scripts/sync-cesdk-skills.ts",
54
54
  "generate-assets": "node scripts/generate-inline-assets.mjs",
55
55
  "generate-spa-assets": "node scripts/generate-spa-assets.mjs",
56
+ "generate-spa-assets:diagnostics": "node scripts/generate-spa-assets.mjs src/diagnostics/spa src/diagnostics/spa-assets.generated.ts",
56
57
  "generate-trial-license": "node scripts/generate-trial-license.mjs",
57
- "refresh-assets": "pnpm run sync:cesdk-api && pnpm run generate-assets && pnpm run generate-spa-assets && pnpm run generate-trial-license",
58
+ "refresh-assets": "pnpm run sync:cesdk-api && pnpm run generate-assets && pnpm run generate-spa-assets && pnpm run generate-spa-assets:diagnostics && pnpm run generate-trial-license",
58
59
  "prebuild": "pnpm run refresh-assets",
59
60
  "precheck": "pnpm run refresh-assets",
60
61
  "pretest": "pnpm run refresh-assets",
@@ -72,9 +73,9 @@
72
73
  "dependencies": {
73
74
  "@aws-sdk/client-s3": "^3.1063.0",
74
75
  "@aws-sdk/s3-request-presigner": "^3.1063.0",
75
- "@cesdk/engine": "1.77.0",
76
- "@cesdk/node": "1.77.0",
77
- "@cesdk/node-native": "1.77.0",
76
+ "@cesdk/engine": "1.77.1",
77
+ "@cesdk/node": "1.77.1",
78
+ "@cesdk/node-native": "1.77.1",
78
79
  "@modelcontextprotocol/sdk": "^1.0.0",
79
80
  "@sinclair/typebox": "^0.34.0",
80
81
  "cors": "^2.8.5",
@@ -1 +0,0 @@
1
- {"version":3,"file":"open-editor.d.ts","sourceRoot":"","sources":["../../src/tools/open-editor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,cAAc,CAAC;AAChE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAEjE,MAAM,WAAW,2BAA2B;IAC1C,UAAU,EAAE,gBAAgB,CAAC;IAC7B,qEAAqE;IACrE,KAAK,EAAE,aAAa,CAAC;CACtB;AASD,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,2BAA2B,GAChC,2BAA2B,CAyD7B"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"open-editor.js","sourceRoot":"","sources":["../../src/tools/open-editor.ts"],"names":[],"mappings":"AAUA,MAAM,WAAW,GACf,mGAAmG;IACnG,wGAAwG;IACxG,sGAAsG;IACtG,uGAAuG;IACvG,6DAA6D,CAAC;AAEhE,MAAM,UAAU,oBAAoB,CAClC,IAAiC;IAEjC,OAAO;QACL,IAAI,EAAE,aAAa;QACnB,WAAW,EAAE,WAAW;QACxB,WAAW,EAAE;YACX,IAAI,EAAE,QAAiB;YACvB,UAAU,EAAE;gBACV,QAAQ,EAAE;oBACR,IAAI,EAAE,QAAiB;oBACvB,WAAW,EACT,qEAAqE;iBACxE;gBACD,GAAG,EAAE;oBACH,IAAI,EAAE,SAAkB;oBACxB,WAAW,EACT,6FAA6F;iBAChG;aACF;YACD,oBAAoB,EAAE,KAAK;SAC5B;QACD,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE;YACtB,MAAM,IAAI,GAAG,CAAC,OAAO,IAAI,EAAE,CAAyC,CAAC;YACrE,IAAI,CAAC;gBACH,IAAI,IAAI,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;oBAChC,OAAO;wBACL,OAAO,EAAE;4BACP;gCACE,IAAI,EAAE,MAAe;gCACrB,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC,UAAU,CAAC,SAAS,EAAE,EAAE,CAAC;6BAC3D;yBACF;qBACF,CAAC;gBACJ,CAAC;gBACD,kEAAkE;gBAClE,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;gBACxD,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;gBACzB,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG;oBAClB,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,iBAAiB,CAAC,IAAI,EAAE,IAAI,CAAC,QAAQ,CAAC;oBACxD,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;gBACpC,OAAO;oBACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC;iBACpE,CAAC;YACJ,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,MAAM,IAAI,GAAI,GAAyB,CAAC,IAAI,CAAC;gBAC7C,MAAM,GAAG,GACP,IAAI,KAAK,QAAQ;oBACf,CAAC,CAAC,oBAAoB,IAAI,CAAC,QAAQ,EAAE;oBACrC,CAAC,CAAC,GAAG,YAAY,KAAK;wBACpB,CAAC,CAAC,GAAG,CAAC,OAAO;wBACb,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;gBACpB,OAAO;oBACL,OAAO,EAAE,IAAI;oBACb,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,gBAAgB,GAAG,EAAE,EAAE,CAAC;iBAClE,CAAC;YACJ,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC","sourcesContent":["import type { RevisionStore } from '../revisions/store.js';\nimport type { CodesignMcpToolRegistration } from '../server.js';\nimport type { ViewerUrlBuilder } from '../viewer/url-builder.js';\n\nexport interface CreateOpenEditorToolOptions {\n urlBuilder: ViewerUrlBuilder;\n /** Resolves a revision to its design root (the root revision id). */\n store: RevisionStore;\n}\n\nconst DESCRIPTION =\n \"Get a URL the human can open to view the design in CE.SDK. With `revision`, returns the design's \" +\n \"stable URL — the viewer keeps it live, re-rendering as you `edit`, so print it ONCE and don't re-emit \" +\n \"a fresh URL every turn. Add `pin: true` for a frozen permalink to that exact revision (for 'look at \" +\n \"exactly this' or comparing revisions). Omit `revision` for the workspace landing page (mini-studio). \" +\n 'The viewer is read-only — agent edits remain authoritative.';\n\nexport function createOpenEditorTool(\n opts: CreateOpenEditorToolOptions\n): CodesignMcpToolRegistration {\n return {\n name: 'open_editor',\n description: DESCRIPTION,\n inputSchema: {\n type: 'object' as const,\n properties: {\n revision: {\n type: 'string' as const,\n description:\n 'Any revision in the design. Omit to get the workspace landing page.'\n },\n pin: {\n type: 'boolean' as const,\n description:\n 'When true, return a frozen permalink to this exact revision instead of the live design URL.'\n }\n },\n additionalProperties: false\n },\n call: async (rawArgs) => {\n const args = (rawArgs ?? {}) as { revision?: string; pin?: boolean };\n try {\n if (args.revision === undefined) {\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({ url: opts.urlBuilder.forStudio() })\n }\n ]\n };\n }\n // historyOf returns the chain root-first; [0] is the design root.\n const chain = await opts.store.historyOf(args.revision);\n const root = chain[0].id;\n const url = args.pin\n ? opts.urlBuilder.forPinnedRevision(root, args.revision)\n : opts.urlBuilder.forDesign(root);\n return {\n content: [{ type: 'text' as const, text: JSON.stringify({ url }) }]\n };\n } catch (err) {\n const code = (err as { code?: string }).code;\n const msg =\n code === 'ENOENT'\n ? `unknown revision ${args.revision}`\n : err instanceof Error\n ? err.message\n : String(err);\n return {\n isError: true,\n content: [{ type: 'text' as const, text: `open_editor: ${msg}` }]\n };\n }\n }\n };\n}\n"]}