@dfosco/storyboard 0.5.0-beta.34 → 0.5.0-beta.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dfosco/storyboard",
3
- "version": "0.5.0-beta.34",
3
+ "version": "0.5.0-beta.35",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Storyboard prototyping framework — core engine, React integration, and canvas",
@@ -659,6 +659,21 @@ export default function CanvasPage({ canvasId: canvasIdProp, name, siblingPages
659
659
  // Local mutable copy of widgets for instant UI updates
660
660
  const [localWidgets, setLocalWidgets] = useState(canvas?.widgets ?? null)
661
661
  const [localConnectors, setLocalConnectors] = useState(canvas?.connectors ?? [])
662
+ // Track widget/connector IDs the user has just deleted locally so the HMR
663
+ // reconcile in the canvas-changed handler doesn't re-add them. Each ID is
664
+ // pruned once a server push confirms it's gone OR after a 5s safety timeout.
665
+ const pendingWidgetDeletionsRef = useRef(new Set())
666
+ const pendingConnectorDeletionsRef = useRef(new Set())
667
+ const markWidgetDeleted = useCallback((widgetId) => {
668
+ if (!widgetId) return
669
+ pendingWidgetDeletionsRef.current.add(widgetId)
670
+ setTimeout(() => pendingWidgetDeletionsRef.current.delete(widgetId), 5000)
671
+ }, [])
672
+ const markConnectorDeleted = useCallback((connectorId) => {
673
+ if (!connectorId) return
674
+ pendingConnectorDeletionsRef.current.add(connectorId)
675
+ setTimeout(() => pendingConnectorDeletionsRef.current.delete(connectorId), 5000)
676
+ }, [])
662
677
  const [trackedCanvas, setTrackedCanvas] = useState(canvas)
663
678
  const [selectedWidgetIds, setSelectedWidgetIds] = useState(() => new Set())
664
679
  const initialViewport = loadViewportState(canvasId)
@@ -899,13 +914,15 @@ export default function CanvasPage({ canvasId: canvasIdProp, name, siblingPages
899
914
  setLocalWidgets((prev) => {
900
915
  if (!prev) return serverWidgets
901
916
  const localIds = new Set(prev.map((w) => w.id))
902
- const additions = serverWidgets.filter((w) => !localIds.has(w.id))
917
+ const pending = pendingWidgetDeletionsRef.current
918
+ const additions = serverWidgets.filter((w) => !localIds.has(w.id) && !pending.has(w.id))
903
919
  return additions.length > 0 ? [...prev, ...additions] : prev
904
920
  })
905
921
  setLocalConnectors((prev) => {
906
- if (!prev || prev.length === 0) return serverConnectors
922
+ if (!prev || prev.length === 0) return serverConnectors.filter((c) => !pendingConnectorDeletionsRef.current.has(c.id))
907
923
  const localIds = new Set(prev.map((c) => c.id))
908
- const additions = serverConnectors.filter((c) => !localIds.has(c.id))
924
+ const pending = pendingConnectorDeletionsRef.current
925
+ const additions = serverConnectors.filter((c) => !localIds.has(c.id) && !pending.has(c.id))
909
926
  return additions.length > 0 ? [...prev, ...additions] : prev
910
927
  })
911
928
  }
@@ -1008,12 +1025,14 @@ export default function CanvasPage({ canvasId: canvasIdProp, name, siblingPages
1008
1025
  debouncedSave.cancel()
1009
1026
 
1010
1027
  undoRedo.snapshot(stateRef.current, 'remove', widgetId)
1028
+ markWidgetDeleted(widgetId)
1011
1029
  setLocalWidgets((prev) => prev ? prev.filter((w) => w.id !== widgetId) : prev)
1012
1030
  // Cascade: remove connectors referencing this widget
1013
1031
  setLocalConnectors((prev) => {
1014
1032
  const orphaned = prev.filter((c) => c.start.widgetId === widgetId || c.end.widgetId === widgetId)
1015
1033
  if (orphaned.length === 0) return prev
1016
1034
  for (const c of orphaned) {
1035
+ markConnectorDeleted(c.id)
1017
1036
  queueWrite(() =>
1018
1037
  removeConnectorApi(canvasId, c.id).catch((err) =>
1019
1038
  console.error('[canvas] Failed to remove orphaned connector:', err)
@@ -1027,7 +1046,7 @@ export default function CanvasPage({ canvasId: canvasIdProp, name, siblingPages
1027
1046
  removeWidgetApi(canvasId, widgetId)
1028
1047
  .catch((err) => console.error('[canvas] Failed to remove widget:', err))
1029
1048
  )
1030
- }, [canvasId, undoRedo, debouncedSave])
1049
+ }, [canvasId, undoRedo, debouncedSave, markWidgetDeleted, markConnectorDeleted])
1031
1050
 
1032
1051
  const handleConnectorAdd = useCallback(async ({ startWidgetId, startAnchor, endWidgetId, endAnchor }) => {
1033
1052
  try {
@@ -1075,6 +1094,7 @@ export default function CanvasPage({ canvasId: canvasIdProp, name, siblingPages
1075
1094
 
1076
1095
  const handleConnectorRemove = useCallback((connectorId) => {
1077
1096
  undoRedo.snapshot(stateRef.current, 'connector-remove')
1097
+ markConnectorDeleted(connectorId)
1078
1098
  setLocalConnectors((prev) => prev.filter((c) => c.id !== connectorId))
1079
1099
  dirtyRef.current = true
1080
1100
  queueWrite(() =>
@@ -1082,7 +1102,7 @@ export default function CanvasPage({ canvasId: canvasIdProp, name, siblingPages
1082
1102
  console.error('[canvas] Failed to remove connector:', err)
1083
1103
  )
1084
1104
  )
1085
- }, [canvasId, undoRedo])
1105
+ }, [canvasId, undoRedo, markConnectorDeleted])
1086
1106
 
1087
1107
  // Connector drag state
1088
1108
  const [connectorDrag, setConnectorDrag] = useState(null)
@@ -2677,11 +2697,15 @@ export default function CanvasPage({ canvasId: canvasIdProp, name, siblingPages
2677
2697
  undoRedo.snapshot(stateRef.current, 'multi-remove')
2678
2698
  debouncedSave.cancel()
2679
2699
  const idsToRemove = new Set(selectedWidgetIds)
2700
+ // Mark deletions before mutating local state so the HMR merge
2701
+ // doesn't re-add them mid-flight.
2702
+ for (const id of idsToRemove) markWidgetDeleted(id)
2680
2703
  // Remove from local state immediately
2681
2704
  setLocalWidgets((prev) => prev ? prev.filter(w => !idsToRemove.has(w.id)) : prev)
2682
2705
  setLocalConnectors((prev) => {
2683
2706
  const orphaned = prev.filter((c) => idsToRemove.has(c.start.widgetId) || idsToRemove.has(c.end.widgetId))
2684
2707
  for (const c of orphaned) {
2708
+ markConnectorDeleted(c.id)
2685
2709
  queueWrite(() =>
2686
2710
  removeConnectorApi(canvasId, c.id).catch((err) =>
2687
2711
  console.error('[canvas] Failed to remove orphaned connector:', err)
@@ -2708,7 +2732,7 @@ export default function CanvasPage({ canvasId: canvasIdProp, name, siblingPages
2708
2732
  }
2709
2733
  document.addEventListener('keydown', handleKeyDown)
2710
2734
  return () => document.removeEventListener('keydown', handleKeyDown)
2711
- }, [selectedWidgetIds, localWidgets, handleWidgetRemove, undoRedo, canvasId, debouncedSave])
2735
+ }, [selectedWidgetIds, localWidgets, handleWidgetRemove, undoRedo, canvasId, debouncedSave, markWidgetDeleted, markConnectorDeleted])
2712
2736
 
2713
2737
  // Ref to store processImageFile for use by drop effect
2714
2738
  const processImageFileRef = useRef(null)
@@ -63,6 +63,14 @@ const DEFAULT_THEME = {
63
63
  selectionBackground: '#264f78',
64
64
  }
65
65
 
66
+ function normalizeStatus(s) {
67
+ if (s === 'completed') return 'done'
68
+ if (s === 'running' || s === 'working' || s === 'pending') return 'pending'
69
+ if (s === 'error') return 'error'
70
+ if (s === 'done') return 'done'
71
+ return 'idle'
72
+ }
73
+
66
74
  function calcMiniDimensions(widthPx, heightPx) {
67
75
  const scale = MINI_FONT_SIZE / 13
68
76
  const cellWidth = 7.8 * scale
@@ -81,7 +89,7 @@ const PromptWidget = forwardRef(function PromptWidget({ id, props, onUpdate, res
81
89
  const width = readProp(props, 'width', promptSchema)
82
90
  const height = readProp(props, 'height', promptSchema)
83
91
  const [draftText, setDraftText] = useState('')
84
- const [execStatus, setExecStatus] = useState(persistedStatus || 'idle')
92
+ const [execStatus, setExecStatus] = useState(normalizeStatus(persistedStatus))
85
93
  const [execError, setExecError] = useState(errorMessage || '')
86
94
  const [showOutput, setShowOutput] = useState(false)
87
95
  const canEdit = typeof onUpdate === 'function'
@@ -125,10 +133,10 @@ const PromptWidget = forwardRef(function PromptWidget({ id, props, onUpdate, res
125
133
  onUpdateRef.current?.({ status: 'idle', sessionId: '', errorMessage: '' })
126
134
  } else if (data.status === 'working') {
127
135
  setExecStatus('pending')
128
- onUpdateRef.current?.({ status: 'working' })
136
+ onUpdateRef.current?.({ status: 'pending' })
129
137
  } else if (data.status === 'running' || data.status === 'pending') {
130
138
  setExecStatus('pending')
131
- onUpdateRef.current?.({ status: 'running' })
139
+ onUpdateRef.current?.({ status: 'pending' })
132
140
  }
133
141
  }
134
142