Skip to content

feat(home): resizable chat/resource panel divider#3648

Merged
waleedlatif1 merged 11 commits intostagingfrom
feat/resize
Mar 18, 2026
Merged

feat(home): resizable chat/resource panel divider#3648
waleedlatif1 merged 11 commits intostagingfrom
feat/resize

Conversation

@waleedlatif1
Copy link
Collaborator

@waleedlatif1 waleedlatif1 commented Mar 18, 2026

Summary

  • Add resizable divider between the chat panel and resource panel in the home view (MothershipView)
  • Drag the divider to resize; panel respects min (280px) and max (65% viewport) constraints with viewport-resize re-clamping
  • Default width set to 60% of viewport via CSS; uses pointer events with setPointerCapture for unified mouse/touch/stylus support
  • Broad codebase audit: replaced useEffect anti-patterns with better React primitives (render-phase guards, inline ref assignments, lazy useState initializers, effect chains collapsed into direct calls) across ~29 files

Type of Change

  • New feature

Testing

Tested manually

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

@cursor
Copy link

cursor bot commented Mar 18, 2026

You have used all Bugbot PR reviews included in your free trial for your GitHub account on this workspace.

To continue using Bugbot reviews, enable Bugbot for your team in the Cursor dashboard.

@vercel
Copy link

vercel bot commented Mar 18, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Mar 18, 2026 9:37am

Request Review

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 18, 2026

Greptile Summary

This PR delivers two distinct layers of change: a new resizable divider between the chat and resource panels in MothershipView, and a broad codebase refactoring (~29 files) that replaces useEffect-driven state synchronisation with render-phase guards and direct ref assignments.

Resize feature

  • useMothershipResize is well-implemented: Pointer Events with setPointerCapture give unified mouse/touch/stylus support, an AbortController removes all three listeners atomically, pointercancel prevents stuck body styles, and cleanupRef handles unmount mid-drag. MothershipView is correctly wrapped in memo(forwardRef(...)) to receive the ref.
  • One minor edge case: the viewport-resize re-clamp handler enforces MAX_PERCENTAGE but can set el.style.width below MOTHERSHIP_WIDTH.MIN on very narrow viewports (e.g. a tablet at ~430 px wide where 430 × 0.65 = 279 px < 280 px). The clamp expression should include a Math.max(MIN, ...) guard.

useEffect refactoring

  • The pattern of replacing useEffect(() => { ref.current = value }, [value]) with a direct render-phase assignment (ref.current = value) is correct and well-supported by the React docs for always-fresh refs.
  • Render-phase guards (prevX / setPrevX pattern) replacing useEffect for derived state are correctly applied in MothershipView, Toolbar, WandPromptBar, PlanModeSection, TodoList, Preview, ConnectionsSection, General, CredentialsManager, SearchModal, and UserInput.
  • The use-autosave.ts fix adds idleTimerRef and cancels it before each new idle reset. One remaining gap: the idle timer from a completed save is not cancelled at the start of the chained save() call, so a chained save that takes >2 seconds can still briefly show 'idle' while the save is in-flight. Cancelling at the top of save() would fully close this race.
  • use-drag-drop.ts removes the useEffect that cleared hoverFolderId/dropIndicator when isDragging went false. This is safe because handleDragEnd always fires after drop in the browser's event lifecycle and covers both state fields — but handleDrop alone does not clear hoverFolderId, so if onDragEnd is ever unwired, stale hover highlighting could persist.

Key items

  • Resize panel collapses reset the dragged width back to CSS 60% on re-expand (no persistence) — acknowledged and intentional per PR description update.
  • No tests were added for the new resize behaviour.

Confidence Score: 4/5

  • PR is safe to merge with minor hardening recommended for the viewport-resize min-width edge case and the autosave idle-timer race.
  • The core resize feature is well-engineered with proper event handling, cleanup paths, and constraint enforcement. The broad useEffect refactoring is technically sound — each replacement follows React's documented patterns for refs and render-phase guards. Two style-level gaps exist: the viewport re-clamp can violate MIN on very narrow screens, and the autosave idle timer is not cancelled at the top of a chained save, leaving a narrow race window. Neither is a blocking correctness issue in practice. No automated tests cover the new behaviour.
  • apps/sim/app/workspace/[workspaceId]/home/hooks/use-mothership-resize.ts (viewport resize min-width guard), apps/sim/hooks/use-autosave.ts (idle timer cancellation at save start)

