feat: branches/worktrees tabs, canvas selection + solo, zoom-bug fix#39
Open
lukataylo wants to merge 8 commits into
Open
feat: branches/worktrees tabs, canvas selection + solo, zoom-bug fix#39lukataylo wants to merge 8 commits into
lukataylo wants to merge 8 commits into
Conversation
Ships the cross-cutting batch the user pulled together this session:
Branches + Worktrees side-panel tabs
- New core/branches + core/worktrees plugins; live in the left
SidePanel alongside Layers
- Selection store (apps/web/src/state/selectionStore.ts) owns
selectedBranchId / soloBranchId / activeWorktreeId, persisted to
localStorage and observable via useSyncExternalStore
- Branch row click pans canvas to fit that branch's frame bounds
via new registerFitToHook() — Cmd-click toggles Solo
- Branches grouped Active / Stale / Merged with filter chips
(active default; merged collapsed)
- First-load dismissable tips explain the model
Solo / isolate mode
- FrameLayer accepts soloBranchId prop; skips non-matching frames
- SoloBanner pinned to canvas top with "Exit Solo" so the mode can
never get stuck even when both panels are collapsed
- Selection store keeps solo + selection state interlocked
Worktree dispatch target
- Active worktree shown as "→ ~/path" chip in TopBar (right of MCP)
- CreateDispatchRequest.worktreeHint? added to @foldo/protocol
- useDispatchFlow + DomEditor save-to-source inject the hint at
dispatch time; server attaches to in-flight Dispatch object;
MCP bridge logs "worktree hint: <path>" as a progress event
SidePanel pill-collapse
- Manual ‹/› collapse on either panel; localStorage-persisted
- Pill: 36px vertical icon rail with one button per tab; click
expands + activates
Canvas zoom toolbar-loss bug (cross-browser)
- Wheel listener now binds once with a zoomRef instead of
re-binding on every zoom event (eliminated race window that
dropped preventDefault under fast trackpad pinches)
- Safari proprietary gesture events (gesturestart/change/end)
drive zoom directly from e.scale, anchored at the snap point
— previously Safari suppressed wheel+ctrlKey during a pinch
and our preventDefault left zoom dead
- All chrome elements (TopBar, panels, plugin toolbar, zoom
control, first-run hint) bumped to z-index 95–110 so canvas
content can never paint over them
Marketing route fix
- CookieBanner.tsx -> ConsentNotice.tsx, CookiePolicy.tsx
-> StoragePolicy.tsx (file paths matched ad-blocker heuristics
causing ERR_BLOCKED_BY_CONTENT_BLOCKER on lazy import)
- /cookies and /cookie-policy URLs preserved
Polish
- Comment pin shadow now uses filter:drop-shadow so the teardrop
outline traces correctly instead of haloing on light backgrounds
- Markdown frame "TINTS ON" button no longer wraps to two lines
(added whitespace-nowrap + flex-shrink-0)
- Inspect (DomEditor) tightened: 28→24px controls, 12→8px stack
gap, smaller ghost/save buttons; pick/save/ghost button styling
- Layers panel typography tightened: search 16→12px, toolbar
buttons 40→24px, tree rows 36→24px
- SidePanel + TopBar align on the same 12px left edge inset
- Demo User chip matches Capture/Tests/Share chip height
Plugin substrate
- PluginRegistry.install is now idempotent by manifest.id, and
bootPlugins() calls registry.reset() each time so dev-mode HMR
no longer duplicates plugin contributions (root cause of the
"3 toolbars" bug)
Cleanup
- Deleted apps/web/src/components/LeftRail.tsx (no longer rendered;
its testids now live on the bottom PluginToolBar)
- Updated core-plugins-still-work.spec to stop asserting the
legacy LeftRail testid alongside the new toolbar testid
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Clicking the Demo User chip opened the dropdown and Safari's default button focus chrome painted a light system colour over `bg-panel`, "removing" the background. `appearance-none` strips that default; `focus:bg-panel active:bg-panel focus-visible:bg-panel` pin the panel colour across every interaction state so the chip matches the Capture/Tests/Share chips next to it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…anels The hover state was painting a 5% white tint that read as "background lost" against the dark chip, especially while the dropdown was open and the chip was also focused. Drop the bg-white/5 hover and signal interactivity via a border-colour nudge instead so the dark fill stays solid behind the white label. Also bump the TopBar wrapper to z-[120] and the user-picker dropdown to z-[130] so the SidePanel/InspectPanel (z-[100]) can never overlap the opened dropdown — previously the dropdown could land under a sibling panel when both share the same z-index and the panel appears later in the DOM. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A soloBranchId persisted to localStorage from an earlier session points at a branch the current board may not have (different board, seed reset, etc.) — FrameLayer's Solo filter would then hide every frame on the canvas. A user landing on the board would see an empty canvas + no obvious affordance to recover (the SoloBanner shows the branch name but not why frames are missing). Auto-clear soloBranchId + selectedBranchId when the soloed branch isn't present in the hydrated board. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When the chrome z-indexes got bumped to 100-110 to stay above canvas
content on zoom, several content-floating overlays were left at z-50/60:
- CommentPopover (z-60): pin dropped, popover rendered, but it sat
UNDER the LeftPanel/RightPanel (now z-100) so the user couldn't
see it or compose. This is the user-reported "click frame to add
comment fails" — the pin appeared, the popover was just hidden.
- EditPanel (z-50): "AI edit" right-side dispatch panel could be
fully obscured by RightPanel.
- ToastStack (z-50): toasts could end up behind the bottom toolbar.
Lifted all three to z-[140] (above panels at z-100/120, toolbar at
z-110, below SoloBanner at z-95 — wait, banner is also bumped via the
same fix). Confirmed CommentPopover is now visible after Comment tool
→ click frame.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rapper
Two issues hit when dropping a comment pin in fresh Safari:
1. POST /api/comments returned 400 "Invalid comment body" because the
server validation rejected empty text. The pin-drop flow creates the
comment optimistically with `text: ''` (the compose popover PATCHes
the body in afterwards) — rejecting empty text broke that handshake
and made the optimistic comment vanish a moment after the pin
appeared. Validation now requires `typeof text === 'string'` so the
empty-then-patch flow works.
2. React warned "Each child in a list should have a unique 'key' prop"
from MarkdownView. The line-author tint wraps the rendered element
in a <div> when there's an entry — the inner element had `key={idx}`
but the wrapper didn't, so React couldn't reconcile the wrapper
case. Threaded `idx` into `lineWrap` and put the key on the wrapper.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…gin spam The "Unable to post message to http://localhost:5174. Recipient has origin http://localhost:5173" console spam came from a race: when the review-mode / overrides effects fired before the iframe finished loading, iframe.contentWindow was still on about:blank (inheriting the parent's origin). postMessage with the expected localhost:5174 targetOrigin then logs a cross-origin error in every browser, even though our try/catch swallows the throw. Track an `iframeLoadedRef` that flips true on either: - `foldo.sample.ready` message from the sample-app (preferred), or - the native iframe `onLoad` event (backstop). Effects gate their postToFrame on the ref. Reset to false whenever the iframe URL changes or the frame re-enters the viewport, so a fresh navigation doesn't reuse the previous load's ready state. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The bottom dock's box-shadow was 0 12px 32px rgba(0,0,0,0.45) — strong enough on Retina at certain canvas zooms to read as a "ghost toolbar" outline above the real dock. Pull the offset (12→6) and blur (32→18) in and trim alpha (0.45→0.35) so the dock reads as a single floating card without the halo. Also add a dev-only useEffect that counts how many data-testid="foldo-canvas-leftrail" elements are on the page after mount and console.warns if there's more than one. If a real double-render slips past the substrate's idempotency (HMR misfire, accidental second <PluginToolBar/>), this surfaces it as a console warning rather than a silent visual bug. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
→ ~/pathchip in the TopBar and threads through to MCP asworktreeHinton the dispatch payload.preventDefaultunder fast pinches, and (b) Safari firesgesturestart/change/endinstead of wheel+ctrl during a pinch, so our handler was silent. Wheel listener now binds once via a ref; Safari gesture handlers drive zoom directly frome.scale. All chrome z-indexes lifted to 95–110.CookieBanner.tsx/CookiePolicy.tsxpaths matched uBlock/Safari Content Blocker heuristics →ERR_BLOCKED_BY_CONTENT_BLOCKERon lazy import. Renamed toConsentNotice.tsx/StoragePolicy.tsx. URLs preserved (/cookies,/cookie-policystill route).What's in the box
Selection model (new)
apps/web/src/state/selectionStore.ts— smalluseSyncExternalStore+localStoragestore owningselectedBranchId/soloBranchId/activeWorktreeId. Setters auto-interlock (selecting a new branch exits Solo; entering Solo selects the branch).registerFitToHook(rect)inplugins/registry.ts— Branches plugin can pan canvas to a world rect without importing canvas internals (same escape-hatch pattern asregisterSelectFrameHook).SoloBannerpinned at canvas top withExit Solobutton so the mode can't get stuck even if both panels are pill-collapsed.Branches panel
Active/Mine/Stale/All); search input.Worktrees panel
/api/worktreesendpoint will return. Banner declares the data is stubbed.activeWorktreeIdto the selection store. No canvas pan (worktree = future-tense dispatch target, not visual focus).TopBar worktree chip
→ ~/pathchip right of the MCP chip; click does nothing (status only); hides when no worktree is active.MCP wire
CreateDispatchRequest.worktreeHint?: stringin@foldo/protocol.useDispatchFlowandDomEditorsave-to-source both readselectionStore.activeWorktreeIdat dispatch time and attach the path.worktreeHintto the in-flightDispatchobject before broadcasting + routing to/ws/mcp(not yet persisted — schema migration is a follow-up).worktree hint: <path>as adispatch.progressevent. Honouring it ascwdinrunApplyEditis the next backend piece.SidePanel pill collapse
localStorageper side.Canvas zoom
Canvas.tsxwheel listener now reads zoom through azoomRefinstead of listingviewport.zoomin effect deps. Listener binds once on mount.gesturestart/gesturechange/gestureendlisteners drive zoom frome.scaleandpreventDefaultSafari's visual-viewport zoom that previously pushed toolbars off-screen.Polish
filter: drop-shadowso the teardrop outline traces correctly instead of haloing.whitespace-nowrap+flex-shrink-0).Plugin substrate (root cause of "3 toolbars" bug)
PluginRegistry.installis now idempotent bymanifest.id; newregistry.reset()called frombootPluginsso dev-mode HMR no longer accumulates duplicate plugin contributions.Cleanup
apps/web/src/components/LeftRail.tsx. Itsfoldo-canvas-leftrail+foldo-rail-tool-*testids now live on the bottomPluginToolBar, so the existing e2e suite keeps clicking tools by name.core-plugins-still-work.spec.tsno longer asserts both legacy and new toolbar testids.Test plan
npm run typecheck— clean across all 8 workspaces.npx vitest run— 150 passed / 12 skipped (pre-existing).npm --workspace @foldo/web run build— clean;App-*.jsis 173.6 → 177.3 KB (+3.7 KB raw / +1.1 KB gzip). Bundle budget headroom: 69%.prettier --checkon new files — formatted.Notes for review
worktreeHintis intentionally not persisted on thedispatchestable yet — it's attached in-flight only. DLQ retries after a process restart will lose the hint but the dispatch still succeeds; persisting it is the next schema migration./api/branches/:id/git,/api/worktrees).🤖 Generated with Claude Code