feat(app): reconfigurable titlebar controls - #277

Open
jeonghun-jj-lee wants to merge 12 commits into
local/amicodefrom
reconfigurable-titlebar
Open

feat(app): reconfigurable titlebar controls#277
jeonghun-jj-lee wants to merge 12 commits into
local/amicodefrom
reconfigurable-titlebar

Conversation

@jeonghun-jj-lee

Copy link
Copy Markdown
Contributor

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

  1. titlebarLayout setting + validation — { left, right } config in the settings store with fail-safe validation. 8 unit tests.
  2. Config-driven two-slot rendering — per-button portal mount points replace the single #opencode-titlebar-right. Controls render in config order via TitlebarControlSlot.
  3. Right-click context menu + edit mode — Customize / Reset context menu, dashed ring visual treatment, suppressed popovers, checkmark exit button, Escape handler.
  4. Drag-and-drop reorder — @dnd-kit/solid sortable within each slot. Pure reorder functions tested independently.

Documentation

  • ADR 0005: packages/app/docs/adr/0005-reconfigurable-titlebar-controls.md
  • CONTEXT.md: Titlebar controls term added to packages/app/CONTEXT.md

Test results

996 unit tests passing, 0 failures. Clean typecheck across all 30 packages.

Closesharmoniqs/amicode#684

@coderabbitai

coderabbitaiBot commented Sep 1, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Team

Run ID: 95de648f-ee1a-4d35-ba4a-5843b4d54ec4


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.

❤️ Share

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

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
The 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.
Sign up for freeto 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.

feat(app): reconfigurable titlebar controls

1 participant

@jeonghun-jj-lee
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat(app): reconfigurable titlebar controls - #277

Open
jeonghun-jj-lee wants to merge 12 commits into
local/amicodefrom
reconfigurable-titlebar
Open

feat(app): reconfigurable titlebar controls#277
jeonghun-jj-lee wants to merge 12 commits into
local/amicodefrom
reconfigurable-titlebar

Conversation

@jeonghun-jj-lee

Copy link
Copy Markdown
Contributor

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

  1. titlebarLayout setting + validation — { left, right } config in the settings store with fail-safe validation. 8 unit tests.
  2. Config-driven two-slot rendering — per-button portal mount points replace the single #opencode-titlebar-right. Controls render in config order via TitlebarControlSlot.
  3. Right-click context menu + edit mode — Customize / Reset context menu, dashed ring visual treatment, suppressed popovers, checkmark exit button, Escape handler.
  4. Drag-and-drop reorder — @dnd-kit/solid sortable within each slot. Pure reorder functions tested independently.

Documentation

  • ADR 0005: packages/app/docs/adr/0005-reconfigurable-titlebar-controls.md
  • CONTEXT.md: Titlebar controls term added to packages/app/CONTEXT.md

Test results

996 unit tests passing, 0 failures. Clean typecheck across all 30 packages.

Closesharmoniqs/amicode#684

@coderabbitai

coderabbitaiBot commented Sep 1, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Team

Run ID: 95de648f-ee1a-4d35-ba4a-5843b4d54ec4


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.

❤️ Share

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

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
The 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.
Sign up for freeto 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.

feat(app): reconfigurable titlebar controls

1 participant

@jeonghun-jj-lee
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(app): reconfigurable titlebar controls - #277

Open
jeonghun-jj-lee wants to merge 12 commits into
local/amicodefrom
reconfigurable-titlebar
Open

feat(app): reconfigurable titlebar controls#277
jeonghun-jj-lee wants to merge 12 commits into
local/amicodefrom
reconfigurable-titlebar

Conversation

@jeonghun-jj-lee

Copy link
Copy Markdown
Contributor

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

  1. titlebarLayout setting + validation — { left, right } config in the settings store with fail-safe validation. 8 unit tests.
  2. Config-driven two-slot rendering — per-button portal mount points replace the single #opencode-titlebar-right. Controls render in config order via TitlebarControlSlot.
  3. Right-click context menu + edit mode — Customize / Reset context menu, dashed ring visual treatment, suppressed popovers, checkmark exit button, Escape handler.
  4. Drag-and-drop reorder — @dnd-kit/solid sortable within each slot. Pure reorder functions tested independently.

Documentation

  • ADR 0005: packages/app/docs/adr/0005-reconfigurable-titlebar-controls.md
  • CONTEXT.md: Titlebar controls term added to packages/app/CONTEXT.md