Important Files Changed

Filename Overview
apps/sim/app/workspace/[workspaceId]/home/hooks/use-mothership-resize.ts New hook implementing the resizable panel feature. Well-structured with AbortController for atomically removing listeners, pointercancel for stuck-styles prevention, unmount cleanup via cleanupRef, and a viewport-resize re-clamp listener. Minor note: the idle width constraint only applies a max cap, not a min-floor re-clamp on viewport resize.
apps/sim/app/workspace/[workspaceId]/home/home.tsx Integrates the resize hook, replaces the two-effect skipResourceTransition chain with a single combined effect, extracts startAnimatingIn with proper timer management, and adds the resize handle DOM element. Render-phase guard for chatId change is correct.
apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/mothership-view.tsx Correctly wraps component in memo(forwardRef(...)) to accept the resize ref while retaining memoization. Render-phase guard for active?.id change replacing useEffect is correctly implemented.
apps/sim/hooks/use-autosave.ts Adds idleTimerRef with clearTimeout before scheduling each idle-reset, eliminating the race where a chained save #2 could be shown as 'idle' while in-flight. Cleanup properly clears both timers on unmount.
apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts Replaces the activeResourceId cleanup effect with a useMemo-derived effectiveActiveResourceId. Removes useLayoutEffect wrapper for processSSEStreamRef/finalizeRef/sendMessageRef in favour of direct render-phase ref assignments — correct pattern for always-fresh function refs.
apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-drag-drop.ts Removed the useEffect that cleared hoverFolderId/dropIndicator when isDragging went false. This is covered by handleDragEnd (which always fires after drop in the browser event lifecycle), so the behaviour is equivalent.
apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/deploy-modal.tsx Merges the stale-output-filter logic into the existing open/workflowId effect, using a ref to break the selectedStreamingOutputs dependency cycle. Validation now runs only when the modal opens rather than on every selection change, which matches the intended behaviour.
apps/sim/stores/constants.ts Adds MOTHERSHIP_WIDTH constant with MIN (280px) and MAX_PERCENTAGE (0.65). Consistent with other panel width constants in the file.

Sequence Diagram

sequenceDiagram
    participant User
    participant Handle as ResizeHandle
    participant Panel as MothershipView
    participant Body as document.body

    User->>Handle: pointerdown
    Handle->>Panel: pin width to current rendered px
    Handle->>Panel: disable CSS transition
    Handle->>Body: set ew-resize cursor and userSelect none
    Handle->>Handle: setPointerCapture, attach move/up/cancel listeners via AbortController

    loop While dragging
        User->>Handle: pointermove
        Handle->>Panel: style.width = clamp(innerWidth - clientX, MIN, MAX_PERCENTAGE)
    end

    alt pointerup
        User->>Handle: release
        Handle->>Panel: restore CSS transition
        Handle->>Body: clear cursor and userSelect
    else pointercancel
        User-->>Handle: browser reclaims gesture
        Handle->>Panel: restore CSS transition
        Handle->>Body: clear cursor and userSelect
    else unmount mid-drag
        Handle->>Panel: cleanupRef fires restore CSS transition
        Handle->>Body: clear cursor and userSelect
    end

    Handle-->>Panel: clearWidth on collapse removes inline style
Loading

