Skip to content

feat multiselect#408

Open
K1mmyn wants to merge 15 commits into
masterfrom
vps-75-unable-to-select-multiple-elements-in-the-editor
Open

feat multiselect#408
K1mmyn wants to merge 15 commits into
masterfrom
vps-75-unable-to-select-multiple-elements-in-the-editor

Conversation

@K1mmyn

@K1mmyn K1mmyn commented May 21, 2026

Copy link
Copy Markdown
Contributor

Issue

// * DONE Text Selection Broken
// * DONE Clipboard text selection
// * DONE Front back implementation
// * DONE Resize
// * DONE Fix copy and paste
// * DONE Object creation
// * DONE fix object mutationbounds visual
// * DONE fix delete
// * DONE Fix Undo Redo
// * DONE Fix Copy Paste
Maybe a few more

Next Cycle
// ! TODO MultiSelect rotation
// ! TODO Text mode needs undo redo support
// ! TODO Paste undo redo bug where undo only undos one at a time
// ! Clicking Duplicate and dragging cause unexpected behaviour

Typescript type to add?
// ! ERROR npm i --save-dev @types/uuid for another type

Solution

the correct ones, we can discuss all the changes in the meeting

Risk

Checklist

  • Acceptance criteria met
  • Wiki documentation is written and up to date
  • Unit tests written and passing
  • Integration tests written and passing
  • Continuous integration build passing

Summary by CodeRabbit

  • New Features

    • Multi-component selection: simultaneous editing, group transforms, multi-select context menu actions
    • Copy/paste upgraded: multi-item clipboard payloads and plain-text→textbox pasting
  • Improvements

    • Batched undo/redo for grouped change replay (single batch undo/redo)
    • Editor selection now an array; many handlers/UI updated for multi-select consistency
    • Active scene restored from local storage; playing scenarios always opens in a new tab
  • Bug Fixes

    • Cut now clears selection after removal

@linear

linear Bot commented May 21, 2026

Copy link
Copy Markdown

VPS-75

@harbassan harbassan marked this pull request as draft May 30, 2026 11:08
@coderabbitai

coderabbitai Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: bd9b3970-882b-4b5c-b1f1-887b813ab2c8

📥 Commits

Reviewing files that changed from the base of the PR and between 061cfbe and e38e00e.

📒 Files selected for processing (2)
  • frontend/src/features/authoring/AuthoringToolPage.jsx
  • frontend/src/features/authoring/stores/editor.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/src/features/authoring/AuthoringToolPage.jsx

📝 Walkthrough

Walkthrough

This PR implements multi-component selection across the canvas editor: selection is now an array of IDs, pointer handlers support group selection/drag/resize, component APIs accept ID arrays, history records batched ChangeRecords with a unified undo/redo, clipboard/context/keyboard/topbar/text UI updated to the new model.

Changes

Multi-Selection Architecture

