20 KiB
TECH.md — Configurable Toolbar Chips (Header Toolbar)
Product spec: specs/harryalbert/APP-3865/PRODUCT.md
Problem
The header bar (in both horizontal and vertical tabs mode) renders panel toggle buttons in a hardcoded order and side assignment. We need to make these configurable (reorderable, moveable between sides, hideable) using the same ChipConfigurator pattern already used for the agent input footer. Critically, the side a button is placed on determines which side its panel opens on and the order determines the panel stacking order.
The entire feature is gated behind FeatureFlag::ConfigurableToolbar (enabled by default in all release builds).
Relevant code
Panel rendering (two-layer system that needs unification)
app/src/workspace/view.rs:16993—render_panels: outer layer composingVTabs | theme chooser | Main | right panels.app/src/workspace/view.rs:16545—render_banner_and_active_tab: inner layer composingLeftPanel(tools) | Terminal | RightPanel(code review).app/src/workspace/view/vertical_tabs.rs:1120—render_vertical_tabs_panel: renders the vertical tabs sidebar with aResizablewrapper andDragBarSide::Right.
Panel toggle state
app/src/pane_group/mod.rs:843—PaneGroup::left_panel_open: bool(tools panel)app/src/pane_group/mod.rs:845—PaneGroup::right_panel_open: bool(code review)app/src/workspace/view.rs—Workspace::vertical_tabs_panel_open: boolapp/src/workspace/view.rs—WorkspaceState::is_agent_management_view_open: bool(replaces main content)app/src/workspace/view.rs—WorkspaceState::is_notification_mailbox_open: bool(popover)
Panel toggle logic
app/src/workspace/view.rs:6711—toggle_left_panel: tools panel open/closeapp/src/workspace/view.rs:6947—toggle_right_panel: code review open/closeapp/src/workspace/view.rs:6669—toggle_vertical_tabs_panelapp/src/workspace/view.rs:18256—ToggleNotificationMailboxhandler
Notification toast positioning
app/src/workspace/view.rs:20265— mailbox popover anchors toNOTIFICATIONS_MAILBOX_POSITION_ID(follows button naturally).app/src/workspace/view.rs:20342— notification toasts anchor toTAB_BAR_POSITION_IDatBottomRight. This needs to become dynamic.
Toolbar button rendering (current hardcoded layout)
app/src/workspace/view.rs:15417—render_tab_bar_contents: left-side buttons at 15522–15535, right-side viaadd_right_side_tab_bar_controlsat 15543.app/src/workspace/view.rs:15639—add_right_side_tab_bar_controls
Existing chip configurator infrastructure
app/src/chip_configurator/mod.rs—ChipConfiguratorwithLeftRightZoneslayout.app/src/ai/blocklist/agent_view/agent_input_footer/editor.rs—AgentToolbarEditorModal(template for our modal).
Existing footer chip selection settings (pattern to follow)
app/src/terminal/session_settings.rs:82—ToolbarChipSelectiontrait,AgentToolbarChipSelectionenum.app/src/terminal/session_settings.rs:277—agent_footer_chip_selectionsetting.
Onboarding
app/src/settings/onboarding.rs:62—apply_ui_customization_settings
Current state
Panel rendering: two-layer system
The workspace renders panels in two separate composition steps:
Outer layer (render_panels, line 16993):
Flex::row: [ VTabs panel | theme chooser(left) | Main content | resource center/AI assistant(right) ]
Inner layer (render_banner_and_active_tab, line 16545):
Flex::row: [ LeftPanelView(tools) | terminal_content | RightPanelView(code review) ]
The vertical tabs panel sits OUTSIDE the left/right panel pair, at a different level. Tools panel and code review are inside render_banner_and_active_tab. This two-layer split exists for a reason:
Per-tab vs workspace-level state: The inner layer panels (tools, code review) have per-tab open state — PaneGroup::left_panel_open and PaneGroup::right_panel_open are on PaneGroup, and each tab has its own PaneGroup. The tools panel can be open in tab 1 and closed in tab 2. The outer layer panels (vertical tabs, theme chooser, resource center) have workspace-level state that persists across tab switches.
To support arbitrary ordering, we need all configurable panels at the same composable level while preserving the per-tab open state for tools panel and code review. The WorkspaceState::is_left_panel_open() method (workspace/util.rs:206) only checks theme chooser state; is_right_panel_open() (util.rs:202) only checks resource center / AI assistant. These are workspace-global concerns separate from the configurable toolbar panels.
Panel types and their current rendering
Each panel type has distinct rendering and state management:
- Vertical tabs (
render_vertical_tabs_panel):Resizablewrapper withDragBarSide::Right. Self-contained rendering invertical_tabs.rs. Open state:Workspace::vertical_tabs_panel_open. - Tools panel (
LeftPanelView): rendered viaChildView::new(&self.left_panel_view). Has its ownResizablewrapper insideLeftPanelView. Open state:PaneGroup::left_panel_open. - Code review (
RightPanelView): rendered viaChildView::new(&self.right_panel_view). Has its ownResizablewrapper. Open state:PaneGroup::right_panel_open. Can be maximized. - Agent management: replaces the terminal content area entirely (
ChildView::new(&self.agent_management_view)at line 16555). Not a side panel. - Notifications mailbox: popover overlay anchored to
NOTIFICATIONS_MAILBOX_POSITION_ID. Not a side panel.
Toolbar button rendering
In the vertical tabs branch of render_tab_bar_contents (line 15522):
- Left side: tabs panel toggle, tools panel toggle, agent management toggle
- Right side (via
add_right_side_tab_bar_controls): code review toggle, notifications mailbox, avatar
These are hardcoded in order and side.
Proposed changes
1. New HeaderToolbarItemKind enum
New file: app/src/workspace/header_toolbar_item.rs
#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub enum HeaderToolbarItemKind {
TabsPanel,
ToolsPanel,
AgentManagement,
CodeReview,
NotificationsMailbox,
}
Methods:
display_label(&self) -> &str— human-readable name for the configurator.icon(&self) -> Icon— icon for the configurator chip.is_available(app: &AppContext) -> bool— checks feature flags and prerequisite settings.default_left() -> Vec<Self>—[TabsPanel, ToolsPanel, AgentManagement].default_right() -> Vec<Self>—[CodeReview, NotificationsMailbox].all_items() -> Vec<Self>— all variants (availability filtering done at call site).is_panel(&self) -> bool— true for items that open a side panel (TabsPanel, ToolsPanel, CodeReview). False for AgentManagement (replaces content) and NotificationsMailbox (popover).
2. New setting: HeaderToolbarChipSelection
In app/src/workspace/tab_settings.rs:
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
pub enum HeaderToolbarChipSelection {
#[default]
Default,
Custom {
left: Vec<HeaderToolbarItemKind>,
right: Vec<HeaderToolbarItemKind>,
},
}
With left_items(), right_items() methods that resolve Default to hardcoded defaults. Add to TabSettings under appearance.tabs hierarchy.
3. Flatten the panel rendering into a single composition step
This is the core architectural change. Currently panels render at two levels. We unify them.
Replace the two-layer system with a single render_configurable_panels method:
In render_banner_and_active_tab (or a new unified method), build the panel row dynamically:
let config = resolved toolbar config;
let left_items = config.left_items filtered by is_available;
let right_items = config.right_items filtered by is_available;
Flex::row:
for item in left_items where item.is_panel() && item.is_open():
render_panel_for_item(item, PanelPosition::Left)
Main content (terminal or agent management)
for item in right_items where item.is_panel() && item.is_open():
render_panel_for_item(item, PanelPosition::Right)
Where render_panel_for_item dispatches to:
TabsPanel→render_vertical_tabs_panel(withDragBarSideflipped if on right)ToolsPanel→ChildView::new(&self.left_panel_view)(with drag bar side adjusted)CodeReview→ChildView::new(&self.right_panel_view)(with drag bar side adjusted)
This moves vertical tabs out of render_panels and tools/code review out of render_banner_and_active_tab, into a unified composition.
Preserving per-tab state: The unified rendering must still read tools panel open state from PaneGroup::left_panel_open and code review from PaneGroup::right_panel_open (per-tab), while reading vertical tabs from Workspace::vertical_tabs_panel_open (workspace-level). The rendering loop checks the appropriate open state per item type, regardless of which side it's configured on.
Drag bar side: When a panel is on the left, its drag bar (resize handle) should be on the right (DragBarSide::Right). When on the right, it should be on the left (DragBarSide::Left). The vertical tabs panel currently hardcodes DragBarSide::Right (vertical_tabs.rs:1174). Tools panel and code review panel handle this internally in their respective views — these will need a set_panel_position method or constructor parameter.
4. Adapt panel toggle logic to be side-aware
Currently toggle_left_panel and toggle_right_panel are tightly coupled to the left/right concept. We need toggle methods that are panel-type-aware rather than side-aware.
Add a method that resolves which side a panel is on from the config:
fn panel_side(&self, item: HeaderToolbarItemKind, app: &AppContext) -> Option<PanelPosition> {
let config = TabSettings::as_ref(app).header_toolbar_chip_selection.value();
if config.left_items().contains(&item) { Some(PanelPosition::Left) }
else if config.right_items().contains(&item) { Some(PanelPosition::Right) }
else { None }
}
The existing ToggleLeftPanel / ToggleRightPanel workspace actions continue to work — they just consult the config to determine which side the panel actually renders on. The PaneGroup::left_panel_open / right_panel_open booleans track open state independently of side placement.
5. Notification toast positioning
Currently notification toasts anchor at PositionedElementAnchor::BottomRight of TAB_BAR_POSITION_ID (view.rs:20350). When the mailbox button is on the left, this should become BottomLeft.
In the toast rendering code, read the toolbar config to determine which side the mailbox is on, and adjust the anchor accordingly.
6. Shared chip-editor modal rendering + HeaderToolbarEditorModal
The AgentToolbarEditorModal (agent_input_footer/editor.rs) and the new HeaderToolbarEditorModal (workspace/header_toolbar_editor.rs) render an identical modal UI (title, chip sections with left/right drop zones, restore-default link, cancel/save buttons, blur overlay). The duplication is purely in rendering — the domain logic (which settings to read/write, action types, event types) is genuinely different.
New file app/src/chip_configurator/modal_shell.rs provides:
ChipEditorModalConfig<A>— struct carrying everything that varies: title, available-section label,is_at_defaults,is_dirty, action values (cancel, save, reset, activate),chip_action_wrapper, and threeMouseStateHandles.render_chip_editor_modal<A>(configurator, config, appearance) -> Box<dyn Element>— free function that renders the full modal (blur overlay, centered card, title, chip sections, buttons). Generic over the action typeA.
Each modal view keeps its own ChipConfigurator, MouseStateHandles, is_dirty, and domain state. Their View::render builds a ChipEditorModalConfig and calls render_chip_editor_modal. Their TypedActionView::handle_action handles save/open/reset with domain-specific logic.
HeaderToolbarEditorModal specifics:
- On
open(): readsHeaderToolbarChipSelectionfromTabSettings, buildsConfigurableItemlist viaControlItemRenderer::new_with_label_and_icon. - On
save(): writes back toTabSettings::header_toolbar_chip_selection. Syncsshow_code_review_buttonandshow_agent_notifications. - On
reset_default(): restores default layout.
7. Integrate modal into Workspace
- Add
header_toolbar_editor_modal: ViewHandle<HeaderToolbarEditorModal>field. - Add
WorkspaceAction::OpenHeaderToolbarEditor. - Add
is_header_toolbar_editor_opentoWorkspaceState. - Render in the overlay stack.
8. Refactor render_tab_bar_contents to read from settings
Replace hardcoded button rendering in the vertical tabs branch with a loop:
let config = TabSettings::as_ref(ctx).header_toolbar_chip_selection.value();
for item in config.resolved_left(ctx) {
tab_bar.add_child(self.render_header_toolbar_button(item, appearance, ctx));
}
// ... search bar ...
for item in config.resolved_right(ctx) {
right_controls.add_child(self.render_header_toolbar_button(item, appearance, ctx));
}
render_header_toolbar_button dispatches to existing per-item render methods.
9. Right-click context menu
Wrap toolbar items/area with an EventHandler that intercepts right-click and opens a Menu with a single "Edit toolbar" entry dispatching WorkspaceAction::OpenHeaderToolbarEditor.
10. Settings page entry point
Add an "Edit toolbar" button widget in appearance_page.rs under the "Tabs" category, dispatching WorkspaceAction::OpenHeaderToolbarEditor.
11. Show/hide setting sync on save
When saving:
CodeReview→ syncTabSettings::show_code_review_buttonNotificationsMailbox→ syncAISettings::show_agent_notificationsToolsPanel→ composite (no single toggle to sync)TabsPanel,AgentManagement→ no existing individual show/hide setting
12. Side-aware UI element flipping
When panels move sides, all overlay/popup UI elements associated with them must flip to point toward the center of the screen. This is implemented via helpers that read HeaderToolbarChipSelection from TabSettings at render time.
tools_panel_menu_direction(app) -> MenuDirection (drive/items/item.rs): Returns MenuDirection::Right when the tools panel is on the left, MenuDirection::Left when on the right. Used by Warp Drive items, conversation list items, and the sorting button.
tabs_panel_side(app) -> PanelPosition (workspace/view.rs): Returns the side the tabs panel button is on. Used by the detail sidecar, action buttons, and right-click menu.
is_mailbox_on_left(app) -> bool (workspace/view.rs): Returns whether the mailbox button is configured on the left. Used for mailbox popover and toast anchoring.
Flipped elements:
- Notification mailbox popover (
view.rs): AnchorsBottomLeft/TopLeftwhen on left,BottomRight/TopRightwhen on right. - Notification toasts (
view.rs): Same side-aware anchoring. - Tabs panel detail sidecar (
vertical_tabs.rs):detail_sidecar_offset_and_max_heightacceptsPanelPositionand flipsTopRight→TopLeft/BottomRight→BottomLeft(and negates the horizontal gap) when on the right. - Vertical tabs action buttons (
vertical_tabs.rs): Overlay position flips fromTopRighttoTopLeft. - Vertical tabs right-click menu (
view.rs): Anchors flip fromBottomLeft/TopLefttoBottomRight/TopRight. - Conversation list tooltips (
conversation_list/item.rs):tooltip_opens_rightfield onItemPropsflipsMiddleRight→MiddleLeft. - Conversation list kebab button + menu (
conversation_list/item.rs): Button position flips fromTopRighttoTopLeft;MenuDirectionflips. - Warp Drive item overflow button (
drive/items/item.rs): Whenoverflow_on_left, the button renders as aStackoverlay atMiddleLeft(flush with edge) instead of appending to the flex row. This avoids pushing item content. - Warp Drive hover previews and dialogs (
drive/index.rs):add_row_overlay_to_stackflips the X-axis anchor pair from(Right, Left)to(Left, Right)and negates the pixel offset. - Panel borders (
left_panel.rs,right_panel.rs): Border side andDragBarSideare driven byself.panel_position. - Vertical tabs panel (
vertical_tabs.rs): Border and drag bar side are driven by thesideparameter.
End-to-end flow
Toolbar rendering:
render_tab_bar_contentsreadsHeaderToolbarChipSelectionfromTabSettings.- For each configured left item (filtered by
is_available), renders the corresponding button. - Search bar renders in the middle.
- For each configured right item, renders the corresponding button.
Panel rendering:
- Unified panel composition reads the same config.
- For each left panel item that is open, renders the panel on the left of main content, in order.
- Main content (terminal or agent management) renders in the middle.
- For each right panel item that is open, renders the panel on the right, in order.
Configuration flow:
- User right-clicks toolbar → context menu → "Edit toolbar".
WorkspaceAction::OpenHeaderToolbarEditoropens the modal.- Modal reads current config, populates
ChipConfigurator. - User drags items, clicks save.
- Modal writes
Custom { left, right }toTabSettings::header_toolbar_chip_selection. - Toolbar and panels re-render from the new config.
Risks and mitigations
Risk: Flattening the two-layer panel system is a significant refactor that touches the core workspace layout. Mitigation: Both the vertical and horizontal tab paths use the same config-driven rendering. The default config produces identical behavior to the previous hardcoded layout, so existing users see no change unless they customize.
Risk: Resizable panel drag bars need to flip direction based on side.
Mitigation: The Resizable element already accepts DragBarSide. Vertical tabs hardcodes Right at vertical_tabs.rs:1174; tools and code review panels handle it internally. We pass the side through.
Risk: PaneGroup::left_panel_open / right_panel_open naming becomes confusing when panels can be on either side.
Mitigation: Keep the field names as-is (they track the tools panel and code review open state respectively, and are per-tab). Rename can be a follow-up. The side a panel renders on is determined by the toolbar config, not by which boolean tracks its state.
Risk: Flattening the panel layers might break the per-tab vs workspace-level open state distinction.
Mitigation: The unified rendering explicitly reads per-tab state (PaneGroup::left_panel_open / right_panel_open) for tools and code review, and workspace-level state (Workspace::vertical_tabs_panel_open) for the tabs panel. The existing WorkspaceState::is_left_panel_open() / is_right_panel_open() methods (which only concern theme chooser and resource center) remain unchanged since those panels are not configurable.
Risk: Show/hide setting desync if user toggles settings directly outside the configurator.
Mitigation: Toolbar rendering always filters by is_available(). The configurator's open() re-evaluates availability each time.
Testing and validation
- Unit tests:
HeaderToolbarChipSelectionserialization, resolution, availability filtering. - Manual testing:
- Move code review to left → verify it opens left of main content.
- Move tabs panel to right → verify sidebar opens right of main content.
- Configure left as
[CodeReview, TabsPanel], open both → verify code review renders left of tabs. - Move notifications to left → verify toasts appear on left side.
- Save config, restart, verify persistence.
Follow-ups
- Rename
PaneGroup::left_panel_open/right_panel_opento panel-type-specific names (e.g.tools_panel_open,code_review_panel_open). - Consider a dedicated
show_agent_managementboolean setting. - Flip additional secondary elements when on the right side: Warp Drive sorting button menu, create-new button menu, section header dialog positioning.
- Vertical tabs settings popup positioning when the tabs panel is on the right side.
- New-session dropdown menu and sidecar positioning when the tabs panel is on the right side.