Comments Outside Diff (3)

  1. apps/sim/app/workspace/[workspaceId]/home/hooks/use-mothership-resize.ts, line 553-563 (link)

    P2 Viewport resize re-clamp can push panel below MIN

    When the viewport narrows, the handler only checks current > maxWidth and clamps to maxWidth. But if maxWidth itself falls below MOTHERSHIP_WIDTH.MIN (e.g., viewport ≈ 430 px: 430 × 0.65 = 279 px < 280 px), the panel is set to a value smaller than its own minimum constraint. The inline style would override Tailwind's min-w-[280px] class, producing a panel narrower than intended.

  2. apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-drag-drop.ts, line 553-559 (link)

    P2 handleDrop does not clear hoverFolderId — relies on handleDragEnd firing

    The removed useEffect(() => { if (!isDragging) { setHoverFolderId(null) … } }, [isDragging]) previously cleared hoverFolderId as a direct consequence of isDragging going false in handleDrop. Now hoverFolderId is only nulled inside handleDragEnd (line 557 here).

    In normal browser drag flow, dragend always fires on the source element after drop fires on the target, so handleDragEnd will run and clear the state. This is safe under the standard drag lifecycle. However, if a consumer ever fails to wire onDragEnd → handleDragEnd (or the source element unmounts between the two events), hoverFolderId will remain set with a stale value, causing a folder to appear highlighted after the drag completes.

    Consider adding setHoverFolderId(null) directly to handleDrop as a defensive measure:

    const handleDrop = useCallback(
      async (e: React.DragEvent) => {
        e.preventDefault()
        e.stopPropagation()
    
        const indicator = dropIndicator
        setDropIndicator(null)
        setIsDragging(false)
        setHoverFolderId(null)   // ← defensive clear
        siblingsCacheRef.current.clear()
        // …
      },
      [dropIndicator, handleSelectionDrop]
    )
  3. apps/sim/hooks/use-autosave.ts, line 1519-1526 (link)

    P2 Idle timer not cancelled at the start of a chained save

    idleTimerRef is cleared right before a new idle timer is scheduled (at the end of each save). When a chained save() is triggered (last line of this block), the idle timer from the current save is already running. The idle timer is only cancelled when the chained save completes — so if the chained save takes longer than 2 seconds the idle timer fires mid-save, incorrectly resetting the status to 'idle' while a save is still in-flight.

    To fully close the race, cancel the idle timer at the very start of save() (before setSaveStatus('saving')) so that no prior idle timer can fire while any save is running:

    const save = useCallback(async () => {
      clearTimeout(idleTimerRef.current)   // ← cancel any pending idle reset
      setSaveStatus('saving')
      // … rest of save logic
    }, [])

Last reviewed commit: "fix(home): restore c..."

- Remove aria-hidden from resize handle outer div so separator role is visible to AT
- Add viewport-resize re-clamping in useMothershipResize to prevent panel exceeding max % after browser window narrows
- Change default MothershipView width from 60% to 50%
@cursor
Copy link

cursor bot commented Mar 18, 2026

You have used all Bugbot PR reviews included in your free trial for your GitHub account on this workspace.

To continue using Bugbot reviews, enable Bugbot for your team in the Cursor dashboard.

…eed-an-effect

- use-chat: remove messageQueue→ref sync Effect; inline assignment like other refs
- use-chat: replace activeResourceId selection Effect with useMemo (derived value, avoids
  extra re-render cycle; activeResourceIdRef now tracks effective value for API payloads)
- use-chat: replace 3x useLayoutEffect ref-sync (processSSEStream, finalize, sendMessage)
  with direct render-phase assignment — consistent with existing resourcesRef pattern
- user-input: fold onEditValueConsumed callback into existing render-phase guard; remove Effect
- home: move isResourceAnimatingIn 400ms timer into expandResource/handleResourceEvent event
  handlers where setIsResourceAnimatingIn(true) is called; remove reactive Effect watcher
@waleedlatif1
Copy link
Collaborator Author

@greptile

@waleedlatif1
Copy link
Collaborator Author

@greptile

@waleedlatif1
Copy link
Collaborator Author

@greptile

@waleedlatif1
Copy link
Collaborator Author

@greptile

@waleedlatif1
Copy link
Collaborator Author

@greptile

@waleedlatif1
Copy link
Collaborator Author

@greptile

@waleedlatif1 waleedlatif1 merged commit 8a4c161 into staging Mar 18, 2026
11 checks passed
@waleedlatif1 waleedlatif1 deleted the feat/resize branch March 18, 2026 09:57
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.

1 participant