Uh oh!
There was an error while loading. Please reload this page.
feat(app): reconfigurable titlebar controls - #277
Open
jeonghun-jj-lee wants to merge 12 commits into
Open
Conversation
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Team Run ID: Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Add TitlebarLayout type, canonical control IDs, validation function, and settings store integration. The validation enforces that left.concat(right) always contains exactly the five canonical IDs; any malformed config falls back to the default (all controls on the right in canonical order). Closesharmoniqs/amicode#686
Replace the single #opencode-titlebar-right portal mount with per-button mount points (#opencode-titlebar-sessions, -status, -side-panel). The titlebar reads titlebarLayout from settings and renders controls in config order via TitlebarControlSlot. Session-scoped buttons portal to their own mount; Profile and Settings render directly. A left slot renders before the tab strip when the config has left-side controls. Closesharmoniqs/amicode#687
Add a right-click context menu on the titlebar with 'Customize' and 'Reset' entries. Customize enters edit mode (a process-global signal): each control gets a dashed ring, click handlers are suppressed, and a checkmark button appears at the rightmost edge. Escape key and the checkmark both exit edit mode. Reset writes the default titlebarLayout config. Closesharmoniqs/amicode#688
Wire @dnd-kit/solid into TitlebarControlSlot for within-slot drag-reorder during edit mode. Each control becomes a sortable item; on drop the layout config saves immediately. Pure reorder functions (reorderWithinSlot, moveToSlot) are tested independently. Cross-slot movement via moveToSlot is wired for programmatic use; the spatial DnD cross-boundary detection is available for a follow-up polish pass. Closesharmoniqs/amicode#689
jeonghun-jj-leeforce-pushed
the
reconfigurable-titlebar
branch
from
September 1, 2026 23:43
a2cad95 to
2922c63CompareThe old TitlebarControlSlot used <Show> with two separate <For> branches (one for normal mode, one for edit mode). Toggling edit mode destroyed the current branch's DOM — including the mount-point <div> elements that session-header portals target — and created fresh ones in the other branch. The portals held stale references to the detached nodes, so Sessions, Status, and Side Panel buttons vanished. Reset didn't help because it also toggled edit mode, triggering the same destruction. Fix: - Single <For> loop always renders, wrapped by an always-present DragDropProvider. Sensors are empty when editMode is off so no drag can start; SortableControlItem gates visual treatment via editMode prop. - Mount-point divs are created once and never destroyed by mode changes. SolidJS <For> tracks string items by value, so reorders move existing DOM nodes without recreating them. - useTitlebarControlMount gains a requestAnimationFrame fallback for late-appearing mount points (belt-and-suspenders). - createMountPointTracker: live getElementById wrapper tested with 3 new unit tests (stable refs, repeated calls, replacement detection). 1011 tests passing, 0 failures.
Three fixes to the reconfigurable titlebar: 1. CSS: replace pointer-events: none on .titlebar-control-edit with a ::before transparent overlay. The old rule blocked all pointer events on sortable wrappers, preventing PointerSensor's pointerdown from ever firing. The overlay captures events for DnD (pseudo-element events fire on the generating element) while suppressing clicks on interactive children underneath. 2. DnD init: use static sensors on DragDropProvider + the disabled prop on useSortable. PointerSensor binds per-element pointerdown listeners at mount time; dynamically adding sensors at runtime doesn't rebind existing sortable elements. Static sensors ensure binding happens when items first mount; the disabled flag gates drag activation. 3. Per-control context menu: right-clicking a control in edit mode shows 'Move to left of tabs' / 'Move to right of tabs', wiring the existing moveToSlot function. New controlSlotLabel pure function with 2 unit tests. Verified end-to-end with Playwright: 28 browser checks (controls visible, edit mode, cross-slot move, Escape, reset — all mount points survive). 1013 unit tests passing, 0 failures.
Lift DragDropProvider from inside TitlebarControlSlot to the V2 layout
level so a single provider wraps both left and right slots. This enables
cross-container drag: sortable items carry a group prop ('left'/'right'),
and the shared onDragEnd handler uses reconcileDragEnd to detect within-
slot reorder vs cross-slot move.
When in edit mode, an empty left slot shows a dashed TitlebarEditDropZone
using useDroppable — the visual hint that controls can be placed to the
left of the tab strip. The drop zone highlights on drag hover via
droppable.isDropTarget().
New pure function reconcileDragEnd encapsulates the same-group vs cross-
group decision, tested with 5 unit cases. TitlebarV2Left now always
renders in edit mode (shows either its controls or the drop zone).
1018 unit tests passing, 0 failures. 19 Playwright E2E checks: controls
visible, drop zone appears, cross-slot context menu move (both
directions), mount-point stability through every transition, reset.@dnd-kit's OptimisticSortingPlugin requires both source and target groups to have at least one useSortable item. When a slot is empty, its drop zone is a plain useDroppable — the plugin ignores it, so source.group and source.index never update and the item snaps back. Add reconcileDropOnEmptySlot: when onDragEnd sees a non-sortable target whose id matches a slot name ('left'/'right'), look up the control from the source group and move it with moveToSlot at index 0. The existing reconcileDragEnd path is unchanged for within-group reorder and cross- group moves between populated slots. 5 new unit tests for the empty-slot path; 1023 total passing, typecheck clean. 15 Playwright E2E checks pass including the new drag-to-empty- slot scenario that previously failed.
TitlebarControlSlot gap-0 → gap-1 so controls aren't jammed together. SessionChatsDropdown trigger p-1.5 → size-9 (fixed 36px square) to match the other IconButtonV2 large buttons in the titlebar.
NewSessionStatus portaled Sessions and Status into #opencode-titlebar-right
which only exists in the legacy titlebar. The V2 titlebar uses per-control
mount points (#opencode-titlebar-sessions, #opencode-titlebar-status, etc.)
so the portals silently landed nowhere — the buttons vanished on every new
session / draft tab.
Switch NewSessionPage to useTitlebarControlMount('sessions') and
useTitlebarControlMount('status'), and split NewSessionStatus into two
individual <Portal> calls matching the SessionHeader pattern for active
sessions.
Also harden useTitlebarControlMount with a polling loop (up to 10 rAF
frames) instead of a single retry, covering the race when the hook is
called from a lazy-loaded route inside <Suspense>.…rops Two interacting bugs caused buttons to vanish when dropped on a tab or empty space during edit mode: 1. onDragEnd fell through to reconcileDragEnd when event.operation.target was null. The OptimisticSortingPlugin does NOT revert source.group / source.index on a non-canceled drop, so stale intermediate values from the plugin produced an unintended cross-slot move. 2. useTitlebarControlMount cached the mount-point element reference once on mount. When a cross-slot move destroyed the old div and the <For> loop created a new one (same ID, different DOM node), the portal still targeted the detached element — rendering into the void. Fixes: - Guard onDragEnd: return early when target is null (void drop), letting the Solid adapter's restorePosition snap the button back. - Harden reconcileDragEnd: reject unrecognized group names and out-of- bounds initialIndex, returning the original layout unchanged. - Harden reorderWithinSlot: reject out-of-bounds fromIndex instead of splicing undefined into the array. - Make useTitlebarControlMount reactive: createEffect tracks titlebarLayout() and re-queries getElementById via queueMicrotask, so portals retarget when mount-point divs are recreated by cross-slot moves (both accidental and intentional via context menu). 7 new unit tests covering the edge cases.
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 freeto 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
Make the five titlebar controls (Sessions, Status, Side Panel, Profile, Settings) drag-reorderable across two configurable slots (left/right of tab strip) via an explicit edit mode.
Changes
Documentation
Test results
996 unit tests passing, 0 failures. Clean typecheck across all 30 packages.
Closesharmoniqs/amicode#684