Test results

996 unit tests passing, 0 failures. Clean typecheck across all 30 packages.

Closesharmoniqs/amicode#684

@coderabbitai

coderabbitaiBot commented Sep 1, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Team

Run ID: 95de648f-ee1a-4d35-ba4a-5843b4d54ec4


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.

❤️ Share

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

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
The 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.
Sign up for freeto 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.

feat(app): reconfigurable titlebar controls

1 participant

@jeonghun-jj-lee
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(app): reconfigurable titlebar controls - #277

Open
jeonghun-jj-lee wants to merge 12 commits into
local/amicodefrom
reconfigurable-titlebar
Open

feat(app): reconfigurable titlebar controls#277
jeonghun-jj-lee wants to merge 12 commits into
local/amicodefrom
reconfigurable-titlebar

Conversation

@jeonghun-jj-lee

Copy link
Copy Markdown
Contributor

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

  1. titlebarLayout setting + validation — { left, right } config in the settings store with fail-safe validation. 8 unit tests.
  2. Config-driven two-slot rendering — per-button portal mount points replace the single #opencode-titlebar-right. Controls render in config order via TitlebarControlSlot.
  3. Right-click context menu + edit mode — Customize / Reset context menu, dashed ring visual treatment, suppressed popovers, checkmark exit button, Escape handler.
  4. Drag-and-drop reorder — @dnd-kit/solid sortable within each slot. Pure reorder functions tested independently.

Documentation

  • ADR 0005: packages/app/docs/adr/0005-reconfigurable-titlebar-controls.md
  • CONTEXT.md: Titlebar controls term added to packages/app/CONTEXT.md

Test results

996 unit tests passing, 0 failures. Clean typecheck across all 30 packages.

Closesharmoniqs/amicode#684

@coderabbitai

coderabbitaiBot commented Sep 1, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Team

Run ID: 95de648f-ee1a-4d35-ba4a-5843b4d54ec4


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.

❤️ Share

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

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
The 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.
Sign up for freeto 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.

feat(app): reconfigurable titlebar controls

1 participant

@jeonghun-jj-lee
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat(app): reconfigurable titlebar controls - #277

Open
jeonghun-jj-lee wants to merge 12 commits into
local/amicodefrom
reconfigurable-titlebar
Open

feat(app): reconfigurable titlebar controls#277
jeonghun-jj-lee wants to merge 12 commits into
local/amicodefrom
reconfigurable-titlebar

Conversation

@jeonghun-jj-lee

Copy link
Copy Markdown
Contributor

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

  1. titlebarLayout setting + validation — { left, right } config in the settings store with fail-safe validation. 8 unit tests.
  2. Config-driven two-slot rendering — per-button portal mount points replace the single #opencode-titlebar-right. Controls render in config order via TitlebarControlSlot.
  3. Right-click context menu + edit mode — Customize / Reset context menu, dashed ring visual treatment, suppressed popovers, checkmark exit button, Escape handler.
  4. Drag-and-drop reorder — @dnd-kit/solid sortable within each slot. Pure reorder functions tested independently.

Documentation

  • ADR 0005: packages/app/docs/adr/0005-reconfigurable-titlebar-controls.md
  • CONTEXT.md: Titlebar controls term added to packages/app/CONTEXT.md

Test results

996 unit tests passing, 0 failures. Clean typecheck across all 30 packages.

Closesharmoniqs/amicode#684

@coderabbitai

coderabbitaiBot commented Sep 1, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Team

Run ID: 95de648f-ee1a-4d35-ba4a-5843b4d54ec4


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.

❤️ Share

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

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
The 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.
Sign up for freeto 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.

feat(app): reconfigurable titlebar controls

1 participant

@jeonghun-jj-lee
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(app): reconfigurable titlebar controls - #277

Open
jeonghun-jj-lee wants to merge 12 commits into
local/amicodefrom
reconfigurable-titlebar
Open

feat(app): reconfigurable titlebar controls#277
jeonghun-jj-lee wants to merge 12 commits into
local/amicodefrom
reconfigurable-titlebar

Conversation

@jeonghun-jj-lee

