@dfosco/storyboard 0.6.4 → 0.6.6

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.
@@ -78,11 +78,16 @@ function scanDirectory(root, watchEntry, config) {
78
78
 
79
79
  /**
80
80
  * Compute the route path for a prototype file (relative to src/prototypes/).
81
- * Mirrors the route regex in src/routes.jsx.
81
+ * Mirrors the route regex in src/routes.jsx — strips `.folder/` segments,
82
+ * `drafts/` segments (scratch dirs share the public URL of their non-draft
83
+ * sibling), and trailing file extension.
82
84
  */
83
85
  function prototypeRoute(relPath) {
84
86
  let route = relPath
85
87
  .replace(/[^/]*\.folder\//g, '')
88
+ .split('/')
89
+ .filter((seg) => seg !== 'drafts')
90
+ .join('/')
86
91
  .replace(/\.(jsx|tsx|mdx)$/, '')
87
92
  .replace(/\/index$/, '')
88
93
 
@@ -92,12 +97,14 @@ function prototypeRoute(relPath) {
92
97
 
93
98
  /**
94
99
  * Compute the route path for a canvas file (relative to src/canvas/).
95
- * Uses toCanvasId() for proper folder handling.
100
+ * Uses toCanvasId() for proper folder handling, then strips `drafts/`
101
+ * segments so moves in/out of `drafts/` are no-ops.
96
102
  */
97
103
  function canvasRoute(relPath) {
98
104
  // relPath is relative to src/canvas/, prepend prefix for toCanvasId()
99
105
  const canvasId = toCanvasId('src/canvas/' + relPath)
100
- return '/canvas/' + canvasId
106
+ const stripped = canvasId.split('/').filter((seg) => seg !== 'drafts').join('/')
107
+ return '/canvas/' + stripped
101
108
  }
102
109
 
103
110
  function computeRoute(relPath, watchType) {
@@ -6,9 +6,10 @@
6
6
  */
7
7
  import { useState, useEffect, useRef, useMemo, useCallback, useSyncExternalStore } from 'react'
8
8
  import { buildPrototypeIndex, listStories, getStoryData, BranchSelect, getCustomerModeConfig } from '../core/index.js'
9
- import { MarkGithubIcon, GitBranchIcon, ChevronDownIcon, ChevronRightIcon, PlusIcon, StarIcon, StarFillIcon, ThreeBarsIcon, XIcon, StackIcon, TrashIcon, ShieldLockIcon, KebabHorizontalIcon, PencilIcon } from '@primer/octicons-react'
9
+ import { MarkGithubIcon, GitBranchIcon, ChevronDownIcon, ChevronRightIcon, PlusIcon, StarIcon, StarFillIcon, ThreeBarsIcon, XIcon, StackIcon, TrashIcon, ShieldLockIcon, KebabHorizontalIcon, PencilIcon, EyeClosedIcon } from '@primer/octicons-react'
10
10
  import { Menu } from '@base-ui/react/menu'
11
11
  import { Dialog } from '@base-ui/react/dialog'
12
+ import { Tooltip } from '@primer/react'
12
13
  import Icon from './Icon.jsx'
13
14
  import { useBranches } from './BranchBar/useBranches.js'
14
15
  import css from './Viewfinder.module.css'
@@ -467,9 +468,23 @@ function ArtifactCard({ item, basePath, starred, onToggleStar, onItemDeleted })
467
468
 
468
469
  return (
469
470
  <>
470
- <Tag className={css.card} {...linkProps} onClick={handleClick}>
471
+ <Tag className={`${css.card}${item.isPrivate ? ' ' + css.cardPrivate : ''}`} {...linkProps} onClick={handleClick}>
471
472
  <div className={css.cardHeader}>
472
- <span className={css.cardBadge}>{getTypeLabel(item.type)}</span>
473
+ <div className={css.cardBadgeGroup}>
474
+ <span className={css.cardBadge}>{getTypeLabel(item.type)}</span>
475
+ {item.isPrivate && (
476
+ <Tooltip text={`${typeLabel} not published, only visible to you`} direction="n">
477
+ <button
478
+ type="button"
479
+ className={css.cardPrivateBadge}
480
+ aria-label={`Private ${typeLabel.toLowerCase()}`}
481
+ onClick={(e) => { e.preventDefault(); e.stopPropagation() }}
482
+ >
483
+ <EyeClosedIcon size={12} />
484
+ </button>
485
+ </Tooltip>
486
+ )}
487
+ </div>
473
488
  <div className={css.cardActions}>
474
489
  <StarBtn active={starred} onClick={() => onToggleStar(item.id)} inline />
475
490
  {item.flows?.length > 1 && <FlowsDropdown flows={item.flows} basePath={basePath} />}
@@ -609,8 +624,12 @@ function PagesDropdown({ pages, basePath }) {
609
624
  <a
610
625
  href={withBase(basePath, page.route)}
611
626
  className={css.flowsItemLink}
627
+ title={page.isPrivate ? 'Local-only page — gitignored, dev-only' : undefined}
612
628
  >
613
629
  {page.name}
630
+ {page.isPrivate && (
631
+ <EyeClosedIcon size={12} className={css.flowsItemPrivateIcon} />
632
+ )}
614
633
  </a>
615
634
  </Menu.Item>
616
635
  ))}
@@ -624,17 +643,36 @@ function PagesDropdown({ pages, basePath }) {
624
643
  /* ─── Folder Section ─── */
625
644
 
626
645
  function FolderSection({ folder, collapsed, onToggle, basePath, starred, onToggleStar, onItemDeleted }) {
646
+ const sectionClass = collapsed ? css.folderSectionCollapsed : css.folderSection
627
647
  return (
628
- <section className={collapsed ? css.folderSectionCollapsed : css.folderSection}>
629
- <button className={css.folderHeader} onClick={onToggle}>
648
+ <section className={`${sectionClass}${folder.isPrivate ? ' ' + css.folderSectionPrivate : ''}`}>
649
+ <div
650
+ className={css.folderHeader}
651
+ role="button"
652
+ tabIndex={0}
653
+ onClick={onToggle}
654
+ onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onToggle() } }}
655
+ >
630
656
  <Icon name={collapsed ? 'folder' : 'folder-open'} size={16} className={css.folderIcon} />
631
657
  <span className={css.folderName}>{folder.name}</span>
658
+ {folder.isPrivate && (
659
+ <Tooltip text="Folder not published, only visible to you" direction="n">
660
+ <button
661
+ type="button"
662
+ className={css.folderPrivateBadge}
663
+ aria-label="Private folder"
664
+ onClick={(e) => { e.preventDefault(); e.stopPropagation() }}
665
+ >
666
+ <EyeClosedIcon size={12} />
667
+ </button>
668
+ </Tooltip>
669
+ )}
632
670
  <span className={css.folderCount}>{folder.items.length}</span>
633
671
  <ChevronRightIcon
634
672
  size={14}
635
673
  className={collapsed ? css.folderChevron : css.folderChevronExpanded}
636
674
  />
637
- </button>
675
+ </div>
638
676
  {!collapsed && (
639
677
  <div className={css.grid}>
640
678
  {folder.items.map(item => (
@@ -1093,6 +1131,8 @@ function WorkspaceImpl({
1093
1131
  basePath,
1094
1132
  title = 'Storyboard',
1095
1133
  subtitle,
1134
+ logo,
1135
+ logoIcon = 'iconoir/key-command',
1096
1136
  showAllArtifacts = false,
1097
1137
  showPrototypes = true,
1098
1138
  showCanvases = true,
@@ -1164,6 +1204,7 @@ function WorkspaceImpl({
1164
1204
  folder: proto.folder,
1165
1205
  description: proto.description,
1166
1206
  flows: proto.flows || [],
1207
+ isPrivate: !!proto.isPrivate,
1167
1208
  })
1168
1209
  }
1169
1210
 
@@ -1187,6 +1228,7 @@ function WorkspaceImpl({
1187
1228
  folder: canvas.folder,
1188
1229
  description: canvas.description,
1189
1230
  pages: canvas.pages || null,
1231
+ isPrivate: !!canvas.isPrivate,
1190
1232
  })
1191
1233
  }
1192
1234
 
@@ -1200,9 +1242,10 @@ function WorkspaceImpl({
1200
1242
  for (const name of storyNames) {
1201
1243
  const data = getStoryData(name)
1202
1244
  if (!data) continue
1245
+ const displayName = (name.split('/').pop() || name).replace(/^~/, '')
1203
1246
  items.push({
1204
1247
  id: `component:${name}`,
1205
- name: name.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' '),
1248
+ name: displayName.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' '),
1206
1249
  type: 'component',
1207
1250
  author: null,
1208
1251
  gitAuthor: null,
@@ -1212,6 +1255,7 @@ function WorkspaceImpl({
1212
1255
  externalUrl: null,
1213
1256
  folder: null,
1214
1257
  description: null,
1258
+ isPrivate: !!data._isPrivate || name.split('/').includes('drafts'),
1215
1259
  })
1216
1260
  }
1217
1261
 
@@ -1298,6 +1342,7 @@ function WorkspaceImpl({
1298
1342
  const folders = Object.entries(folderItems).map(([dirName, fItems]) => ({
1299
1343
  dirName,
1300
1344
  name: folderMeta[dirName]?.name || dirName,
1345
+ isPrivate: !!folderMeta[dirName]?.isPrivate || dirName === 'drafts',
1301
1346
  items: fItems,
1302
1347
  }))
1303
1348
  folders.sort((a, b) => {
@@ -1357,13 +1402,9 @@ function WorkspaceImpl({
1357
1402
  >
1358
1403
  {sidebarOpen ? <XIcon size={18} /> : <ThreeBarsIcon size={18} />}
1359
1404
  </button>
1360
- <img
1361
- src={`${basePath || '/'}favicon.svg`}
1362
- alt=""
1363
- width={48}
1364
- height={48}
1365
- className={css.logo}
1366
- />
1405
+ {logo
1406
+ ? <div className={`${css.logo} smooth-corners`}>{logo}</div>
1407
+ : <div className={`${css.logo} smooth-corners`}><Icon name={logoIcon} size={22} color="#fff" /></div>}
1367
1408
  <div>
1368
1409
  <div className={css.appName}>{title}</div>
1369
1410
  {subtitle && <div className={css.appSubtitle}>{subtitle}</div>}
@@ -30,8 +30,13 @@
30
30
  .logo {
31
31
  width: 48px;
32
32
  height: 48px;
33
+ background: var(--bgColor-emphasis, #313131);
34
+ border-radius: 8px;
33
35
  transform: rotate(-1deg);
34
- display: block;
36
+ display: flex;
37
+ align-items: center;
38
+ justify-content: center;
39
+ color: var(--fgColor-onEmphasis, #fff);
35
40
  flex-shrink: 0;
36
41
  }
37
42
 
@@ -637,6 +642,14 @@
637
642
  color: inherit;
638
643
  }
639
644
 
645
+ .cardPrivate {
646
+ opacity: 0.8;
647
+ }
648
+
649
+ .cardPrivate:hover {
650
+ opacity: 1;
651
+ }
652
+
640
653
  .cardThumb {
641
654
  height: 140px;
642
655
  background: var(--bgColor-muted, #f5f5f5);
@@ -658,6 +671,22 @@
658
671
  color: var(--fgColor-muted, #555);
659
672
  }
660
673
 
674
+ .cardPrivateBadge {
675
+ display: inline-flex;
676
+ align-items: center;
677
+ justify-content: center;
678
+ color: var(--fgColor-muted, #656d76);
679
+ background: none;
680
+ border: none;
681
+ padding: 2px;
682
+ }
683
+
684
+ .cardBadgeGroup {
685
+ display: flex;
686
+ align-items: center;
687
+ gap: 6px;
688
+ }
689
+
661
690
  /* Icon buttons (star, flows, etc.) */
662
691
 
663
692
  .iconBtn {
@@ -1031,6 +1060,25 @@
1031
1060
  flex: 1;
1032
1061
  }
1033
1062
 
1063
+ .folderSectionPrivate {
1064
+ opacity: 0.8;
1065
+ }
1066
+
1067
+ .folderSectionPrivate:hover {
1068
+ opacity: 1;
1069
+ }
1070
+
1071
+ .folderPrivateBadge {
1072
+ display: inline-flex;
1073
+ align-items: center;
1074
+ justify-content: center;
1075
+ color: var(--fgColor-muted, #656d76);
1076
+ background: none;
1077
+ border: none;
1078
+ padding: 2px;
1079
+ margin-right: 6px;
1080
+ }
1081
+
1034
1082
  .folderCount {
1035
1083
  font-size: 14px;
1036
1084
  font-weight: 400;
@@ -1114,11 +1162,19 @@
1114
1162
  }
1115
1163
 
1116
1164
  .flowsItemLink {
1117
- display: block;
1165
+ display: flex;
1166
+ align-items: center;
1167
+ gap: 6px;
1118
1168
  color: inherit;
1119
1169
  text-decoration: none;
1120
1170
  }
1121
1171
 
1172
+ .flowsItemPrivateIcon {
1173
+ color: var(--fgColor-muted, #656d76);
1174
+ flex-shrink: 0;
1175
+ margin-left: auto;
1176
+ }
1177
+
1122
1178
  /* Avatar stack */
1123
1179
 
1124
1180
  .avatarStack {
@@ -497,11 +497,25 @@ export default function PageSelector({ currentName, pages: initialPages, isLocal
497
497
  ) : (
498
498
  <a
499
499
  href={getPageHref(page)}
500
- className={styles.itemLink}
500
+ className={`${styles.itemLink} ${page.isPrivate ? styles.itemPrivate : ''}`}
501
501
  onClick={(e) => handleItemClick(page, e)}
502
502
  onDoubleClick={(e) => e.stopPropagation()}
503
+ title={page.isPrivate ? 'Local-only page — gitignored, dev-only' : undefined}
503
504
  >
504
505
  <span className={styles.itemContent}>{page.title}</span>
506
+ {page.isPrivate && (
507
+ <svg
508
+ className={styles.privateIcon}
509
+ width="14"
510
+ height="14"
511
+ viewBox="0 0 16 16"
512
+ fill="currentColor"
513
+ aria-hidden="true"
514
+ >
515
+ <path d="M.143 2.31a.75.75 0 0 1 1.047-.167l14.5 10.5a.75.75 0 1 1-.88 1.214l-2.248-1.628C11.346 13.19 9.792 14 8 14c-1.981 0-3.67-.992-4.933-2.078C1.797 10.832.88 9.577.43 8.9a1.62 1.62 0 0 1 0-1.797c.353-.533 1.068-1.495 2.062-2.42L.31 3.357A.75.75 0 0 1 .143 2.31Zm1.536 5.622A.12.12 0 0 0 1.659 8c0 .021.006.045.02.068.387.582 1.211 1.703 2.31 2.646C5.1 11.668 6.42 12.5 8 12.5c1.286 0 2.413-.55 3.359-1.252L9.81 10.13a2.5 2.5 0 0 1-3.498-3.498L4.388 5.226c-1.014.91-1.749 1.886-2.108 2.426Zm6.728 1.42-1.668-1.208a1 1 0 0 0 1.36 1.207Z" />
516
+ <path d="M8 3.5c-.387 0-.74.057-1.063.145L5.793 2.503A7.84 7.84 0 0 1 8 2.207c1.981 0 3.67.992 4.933 2.078 1.27 1.09 2.187 2.345 2.637 3.022a1.62 1.62 0 0 1 0 1.795c-.247.371-.622.86-1.118 1.384L13.39 9.4c.402-.43.706-.83.91-1.137a.121.121 0 0 0 .012-.022.122.122 0 0 0-.012-.023c-.387-.582-1.211-1.703-2.31-2.646C10.9 4.733 9.58 3.9 8 3.9Zm.482 2.067-.044-.011a.748.748 0 0 0 .044.011Zm-.482 0c-.272 0-.5.228-.5.5 0 .272.228.5.5.5h.022l1.024.741A2.5 2.5 0 0 0 8 5.5Z" />
517
+ </svg>
518
+ )}
505
519
  </a>
506
520
  )}
507
521
  {!isEditing && isLocalDev && (
@@ -166,6 +166,20 @@
166
166
  background: var(--bgColor-accent-muted, #ddf4ff);
167
167
  }
168
168
 
169
+ .itemPrivate {
170
+ opacity: 0.55;
171
+ }
172
+
173
+ .itemPrivate:hover {
174
+ opacity: 1;
175
+ }
176
+
177
+ .privateIcon {
178
+ color: var(--fgColor-muted, #656d76);
179
+ flex-shrink: 0;
180
+ margin-left: 4px;
181
+ }
182
+
169
183
  .separator {
170
184
  height: 1px;
171
185
  background: var(--borderColor-default, rgba(0, 0, 0, 0.15));
@@ -48,32 +48,41 @@ export default function FrozenTerminalOverlay({ widgetId, onActivate }) {
48
48
 
49
49
  async function fetchSnapshot() {
50
50
  const baseUrl = getBaseUrl()
51
- const urls = [
52
- `${baseUrl}_storyboard/canvas/terminal-snapshot/${widgetId}`,
53
- `${baseUrl}_storyboard/terminal-snapshots/${widgetId}.snapshot.json`,
54
- ]
55
-
56
- for (const url of urls) {
57
- try {
58
- const res = await fetch(url)
59
- if (!res.ok) continue
51
+ // Try the dev-server REST endpoint first, then fall back to the static
52
+ // consolidated index (production builds / no server).
53
+ const restUrl = `${baseUrl}_storyboard/canvas/terminal-snapshot/${widgetId}`
54
+ const indexUrl = `${baseUrl}_storyboard/terminal-snapshots/agents.snapshot.json`
55
+
56
+ try {
57
+ const res = await fetch(restUrl)
58
+ if (res.ok) {
60
59
  const data = await res.json()
61
60
  if (cancelled) return
62
61
  const text = data.paneContent || data.content || data.output || ''
63
- if (!text) continue
64
-
65
- const converter = await getConverter()
66
- if (cancelled) return
67
- if (converter) {
68
- setHtml(converter.toHtml(text))
69
- } else {
70
- setPlainText(stripAnsi(text))
62
+ if (text) {
63
+ const converter = await getConverter()
64
+ if (cancelled) return
65
+ if (converter) setHtml(converter.toHtml(text))
66
+ else setPlainText(stripAnsi(text))
67
+ return
71
68
  }
72
- return
73
- } catch {
74
- continue
75
69
  }
76
- }
70
+ } catch { /* fall through to static index */ }
71
+
72
+ try {
73
+ const res = await fetch(indexUrl)
74
+ if (!res.ok) return
75
+ const data = await res.json()
76
+ if (cancelled) return
77
+ const entry = data?.agents?.[widgetId]
78
+ if (!entry) return
79
+ const text = entry.paneContent || entry.content || entry.output || ''
80
+ if (!text) return
81
+ const converter = await getConverter()
82
+ if (cancelled) return
83
+ if (converter) setHtml(converter.toHtml(text))
84
+ else setPlainText(stripAnsi(text))
85
+ } catch { /* empty */ }
77
86
  }
78
87
 
79
88
  fetchSnapshot()
@@ -54,13 +54,41 @@ export default forwardRef(function PrototypeEmbed({ id: widgetId, props, onUpdat
54
54
 
55
55
  const basePath = (import.meta.env.BASE_URL || '/').replace(/\/$/, '')
56
56
  const baseSegment = basePath.replace(/^\//, '')
57
+ // Internal prototype iframes load the isolated prototypes.html entry
58
+ // (see .agents/plans/vite-isolation.md). prototypes.html uses
59
+ // createHashRouter so the prototype route lives in the URL hash —
60
+ // any path /MyProto/SignupForm becomes prototypes.html#/MyProto/SignupForm.
61
+ // External http(s) URLs are left alone.
57
62
  const rawSrc = useMemo(() => {
58
63
  if (!src) return ''
59
64
  if (/^https?:\/\//.test(src)) return src
60
65
  const cleaned = src.replace(/^\/branch--[^/]+/, '')
61
- if (baseSegment && cleaned.startsWith(basePath)) return cleaned
62
- if (baseSegment && cleaned.startsWith(baseSegment)) return `/${cleaned}`
63
- return `${basePath}${cleaned}`
66
+ let normalized
67
+ if (baseSegment && cleaned.startsWith(basePath)) normalized = cleaned
68
+ else if (baseSegment && cleaned.startsWith(baseSegment)) normalized = `/${cleaned}`
69
+ else normalized = `${basePath}${cleaned}`
70
+ // Strip basePath so we can split path/query/hash and re-anchor on
71
+ // prototypes.html. Any pre-existing #hash on the prototype URL is
72
+ // preserved by appending it after the route hash with an extra `#`,
73
+ // so the inner router still parses /MyProto correctly.
74
+ const withoutBase = baseSegment && normalized.startsWith(basePath)
75
+ ? normalized.slice(basePath.length) || '/'
76
+ : normalized
77
+ const hashIdx = withoutBase.indexOf('#')
78
+ const innerHash = hashIdx >= 0 ? withoutBase.slice(hashIdx + 1) : ''
79
+ const pathAndQuery = hashIdx >= 0 ? withoutBase.slice(0, hashIdx) : withoutBase
80
+ const routeHash = pathAndQuery.startsWith('/') ? pathAndQuery : `/${pathAndQuery}`
81
+ const suffix = innerHash ? `#${innerHash}` : ''
82
+ // Extract the prototype name (first path segment) so the dev iframe can
83
+ // narrow its router to just that prototype's subtree — broken siblings
84
+ // never appear in the matched lazy() chain. The consumer's
85
+ // prototypes-entry.jsx reads ?proto= and calls getRoutesForProto(); if
86
+ // it doesn't (older scaffold), the param is harmlessly ignored and the
87
+ // full route tree loads. Prod builds skip the param entirely.
88
+ const pathOnly = pathAndQuery.split('?')[0]
89
+ const protoName = pathOnly.split('/').filter(Boolean)[0] || ''
90
+ const queryStr = (import.meta.env.DEV && protoName) ? `?proto=${encodeURIComponent(protoName)}` : ''
91
+ return `${basePath}/prototypes.html${queryStr}#${routeHash}${suffix}`
64
92
  }, [src, basePath, baseSegment])
65
93
 
66
94
  const scale = zoom / 100
@@ -31,11 +31,23 @@ function resolveStorySetUrl(storyId, layout, selected, density, theme) {
31
31
  const story = getStoryData(storyId)
32
32
  if (!story?._storyModule) return ''
33
33
  const base = (import.meta.env.BASE_URL || '/').replace(/\/$/, '')
34
+
35
+ // In dev, prefer the dedicated isolate-set middleware: it serves a
36
+ // minimal HTML shell + dynamic-imports just this one story module, so a
37
+ // broken story cannot poison the canvas SPA's module graph. The
38
+ // middleware doesn't exist in deployed builds, so production falls
39
+ // through to the real story page with `_sb_component_set`.
40
+ if (import.meta.env.DEV) {
41
+ const params = new URLSearchParams()
42
+ params.set('module', story._storyModule)
43
+ if (layout) params.set('layout', layout)
44
+ if (selected) params.set('selected', selected)
45
+ if (density) params.set('density', density)
46
+ if (theme) params.set('theme', theme)
47
+ return `${base}/_storyboard/canvas/isolate-set?${params}`
48
+ }
49
+
34
50
  const params = new URLSearchParams()
35
- // Route via the real story page (works in dev AND prod). The dev-only
36
- // `_storyboard/canvas/isolate-set` middleware doesn't exist in deployed
37
- // builds, so we mount ComponentSetPage at the story's route with
38
- // `_sb_component_set` instead. `_sb_embed` keeps the canvas chrome off.
39
51
  params.set('_sb_embed', '')
40
52
  params.set('_sb_component_set', '')
41
53
  if (layout) params.set('layout', layout)
@@ -55,26 +55,24 @@ export default function TerminalReadWidget({ id, props }) {
55
55
  const canvasId = getCanvasId()
56
56
  if (!canvasId) { setFailed(true); return }
57
57
 
58
- const urls = isProduction()
59
- ? [
60
- // New flat format: <widgetId>.snapshot.json
61
- `${baseUrl}_storyboard/terminal-snapshots/${id}.snapshot.json`,
62
- // Legacy nested format: <canvasDir>/<widgetId>.json
63
- `${baseUrl}_storyboard/terminal-snapshots/${canvasId.replace(/\//g, '--')}/${id}.json`,
64
- ]
65
- : [
66
- `${baseUrl}_storyboard/canvas/terminal-snapshot/${id}`,
67
- `${baseUrl}_storyboard/terminal-snapshots/${id}.snapshot.json`,
68
- `${baseUrl}_storyboard/terminal-snapshots/${canvasId.replace(/\//g, '--')}/${id}.json`,
69
- ]
70
-
71
- for (const url of urls) {
58
+ const indexUrl = `${baseUrl}_storyboard/terminal-snapshots/agents.snapshot.json`
59
+ const legacyUrl = `${baseUrl}_storyboard/terminal-snapshots/${canvasId.replace(/\//g, '--')}/${id}.json`
60
+ const restUrl = `${baseUrl}_storyboard/canvas/terminal-snapshot/${id}`
61
+
62
+ const tryUrls = isProduction()
63
+ ? [{ url: indexUrl, fromIndex: true }, { url: legacyUrl, fromIndex: false }]
64
+ : [{ url: restUrl, fromIndex: false }, { url: indexUrl, fromIndex: true }, { url: legacyUrl, fromIndex: false }]
65
+
66
+ for (const { url, fromIndex } of tryUrls) {
72
67
  try {
73
68
  const res = await fetch(url)
74
69
  if (!res.ok) continue
75
70
  const data = await res.json()
76
71
  if (cancelled) return
77
- const text = data.paneContent || data.content || data.output || ''
72
+ const entry = fromIndex ? data?.agents?.[id] : data
73
+ if (!entry) continue
74
+ const text = entry.paneContent || entry.content || entry.output || ''
75
+ if (!text) continue
78
76
  setContent(text)
79
77
 
80
78
  const converter = await getConverter()
@@ -149,6 +149,21 @@
149
149
  top: calc(100% + 10px);
150
150
  }
151
151
 
152
+ /* Invisible hit-area that bridges the 10px gap between the widget and the
153
+ toolbar. Keeps the parent .chromeContainer in :hover (and React mouseenter)
154
+ when the pointer crosses the padding, and — crucially — stops the pointer
155
+ from falling through to widgets stacked underneath, so the toolbar of an
156
+ overlapping widget on top stays interactive. */
157
+ .toolbar::before {
158
+ content: '';
159
+ position: absolute;
160
+ left: 0;
161
+ right: 0;
162
+ bottom: 100%;
163
+ height: 10px;
164
+ pointer-events: auto;
165
+ }
166
+
152
167
  /* Trigger dot — positioned in the toolbar, visible at rest */
153
168
  .triggerDot {
154
169
  width: 6px;
@@ -70,6 +70,7 @@ function getCanvasGroupMap() {
70
70
  name,
71
71
  route,
72
72
  title: data?.title || name.split('/').pop(),
73
+ isPrivate: !!data?._isPrivate,
73
74
  _canvasMeta: data?._canvasMeta || null,
74
75
  })
75
76
  }
@@ -428,7 +429,7 @@ function StoryboardProviderInner({ flowName, sceneName, recordName, recordParam,
428
429
  const _hmrTick = canvasIndexKey
429
430
  const siblingPages = group
430
431
  ? getCanvasGroupMap().get(group) || []
431
- : [{ name: canvasId, route: canvasData?._route || `/canvas/${canvasId}`, title: canvasData?.title || canvasId.split('/').pop() }]
432
+ : [{ name: canvasId, route: canvasData?._route || `/canvas/${canvasId}`, title: canvasData?.title || canvasId.split('/').pop(), isPrivate: !!canvasData?._isPrivate }]
432
433
  const canvasMeta = canvasData?._canvasMeta || null
433
434
  const canvasValue = {
434
435
  data: null,