-
Notifications
You must be signed in to change notification settings - Fork 0
Dev #19
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Dev #19
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -45,6 +45,13 @@ import { ShapePanel, type ShapeDragPayload } from "./shape-panel"; | |
| import { CanvasNodeRenderer } from "./canvas-node"; | ||
| import { CanvasEdgeRenderer } from "./canvas-edge"; | ||
| import { CanvasControls } from "./canvas-controls"; | ||
| import { LiveCursors, useCursorPresence } from "./live-cursors"; | ||
| import { PresenceAvatars } from "./presence-avatars"; | ||
| import { StarterTemplatesModal } from "./starter-templates-modal"; | ||
| import { | ||
| cloneTemplateWithFreshIds, | ||
| type CanvasTemplate, | ||
| } from "./starter-templates"; | ||
|
|
||
| /** Default style for newly created edges — light stroke, rounded ends, arrow. */ | ||
| const DEFAULT_EDGE_OPTIONS: DefaultEdgeOptions = { | ||
|
|
@@ -67,6 +74,9 @@ const DEFAULT_EDGE_OPTIONS: DefaultEdgeOptions = { | |
|
|
||
| interface CanvasProps { | ||
| roomId: string; | ||
| /** Controlled open state for the starter templates import modal. */ | ||
| templatesOpen?: boolean; | ||
| onTemplatesOpenChange?: (open: boolean) => void; | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -77,17 +87,24 @@ interface CanvasProps { | |
| * the foundation of the editor — node/edge rendering and persistence | ||
| * land in later features. | ||
| */ | ||
| export function Canvas({ roomId }: CanvasProps) { | ||
| export function Canvas({ | ||
| roomId, | ||
| templatesOpen = false, | ||
| onTemplatesOpenChange, | ||
| }: CanvasProps) { | ||
| return ( | ||
| <LiveblocksProvider authEndpoint="/api/liveblocks-auth"> | ||
| <RoomProvider | ||
| id={roomId} | ||
| initialPresence={{ cursor: null, isThinking: false }} | ||
| initialPresence={{ cursor: null, thinking: false }} | ||
| > | ||
| <CanvasErrorBoundary> | ||
| <ClientSideSuspense fallback={<CanvasLoading />}> | ||
| <ReactFlowProvider> | ||
| <CollaborativeFlow /> | ||
| <CollaborativeFlow | ||
| templatesOpen={templatesOpen} | ||
| onTemplatesOpenChange={onTemplatesOpenChange} | ||
| /> | ||
| </ReactFlowProvider> | ||
| </ClientSideSuspense> | ||
| </CanvasErrorBoundary> | ||
|
|
@@ -155,13 +172,23 @@ function generateNodeId(shape: NodeShape): string { | |
| return `${shape}-${timestamp}-${nodeIdCounter}`; | ||
| } | ||
|
|
||
| interface CollaborativeFlowProps { | ||
| templatesOpen: boolean; | ||
| onTemplatesOpenChange?: (open: boolean) => void; | ||
| } | ||
|
|
||
| /** | ||
| * The actual React Flow surface, rendered after Liveblocks has connected | ||
| * and the storage layer is ready (via Suspense). | ||
| */ | ||
| function CollaborativeFlow() { | ||
| function CollaborativeFlow({ | ||
| templatesOpen, | ||
| onTemplatesOpenChange, | ||
| }: CollaborativeFlowProps) { | ||
| const wrapperRef = useRef<HTMLDivElement>(null); | ||
| const { screenToFlowPosition } = useReactFlow(); | ||
| const { screenToFlowPosition, fitView } = useReactFlow(); | ||
| const { onPointerMove: onCursorMove, onPointerLeave: onCursorLeave } = | ||
| useCursorPresence(); | ||
|
|
||
| // `useLiveblocksFlow` returns the synced nodes/edges plus change | ||
| // handlers. With `suspense: true`, `nodes` and `edges` are guaranteed | ||
|
|
@@ -187,6 +214,48 @@ function CollaborativeFlow() { | |
| [lbOnNodesChange], | ||
| ); | ||
|
|
||
| /** | ||
| * Replace the entire collaborative graph with a starter template. | ||
| * Clears existing nodes/edges first, then adds the template graph and | ||
| * fits the view once the new elements are in place. | ||
| */ | ||
| const handleImportTemplate = useCallback( | ||
| (template: CanvasTemplate) => { | ||
| const { nodes: nextNodes, edges: nextEdges } = | ||
| cloneTemplateWithFreshIds(template); | ||
|
|
||
| // Remove every existing edge, then every existing node. | ||
| if (edges.length > 0) { | ||
| onEdgesChange( | ||
| edges.map((edge) => ({ type: "remove" as const, id: edge.id })), | ||
| ); | ||
| } | ||
| if (nodes.length > 0) { | ||
| onNodesChange( | ||
| nodes.map((node) => ({ type: "remove" as const, id: node.id })), | ||
| ); | ||
| } | ||
|
|
||
| // Add template graph into collaborative storage. | ||
| if (nextNodes.length > 0) { | ||
| onNodesChange( | ||
| nextNodes.map((node) => ({ type: "add" as const, item: node })), | ||
| ); | ||
| } | ||
| if (nextEdges.length > 0) { | ||
| onEdgesChange( | ||
| nextEdges.map((edge) => ({ type: "add" as const, item: edge })), | ||
| ); | ||
| } | ||
|
Comment on lines
+227
to
+249
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift 🧩 Analysis chain🏁 Script executed: #!/bin/bash
fd -a '^(package\.json|.*lock.*)$' . -x sh -c \
'echo "### $1"; rg -n "liveblocks|react-flow|xyflow" "$1"' sh {}
rg -n -C5 'batch|transaction|pauseHistory|resumeHistory|useLiveblocksFlow' \
--type=ts --type=tsx .Repository: tejasM17/ghost-dev Length of output: 3922 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Find the target file and nearby Liveblocks/XYFlow usage.
fd -a 'canvas\.tsx$|liveblocks\.config\.ts$|route\.ts$|SKILL\.md$' .
echo '--- package.json ---'
cat -n package.json | sed -n '1,120p'
echo '--- relevant Next/Liveblocks docs in node_modules/next/dist/docs (if present) ---'
fd -a 'node_modules/next/dist/docs' . -t d
fd -a 'node_modules/next/dist/docs' . -t f | sed -n '1,40p'
echo '--- canvas.tsx outline ---'
ast-grep outline components/editor/canvas.tsx --view expanded
echo '--- canvas.tsx relevant slice ---'
cat -n components/editor/canvas.tsx | sed -n '180,290p'
echo '--- search for Liveblocks/React Flow batching APIs in repo files ---'
rg -n -C 3 'batch|transaction|pauseHistory|resumeHistory|useLiveblocksFlow|onNodesChange|onEdgesChange|applyNodeChanges|applyEdgeChanges|createStore|room\.update|storage\.update' components lib app context .agents .claude 2>/dev/null || trueRepository: tejasM17/ghost-dev Length of output: 2843 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo '--- components/editor/canvas.tsx slice ---'
cat -n components/editor/canvas.tsx | sed -n '200,270p'
echo '--- Liveblocks/React Flow API references in repo ---'
rg -n -C 2 'useLiveblocksFlow|pauseHistory|resumeHistory|transaction|batch|useMutation|useStorage|onNodesChange|onEdgesChange' \
components lib app . 2>/dev/null || true
echo '--- package lock entries for `@liveblocks/react-flow` ---'
rg -n -C 4 '"node_modules/@liveblocks/react-flow"|`@liveblocks/react-flow`' package-lock.json
echo '--- installed package docs (if present) ---'
fd --full-path 'node_modules/@liveblocks/react-flow' . 2>/dev/null || true
fd --full-path 'node_modules/@liveblocks/client' . 2>/dev/null || trueRepository: tejasM17/ghost-dev Length of output: 18375 🌐 Web query:
💡 Result: In Liveblocks (including when using React Flow), transaction batching and undo/redo history are managed through specific APIs that allow you to group operations into single undoable actions. Batching and Undo/Redo API When using Citations:
🏁 Script executed: #!/bin/bash
set -euo pipefail
echo '--- top of components/editor/canvas.tsx ---'
cat -n components/editor/canvas.tsx | sed -n '1,120p'
echo '--- search for room/history hooks in canvas.tsx ---'
rg -n 'useRoom|useHistory|history\.|room\.|batch\(|pause|resume|undo|redo' components/editor/canvas.tsx
echo '--- search for Liveblocks room/history hooks elsewhere in repo ---'
rg -n 'useRoom|useHistory|history\.|room\.|batch\(|pause|resume|undo|redo' components lib app context . 2>/dev/null || trueRepository: tejasM17/ghost-dev Length of output: 10734 🌐 Web query:
💡 Result: When using Undo Redo ); } The @liveblocks/react-flow hook useLiveblocksFlow provides the necessary onNodesChange, onEdgesChange, and onDelete handlers that you pass directly to the React Flow component to ensure all changes are synced and history-aware [1][5]. Local-only properties, however, are not recorded on the undo stack [1].
Citations:
🌐 Web query:
💡 Result: When using useLiveblocksFlow, you do not need to manually manage callback grouping for undo history, as the hook handles this automatically for standard React Flow interactions [1][2]. The useLiveblocksFlow hook is designed to automatically record synced changes to nodes and edges on the undo stack in an intuitive way [1][2]: 1. Atomic Actions: Actions like deleting a node and its related edges are handled atomically, meaning they are recorded as a single undoable event [1][2]. 2. Continuous Updates: For high-frequency events like dragging or resizing, Liveblocks automatically consolidates these numerous intermediate state changes into a single action on the undo history stack [1][2]. This prevents the undo stack from becoming cluttered with granular coordinate changes. Because this behavior is built into the hook, passing the provided onNodesChange, onEdgesChange, and onDelete functions directly to your React Flow component is sufficient to achieve this behavior [1][3]. You do not need to write a custom wrapper callback to group these updates yourself [1][2]. If you require advanced control over history (for instance, to pause or resume history recording during complex custom operations), you can use the useHistory hook from Citations:
Group the template import into one Liveblocks history entry. These four storage updates are still separate mutations; wrap the swap in a Liveblocks batch/history pause-resume so ⌘Z reverts the whole replacement in one step. 🤖 Prompt for AI Agents |
||
|
|
||
| // Fit after React Flow has applied the new graph. | ||
| window.setTimeout(() => { | ||
| void fitView({ duration: 200, padding: 0.15 }); | ||
| }, 50); | ||
|
Comment on lines
+251
to
+254
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== Next.js docs candidates =="
git ls-files 'node_modules/next/dist/docs/**' | sed -n '1,120p'
echo
echo "== Canvas file outline =="
ast-grep outline components/editor/canvas.tsx --view expanded || true
echo
echo "== Relevant canvas excerpt =="
sed -n '1,340p' components/editor/canvas.tsxRepository: tejasM17/ghost-dev Length of output: 10289 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Narrowly inspect the editor canvas and any state used to gate fitView.
rg -n "fitView|setTimeout|imported|initialized|nodesInitialized|onInit|onNodesChange|nodes.*ready|ready.*nodes" components/editor/canvas.tsx
echo
echo "== canvas.tsx with line numbers =="
cat -n components/editor/canvas.tsx | sed -n '1,340p'
echo
echo "== Next.js docs under node_modules/next/dist/docs =="
find node_modules/next/dist/docs -maxdepth 2 -type f | sed -n '1,120p'Repository: tejasM17/ghost-dev Length of output: 12726 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== repository files matching canvas/editor =="
git ls-files | rg '^components/editor/canvas\.tsx$|^components/editor/.*'
echo
echo "== components/editor/canvas.tsx (selected range) =="
nl -ba components/editor/canvas.tsx | sed -n '1,340p'
echo
echo "== Next.js docs files with client/component/init/reactflow keywords =="
find node_modules/next/dist/docs -type f \( -name '*client*' -o -name '*component*' -o -name '*app*' -o -name '*routing*' \) | sed -n '1,120p'Repository: tejasM17/ghost-dev Length of output: 1125 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Show only the code around the reported lines and any initialization gate.
nl -ba components/editor/canvas.tsx | sed -n '220,280p'Repository: tejasM17/ghost-dev Length of output: 196 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== canvas tail around ReactFlow props =="
sed -n '330,430p' components/editor/canvas.tsx
echo
echo "== initialization-related symbols in editor components =="
rg -n "useNodesInitialized|onInit|onNodesInitialized|nodesInitialized|fitView|initialized" components/editor
echo
echo "== fitView usage across repo =="
rg -n "fitView" componentsRepository: tejasM17/ghost-dev Length of output: 4107 Wait for the imported graph to be ready before fitting. The fixed 50 ms timeout can still fire before the template nodes finish mounting/measuring, so 🤖 Prompt for AI Agents |
||
| }, | ||
| [edges, nodes, onEdgesChange, onNodesChange, fitView], | ||
| ); | ||
|
|
||
| // Memoized type maps — stable refs so React Flow does not remount nodes/edges | ||
| const nodeTypes = useMemo<NodeTypes>( | ||
| () => ({ | ||
|
|
@@ -272,6 +341,8 @@ function CollaborativeFlow() { | |
| onEdgesChange={onEdgesChange} | ||
| onConnect={onConnect} | ||
| onDelete={onDelete} | ||
| onMouseMove={onCursorMove} | ||
| onMouseLeave={onCursorLeave} | ||
| nodeTypes={nodeTypes} | ||
| edgeTypes={edgeTypes} | ||
| defaultEdgeOptions={DEFAULT_EDGE_OPTIONS} | ||
|
|
@@ -288,9 +359,20 @@ function CollaborativeFlow() { | |
| /> | ||
| </ReactFlow> | ||
|
|
||
| <LiveCursors /> | ||
| <PresenceAvatars /> | ||
|
|
||
| {/* Zoom + undo/redo bar (bottom-left); shape palette (bottom-center) */} | ||
| <CanvasControls /> | ||
| <ShapePanel /> | ||
|
|
||
| {onTemplatesOpenChange ? ( | ||
| <StarterTemplatesModal | ||
| open={templatesOpen} | ||
| onOpenChange={onTemplatesOpenChange} | ||
| onImport={handleImportTemplate} | ||
| /> | ||
| ) : null} | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: tejasM17/ghost-dev
Length of output: 6629
🏁 Script executed:
Repository: tejasM17/ghost-dev
Length of output: 19044
🏁 Script executed:
Repository: tejasM17/ghost-dev
Length of output: 9367
🏁 Script executed:
Repository: tejasM17/ghost-dev
Length of output: 6991
🏁 Script executed:
Repository: tejasM17/ghost-dev
Length of output: 12762
🌐 Web query:
Liveblocks ID token lifetime room access usersAccesses reconnect permissions docs💡 Result:
In Liveblocks, authentication is handled through either ID tokens or access tokens, each with distinct approaches to room permissions and session management. ID Tokens ID tokens function as a secure user identity [1][2]. They do not contain explicit permissions [1]. Instead, authorization is handled server-side by Liveblocks, which checks permissions defined via the REST API for each room [3][1]. Permissions are managed using the defaultAccesses, groupsAccesses, and usersAccesses properties when creating or updating a room [3][4]. This method is recommended for complex permission systems as it centralizes the source of truth [3][5]. Access Tokens Access tokens (JWTs) explicitly grant a user permission to enter specific rooms at the time of authentication [5]. You must manually track and grant these permissions in your authentication endpoint using session.allow [5][6]. While you can grant access to individual rooms [7], it is recommended to use naming patterns and wildcards to scale access across groups or organizations [5][6]. Access tokens do not automatically adapt to new room permissions; they require re-authorization if access needs to change [5]. Reconnection and Permissions If a client's token expires or needs to be refreshed, the Liveblocks client handles re-authorization automatically or through manual triggers [8]: - Token Expiration: When a token expires (error code 4109), the client will automatically attempt to re-authorize [8]. - Manual Reconnection: You can trigger a manual re-authentication by calling room.reconnect on the client side [9]. This allows users to update their permissions or credentials without refreshing the page or losing local application state, such as undo/redo history [9]. Management API Tokens Note that Management API access tokens—used for administrative tasks via the Liveblocks API—are distinct from user authentication tokens [10]. These tokens are team-linked, require manual generation, and have configurable expiration periods ranging from 7 days to 1 year [10]. Top Results: [3][5][9][8][10]
Citations:
TOKEN_EXPIREDcode 4109, change client logic based on error ranges liveblocks/liveblocks#1025🏁 Script executed:
Repository: tejasM17/ghost-dev
Length of output: 5092
🌐 Web query:
Liveblocks ID tokens expire refresh reconnect authentication docs💡 Result:
In Liveblocks, authentication tokens (both ID tokens and access tokens) are managed automatically by the Liveblocks client [1][2][3]. You should never cache these tokens manually on the client side, as the Liveblocks client is designed to cache them for you and will automatically request a fresh token from your authentication endpoint only when necessary, such as when the current token has expired [1][2]. Key mechanisms regarding token lifecycle and reconnection include: 1. Token Refresh: The Liveblocks client transparently handles token expiration [1][3]. By providing an
authEndpoint(either as a string URL or an async callback function) to your client configuration, the client will invoke this endpoint to fetch a new, valid token whenever required [1][4]. 2. Reauthentication: If you need to force a reauthentication (for example, if a user logs out and signs back into your application, or you need to update user metadata without a page refresh), you can use theroom.reconnect()method [5]. This triggers a reconnection and reauthorization flow while maintaining the current client state, such as undo/redo history [5]. 3. Client Logout: Theclient.logout()method can be used to purge all authentication tokens from the client's memory [4]. This forces any currently connected rooms to reauthorize, which is useful in single-page applications (SPAs) when a user explicitly logs out [4]. When implementing your authentication endpoint, you must return a valid token in the format expected by Liveblocks (e.g., as returned byliveblocks.identifyUserorliveblocks.prepareSession) [1][2][4]. If your endpoint returns a forbidden error, the client will stop attempting to reauthorize [1][4].Citations:
Fail closed when Liveblocks revoke fails.
Returning 204 here hides a failed
updateRoom, so a removed collaborator can keep the stale room grant until their client reauthorizes. Revoke before deleting the DB row, or return an error / queue a retry on revoke failures.🤖 Prompt for AI Agents