Layer / File(s) Summary
Editor selection state model
frontend/src/features/authoring/stores/editor.ts
Editor store refactored: selected changes from string | null to string[], setSelected() accepts ID arrays, and setSelection derives activeStyle from selected[0].
Safe component access
frontend/src/features/authoring/scene/scene.ts
Getters updated with optional chaining to safely handle missing scene.components and absent component IDs.
Multi-ID component operations
frontend/src/features/authoring/scene/operations/component.ts
APIs generalized from single-ID to multi-ID: duplicateComponent, modifyComponentProp, modifyComponentBounds, bringForward, sendBackward, bringToFront, sendToBack now accept and process ID arrays with z-index reordering logic.
Multi-ID text editing operations
frontend/src/features/authoring/scene/operations/text.ts
Text operations (insertChar, deleteChar, deleteSelection, createBlock, applySelectionStyle, mergeDocs) updated to accept ID arrays, with document access using id[0].
Batch operation helpers
frontend/src/features/authoring/scene/operations/modifiers.ts
Modify/remove helpers updated to snapshot prior states per ID and record history as change-record batches in a single call.
Batched undo/redo system
frontend/src/features/authoring/scene/history.ts
History refactored: introduces ChangeRecord type, converts stacks to hold batches of changes, exports unified handleUndoRedo(isUndo) replacing separate undo()/redo(), and moves deletion to undo/redo logic.
Pointer-based multi-selection and bounds
frontend/src/features/authoring/handlers/pointer/pointer.ts
Pointer events updated: component click appends to selection array, canvas click clears to empty array, document click sets single-element array, and new exported getSelectedComponentBounds() derives axis-aligned group bounds and rotation from selected items.
Group mutation and delta application
frontend/src/features/authoring/handlers/pointer/pointer.ts
Drag/mutation workflow updated: handleComponentDrag computes group bounds, handleMutationEnd applies position/scale delta to all selected vertices, and bounds committed via multi-ID operations.
Resize handle computation for multi-select
frontend/src/features/authoring/handlers/pointer/resize.ts
Resize handler refactored to use getSelectedComponentBounds() for group geometry and introduced BOX_CENTER_VALUE constant for center-point checks.
Overlay and handle components for multi-select
frontend/src/features/authoring/canvas/Overlay.tsx, frontend/src/features/authoring/canvas/handles/ConstrainedHandle.tsx, frontend/src/features/authoring/canvas/handles/RotationHandle.tsx
Overlay refactored with ResolveHandles component selecting handle types by component type and multi-select state; resize/rotation handles updated to source bounds from getSelectedComponentBounds() and access mode via store getState().
Keyboard shortcuts for multi-select operations
frontend/src/features/authoring/handlers/keyboard/keyboard.ts, frontend/src/features/authoring/handlers/keyboard/text.ts
Undo/redo routed through handleUndoRedo(), duplicate processes all selected IDs, backspace clears selection before removing, and text mode requires exactly one selected target.
Clipboard operations for multi-select
frontend/src/features/authoring/handlers/keyboard/clipboard.ts
Cut/paste/copy updated: cut clears selection, paste rebuilds from multiple component objects, and addToClipboard accumulates documents for all selected IDs.
Context menu for multi-select actions
frontend/src/features/authoring/handlers/pointer/ComponentContext.tsx, frontend/src/features/authoring/handlers/pointer/context.ts
Menu source switched from data-id attribute to editor store selected, and menu actions (duplicate, delete, z-index reorder) updated to process ID arrays.
Component creation and selection update
frontend/src/features/authoring/handlers/pointer/create.ts
Creation handlers refactored: create-start clears to empty array, create-end sets selection to single-element array with created component ID.
Topbar component updates
frontend/src/features/authoring/topbar/Topbar.tsx
Derives target component from selected[0] or null, wires undo/redo to handleUndoRedo(), updates conditional rendering with optional chaining, and removes element reorder controls.
Shape property UI for multi-select
frontend/src/features/authoring/topbar/ShapeSection.tsx
Extracts properties from first selected component or returns defaults when selection is empty.
Text highlight and selection rendering
frontend/src/features/authoring/text/Text.tsx, frontend/src/features/authoring/text/Highlight.tsx, frontend/src/features/authoring/text/Cursor.tsx
Text components updated to use selected[0] for document indexing, removes duplicate TextHighlight rendering, and updates selection state checks.
Text cursor and selection synchronization
frontend/src/features/authoring/text/cursor.ts
Sync functions add guards requiring selected.length === 1 before accessing selected[0] for blocks and model operations.
Scene initialization and play navigation
frontend/src/features/authoring/AuthoringToolPage.jsx
Active scene resolution consolidated into single effect, and playScenario handler simplified to always open play route without startScene parameter.
Vector utility for scaled component-wise multiplication
frontend/src/features/authoring/util.ts
New exported utility scaleVecByScaleVec() for component-wise vector scaling used in resize operations.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 I hop through code with nimble feet,
Now selections gather, grouped and neat,
Handles turn and histories bind,
Batches and bounds all well-aligned,
A carrot-tuned editor, bold and sweet.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is largely incomplete and does not follow the provided template structure. It contains a list of completed tasks and TODOs without clear sections, missing proper Issue/Solution/Risk organization, and no checklist items are marked complete. Reorganize the description to follow the template: provide a clear Issue statement, Solution summary, Risk assessment, and mark applicable Checklist items as complete or explain their status.
Docstring Coverage ⚠️ Warning Docstring coverage is 10.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat multiselect' is concise and directly describes the main feature being implemented across the changeset: multi-selection support in the editor.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch vps-75-unable-to-select-multiple-elements-in-the-editor
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch vps-75-unable-to-select-multiple-elements-in-the-editor

Comment @coderabbitai help to get the list of available commands and usage tips.

@K1mmyn K1mmyn marked this pull request as ready for review June 1, 2026 05:20
@K1mmyn K1mmyn requested review from del-ereno, harbassan and justinedagreat and removed request for harbassan and justinedagreat June 1, 2026 05:20

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 16

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
frontend/src/features/authoring/scene/operations/text.ts (1)

15-43: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

No guard for empty id array before accessing id[0].

If id is an empty array, id[0] returns undefined, and getComponentProp(undefined, "document") will fail. This applies to all text operations that access id[0].