Copy link
Copy Markdown
Contributor

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

  1. titlebarLayout setting + validation — { left, right } config in the settings store with fail-safe validation. 8 unit tests.
  2. Config-driven two-slot rendering — per-button portal mount points replace the single #opencode-titlebar-right. Controls render in config order via TitlebarControlSlot.
  3. Right-click context menu + edit mode — Customize / Reset context menu, dashed ring visual treatment, suppressed popovers, checkmark exit button, Escape handler.
  4. Drag-and-drop reorder — @dnd-kit/solid sortable within each slot. Pure reorder functions tested independently.

Documentation

  • ADR 0005: packages/app/docs/adr/0005-reconfigurable-titlebar-controls.md
  • CONTEXT.md: Titlebar controls term added to packages/app/CONTEXT.md

Test results

996 unit tests passing, 0 failures. Clean typecheck across all 30 packages.

Closesharmoniqs/amicode#684

@coderabbitai

coderabbitaiBot commented Sep 1, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Team

Run ID: 95de648f-ee1a-4d35-ba4a-5843b4d54ec4


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.

❤️ Share

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

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
The 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.
Sign up for freeto 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.

feat(app): reconfigurable titlebar controls

1 participant

@jeonghun-jj-lee
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(app): reconfigurable titlebar controls - #277

Open
jeonghun-jj-lee wants to merge 12 commits into
local/amicodefrom
reconfigurable-titlebar
Open

feat(app): reconfigurable titlebar controls#277
jeonghun-jj-lee wants to merge 12 commits into
local/amicodefrom
reconfigurable-titlebar

Conversation

@jeonghun-jj-lee

Copy link
Copy Markdown
Contributor

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

  1. titlebarLayout setting + validation — { left, right } config in the settings store with fail-safe validation. 8 unit tests.
  2. Config-driven two-slot rendering — per-button portal mount points replace the single #opencode-titlebar-right. Controls render in config order via TitlebarControlSlot.
  3. Right-click context menu + edit mode — Customize / Reset context menu, dashed ring visual treatment, suppressed popovers, checkmark exit button, Escape handler.
  4. Drag-and-drop reorder — @dnd-kit/solid sortable within each slot. Pure reorder functions tested independently.

Documentation

  • ADR 0005: packages/app/docs/adr/0005-reconfigurable-titlebar-controls.md
  • CONTEXT.md: Titlebar controls term added to packages/app/CONTEXT.md

Test results

996 unit tests passing, 0 failures. Clean typecheck across all 30 packages.

Closesharmoniqs/amicode#684

@coderabbitai

coderabbitaiBot commented Sep 1, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Team

Run ID: 95de648f-ee1a-4d35-ba4a-5843b4d54ec4


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.

❤️ Share

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

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
The 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.
Sign up for freeto 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.

feat(app): reconfigurable titlebar controls

1 participant

@jeonghun-jj-lee
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat(app): reconfigurable titlebar controls - #277

Open
jeonghun-jj-lee wants to merge 12 commits into
local/amicodefrom
reconfigurable-titlebar
Open

feat(app): reconfigurable titlebar controls#277
jeonghun-jj-lee wants to merge 12 commits into
local/amicodefrom
reconfigurable-titlebar

Conversation

@jeonghun-jj-lee

Copy link
Copy Markdown
Contributor

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

  1. titlebarLayout setting + validation — { left, right } config in the settings store with fail-safe validation. 8 unit tests.
  2. Config-driven two-slot rendering — per-button portal mount points replace the single #opencode-titlebar-right. Controls render in config order via TitlebarControlSlot.
  3. Right-click context menu + edit mode — Customize / Reset context menu, dashed ring visual treatment, suppressed popovers, checkmark exit button, Escape handler.
  4. Drag-and-drop reorder — @dnd-kit/solid sortable within each slot. Pure reorder functions tested independently.

Documentation

  • ADR 0005: packages/app/docs/adr/0005-reconfigurable-titlebar-controls.md
  • CONTEXT.md: Titlebar controls term added to packages/app/CONTEXT.md

Test results

996 unit tests passing, 0 failures. Clean typecheck across all 30 packages.

Closesharmoniqs/amicode#684

@coderabbitai

coderabbitaiBot commented Sep 1, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Team

Run ID: 95de648f-ee1a-4d35-ba4a-5843b4d54ec4


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.

❤️ Share

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

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
The 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.
Sign up for freeto 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.

feat(app): reconfigurable titlebar controls

1 participant

@jeonghun-jj-lee