🛡️ Proposed fix to add early return
 export const insertChar = modify(
   (id: string[], cursor: ModelCursor, char: string) => {
+    if (!id.length) return cursor;
     const doc = getComponentProp(id[0], "document") as ModelDocument;

Apply similar guards to deleteChar, deleteSelection, createBlock, applySelectionStyle, and mergeDocs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/features/authoring/scene/operations/text.ts` around lines 15 -
43, The functions that call getComponentProp(id[0], "document") (notably
insertChar) lack a guard for an empty id array; add an early-return guard at the
start of insertChar (and mirror in deleteChar, deleteSelection, createBlock,
applySelectionStyle, mergeDocs) that checks if id.length === 0 or !id[0] and
returns a no-op or error result before calling getComponentProp, ensuring
subsequent code never passes undefined to getComponentProp.
frontend/src/features/authoring/handlers/keyboard/keyboard.ts (1)

27-27: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use selected.length > 0 checks; array truthiness is incorrect here.

On Line 27 and Lines 36–46, empty selection ([]) still passes current guards, so duplicate/reorder/component ops can fire with no targets.

Suggested fix
 export function handleGlobal(e: KeyboardEvent) {
   const mode = useEditorStore.getState().mode;
   const { selected } = useEditorStore.getState();
+  const hasSelection = selected.length > 0;
@@
-  else if (selected) handleComponentOperations(e, selected);
+  else if (hasSelection) handleComponentOperations(e, selected);
 }
@@
 function handleCtrlOperations(e: KeyboardEvent) {
   const { selected, setSelected } = useEditorStore.getState();
+  const hasSelection = selected.length > 0;
@@
-  else if (e.key === "d" && selected) {
+  else if (e.key === "d" && hasSelection) {
@@
-  } else if (e.key === "ArrowUp" && selected) {
+  } else if (e.key === "ArrowUp" && hasSelection) {
@@
-  } else if (e.key === "ArrowDown" && selected) {
+  } else if (e.key === "ArrowDown" && hasSelection) {

Also applies to: 36-46

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/features/authoring/handlers/keyboard/keyboard.ts` at line 27,
The guards that check the current selection use the array's truthiness, which
treats an empty array as truthy and allows ops to run with no targets; update
all checks that use `selected` as a boolean (including the branch that calls
`handleComponentOperations(e, selected)` and the duplicate/reorder handlers) to
explicitly verify `selected.length > 0` before proceeding so operations only run
when there are actual selected items.
frontend/src/features/authoring/handlers/keyboard/clipboard.ts (1)

24-24: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fix selection guards for array-based selected.

On Lines 24, 33, and 50, selected is an array; truthiness checks treat [] as selected. This can incorrectly intercept clipboard events and route into text-mode paste without targets.

Suggested fix
 export function copy(e: ClipboardEvent) {
   const { selected } = useEditorStore.getState();
-  if (!selected) return;
+  if (selected.length === 0) return;
@@
 export function cut(e: ClipboardEvent) {
   const { selected, setSelected } = useEditorStore.getState();
-  if (!selected) return;
+  if (selected.length === 0) return;
@@
-  if (selected && mode.includes("text")) {
+  if (selected.length > 0 && mode.includes("text")) {

Also applies to: 33-33, 50-50

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/features/authoring/handlers/keyboard/clipboard.ts` at line 24,
The guard uses truthiness on the array variable `selected`, but an empty array
is truthy; update all clipboard handler guards that check `selected` (the
occurrences around the current checks at lines ~24, ~33, ~50) to explicitly test
for an empty array and null/undefined, e.g. replace `if (!selected) return;`
with `if (!selected || selected.length === 0) return;` (or `if
(!selected?.length) return;`) so empty selection arrays don't incorrectly route
clipboard events into text-mode paste.
🧹 Nitpick comments (5)
frontend/src/features/authoring/util.ts (1)

38-40: ⚡ Quick win

Avoid duplicating component-wise multiply logic.

scaleVecByScaleVec duplicates multiply behavior, which increases API/maintenance surface without adding behavior. Prefer delegating to multiply (or replacing call sites with multiply) to keep one source of truth.

♻️ Proposed refactor
 export function scaleVecByScaleVec(v: Vec2, scale: Vec2) {
-  return { x: v.x * scale.x, y: v.y * scale.y };
+  return multiply(v, scale);
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/features/authoring/util.ts` around lines 38 - 40, The function
scaleVecByScaleVec duplicates the component-wise multiply logic already provided
by multiply for Vec2; update scaleVecByScaleVec to delegate to multiply (e.g.,
return multiply(v, scale)) or remove scaleVecByScaleVec and update call sites to
use multiply directly so there is a single source of truth for component-wise
multiplication of Vec2 values; locate the symbol scaleVecByScaleVec and the
existing multiply(Vec2, Vec2) implementation and refactor accordingly.
frontend/src/features/authoring/scene/operations/component.ts (1)

191-197: ⚖️ Poor tradeoff

Multiple history entries created for single z-index operation.

Each modifyComponentProp call triggers the modify wrapper, creating a separate history entry. For a multi-select bring-forward operation, this results in multiple undo steps instead of one atomic batch operation.

Consider either:

  1. Wrapping the entire operation in a single history batch
  2. Using the raw mutation function without the modify wrapper, then manually recording history once
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/features/authoring/scene/operations/component.ts` around lines
191 - 197, The loop over sortedComponents calls modifyComponentProp for each
item, which invokes the modify history wrapper and creates multiple undo
entries; change this to perform all zIndex changes in a single history
transaction by either wrapping the loop in the existing batch history API (use
the modify/history batch function that currently wraps single ops) or call the
underlying raw mutation (bypass modifyComponentProp's modify wrapper) to set
each component's zIndex and then record one history entry once after the loop;
locate sortedComponents and modifyComponentProp in component.ts and implement
one atomic history operation instead of multiple modifyComponentProp calls.
frontend/src/features/authoring/canvas/handles/RotationHandle.tsx (1)

3-3: 💤 Low value

Remove unused import.

useVisualScene is imported but never used in this file.

♻️ Remove unused import
 import { getBoxCenter, rotate } from "../../util";
 import useEditorStore from "../../stores/editor";
-import useVisualScene from "../../stores/visual";
 import { getSelectedComponentBounds } from "../../handlers/pointer/pointer";
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/features/authoring/canvas/handles/RotationHandle.tsx` at line 3,
The file imports useVisualScene but never uses it; remove the unused import
statement for useVisualScene from RotationHandle.tsx to clean up imports and
avoid linter errors—locate the import line that reads "import useVisualScene
from \"../../stores/visual\";" and delete it (ensure no other code in
RotationHandle references useVisualScene afterwards).
frontend/src/features/authoring/handlers/pointer/pointer.ts (1)

86-106: 💤 Low value

Remove debug comments before merging.

These TODO/DONE tracking comments and the TEMPORARY marker should be removed before merge. They document implementation progress but add noise to the production codebase.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/features/authoring/handlers/pointer/pointer.ts` around lines 86
- 106, Remove the temporary debug block and all TODO/DONE/TEMPORARY comments:
delete the conditional that forcibly adds id to selected (the if
(!selected.includes(id)) { setSelected([...selected, id]); } block) and remove
the surrounding progress-tracking comments (lines with TODO/DONE/! TEMPORARY and
related notes) so the pointer handler only contains production logic; ensure no
behavior relied on that debug branch remains and run tests to confirm no
regressions in selection behavior.
frontend/src/features/authoring/handlers/pointer/resize.ts (1)

127-148: ⚡ Quick win

Dead code: speech-triangle handling is unreachable.

Since type is hardcoded to "box" on line 127, the conditions if (type === "speech") on lines 137 and 144 will never evaluate to true. Either remove the dead code or restore the dynamic type determination.

♻️ Option A: Remove dead speech code
   let type = "box";
   let verts = modifyVerts(bounds.verts, coords, position);

   if (!coords.includes(2)) {
     // none of these apply to the speech triangle
     // shift modifier
     if (fixed && !coords.includes(BOX_CENTER_VALUE)) {
       lockAspect(bounds.verts, verts, coords);
     }

-    if (type === "speech") {
-      verts[2] = getNewTail(bounds.verts, verts, coords);
-    }
-
     // alt modifier
     if (anchorCenter) {
       const mirrored = mirror(verts, center, coords);
-      if (type === "speech") {
-        mirrored[2] = getNewTail(verts, mirrored, inverse(coords));
-      }
       verts = mirrored;
     }
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/features/authoring/handlers/pointer/resize.ts` around lines 127
- 148, The code sets type = "box" making the speech-specific branches (if (type
=== "speech")) in resize logic unreachable; either remove those dead branches
(calls to getNewTail and the mirrored tail handling) or restore dynamic type
detection by computing type from the shape/metadata used by modifyVerts/bounds
(e.g., derive type = getPointerType(bounds) or use an existing property on
bounds/verts) so the getNewTail(bounds.verts, verts, coords) and mirrored[2] =
getNewTail(verts, mirrored, inverse(coords)) calls run only when type actually
equals "speech"; update usages of type, verts, and related helpers (modifyVerts,
lockAspect, mirror, inverse) accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@frontend/src/features/authoring/AuthoringToolPage.jsx`:
- Around line 52-53: The code currently calls replace(...) with the result of
scenes.find(...) or scenes[0], which can be undefined and crash replace() when
structuredClone is used; update the logic in AuthoringToolPage.jsx to first
locate the scene with scenes.find((s) => s._id === activeScene) and check that
it is truthy (and that scenes.length > 0) before calling replace; if the find
returns undefined or scenes is empty, either skip calling replace or pass a safe
fallback (e.g., a newly constructed default scene object) so replace() never
receives undefined; ensure you update the branch that uses replace, referencing
the same replace, scenes, and activeScene identifiers.

In `@frontend/src/features/authoring/canvas/handles/ConstrainedHandle.tsx`:
- Around line 10-15: The component uses useEditorStore.getState() to read mode
inside ResizeHandle, which doesn't subscribe to changes so pointerEvents won't
update; replace that call with the selector hook (e.g. const mode =
useEditorStore(s => s.mode)) so ResizeHandle subscribes to mode updates and will
re-render when mode changes; ensure the pointerEvents prop still reads the new
mode variable and remove the getState() usage (leave other calls like
getSelectedComponentBounds() unchanged unless they also need subscription).

In `@frontend/src/features/authoring/canvas/handles/RotationHandle.tsx`:
- Around line 6-11: The component reads mode via useEditorStore.getState() which
breaks reactivity; replace that with the selector hook so RotationHandle
subscribes to mode changes (e.g., useEditorStore(state => state.mode)) and use
the selected-mode variable wherever mode is referenced (not
useEditorStore.getState()); ensure pointerEvents prop and any other
mode-dependent usages in RotationHandle use this subscribed mode so the
component re-renders when mode changes.

In `@frontend/src/features/authoring/canvas/Overlay.tsx`:
- Around line 45-55: The Overlay component currently reads state with
useEditorStore.getState() and useVisualScene.getState(), which bypasses
subscriptions and prevents re-renders; replace those calls with selector hooks
like const { selected, mode, createType, mutationBounds } = useEditorStore(state
=> ({ selected: state.selected, mode: state.mode, createType: state.createType,
mutationBounds: state.mutationBounds })) and const components =
useVisualScene(state => state.components) (use shallow or individual selectors
if needed), then compute primaryComponent from components and selected
(primaryComponent = selected.length ? components[selected[0]] : null) and keep
getSelectedComponentBounds() usage if it already derives from subscribed state
so the Overlay will re-render on selection/mode/mutationBounds/components
changes.

In `@frontend/src/features/authoring/handlers/keyboard/clipboard.ts`:
- Around line 55-56: Wrap the JSON.parse calls that parse the external clipboard
data (the code that assigns parsed/parsed = JSON.parse(components) and then
items = Array.isArray(parsed) ? parsed : [parsed]) in a try/catch so malformed
clipboard content doesn't throw and abort the handler; on parse failure
log/debug the error (or silently ignore) and set items to an empty array or
return early to fail gracefully. Apply the same defensive try/catch pattern to
the other parse site that uses JSON.parse in this file (the second instance
around the parsed/items assignment).
- Line 52: The code uses the non-null assertion selection.start! to set let
cursor = selection.start which can throw if selection.start is undefined; update
the paste/merge handler in clipboard.ts to guard against a missing cursor by
checking selection.start (or selection) before using it, e.g., if
(!selection?.start) return early or compute a safe fallback position, and
replace selection.start! with the guarded value when assigning to the cursor
variable.

In `@frontend/src/features/authoring/handlers/pointer/pointer.ts`:
- Around line 242-245: findComponentRotation currently dereferences
components[id] and will throw if the component is missing; update the function
to check useVisualScene.getState().components[id] (and its .bounds) for
null/undefined and return a safe default rotation (e.g., 0) when missing instead
of accessing .bounds.rotation directly; keep the function name
findComponentRotation and the components retrieval via
useVisualScene.getState().components so callers remain unchanged.
- Around line 293-303: handleTextSelection reads selected[0] without ensuring
the selected array has elements, which can throw when state is inconsistent; add
an early-return guard at the top of handleTextSelection that checks selected
exists and selected.length > 0 (or !selected[0]) before using it, so the
function returns immediately if nothing is selected; keep the rest of the logic
(calling useVisualScene.getState(), parseHit, getRelativePosition, and
setVisualSelection) unchanged and only run when the guard passes.
- Around line 204-215: getNewResizePosition currently mutates the input verts
array (e.g., currentBounds.verts) which corrupts component state; clone the
verts input at the start (e.g., create a new array via slice/map/spread) and
perform all modifications against that cloned array (update verts[i] on the
clone) so the original currentBounds.verts is never mutated, then return the
cloned array; refer to the getNewResizePosition function and the verts variable
when making this change.

In `@frontend/src/features/authoring/scene/history.ts`:
- Around line 78-79: handleUndoRedo currently calls switchToScene(sceneID) but
proceeds even if the switch fails, risking applying batches to the wrong scene;
update handleUndoRedo (and the similar block around lines 108-114) to check the
result of switchToScene(sceneID) and immediately abort/return (no further
mutation or applyBatch calls) when the switch fails or returns falsy, ensuring
no batch/commit is applied unless the active scene was successfully switched.
- Around line 80-87: The snapshot code in ids.map assumes getComponent(id)
returns a component and calls structuredClone(comp) unguarded; update the logic
that builds validChanges (the ids.map / HistoryObject creation) to check the
result of getComponent(id) and skip or omit entries where comp is null/undefined
so you never store an invalid state for the inverse history. Locate the
validChanges creation (ids.map), call getComponent(id) into a local variable
(comp), and only push/return a HistoryObject with sceneId, id, and
structuredClone(comp) when comp is truthy (otherwise filter it out), ensuring
TypeScript type narrowing for HistoryObject and preventing later restore/render
crashes.

In `@frontend/src/features/authoring/scene/operations/component.ts`:
- Around line 124-129: duplicateComponent calls
structuredClone(getComponent(id)) without guarding for getComponent returning
undefined, which causes parseComponent(undefined) to throw; update
duplicateComponent to check the result of getComponent(id) before
cloning/processing (e.g., const comp = getComponent(id); if (!comp) return
null/skip or throw) and only call structuredClone and parseComponent when comp
is defined, referencing duplicateComponent, getComponent, structuredClone, and
parseComponent to locate the change.

In `@frontend/src/features/authoring/scene/operations/modifiers.ts`:
- Around line 26-55: The wrapper modify() is recording ChangeRecords for ids
that don't exist (prevState undefined) which breaks undo; before building
previousStates and later visual updates, filter the incoming ids via
getComponent to only include existing components. Keep calling fn(...args) with
the original args, but compute previousStates = validIds.map(id => ({ id,
prevState: structuredClone(getComponent(id)) })) where validIds = ids.filter(id
=> getComponent(id) != null), pass that to updateHistory(previousStates), and
iterate validIds (not ids) when calling
useVisualScene.getState().updateComponent(buildVisualComponent(...)). Ensure you
reference modify, getComponent, ChangeRecord, updateHistory,
useVisualScene.getState().updateComponent, and buildVisualComponent when
applying the change.

In `@frontend/src/features/authoring/stores/editor.ts`:
- Around line 74-76: The code dereferences getComponent(mainTarget).type without
guarding for null; change to first fetch const comp = getComponent(mainTarget)
and check comp is non-null and comp.type === "textbox" before calling
getStyleForSelection(mainTarget, selection), so avoid calling .type on null and
skip the style call when comp is null.

In `@frontend/src/features/authoring/text/cursor.ts`:
- Around line 131-137: In syncVisualCursor(), remove the visualSelection.start
requirement so the function only requires a single selected component and a
model selection start: delete the "!editorState.visualSelection.start" check
from the first guard (and remove the redundant second guard if you keep the
selection.start check there) and ensure the code uses
"editorState.selection.start" as the required gate along with
"editorState.selected.length === 1"; reference symbols: syncVisualCursor,
editorState.selected, editorState.selection.start,
editorState.visualSelection.start.

In `@frontend/src/features/authoring/topbar/Topbar.tsx`:
- Around line 35-37: The render guard currently checks "selected &&" which
treats an empty array as truthy and causes property controls/actions to render
incorrectly; change those guards to use the computed hasSelection (const
hasSelection = selected && selected.length > 0) instead of "selected &&"
wherever you reference selection (notably the block using component =
hasSelection ? getComponent(selected[0]) : null and the render blocks around the
previous "selected &&" checks between the property/actions area), so that you
only access selected[0], call getComponent, and render property/action controls
when hasSelection is true.

---

Outside diff comments:
In `@frontend/src/features/authoring/handlers/keyboard/clipboard.ts`:
- Line 24: The guard uses truthiness on the array variable `selected`, but an
empty array is truthy; update all clipboard handler guards that check `selected`
(the occurrences around the current checks at lines ~24, ~33, ~50) to explicitly
test for an empty array and null/undefined, e.g. replace `if (!selected)
return;` with `if (!selected || selected.length === 0) return;` (or `if
(!selected?.length) return;`) so empty selection arrays don't incorrectly route
clipboard events into text-mode paste.

In `@frontend/src/features/authoring/handlers/keyboard/keyboard.ts`:
- Line 27: The guards that check the current selection use the array's
truthiness, which treats an empty array as truthy and allows ops to run with no
targets; update all checks that use `selected` as a boolean (including the
branch that calls `handleComponentOperations(e, selected)` and the
duplicate/reorder handlers) to explicitly verify `selected.length > 0` before
proceeding so operations only run when there are actual selected items.

In `@frontend/src/features/authoring/scene/operations/text.ts`:
- Around line 15-43: The functions that call getComponentProp(id[0], "document")
(notably insertChar) lack a guard for an empty id array; add an early-return
guard at the start of insertChar (and mirror in deleteChar, deleteSelection,
createBlock, applySelectionStyle, mergeDocs) that checks if id.length === 0 or
!id[0] and returns a no-op or error result before calling getComponentProp,
ensuring subsequent code never passes undefined to getComponentProp.

---

Nitpick comments:
In `@frontend/src/features/authoring/canvas/handles/RotationHandle.tsx`:
- Line 3: The file imports useVisualScene but never uses it; remove the unused
import statement for useVisualScene from RotationHandle.tsx to clean up imports
and avoid linter errors—locate the import line that reads "import useVisualScene
from \"../../stores/visual\";" and delete it (ensure no other code in
RotationHandle references useVisualScene afterwards).

In `@frontend/src/features/authoring/handlers/pointer/pointer.ts`:
- Around line 86-106: Remove the temporary debug block and all
TODO/DONE/TEMPORARY comments: delete the conditional that forcibly adds id to
selected (the if (!selected.includes(id)) { setSelected([...selected, id]); }
block) and remove the surrounding progress-tracking comments (lines with
TODO/DONE/! TEMPORARY and related notes) so the pointer handler only contains
production logic; ensure no behavior relied on that debug branch remains and run
tests to confirm no regressions in selection behavior.

In `@frontend/src/features/authoring/handlers/pointer/resize.ts`:
- Around line 127-148: The code sets type = "box" making the speech-specific
branches (if (type === "speech")) in resize logic unreachable; either remove
those dead branches (calls to getNewTail and the mirrored tail handling) or
restore dynamic type detection by computing type from the shape/metadata used by
modifyVerts/bounds (e.g., derive type = getPointerType(bounds) or use an
existing property on bounds/verts) so the getNewTail(bounds.verts, verts,
coords) and mirrored[2] = getNewTail(verts, mirrored, inverse(coords)) calls run
only when type actually equals "speech"; update usages of type, verts, and
related helpers (modifyVerts, lockAspect, mirror, inverse) accordingly.

In `@frontend/src/features/authoring/scene/operations/component.ts`:
- Around line 191-197: The loop over sortedComponents calls modifyComponentProp
for each item, which invokes the modify history wrapper and creates multiple
undo entries; change this to perform all zIndex changes in a single history
transaction by either wrapping the loop in the existing batch history API (use
the modify/history batch function that currently wraps single ops) or call the
underlying raw mutation (bypass modifyComponentProp's modify wrapper) to set
each component's zIndex and then record one history entry once after the loop;
locate sortedComponents and modifyComponentProp in component.ts and implement
one atomic history operation instead of multiple modifyComponentProp calls.

In `@frontend/src/features/authoring/util.ts`:
- Around line 38-40: The function scaleVecByScaleVec duplicates the
component-wise multiply logic already provided by multiply for Vec2; update
scaleVecByScaleVec to delegate to multiply (e.g., return multiply(v, scale)) or
remove scaleVecByScaleVec and update call sites to use multiply directly so
there is a single source of truth for component-wise multiplication of Vec2
values; locate the symbol scaleVecByScaleVec and the existing multiply(Vec2,
Vec2) implementation and refactor accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4b49b47c-a3e3-4c91-96df-a357e09dc35b

📥 Commits

Reviewing files that changed from the base of the PR and between 3ef2b7d and 4bbc166.

📒 Files selected for processing (25)
  • frontend/src/features/authoring/AuthoringToolPage.jsx
  • frontend/src/features/authoring/canvas/Overlay.tsx
  • frontend/src/features/authoring/canvas/handles/ConstrainedHandle.tsx
  • frontend/src/features/authoring/canvas/handles/RotationHandle.tsx
  • frontend/src/features/authoring/handlers/keyboard/clipboard.ts
  • frontend/src/features/authoring/handlers/keyboard/keyboard.ts
  • frontend/src/features/authoring/handlers/keyboard/text.ts
  • frontend/src/features/authoring/handlers/pointer/ComponentContext.tsx
  • frontend/src/features/authoring/handlers/pointer/context.ts
  • frontend/src/features/authoring/handlers/pointer/create.ts
  • frontend/src/features/authoring/handlers/pointer/pointer.ts
  • frontend/src/features/authoring/handlers/pointer/resize.ts
  • frontend/src/features/authoring/scene/history.ts
  • frontend/src/features/authoring/scene/operations/component.ts
  • frontend/src/features/authoring/scene/operations/modifiers.ts
  • frontend/src/features/authoring/scene/operations/text.ts
  • frontend/src/features/authoring/scene/scene.ts
  • frontend/src/features/authoring/stores/editor.ts
  • frontend/src/features/authoring/text/Cursor.tsx
  • frontend/src/features/authoring/text/Highlight.tsx
  • frontend/src/features/authoring/text/Text.tsx
  • frontend/src/features/authoring/text/cursor.ts
  • frontend/src/features/authoring/topbar/ShapeSection.tsx
  • frontend/src/features/authoring/topbar/Topbar.tsx
  • frontend/src/features/authoring/util.ts

Comment thread frontend/src/features/authoring/AuthoringToolPage.jsx Outdated
Comment thread frontend/src/features/authoring/canvas/handles/ConstrainedHandle.tsx Outdated
Comment thread frontend/src/features/authoring/canvas/handles/RotationHandle.tsx Outdated
Comment thread frontend/src/features/authoring/canvas/Overlay.tsx
Comment thread frontend/src/features/authoring/handlers/keyboard/clipboard.ts Outdated
Comment thread frontend/src/features/authoring/scene/operations/component.ts
Comment thread frontend/src/features/authoring/scene/operations/modifiers.ts
Comment thread frontend/src/features/authoring/stores/editor.ts Outdated
Comment thread frontend/src/features/authoring/text/cursor.ts Outdated
Comment thread frontend/src/features/authoring/topbar/Topbar.tsx
@K1mmyn

K1mmyn commented Jun 1, 2026

Copy link
Copy Markdown
Contributor Author

code rabbit comments should be fixed now

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
frontend/src/features/authoring/scene/operations/modifiers.ts (1)

60-67: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

remove should guard against missing components like modify does.

If getComponent(id) returns undefined, structuredClone(comp) stores undefined as prevState, violating the ChangeRecord type (Component | null). During undo, the isDelete = batch[0].state === null check will be false for undefined, causing the batch to take the restore path instead of delete, corrupting state.

🔧 Proposed fix to filter valid components
 export function remove(ids: string[], history = true) {
-  const previousStates: ChangeRecord[] = ids.map((id) => {
-    const comp = getComponent(id);
-    return {
-      id,
-      prevState: structuredClone(comp),
-    };
-  });
+  const previousStates: ChangeRecord[] = ids
+    .map((id) => {
+      const comp = getComponent(id);
+      if (!comp) return null;
+      return {
+        id,
+        prevState: structuredClone(comp),
+      };
+    })
+    .filter((record): record is ChangeRecord => record !== null);
+
+  const validIds = previousStates.map((r) => r.id);
 
-  ids.forEach((id) => {
+  validIds.forEach((id) => {
     delete getScene().components[id];
     useVisualScene.getState().deleteComponent(id);
   });
 
-  if (history) updateHistory(previousStates);
+  if (history && previousStates.length) updateHistory(previousStates);
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/features/authoring/scene/operations/modifiers.ts` around lines
60 - 67, The remove function currently stores undefined prevState when
getComponent(id) returns undefined; change remove so it mirrors modify by
filtering out IDs with no component: for each id call getComponent(id), skip any
undefined results, and only push ChangeRecord entries with id and prevState:
structuredClone(comp) for existing components; ensure the later
arrays/operations (previousStates, any batch creation) use this filtered list so
no ChangeRecord.prevState is undefined.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@frontend/src/features/authoring/scene/operations/modifiers.ts`:
- Around line 60-67: The remove function currently stores undefined prevState
when getComponent(id) returns undefined; change remove so it mirrors modify by
filtering out IDs with no component: for each id call getComponent(id), skip any
undefined results, and only push ChangeRecord entries with id and prevState:
structuredClone(comp) for existing components; ensure the later
arrays/operations (previousStates, any batch creation) use this filtered list so
no ChangeRecord.prevState is undefined.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f813c1e8-fa26-4013-b950-2611cd90ae7c

📥 Commits

Reviewing files that changed from the base of the PR and between 4bbc166 and d18101b.

📒 Files selected for processing (12)
  • frontend/src/features/authoring/AuthoringToolPage.jsx
  • frontend/src/features/authoring/canvas/Overlay.tsx
  • frontend/src/features/authoring/canvas/handles/ConstrainedHandle.tsx
  • frontend/src/features/authoring/canvas/handles/RotationHandle.tsx
  • frontend/src/features/authoring/handlers/keyboard/clipboard.ts
  • frontend/src/features/authoring/handlers/pointer/pointer.ts
  • frontend/src/features/authoring/scene/history.ts
  • frontend/src/features/authoring/scene/operations/component.ts
  • frontend/src/features/authoring/scene/operations/modifiers.ts
  • frontend/src/features/authoring/stores/editor.ts
  • frontend/src/features/authoring/text/cursor.ts
  • frontend/src/features/authoring/topbar/Topbar.tsx
🚧 Files skipped from review as they are similar to previous changes (5)
  • frontend/src/features/authoring/canvas/Overlay.tsx
  • frontend/src/features/authoring/scene/operations/component.ts
  • frontend/src/features/authoring/topbar/Topbar.tsx
  • frontend/src/features/authoring/handlers/pointer/pointer.ts
  • frontend/src/features/authoring/handlers/keyboard/clipboard.ts

@harbassan harbassan left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

placeholder for the minor things we talked about at the meeting, which was function and variable naming, some unnecessary rotation modifications, and the location / potentially unnecessary complexity of some of the logic / use of helpers.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants