Initial public release of Warp.
Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
# Notification Toast & Mailbox UI Updates
|
||||
|
||||
## Summary
|
||||
Update the notification toast and mailbox UI to match the new Figma designs. This covers layout, spacing, and content changes across both the floating toast notifications and the persistent mailbox dropdown. The changes introduce a branch name context row, line-clamped text with expand affordances, shared avatar rendering with vertical tabs, and various padding/spacing refinements.
|
||||
|
||||
## Problem
|
||||
The current notification UI was built as a first pass behind the `hoa_notifications` feature flag. The toast and mailbox designs have since been refined with better information density (branch context), improved visual consistency with other surfaces (avatar rendering matching vertical tabs), and more polished spacing/sizing.
|
||||
|
||||
## Goals
|
||||
- Match the Figma spec for both toast and mailbox UI
|
||||
- Introduce branch name context so users can immediately see which branch a notification relates to
|
||||
- Improve text density with character-count truncation and expand affordances
|
||||
- Unify avatar rendering between notifications and vertical tabs
|
||||
|
||||
## Non-goals
|
||||
- Inline approve/decline buttons on toasts (punted)
|
||||
- Changes to notification data model beyond adding branch context
|
||||
- Changes to notification timing, auto-dismiss, or sound behavior
|
||||
- Changes to the notification mailbox's keyboard navigation or action dispatch
|
||||
|
||||
## Figma
|
||||
- Toast: https://www.figma.com/design/CsBdBW4YoLgSAbr5eSkwV6/House-of-Agents?node-id=7411-88337&m=dev
|
||||
- Mailbox: https://www.figma.com/design/CsBdBW4YoLgSAbr5eSkwV6/House-of-Agents?node-id=7411-91197&m=dev
|
||||
|
||||
## User Experience
|
||||
|
||||
### Toast
|
||||
|
||||
#### Container
|
||||
- Width: 420px (was 360px).
|
||||
- Padding: 12px uniform (was 10px vertical / 16px horizontal).
|
||||
- Corner radius: 8px (was 6px).
|
||||
- No drop shadow.
|
||||
|
||||
#### Branch name header row
|
||||
- When a notification has branch context, show a row above the title containing:
|
||||
- Left: git-branch icon (10×10) + branch name text (12px, sub_text color).
|
||||
- Right: chevron icon (expand/collapse toggle, shown only when content is truncated).
|
||||
- When a notification has no branch context, this row is omitted and the toast shows the title only (no timestamp — see below).
|
||||
|
||||
#### Title
|
||||
- Truncated at 100 characters (collapsed) / 500 characters (expanded).
|
||||
- Appends `…` when truncated.
|
||||
|
||||
#### Message / subtext
|
||||
- Truncated at 100 characters (collapsed) / 500 characters (expanded).
|
||||
- Appends `…` when truncated.
|
||||
- The expand chevron (in the branch/title row) is only rendered when content is actually truncated.
|
||||
|
||||
#### Timestamp and unread dot
|
||||
- Removed from the toast entirely.
|
||||
(Toasts are inherently unread and short-lived, so these indicators add no value.)
|
||||
|
||||
#### Avatar
|
||||
- Use the same avatar rendering system as vertical tabs (`render_pane_icon_with_status` with `IconWithStatusVariant`).
|
||||
- This gives proper brand colors for CLI agents, `main_text_color` for Oz, and status badges with cutout rings.
|
||||
- The notification's `NotificationCategory` (Complete/Request/Error) maps to the same status badge icons/colors.
|
||||
|
||||
#### Close button
|
||||
- Positioned at the **top-left** corner of the toast, partially outside the bounds.
|
||||
- Only visible on hover (current behavior preserved).
|
||||
- Has a visible border on the circle.
|
||||
|
||||
#### Keybinding hint
|
||||
- Still shown on the newest toast ("Open conversation" + keyboard shortcut).
|
||||
- No change from current behavior.
|
||||
|
||||
#### Artifact buttons
|
||||
- 48px left padding to align past the avatar column.
|
||||
|
||||
### Mailbox
|
||||
|
||||
#### Outer container
|
||||
- Add 4px top padding before the header.
|
||||
- No drop shadow.
|
||||
|
||||
#### Header
|
||||
- Left side: "Notifications" text (14px, **regular** weight — was semibold).
|
||||
- Right side: X close button.
|
||||
- Padding: `8px vertical, 12px horizontal` (was 8px vertical, 16px left, 8px right).
|
||||
|
||||
#### Filter bar
|
||||
- Button gap: 2px (was 4px).
|
||||
- "All" filter now shows count: e.g. "All tabs (11)" instead of just "All Tabs".
|
||||
- Bar padding: `12px vertical, 12px left, 6px right` (was 12px vertical, 16px horizontal).
|
||||
|
||||
#### Rich notification items (with branch context)
|
||||
- Padding: 12px uniform (was 12px vertical, 16px horizontal).
|
||||
- Branch row at top of the text column:
|
||||
- Left: git-branch icon (10px) + branch name (12px, sub_text color).
|
||||
- Right: timestamp (12px, disabled gray) + unread dot (when unread).
|
||||
- Title: 14px semibold, truncated at 100 characters (collapsed) / 500 characters (expanded).
|
||||
- Message: 14px regular, sub_text color, truncated at 100 characters (collapsed) / 500 characters (expanded).
|
||||
- Artifact buttons: 48px left padding.
|
||||
|
||||
#### Simple notification items (without branch context)
|
||||
- Padding: stays 12px vertical, 16px horizontal (unchanged).
|
||||
- Title row: SpaceBetween with title (14px semibold) | timestamp + optional unread dot (current layout, unchanged).
|
||||
- Title and message use the same 100-character collapsed / 500-character expanded truncation behavior as rich items.
|
||||
- In the mailbox, simple items do not render an expand chevron; in toasts, the chevron appears in the title row when content is truncated.
|
||||
|
||||
#### Shared item changes (both layouts)
|
||||
- Avatar: use vertical tabs `render_pane_icon_with_status` system.
|
||||
- Timestamp font size: 12px (was 14px).
|
||||
|
||||
### Data model
|
||||
- `NotificationItem` needs a new `branch: Option<String>` field.
|
||||
- When present, items render using the "rich" layout (with branch row).
|
||||
- When absent, items render using the "simple" layout (current style).
|
||||
- The branch value must be threaded from the conversation or CLI session context when creating notifications.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
1. Toast container is 420px wide with 12px uniform padding, 8px corner radius, and no drop shadow.
|
||||
2. When a notification has branch context, the toast shows a branch name header row with git-branch icon, branch name, and chevron-right button when the title or message is truncated.
|
||||
3. Toast title text is truncated at 100 characters when collapsed.
|
||||
4. Toast message text is truncated at 100 characters with an expand chevron shown only when content is truncated. Clicking the chevron reveals the full message (up to 500 characters).
|
||||
5. Toast does not display a timestamp or unread dot.
|
||||
6. Toast close button is positioned at the top-left, partially outside the toast, visible only on hover, with a border.
|
||||
7. Toast keybinding hint ("Open conversation" + shortcut) is still shown on the newest toast.
|
||||
8. Toast artifact buttons have 48px left padding.
|
||||
9. All toast and mailbox avatars use the vertical tabs `render_pane_icon_with_status` system with proper brand colors, Oz styling, and status badge cutout rings.
|
||||
10. Mailbox outer container has 4px top padding and no drop shadow.
|
||||
11. Mailbox header has regular-weight "Notifications" title on the left, close button on the right, padded at 8px vertical / 12px horizontal.
|
||||
12. Mailbox filter bar has 2px button gap, the "All" filter shows a count, and padding is 12px vertical / 12px left / 6px right.
|
||||
13. Mailbox rich items (with branch) use 12px uniform padding, show a branch row (with timestamp + unread dot on right), truncated title, truncated message, and 48px left-padded artifact buttons.
|
||||
14. Mailbox simple items (without branch) keep the current layout with 12px vertical / 16px horizontal padding.
|
||||
15. Timestamp font size is 12px in both mailbox item layouts.
|
||||
|
||||
## Validation
|
||||
- Build the app and visually compare toasts and mailbox against the Figma mocks.
|
||||
- Verify toast hover shows close button at top-left with border.
|
||||
- Verify long titles (>100 chars) are truncated with `…` in both toast and mailbox.
|
||||
- Verify long messages (>100 chars) show the expand chevron, and clicking expands the full text (up to 500 chars).
|
||||
- Verify notifications with branch context render the branch row; notifications without branch context render the simple layout.
|
||||
- Verify avatars match vertical tabs (Oz icon with dark background, CLI agents with brand colors, status badges with cutout rings).
|
||||
- Verify keybinding hint still appears on the newest toast.
|
||||
- Verify filter bar "All tabs" shows count and spacing matches Figma.
|
||||
|
||||
## Open Questions
|
||||
1. ~~Where does the branch name come from?~~ Resolved: uses `TerminalView::current_git_branch()`, the same source as the branch chip and vertical tabs.
|
||||
2. ~~What does the settings gear icon in the mailbox header do?~~ Deferred: settings gear removed from this pass.
|
||||
3. ~~Should the expand affordance collapse back after expanding?~~ Resolved: yes, toggle behavior — clicking the chevron toggles between collapsed and expanded.
|
||||
@@ -0,0 +1,281 @@
|
||||
# Notification Toast & Mailbox UI Updates — Tech Spec
|
||||
|
||||
## Problem
|
||||
The notification toast and mailbox UI need visual updates to match new Figma designs. The changes span container sizing, a new branch context row, line-clamped text with expand affordances, shared avatar rendering with vertical tabs, and various padding/spacing adjustments. See `specs/harryalbert/APP-3752/PRODUCT.md` for the full product spec.
|
||||
|
||||
## Relevant Code
|
||||
|
||||
### Notification module
|
||||
- `app/src/ai/agent_management/notifications/mod.rs` — module root, re-exports
|
||||
- `app/src/ai/agent_management/notifications/item.rs` — `NotificationItem`, `NotificationItems`, `NotificationCategory`, `NotificationSourceAgent`
|
||||
- `app/src/ai/agent_management/notifications/item_rendering.rs` — shared rendering: `render_notification_item_content`, `render_agent_avatar`
|
||||
- `app/src/ai/agent_management/notifications/toast_stack.rs` — `AgentNotificationToastStack`, `render_toast`, `render_close_button`, `render_keybinding_hint`
|
||||
- `app/src/ai/agent_management/notifications/view.rs` — `NotificationMailboxView`, header, filter bar, item rendering
|
||||
|
||||
### Avatar rendering (vertical tabs)
|
||||
- `app/src/ui_components/icon_with_status.rs` — `IconWithStatusVariant`, `IconWithStatusSizing`, `render_icon_with_status`
|
||||
- `app/src/workspace/view/vertical_tabs.rs:84-240` — vertical-tab sizing presets and `render_pane_icon_with_status`
|
||||
- `app/src/workspace/view/vertical_tabs.rs:1227-1327` — `resolve_icon_with_status_variant` (maps pane types to avatar variants)
|
||||
|
||||
### Status icons
|
||||
- `app/src/ai/agent/conversation.rs:3531-3538` — `ConversationStatus::status_icon_and_color`
|
||||
- `app/src/ai/conversation_status_ui.rs` — `render_status_element` (used by agent management views)
|
||||
|
||||
### Notification creation
|
||||
- `app/src/ai/agent_management/agent_management_model.rs:287-408` — `handle_history_event_for_mailbox`, `add_notification` (where Oz conversation notifications are created)
|
||||
- `app/src/ai/agent_management/agent_management_model.rs:123-191` — `handle_cli_agent_session_event` (where CLI session notifications are created)
|
||||
|
||||
### CLI session context
|
||||
- `app/src/terminal/cli_agent_sessions/mod.rs:39-48` — `CLIAgentSessionContext` (cwd, project, query, etc. — no branch field currently)
|
||||
|
||||
### Existing text primitives
|
||||
- `warp_core/src/ui/builder.rs:826-840` — `wrappable_text` (the text component used in notifications)
|
||||
- No existing max-lines / line-clamp primitive in the UI framework.
|
||||
|
||||
## Current State
|
||||
|
||||
### Notification items
|
||||
`NotificationItem` (`item.rs:64-78`) holds: `id`, `origin`, `title`, `message`, `category`, `agent`, `is_read`, `created_at`, `terminal_view_id`, `artifacts`. No branch context.
|
||||
|
||||
### Shared rendering
|
||||
`render_notification_item_content` (`item_rendering.rs:62-164`) is used by both toasts and the mailbox. It renders: avatar + title row (title | timestamp + unread dot) + message + optional artifact buttons. The title and message text wrap freely with no line limits.
|
||||
|
||||
### Avatar rendering
|
||||
Notifications use `render_agent_avatar` (`item_rendering.rs:166-236`) which has its own icon/color logic. Vertical tabs use `render_pane_icon_with_status` with `IconWithStatusVariant` (`vertical_tabs.rs:108-240`) which has better styling: proper CLI brand colors, cutout rings on status badges, and background color matching the panel. These two implementations are independent and visually inconsistent.
|
||||
|
||||
### Toast
|
||||
`render_toast` (`toast_stack.rs:347-435`): 360px wide, 10v/16h padding, 6px radius, default shadow. Close button at top-right via `OffsetPositioning`. Keybinding hint on newest toast.
|
||||
|
||||
### Mailbox
|
||||
`NotificationMailboxView::render` (`view.rs:340-408`): 420px wide, 500px max height. Header has "Notifications" (semibold) + X close button. Filter bar has 4px button gap, 16px horizontal padding. "All" filter doesn't show a count. Items use 12v/16h padding.
|
||||
|
||||
## Proposed Changes
|
||||
|
||||
### 1. Add `branch` field to `NotificationItem`
|
||||
|
||||
**File**: `app/src/ai/agent_management/notifications/item.rs`
|
||||
|
||||
Add `pub branch: Option<String>` to `NotificationItem` and the `new()` constructor. This determines whether the item renders with the "rich" layout (branch row) or "simple" layout (current style).
|
||||
|
||||
### 2. Thread branch context through notification creation
|
||||
|
||||
**File**: `app/src/ai/agent_management/agent_management_model.rs`
|
||||
|
||||
Update `add_notification` to accept an `Option<String>` branch parameter. Pass it through to `NotificationItem::new`.
|
||||
|
||||
The branch name should come from the same source as the branch chip in the prompt and the vertical tabs subtitle. The vertical tabs already call `TerminalView::current_git_branch(ctx)` (`app/src/terminal/view/tab_metadata.rs:49-63`), which reads the `ShellGitBranch` context chip or falls back to `GitRepoStatusModel`. Since `add_notification` already receives `terminal_view_id`, the implementation should:
|
||||
|
||||
1. Resolve the `TerminalView` from `terminal_view_id`.
|
||||
2. Call `terminal_view.current_git_branch(ctx)` to get the branch name.
|
||||
3. Pass it as the `branch` parameter.
|
||||
|
||||
This works for both Oz conversations and CLI sessions since both are associated with a terminal view that has prompt chip state.
|
||||
|
||||
**Fallback**: If the terminal view is not accessible (e.g. window closed), pass `None`. The UI gracefully falls back to the simple layout.
|
||||
|
||||
### 3. Extract shared avatar rendering
|
||||
|
||||
**New file**: `app/src/ui_components/icon_with_status.rs`
|
||||
|
||||
Extract the circle icon rendering into a shared UI component that both toasts/mailbox and vertical tabs can use.
|
||||
|
||||
Extract from `vertical_tabs.rs`:
|
||||
- `render_pane_icon_with_status` and its helpers (`render_with_optional_status_badge`)
|
||||
- The `IconWithStatusVariant` enum and the icon sizing constants
|
||||
|
||||
The extracted functions should accept a **size parameter** rather than using hardcoded constants, since vertical tabs and notifications use different sizes:
|
||||
- Vertical tabs: `CIRCLE_NEUTRAL_ICON_SIZE = 16.`, `CIRCLE_AGENT_ICON_SIZE = 10.`, `CIRCLE_NEUTRAL_PADDING = 4.`, `CIRCLE_AGENT_PADDING = 5.`
|
||||
- Notifications (current `render_agent_avatar`): icon 16px, padding 8px → 32px circle total
|
||||
|
||||
The shared rendering function should take a size config struct or individual size parameters so both call sites can specify their dimensions.
|
||||
|
||||
Keep a notification-specific mapping helper in `item_rendering.rs`:
|
||||
```rust
|
||||
fn render_notification_avatar(
|
||||
agent: NotificationSourceAgent,
|
||||
category: NotificationCategory,
|
||||
theme: &WarpTheme,
|
||||
) -> Box<dyn Element>
|
||||
```
|
||||
|
||||
This function maps `NotificationSourceAgent` → `IconWithStatusVariant` and `NotificationCategory` → status badge, then delegates to the shared circle icon renderer with notification-appropriate sizing.
|
||||
|
||||
The mapping:
|
||||
- `NotificationSourceAgent::Oz` → `IconWithStatusVariant::OzAgent` with `status` derived from category
|
||||
- `NotificationSourceAgent::CLI(agent)` → `IconWithStatusVariant::CLIAgent` with `status` derived from category
|
||||
|
||||
For the `status` parameter, add a helper that converts `NotificationCategory` → `ConversationStatus`:
|
||||
- `NotificationCategory::Complete` → `ConversationStatus::Success`
|
||||
- `NotificationCategory::Request` → `ConversationStatus::Blocked { blocked_action: String::new() }`
|
||||
- `NotificationCategory::Error` → `ConversationStatus::Error`
|
||||
|
||||
This reuses `ConversationStatus::status_icon_and_color` (`conversation.rs:3531`) which the vertical tabs already use.
|
||||
|
||||
|
||||
### 4. Split `render_notification_item_content` into rich and simple variants
|
||||
|
||||
**File**: `app/src/ai/agent_management/notifications/item_rendering.rs`
|
||||
|
||||
Currently `render_notification_item_content` is one function. Refactor into:
|
||||
|
||||
**`render_rich_text_column`** (when `item.branch.is_some()`):
|
||||
- Branch row: git-branch icon (10px) + branch name (12px, sub_text color) on left.
|
||||
- Toast: chevron-right icon on right (when content is truncated).
|
||||
- Mailbox: timestamp + unread dot on right.
|
||||
- Title: 14px semibold, character-count truncated.
|
||||
- Message: 14px regular, sub_text, character-count truncated.
|
||||
- Artifact buttons with 48px left padding.
|
||||
|
||||
**`render_simple_text_column`** (when `item.branch.is_none()`):
|
||||
- Current layout: title | timestamp + unread dot in SpaceBetween row.
|
||||
- Message below.
|
||||
- Title and message use the same character-count truncation behavior as rich items.
|
||||
- Mailbox simple rows do not show an expand chevron; toast simple rows show the chevron in the title row when content is truncated.
|
||||
|
||||
Both variants call the shared avatar renderer from step 3.
|
||||
|
||||
Extract shared sub-rendering into free functions that both variants can use:
|
||||
- `render_branch_row(branch, right_side_content, appearance)` — the git-branch icon + text row
|
||||
- `render_clamped_title(title, max_lines, appearance)` — title text with line clamping
|
||||
- `render_clamped_message(message, max_lines, expand_affordance, appearance)` — message with optional expand
|
||||
- `render_timestamp_with_dot(created_at, is_read, appearance)` — timestamp + optional unread dot
|
||||
|
||||
The existing `render_git_branch_text` in `vertical_tabs.rs:1899-1921` is a good reference for the branch row rendering and could be reused or extracted to a shared location.
|
||||
|
||||
A public entry point selects the variant:
|
||||
```rust
|
||||
fn render_notification_item_content(
|
||||
item: &NotificationItem,
|
||||
artifact_buttons: Option<&ViewHandle<ArtifactButtonsRow>>,
|
||||
context: NotificationRenderContext,
|
||||
appearance: &Appearance,
|
||||
) -> Box<dyn Element>
|
||||
```
|
||||
|
||||
Where `NotificationRenderContext` is an enum:
|
||||
```rust
|
||||
enum NotificationRenderContext {
|
||||
Toast,
|
||||
Mailbox,
|
||||
}
|
||||
```
|
||||
|
||||
This context determines whether the branch row's right side shows a chevron (toast) or timestamp + unread dot (mailbox).
|
||||
|
||||
### 5. Implement character-count truncation with expand
|
||||
|
||||
Truncate title and message text using a character-count heuristic rather than a visual line-clamp primitive (which doesn't exist in the UI framework).
|
||||
|
||||
Constants:
|
||||
- `COLLAPSED_MAX_CHARS = 100` — max characters when collapsed
|
||||
- `EXPANDED_MAX_CHARS = 500` — max characters when expanded
|
||||
|
||||
A `truncate_text(text, max_chars)` helper appends `…` when the text exceeds the limit.
|
||||
|
||||
A `content_is_truncated(title, message)` helper returns true when either exceeds `COLLAPSED_MAX_CHARS`, which controls whether the expand chevron is rendered.
|
||||
|
||||
For the expand affordance:
|
||||
- Add `message_expanded: bool` state to `NotificationToastItem` (toast_stack.rs) and per-item state in `NotificationMailboxView` (view.rs).
|
||||
- Both toast and mailbox use a chevron icon (ChevronRight when collapsed, ChevronDown when expanded) as the expand affordance.
|
||||
- The chevron is rendered in the branch row (rich layout) or title row (simple layout, toast only).
|
||||
|
||||
### 6. Update toast container and close button
|
||||
|
||||
**File**: `app/src/ai/agent_management/notifications/toast_stack.rs`
|
||||
|
||||
In `render_toast`:
|
||||
- Width: `360.` → `420.` (line 390)
|
||||
- Padding: `.with_vertical(10.).with_horizontal(16.)` → `.with_uniform(12.)` (line 383)
|
||||
- Corner radius: `Radius::Pixels(6.)` → `Radius::Pixels(8.)` (line 386)
|
||||
- Drop shadow: removed (was `DropShadow::default()`)
|
||||
|
||||
In `render_close_button`:
|
||||
- Change `PositionedElementAnchor::TopRight` / `ChildAnchor::TopRight` → `PositionedElementAnchor::TopLeft` / `ChildAnchor::TopLeft` (line 414)
|
||||
- Adjust offset vector from `(4., -4.)` → `(-4., -4.)` or similar to position outside the top-left
|
||||
- Add a visible border to the close button circle (add `Border::all(0.67)` with outline color)
|
||||
|
||||
### 7. Remove timestamp and unread dot from toast
|
||||
|
||||
In the rich/simple layout variants, when `context == NotificationRenderContext::Toast`:
|
||||
- Rich layout: no timestamp/dot in branch row (chevron-right only when truncated).
|
||||
- Simple layout: no timestamp — show only the title (plus expand chevron when truncated).
|
||||
|
||||
### 8. Update mailbox header
|
||||
|
||||
**File**: `app/src/ai/agent_management/notifications/view.rs`
|
||||
|
||||
In `render_header`:
|
||||
- Change title font weight from `Weight::Semibold` → remove the weight override (defaults to regular).
|
||||
- Update padding from `.with_vertical(8.).with_left(16.).with_right(8.)` → `.with_vertical(8.).with_horizontal(12.)` (line 436-440).
|
||||
|
||||
### 9. Update mailbox filter bar
|
||||
|
||||
**File**: `app/src/ai/agent_management/notifications/view.rs`
|
||||
|
||||
In `render_filter_bar`:
|
||||
- Change filter button spacing from `4.` → `2.` (line 456).
|
||||
- Update "All" filter label to include count: change the conditional at lines 465-473 so `NotificationFilter::All` also shows `"All tabs ({count})"`.
|
||||
- Update bar padding from `.with_vertical(12.).with_horizontal(16.)` → `.with_vertical(12.).with_left(12.).with_right(6.)` (line 537).
|
||||
|
||||
### 10. Update mailbox item padding
|
||||
|
||||
**File**: `app/src/ai/agent_management/notifications/view.rs`
|
||||
|
||||
In `render_notification_item`:
|
||||
- Rich items (branch present): padding `Padding::uniform(12.)` (was `.with_vertical(12.).with_horizontal(16.)`)
|
||||
- Simple items (branch absent): keep `.with_vertical(12.).with_horizontal(16.)`
|
||||
|
||||
### 11. Timestamp font size
|
||||
|
||||
In both rich and simple item layouts, change timestamp font size from `14.` → `12.` in `item_rendering.rs`.
|
||||
|
||||
## End-to-End Flow
|
||||
|
||||
### Notification creation
|
||||
1. Conversation status changes or CLI agent session event fires.
|
||||
2. `AgentNotificationsModel::add_notification` is called with the new `branch` parameter.
|
||||
3. `NotificationItem` is created with `branch: Option<String>`.
|
||||
4. `AgentManagementEvent::NotificationAdded` is emitted.
|
||||
|
||||
### Toast rendering
|
||||
1. `AgentNotificationToastStack` receives the event, creates a `NotificationToastItem`.
|
||||
2. `render_toast` builds the toast container with updated sizing (420px, 12px padding, 8px radius, no drop shadow).
|
||||
3. `render_notification_item_content` is called with `NotificationRenderContext::Toast`.
|
||||
4. If `item.branch.is_some()`, renders rich layout (branch row with chevron, clamped title/message).
|
||||
5. If `item.branch.is_none()`, renders simple layout (title only, no timestamp).
|
||||
6. Avatar uses `render_notification_avatar` → delegates to vertical tabs primitives.
|
||||
7. Close button positioned at top-left with border.
|
||||
|
||||
### Mailbox rendering
|
||||
1. `NotificationMailboxView::render` builds the popup with updated container (4px top padding, no drop shadow).
|
||||
2. Header renders with regular-weight title + close button.
|
||||
3. Filter bar renders with 2px gap, count on "All", updated padding.
|
||||
4. Each item calls `render_notification_item_content` with `NotificationRenderContext::Mailbox`.
|
||||
5. Rich items show branch row (with timestamp + unread dot on right), truncated title/message.
|
||||
6. Simple items show current layout with timestamp in title row.
|
||||
|
||||
## Risks and Mitigations
|
||||
|
||||
**Text truncation detection**: Precisely detecting whether wrapped text exceeds N lines is non-trivial without a max-lines primitive. Mitigation: use `ConstrainedBox::with_max_height` for visual clamping and a character-length heuristic to decide whether to show the expand affordance. This won't be pixel-perfect but will be correct for the vast majority of cases.
|
||||
|
||||
**Avatar extraction scope**: Moving `IconWithStatusVariant` and friends out of `vertical_tabs.rs` touches a high-traffic file. Mitigation: keep the extraction mechanical — move types and functions without changing behavior, then add the notification-specific mapping in the new `avatar.rs` module.
|
||||
|
||||
**Branch availability**: Branch data comes from `TerminalView::current_git_branch()` which reads the shell's git branch chip. It is available for any terminal in a git repo but may be `None` for sessions outside a repo. The UI gracefully falls back to the simple layout when branch is `None`.
|
||||
|
||||
## Testing and Validation
|
||||
|
||||
- `cargo check` to verify compilation after each step.
|
||||
- Visual comparison against Figma mocks for both toast and mailbox.
|
||||
- Verify toast: 420px width, 12px padding, 8px radius, close button at top-left on hover, keybinding hint on newest.
|
||||
- Verify mailbox: 4px top padding, header with title + close, filter bar spacing, "All tabs (N)" count.
|
||||
- Verify rich items: branch row, truncated title, truncated message, expand chevron works.
|
||||
- Verify simple items: current layout preserved, timestamp at 12px font.
|
||||
- Verify avatars match vertical tabs (Oz, CLI brand colors, status badges with cutout rings).
|
||||
- Existing notification unit tests in `item_tests.rs` should still pass after adding the `branch` field.
|
||||
|
||||
## Follow-ups
|
||||
|
||||
- **CLI session branch**: Extend the CLI agent plugin protocol to surface the working branch name in `CLIAgentSessionContext`.
|
||||
- **Settings gear**: Punted from this pass. Add to mailbox header when design is finalized.
|
||||
- **Approve/decline on toasts**: Punted from this pass. Will need plumbing to dispatch permission responses directly from toasts.
|
||||
- **Precise line measurement**: If the character-count heuristic for expand affordances proves insufficient, invest in a proper max-lines text primitive in the UI framework.
|
||||
@@ -0,0 +1,175 @@
|
||||
# HOA Onboarding Flow for Existing Users
|
||||
|
||||
Linear: APP-3809
|
||||
|
||||
## Summary
|
||||
|
||||
A guided onboarding flow that introduces existing Warp users to House of Agents (HOA) features: vertical tabs, agent inbox, and default tab config creation. The flow is shown once, behind a feature flag (`HOAOnboardingFlow`), and is only shown to users who did not go through the new-user onboarding (i.e. existing users who update to the HOA release).
|
||||
|
||||
## Problem
|
||||
|
||||
Existing Warp users will receive a major update with HOA features (vertical tabs, agent inbox/notifications, native code review, CLI agent integrations) but have no guided introduction to these changes. Without a targeted onboarding flow, users may not discover or understand the new capabilities, leading to lower adoption of key features.
|
||||
|
||||
## Goals
|
||||
|
||||
- Introduce existing users to HOA features through a 4-step guided flow.
|
||||
- Let users configure vertical vs. horizontal tabs with a live toggle.
|
||||
- Guide users to create their default tab config (session type, directory, worktree).
|
||||
- Show the flow exactly once per user, gated behind `HOAOnboardingFlow` feature flag.
|
||||
- Reuse the existing session config modal rendering code for the tab config step.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Redesigning the new-user onboarding flow.
|
||||
- Onboarding for features not shown in the Figma (e.g. MCP servers, voice input).
|
||||
- Animation of the welcome banner hero art (static image is acceptable for v1).
|
||||
- Allowing users to navigate backward through steps.
|
||||
|
||||
## Figma
|
||||
|
||||
https://www.figma.com/design/CsBdBW4YoLgSAbr5eSkwV6/House-of-Agents?node-id=7495-108638&m=dev
|
||||
|
||||
## User Experience
|
||||
|
||||
### Triggering conditions
|
||||
|
||||
- The `HOAOnboardingFlow` feature flag is enabled (dogfood initially).
|
||||
- The user has NOT gone through the new-user onboarding for this version.
|
||||
(Use a `private_user_preferences` key, similar to `HasCompletedOnboarding`, to track whether this flow has been shown.)
|
||||
- The flow is shown at most once. Completing any step or dismissing marks it as done.
|
||||
- New users who complete the standard onboarding flow should have this flag pre-set so they never see the HOA onboarding.
|
||||
|
||||
### Flow overview
|
||||
|
||||
The flow has 4 sequential steps. The user progresses forward only (no back navigation). Each step must be completed or the flow dismissed before normal interaction resumes.
|
||||
|
||||
---
|
||||
|
||||
### Step 1: Welcome Banner
|
||||
|
||||
**Presentation**: A centered modal dialog overlaying the terminal window with a semi-transparent background scrim.
|
||||
|
||||
**Content**:
|
||||
- Hero image area at top (Claude, OpenAI, OpenCode logos — static image).
|
||||
- X (close) button in the top-right corner of the hero area.
|
||||
- "New" badge (magenta pill).
|
||||
- Title: "Introducing first-class support for Claude Code, Codex, and OpenCode"
|
||||
- 4 feature bullet points, each with an icon:
|
||||
1. **Vertical tabs** (layout-left icon): "Rich tab-titles and customizable content, so you can keep an eye on your agents how you want."
|
||||
2. **Agent inbox and notifications** (inbox-01 icon): "Get notified when agents need approval or feedback, so nothing blocks progress."
|
||||
3. **Native code review** (message-check-square icon): "Review and refine code with your CLI agent without leaving Warp."
|
||||
4. **Warp's input bar** (text-input icon): "Move faster with a smarter input. Attach images, use voice, and trigger actions - all from one place."
|
||||
- Primary CTA button: "See what's new" (light text on dark background, full width within the card).
|
||||
|
||||
**Behavior**:
|
||||
- Clicking "See what's new" advances to Step 2.
|
||||
- Clicking the X button dismisses the entire flow and marks it complete. The user never sees it again.
|
||||
- The scrim blocks interaction with the terminal behind it.
|
||||
|
||||
---
|
||||
|
||||
### Step 2: Vertical Tabs Callout
|
||||
|
||||
**Presentation**: A tooltip/popover (480px wide) anchored to the vertical tab sidebar area, with a pointer/arrow indicating the target. The terminal remains visible behind it, but workspace interactions are blocked until the user advances or dismisses the flow. The vertical tabs sidebar should already be open/visible at this point.
|
||||
|
||||
**Content**:
|
||||
- Title: "Introducing vertical tabs - the new default"
|
||||
- Description: "Vertical tabs display all panes within each terminal window and have rich headers and context for all CLI agents."
|
||||
- Checkbox (unchecked by default): "Switch back to horizontal tabs"
|
||||
- Progress indicator: 3 dots, dot 1 active (filled blue).
|
||||
- "Next" button (primary, right-aligned in footer).
|
||||
|
||||
**Behavior**:
|
||||
- The checkbox is a **live toggle**: checking it immediately switches the tab layout to horizontal tabs. Unchecking switches back to vertical. The underlying tab layout setting is persisted when toggled.
|
||||
- The popover arrow/pointer updates position based on the current tab layout:
|
||||
- Vertical tabs: pointer points toward the left sidebar.
|
||||
- Horizontal tabs: pointer points toward the top tab bar.
|
||||
- Clicking "Next" advances to Step 3.
|
||||
|
||||
---
|
||||
|
||||
### Step 3: Agent Inbox Callout
|
||||
|
||||
**Presentation**: A tooltip/popover (480px wide) anchored to the inbox icon in the title bar (top-right area), with a pointer/arrow indicating the inbox icon.
|
||||
|
||||
**Content**:
|
||||
- Title: "Meet your new agent inbox"
|
||||
- Description: "Your inbox is your central place to manage agent notifications and access produced artifacts like plans and PRs."
|
||||
- Progress indicator: 3 dots, dot 2 active.
|
||||
- "Next" button (primary, right-aligned in footer).
|
||||
|
||||
**Behavior**:
|
||||
- Clicking "Next" advances to Step 4.
|
||||
|
||||
---
|
||||
|
||||
### Step 4: Default Tab Config
|
||||
|
||||
**Presentation**: A popover/panel (480px wide) anchored to the vertical tabs panel in vertical-tabs mode, or to the new-tab button in horizontal-tabs mode.
|
||||
|
||||
**Content**:
|
||||
- Title: "Create your default tab config"
|
||||
- Description: "A tab config defines what opens when you create a new tab. Select a repo, choose a session type (terminal, Warp agent, or third-party agents like Claude or Codex), and optionally attach a worktree. This setup is used for every new tab."
|
||||
- **Session type** selector: pill/chip buttons matching the existing session config modal (reuse the same rendering code and logic, including dynamic filtering of session types based on whether Oz/AI is enabled).
|
||||
- **Select directory** button: opens the native file picker. Displays the selected path in user-friendly form (e.g. `~/warp-internal`). Defaults to the user's home directory.
|
||||
- **Enable worktree support** checkbox with description: "Work on multiple branches at once. Worktrees give each tab its own copy of the repo, so you don't need to switch branches or stash changes." Disabled when the selected directory is not a git repo, with a tooltip explaining why.
|
||||
- Progress indicator: 3 dots, dot 3 active.
|
||||
- "Finish" button (primary, right-aligned in footer).
|
||||
|
||||
**Behavior**:
|
||||
- Session type, directory, and worktree selections follow the same logic as the existing `SessionConfigModal`.
|
||||
- Clicking "Finish" saves the tab config as the user's default and closes the flow. The flow is marked complete.
|
||||
- Clicking "Finish" is the only way to leave this step. The tab config is saved and the flow is marked complete.
|
||||
|
||||
---
|
||||
|
||||
### Shared rendering
|
||||
|
||||
The session type picker, directory selector, and worktree checkbox in Step 4 should be extracted from the existing `SessionConfigModal` into shared rendering functions so the same code is used in both the onboarding flow and the standalone modal.
|
||||
|
||||
### Feature flag
|
||||
|
||||
- Flag name: `HOAOnboardingFlow`
|
||||
- Added to `DOGFOOD_FLAGS` initially.
|
||||
- Gates the entire flow: when disabled, existing users see nothing.
|
||||
|
||||
### Persistence
|
||||
|
||||
- A `private_user_preferences` key (e.g. `HasCompletedHOAOnboarding`) tracks whether the flow has been shown.
|
||||
- The key is set to `true` when:
|
||||
- The user clicks "Finish" on Step 4.
|
||||
- The user clicks X on the welcome banner.
|
||||
- The key is also set to `true` at the end of new-user onboarding, so new users never see this flow.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
1. When `HOAOnboardingFlow` is enabled and the user has not seen the flow, the welcome banner appears on app launch.
|
||||
2. The welcome banner displays all 4 feature descriptions with correct icons and text matching the Figma.
|
||||
3. Clicking "See what's new" transitions to the vertical tabs callout (Step 2) with vertical tabs visible.
|
||||
4. The "Switch back to horizontal tabs" checkbox live-toggles the tab layout setting. The UI immediately reflects the change (sidebar ↔ top tabs). The popover re-anchors its pointer.
|
||||
5. Clicking "Next" on Step 2 transitions to the agent inbox callout (Step 3), anchored to the inbox icon.
|
||||
6. Clicking "Next" on Step 3 transitions to the tab config step (Step 4).
|
||||
7. The session type picker in Step 4 displays the same options as the existing `SessionConfigModal` (filtered by AI availability).
|
||||
8. The directory picker opens a native file dialog and the worktree checkbox disables for non-git directories, matching existing behavior.
|
||||
9. Clicking "Finish" saves the default tab config and closes the flow.
|
||||
10. After completing or dismissing the flow, it never appears again for that user.
|
||||
11. Users who complete the new-user onboarding never see this flow.
|
||||
12. The flow is forward-only: no back button, no clickable progress dots.
|
||||
13. Progress dots accurately reflect the current step (1/3, 2/3, 3/3) across Steps 2–4.
|
||||
|
||||
## Validation
|
||||
|
||||
- **Unit tests**: Verify the persistence logic (flag set on completion, flag set on dismiss, flag prevents re-show, flag set after new-user onboarding).
|
||||
- **Manual / computer-use verification**:
|
||||
- Walk through all 4 steps and confirm layout, text, and icons match the Figma.
|
||||
- Verify the vertical tabs checkbox live-toggles the layout.
|
||||
- Verify the tab config step reuses session config modal behavior (session types, directory picker, worktree).
|
||||
- Verify dismissing via X at any point marks the flow complete.
|
||||
- Verify a fresh user who completes new-user onboarding does not see this flow.
|
||||
- **Integration tests**: If feasible, test the full step progression and that the flow doesn't appear after completion.
|
||||
|
||||
## Resolved Design Decisions
|
||||
|
||||
1. **Hero art**: Static raster image asset for v1.
|
||||
2. **Popover anchoring**: The existing FTU callout pattern (`app/src/ai/blocklist/agent_view/agent_input_footer/mod.rs:1758-1840`) uses stacked `CalloutTriangleBorderDown` / `CalloutTriangleFillDown` icons for the arrow and `OffsetPositioning::offset_from_save_position_element` for anchoring to named UI elements. Extract this into a shared callout helper and reuse it for Steps 2–3.
|
||||
3. **Dismiss controls**: Only Step 1 has an X/close button. Steps 2–3 use "Next" and Step 4 uses "Finish".
|
||||
@@ -0,0 +1,238 @@
|
||||
# HOA Onboarding Flow — Tech Spec
|
||||
|
||||
Linear: APP-3809
|
||||
Product spec: `specs/harryalbert/APP-3809/PRODUCT.md`
|
||||
|
||||
## Problem
|
||||
|
||||
We need a 4-step guided onboarding flow for existing users that introduces HOA features (vertical tabs, agent inbox, tab configs). The flow must be shown exactly once, gated behind a feature flag, and must reuse existing rendering code from `SessionConfigModal` and the FTU callout pattern.
|
||||
|
||||
## Relevant Code
|
||||
|
||||
### Triggering & persistence
|
||||
- `app/src/workspace/one_time_modal_model.rs` — `OneTimeModalModel`: existing pattern for one-time modals (Oz launch, build plan migration). Subscribes to auth events, waits for cloud preferences sync, then triggers.
|
||||
- `app/src/root_view.rs:1614-1631` — `HAS_COMPLETED_ONBOARDING_KEY` / `private_user_preferences`: the other persistence approach (local-only, no cloud sync needed).
|
||||
- `app/src/root_view.rs:2143-2155` — post-onboarding flow: opens vertical tabs panel + shows `SessionConfigModal` for new users.
|
||||
|
||||
### Session config modal (for extraction)
|
||||
- `app/src/tab_configs/session_config_modal.rs` — `SessionConfigModal`: the full modal with session type pills, directory picker, worktree checkbox. All rendering is in private methods (`render_session_type_section`, `render_directory_section`, `render_checkboxes`).
|
||||
- `app/src/tab_configs/session_config.rs` — `SessionConfigSelection`, `SessionType`, `is_git_repo`, `build_tab_config`, `write_tab_config`.
|
||||
|
||||
### FTU callout (for extraction)
|
||||
- `app/src/ai/blocklist/agent_view/agent_input_footer/mod.rs:1758-1840` — `render_ftu_callout`: renders a bubble with a triangle arrow using stacked `CalloutTriangleBorderDown` / `CalloutTriangleFillDown` icons. Positions via `OffsetPositioning::offset_from_save_position_element`.
|
||||
|
||||
### Tab layout setting
|
||||
- `app/src/workspace/tab_settings.rs:245` — `use_vertical_tabs: bool` setting.
|
||||
- `app/src/workspace/view.rs:1607-1614` — `open_vertical_tabs_panel_if_enabled`: opens the vertical tabs panel based on the setting.
|
||||
|
||||
### Anchoring targets
|
||||
- `app/src/workspace/view.rs:535` — `NOTIFICATIONS_MAILBOX_POSITION_ID = "workspace:notifications_mailbox"`: the inbox icon already uses `SavePosition` for anchoring (line 15262).
|
||||
- `app/src/workspace/view.rs:515` — `TAB_BAR_POSITION_ID`: the tab bar already has a `SavePosition`.
|
||||
|
||||
### Feature flags
|
||||
- `crates/warp_features/src/lib.rs:9` — `FeatureFlag` enum.
|
||||
- `crates/warp_features/src/lib.rs:797` — `DOGFOOD_FLAGS` array.
|
||||
|
||||
### Workspace integration
|
||||
- `app/src/workspace/view.rs:779-901` — `Workspace` struct: holds all modal view handles, panel state.
|
||||
- `app/src/workspace/view.rs:1463-1494` — `build_session_config_modal`: how modals are constructed and subscribed.
|
||||
- `app/src/workspace/view.rs:1617-1638` — `show_session_config_modal` / `close_session_config_modal`.
|
||||
|
||||
## Current State
|
||||
|
||||
- New users go through `AgentOnboardingView` → then post-onboarding opens vertical tabs + `SessionConfigModal` (`root_view.rs:2143-2155`).
|
||||
- Existing users get no introduction to HOA features.
|
||||
- The `SessionConfigModal` rendering methods are private and tightly coupled to the modal's `View` impl — they can't be reused without extraction.
|
||||
- The FTU callout rendering is a standalone `fn render_ftu_callout` but hardcodes text, colors, and arrow direction. The general pattern (bubble + positioned triangle) is reusable.
|
||||
- `OneTimeModalModel` handles one-time modal triggering for existing users (auth → cloud sync → check flag → show). It uses `AISettings` to persist "shown" state via cloud-synced settings.
|
||||
|
||||
## Proposed Changes
|
||||
|
||||
### 1. Feature flag
|
||||
|
||||
Add `HOAOnboardingFlow` to the `FeatureFlag` enum and `DOGFOOD_FLAGS`. Read the create feature flag skill for information on how to do this in a complete way.
|
||||
|
||||
File: `crates/warp_features/src/lib.rs`
|
||||
|
||||
### 2. Shared callout bubble component
|
||||
|
||||
Extract from the FTU callout into a general-purpose helper in a new file.
|
||||
|
||||
File: `app/src/view_components/callout_bubble.rs`
|
||||
|
||||
```rust
|
||||
pub enum CalloutArrowDirection {
|
||||
Up,
|
||||
Left,
|
||||
}
|
||||
|
||||
pub enum CalloutArrowPosition {
|
||||
Start(f32),
|
||||
Center,
|
||||
End(f32),
|
||||
}
|
||||
|
||||
pub struct CalloutBubbleConfig {
|
||||
pub width: f32,
|
||||
pub arrow_direction: CalloutArrowDirection,
|
||||
pub arrow_position: CalloutArrowPosition,
|
||||
}
|
||||
```
|
||||
|
||||
A `render_callout_bubble(content, config, appearance) -> Box<dyn Element>` function that:
|
||||
- Wraps `content` in a bordered, rounded container with accent-tinted background (matching FTU style).
|
||||
- Appends/prepends a triangle arrow (using the existing `CalloutTriangleBorderDown` / `CalloutTriangleFillDown` icon stacking technique) positioned per the config.
|
||||
|
||||
This is reused by the HOA flow's Steps 2–4.
|
||||
|
||||
### 3. Shared session config rendering
|
||||
|
||||
Extract the rendering portions of `SessionConfigModal` into free functions that both the modal and the onboarding step can call.
|
||||
|
||||
File: `app/src/tab_configs/session_config_rendering.rs`
|
||||
|
||||
Extract these from `SessionConfigModal`:
|
||||
- `render_session_type_pills(session_types, selected_index, pill_mouse_states, appearance) -> Box<dyn Element>`
|
||||
- `render_directory_picker(selected_directory, mouse_state, appearance) -> Box<dyn Element>`
|
||||
- `render_worktree_checkbox(enabled, is_git_repo, checkbox_mouse_state, tooltip_mouse_state, appearance) -> Box<dyn Element>`
|
||||
|
||||
`SessionConfigModal` calls these extracted functions in its `render` method instead of its current private methods. The onboarding Step 4 view calls the same functions.
|
||||
|
||||
The action dispatch, state management, and file picker logic remain in each caller (`SessionConfigModal` and the onboarding view) — only the rendering is shared.
|
||||
|
||||
### 4. HOA onboarding view
|
||||
|
||||
New module: `app/src/workspace/hoa_onboarding/`
|
||||
- `mod.rs` — re-exports.
|
||||
- `hoa_onboarding_flow.rs` — the main `HoaOnboardingFlow` view.
|
||||
- `welcome_banner.rs` — Step 1 rendering.
|
||||
- `tab_config_step.rs` — Step 4 rendering (uses shared session config rendering).
|
||||
|
||||
#### `HoaOnboardingFlow` view
|
||||
|
||||
```rust
|
||||
enum HoaOnboardingStep {
|
||||
WelcomeBanner,
|
||||
VerticalTabsCallout,
|
||||
AgentInboxCallout,
|
||||
TabConfig,
|
||||
}
|
||||
|
||||
struct HoaOnboardingFlow {
|
||||
step: HoaOnboardingStep,
|
||||
// Step 1 state
|
||||
close_button: ViewHandle<ActionButton>,
|
||||
// Step 2 state
|
||||
switch_to_horizontal_checkbox_mouse_state: MouseStateHandle,
|
||||
// Step 4 state (mirrors SessionConfigModal fields)
|
||||
session_types: Vec<SessionType>,
|
||||
selected_session_type_index: usize,
|
||||
selected_directory: PathBuf,
|
||||
is_git_repo: bool,
|
||||
enable_worktree: bool,
|
||||
session_pill_mouse_states: Vec<MouseStateHandle>,
|
||||
directory_button_mouse_state: MouseStateHandle,
|
||||
worktree_checkbox_mouse_state: MouseStateHandle,
|
||||
worktree_tooltip_mouse_state: MouseStateHandle,
|
||||
}
|
||||
```
|
||||
|
||||
**Events**:
|
||||
```rust
|
||||
enum HoaOnboardingFlowEvent {
|
||||
Completed(Option<SessionConfigSelection>),
|
||||
Dismissed,
|
||||
}
|
||||
```
|
||||
|
||||
- `Completed(Some(selection))` — user clicked Finish with a valid tab config.
|
||||
- `Completed(None)` — user completed the flow but didn't reach the tab config (shouldn't happen in normal flow, but covers edge cases).
|
||||
- `Dismissed` — user clicked X.
|
||||
|
||||
**Rendering** dispatches to different renderers per step:
|
||||
- Steps 1: a centered modal with scrim (similar to how `SessionConfigModal` is wrapped in a `Modal`).
|
||||
- Steps 2–3: the shared `render_callout_bubble` positioned via `OffsetPositioning::offset_from_save_position_element`, anchored to the vertical tabs panel or `NOTIFICATIONS_MAILBOX_POSITION_ID`.
|
||||
- Step 4: the shared `render_callout_bubble` with the tab config form body, anchored to the vertical tabs panel in vertical tabs mode or the tab bar in horizontal tabs mode.
|
||||
|
||||
### 5. Persistence
|
||||
|
||||
Add a new `private_user_preferences` key: `HasCompletedHOAOnboarding`.
|
||||
|
||||
Use the same pattern as `HAS_COMPLETED_ONBOARDING_KEY` in `root_view.rs:1614-1631` — a simple boolean read/write via `ctx.private_user_preferences()`. This should be cloud synced.
|
||||
|
||||
Helper functions (placed near the onboarding flow or in a shared utils module):
|
||||
```rust
|
||||
const HAS_COMPLETED_HOA_ONBOARDING_KEY: &str = "HasCompletedHOAOnboarding";
|
||||
|
||||
fn has_completed_hoa_onboarding(ctx: &AppContext) -> bool { ... }
|
||||
fn mark_hoa_onboarding_completed(ctx: &AppContext) { ... }
|
||||
```
|
||||
|
||||
### 6. Triggering
|
||||
|
||||
Integrate into `OneTimeModalModel`:
|
||||
- Add `is_hoa_onboarding_open: bool` field.
|
||||
- Add a `check_and_trigger_hoa_onboarding` method that:
|
||||
1. Checks `FeatureFlag::HOAOnboardingFlow.is_enabled()`.
|
||||
2. Checks `!has_completed_hoa_onboarding(ctx)`.
|
||||
3. Checks `has_completed_local_onboarding(ctx)` — ensures this is an existing user (not someone mid-new-user-onboarding).
|
||||
4. Sets `is_hoa_onboarding_open = true` and emits the event.
|
||||
- Call this from `check_and_trigger_all_modals` alongside the existing Oz launch and build plan migration checks.
|
||||
|
||||
In `Workspace`, subscribe to `OneTimeModalEvent` and when the HOA onboarding should show:
|
||||
- Ensure vertical tabs are enabled (set the `use_vertical_tabs` setting to `true` if not already).
|
||||
- Open the vertical tabs panel.
|
||||
- Show the `HoaOnboardingFlow` view as an overlay.
|
||||
|
||||
### 7. New-user onboarding integration
|
||||
|
||||
In `root_view.rs`, after new-user onboarding completes (`AgentOnboardingEvent::OnboardingCompleted`), call `mark_hoa_onboarding_completed(ctx)` to ensure the HOA flow never shows for new users (only if the hoa flow feature flag is enabled).
|
||||
|
||||
### 8. Workspace rendering integration
|
||||
|
||||
Add `hoa_onboarding_flow: Option<ViewHandle<HoaOnboardingFlow>>` to `Workspace`.
|
||||
|
||||
In the workspace's `render` method:
|
||||
- When `hoa_onboarding_flow` is `Some` and the step is `WelcomeBanner`, render a full-window scrim overlay with the banner centered.
|
||||
- When the step is `VerticalTabsCallout` or `AgentInboxCallout`, render the callout bubble as a positioned overlay (using `Stack::add_positioned_overlay_child`) anchored to the appropriate `SavePosition` element.
|
||||
- When the step is `TabConfig`, render the tab config popover as a positioned overlay anchored to the content area.
|
||||
|
||||
Handle `HoaOnboardingFlowEvent`:
|
||||
- On `Dismissed`: call `mark_hoa_onboarding_completed`, drop the flow view.
|
||||
- On `Completed(Some(selection))`: call `mark_hoa_onboarding_completed`, process the tab config selection (reuse `handle_session_config_completed` logic), drop the flow view.
|
||||
- On `Completed(None)`: call `mark_hoa_onboarding_completed`, drop the flow view.
|
||||
|
||||
## End-to-End Flow
|
||||
|
||||
1. Existing user launches Warp with `HOAOnboardingFlow` enabled.
|
||||
2. `OneTimeModalModel` receives auth + cloud sync completion → calls `check_and_trigger_hoa_onboarding`.
|
||||
3. Checks pass (flag enabled, not completed, is existing user) → emits `OneTimeModalEvent`.
|
||||
4. `Workspace` receives event → enables vertical tabs setting → opens vertical tabs panel → creates `HoaOnboardingFlow` in `WelcomeBanner` step → renders scrim + banner.
|
||||
5. User clicks "See what's new" → flow advances to `VerticalTabsCallout` → callout bubble rendered anchored to vertical tabs panel.
|
||||
6. User optionally toggles "Switch back to horizontal tabs" → `use_vertical_tabs` setting toggled, UI updates live, callout re-anchors.
|
||||
7. User clicks "Next" → flow advances to `AgentInboxCallout` → callout bubble rendered anchored to `NOTIFICATIONS_MAILBOX_POSITION_ID`.
|
||||
8. User clicks "Next" → flow advances to `TabConfig` → popover with session config fields rendered.
|
||||
9. User configures and clicks "Finish" → `HoaOnboardingFlowEvent::Completed(Some(selection))` emitted → tab config saved, flow closed, preferences key set.
|
||||
|
||||
## Risks and Mitigations
|
||||
|
||||
1. **Rendering extraction breaking `SessionConfigModal`**: The extraction of rendering functions is a refactor of existing code. Risk: subtle visual regressions. Mitigation: compare screenshots before/after extraction to verify pixel-identical output.
|
||||
|
||||
2. **Callout positioning edge cases**: If the vertical tabs panel or inbox icon aren't rendered yet when the callout tries to anchor, `offset_from_save_position_element` may position incorrectly. Mitigation: the flow ensures vertical tabs are opened before Step 2, and the inbox icon is always in the title bar when `HOANotifications` is enabled.
|
||||
|
||||
3. **Interaction with other one-time modals**: `OneTimeModalModel` runs checks sequentially — Oz launch modal takes priority. If the Oz launch modal shows, the HOA flow won't trigger until the next app launch. This is acceptable; the flow persists across launches until shown.
|
||||
|
||||
4. **Feature flag dependency**: The flow assumes `VerticalTabs`, `HOANotifications`, and `TabConfigs` flags are also enabled. If any are disabled, certain steps may reference UI that doesn't exist. Mitigation: gate the HOA flow on all required flags in `check_and_trigger_hoa_onboarding`.
|
||||
|
||||
## Testing and Validation
|
||||
|
||||
- **Unit tests**: persistence helpers (`has_completed_hoa_onboarding`, `mark_hoa_onboarding_completed`), step transitions in `HoaOnboardingFlow`, new-user exclusion logic.
|
||||
- **Visual verification**: local walkthrough of all 4 steps after building.
|
||||
- **Extraction regression**: verify `SessionConfigModal` still renders identically after extracting shared rendering functions.
|
||||
- **Edge cases**: dismiss at Step 1, dismiss at Step 4, toggle horizontal tabs back and forth, select non-git directory for worktree.
|
||||
|
||||
## Follow-ups
|
||||
|
||||
- Remove `HOAOnboardingFlow` feature flag once stable (promote through preview → release).
|
||||
- Refactor the existing FTU callout in `agent_input_footer` to use the shared `callout_bubble` component.
|
||||
- Hero art asset: needs a static image to be produced and bundled.
|
||||
@@ -0,0 +1,73 @@
|
||||
# APP-3858: Change CLI Sharing Entrypoint to Remote Control
|
||||
|
||||
## Summary
|
||||
Rebrand the CLI agent footer "Share session" button as "Remote control" and streamline the interaction so that a single click starts sharing, auto-copies the link to the clipboard, and shows a toast. The share modal is bypassed entirely. Once sharing is active, the same button becomes a "Stop sharing" button.
|
||||
|
||||
## Problem
|
||||
The current "Share session" label doesn't communicate the primary use case: accessing a CLI agent session on your phone to track progress or steer it. Users don't equate "sharing" with mobile access. The current flow also requires an extra step through the share modal, which is unexpected for this use case.
|
||||
|
||||
## Goals
|
||||
- Rename the feature to "Remote control" across the CLI agent footer entrypoint.
|
||||
- One-click start: clicking the chip starts sharing immediately (without scrollback, just the current block), auto-copies the link, and shows a toast.
|
||||
- Once sharing is active, the chip switches to a "Stop sharing" chip.
|
||||
- Replace the `Icon::Share` icon with a phone icon (`phone-01.svg`). Use the existing `Icon::StopFilled` for the stop state.
|
||||
- Add a `/remote-control` slash command that triggers the same action as clicking the chip.
|
||||
|
||||
## Non-goals
|
||||
- Renaming the feature in contexts outside the CLI agent footer (pane header overflow menu, right-click context menu, the share modal itself). Those are separate entry points with different audiences.
|
||||
- "Text link to my device" functionality (stretch goal for a follow-up).
|
||||
- Changes to the viewer experience or the sharing protocol.
|
||||
- Changes to the ambient agent / cloud mode share button.
|
||||
|
||||
## User Experience
|
||||
|
||||
### Before sharing
|
||||
- The CLI agent footer renders a chip with the phone icon (`Icon::Phone01`) and label `/remote-control`.
|
||||
- Typing `/remote-control` in the input triggers the same start-sharing action.
|
||||
- The chip is only visible when `FeatureFlag::CreatingSharedSessions` and `ContextFlag::CreateSharedSession` are enabled (same gating as today).
|
||||
|
||||
### Starting a share (single click or slash command)
|
||||
1. User clicks the `/remote-control` chip or types `/remote-control` in the input.
|
||||
2. The session begins sharing immediately without scrollback (`SharedSessionScrollbackType::None`). This must be allowed even when agent conversations exist (bypassing the normal guard that blocks no-scrollback shares when `FeatureFlag::AgentSharedSessions` is enabled and conversations are present).
|
||||
3. As soon as the session is established, the sharing link is auto-copied to the user's clipboard.
|
||||
4. A toast appears: "Remote control link copied."
|
||||
5. The share modal does **not** open.
|
||||
|
||||
### While sharing is active
|
||||
- The chip changes to show a filled stop square (`Icon::StopFilled`) with label `Stop sharing`.
|
||||
- Clicking the chip stops the sharing session.
|
||||
|
||||
### After sharing ends
|
||||
- The chip reverts to its pre-sharing state (phone icon, `/remote-control` label).
|
||||
|
||||
### Edge cases
|
||||
- If the session is already being shared (e.g. started from the pane header menu), the footer button should reflect the active sharing state (show stop button).
|
||||
- If another share was started via a different entry point that used the share modal, the footer chip still shows "Stop sharing" while the session is active.
|
||||
- If sharing fails to start (e.g. network error), the button should revert to its default state. Existing error handling for `SharePending` → failure applies.
|
||||
|
||||
### Banners
|
||||
When sharing is started from the remote control entrypoint, the inline banners use different copy than the default:
|
||||
- Start banner: "Remote control active" (instead of "Sharing started")
|
||||
- End banner: "Remote control stopped" (instead of "Sharing ended")
|
||||
|
||||
Shares started from other entry points (pane header, share modal, etc.) keep the default "Sharing started" / "Sharing ended" copy. This intentionally deviates from the Figma mocks, which show the generic phrasing.
|
||||
|
||||
## Success Criteria
|
||||
1. The CLI agent footer chip displays the phone icon and `/remote-control` label when no session is being shared.
|
||||
2. Clicking the chip starts sharing without scrollback and does **not** open the share modal.
|
||||
3. After sharing starts, the link is in the user's clipboard and a "Remote control link copied." toast is visible.
|
||||
4. While sharing is active, the chip shows `StopFilled` icon with label "Stop sharing".
|
||||
5. Clicking the stop chip ends the sharing session and the chip reverts to its default state.
|
||||
6. The chip correctly reflects sharing state even when sharing was started/stopped from a different entry point.
|
||||
7. Typing `/remote-control` in the agent input triggers the same start-sharing flow.
|
||||
|
||||
## Validation
|
||||
- Build the app and verify the button icon, tooltip, and click behavior in a CLI agent session.
|
||||
- Verify that clicking the button copies a valid sharing link to the clipboard.
|
||||
- Verify the toast appears.
|
||||
- Verify the button toggles to stop state while sharing is active.
|
||||
- Verify the button reverts after stopping.
|
||||
- Existing shared session tests should continue to pass since the underlying sharing mechanism is unchanged.
|
||||
|
||||
## Open Questions
|
||||
- Should the agent view (non-CLI) share button also be rebranded to "Remote control", or only the CLI agent footer? (Current decision: only CLI agent footer for now.)
|
||||
@@ -0,0 +1,146 @@
|
||||
# APP-3858: Tech Spec — Remote Control Entrypoint
|
||||
|
||||
## Problem
|
||||
The CLI agent footer's "Share session" button opens the share modal, which is an unnecessary step for the primary use case (quick mobile handoff). We need to make it a one-click action that starts sharing without scrollback, auto-copies the link, and shows a stop button while active.
|
||||
|
||||
## Relevant Code
|
||||
- `crates/warp_core/src/ui/icons.rs (10-296)` — `Icon` enum; needs `Phone01` variant
|
||||
- `app/assets/bundled/svg/phone-01.svg` — phone icon for start-state (on branch)
|
||||
- `Icon::StopFilled` — existing icon reused for stop-state
|
||||
- `app/src/search/slash_command_menu/static_commands/commands.rs` — slash command definitions
|
||||
- `app/src/ai/blocklist/agent_view/agent_input_footer/mod.rs (491-500)` — `share_session_button` creation
|
||||
- `app/src/ai/blocklist/agent_view/agent_input_footer/mod.rs (1104-1108)` — CLI toolbar `ShareSession` render
|
||||
- `app/src/ai/blocklist/agent_view/agent_input_footer/mod.rs (1636-1644)` — agent view toolbar `ShareSession` render
|
||||
- `app/src/ai/blocklist/agent_view/agent_input_footer/mod.rs (1893, 2025-2027)` — `AgentInputFooterAction::ShareSession` handler
|
||||
- `app/src/ai/blocklist/agent_view/agent_input_footer/mod.rs (2037-2045)` — `AgentInputFooterEvent` enum
|
||||
- `app/src/ai/blocklist/agent_view/agent_input_footer/toolbar_item.rs (48, 74, 88)` — `AgentToolbarItemKind::ShareSession` display label and icon
|
||||
- `app/src/terminal/view/use_agent_footer/mod.rs (217-218, 851-852)` — event forwarding
|
||||
- `app/src/terminal/view/use_agent_footer/mod.rs (1019-1041)` — `UseAgentToolbarEvent` enum
|
||||
- `app/src/terminal/view/shared_session/view_impl.rs (415-472)` — `attempt_to_share_session` (scrollback guard at 436-448)
|
||||
- `app/src/terminal/view/shared_session/view_impl.rs (476-520)` — `on_session_share_started` (auto-opens sharing dialog at 515)
|
||||
- `app/src/terminal/view/shared_session/view_impl.rs (522-529)` — `stop_sharing_session`
|
||||
- `app/src/terminal/view/shared_session/view_impl.rs (1288-1311)` — `copy_shared_session_link`
|
||||
- `app/src/terminal/shared_session/mod.rs (39)` — `COPY_LINK_TEXT` constant
|
||||
- `app/src/terminal/shared_session/mod.rs (267-290)` — `SharedSessionActionSource` enum
|
||||
|
||||
## Current State
|
||||
|
||||
**Button**: A single static `share_session_button` with `Icon::Share`, tooltip "Share session". Click dispatches `AgentInputFooterAction::ShareSession` → emits `AgentInputFooterEvent::OpenShareSessionModal`.
|
||||
|
||||
**Event flow**: `OpenShareSessionModal` bubbles through `UseAgentToolbarEvent::OpenShareSessionModal` → `TerminalView::open_share_session_modal` → opens the sharing dialog.
|
||||
|
||||
**Post-share**: `on_session_share_started` unconditionally auto-opens the sharing dialog so the user can copy the link.
|
||||
|
||||
**No stop affordance**: The button never changes. Stopping requires the pane header overflow menu.
|
||||
|
||||
## Proposed Changes
|
||||
|
||||
### 1. Add icon variant
|
||||
In `crates/warp_core/src/ui/icons.rs`, add one variant:
|
||||
- `Phone01` → `"bundled/svg/phone-01.svg"`
|
||||
|
||||
The stop state reuses the existing `Icon::StopFilled`.
|
||||
|
||||
### 2. Two buttons instead of one
|
||||
Replace the single `share_session_button` field in `AgentInputFooter` with two fields:
|
||||
|
||||
**`start_remote_control_button`**: `Phone01` icon, label "/remote-control". On click dispatches `AgentInputFooterAction::StartRemoteControl`.
|
||||
|
||||
**`stop_remote_control_button`**: `StopFilled` icon, label "Stop sharing". On click dispatches `AgentInputFooterAction::StopRemoteControl`.
|
||||
|
||||
### 3. Split action and event enums
|
||||
**`AgentInputFooterAction`** (mod.rs:1893): Replace `ShareSession` with:
|
||||
- `StartRemoteControl`
|
||||
- `StopRemoteControl`
|
||||
|
||||
**`AgentInputFooterEvent`** (mod.rs:2037): Replace `OpenShareSessionModal` with:
|
||||
- `StartRemoteControl`
|
||||
- `StopRemoteControl`
|
||||
|
||||
**`UseAgentToolbarEvent`** (use_agent_footer/mod.rs:1019): Replace `OpenShareSessionModal` with:
|
||||
- `StartRemoteControl`
|
||||
- `StopRemoteControl`
|
||||
|
||||
Each action handler is trivial — it just emits the corresponding event up the chain.
|
||||
|
||||
### 4. Swap buttons at render time
|
||||
In `render_cli_toolbar_item` and `render_toolbar_item` for the `ShareSession` variant, instead of always rendering the start button, check `terminal_model.lock().shared_session_status()`:
|
||||
- Not sharing → render `start_remote_control_button`
|
||||
- `is_active_sharer()` or `is_share_pending()` → render `stop_remote_control_button`
|
||||
|
||||
The footer already re-renders on `CLIAgentSessionsModel` changes. We need to verify shared session status changes also trigger re-renders; if not, subscribe to the relevant model event.
|
||||
|
||||
### 5. Handle `StartRemoteControl` in `TerminalView`
|
||||
In `handle_use_agent_footer_event` (use_agent_footer/mod.rs:217):
|
||||
- Call `attempt_to_share_session(SharedSessionScrollbackType::None, Some(FooterChip), SessionSourceType::Terminal, ctx)` with a new `bypass_conversation_guard: bool` parameter set to `true`.
|
||||
|
||||
**Bypass the scrollback guard**: Add `bypass_conversation_guard: bool` to `attempt_to_share_session` (view_impl.rs:415). When `true`, skip the guard at lines 436-448. All existing callers pass `false`.
|
||||
|
||||
### 6. Auto-copy link on share start
|
||||
Thread `source: Option<SharedSessionActionSource>` through the share-started flow:
|
||||
- `Event::StartSharingCurrentSession` gains a `source` field
|
||||
- Terminal manager forwards it to `on_session_share_started`
|
||||
|
||||
In `on_session_share_started` (view_impl.rs:476):
|
||||
- When source is `FooterChip`: skip `toggle_sharing_dialog`, call `copy_shared_session_link`, show toast "Remote control link copied".
|
||||
- All other sources: keep existing behavior (auto-open sharing dialog).
|
||||
|
||||
### 7. Handle `StopRemoteControl` in `TerminalView`
|
||||
Call `self.stop_sharing_session(SharedSessionActionSource::FooterChip, ctx)`.
|
||||
|
||||
### 8. Update toolbar item metadata
|
||||
In `toolbar_item.rs`:
|
||||
- Display label: `"Share Session"` → `"/remote-control"`
|
||||
- Icon: `Icon::Share` → `Icon::Phone01`
|
||||
|
||||
### 9. Add `/remote-control` slash command
|
||||
Add a `REMOTE_CONTROL` static command in `commands.rs` with:
|
||||
- name: `/remote-control`
|
||||
- description: "Start remote control for this session"
|
||||
- icon_path: `"bundled/svg/phone-01.svg"`
|
||||
- availability: CLI agent sessions only
|
||||
|
||||
Register it in the command list. Handle it in the slash command dispatch to emit the same `StartRemoteControl` action as the chip.
|
||||
|
||||
### 10. Banner copy
|
||||
When sharing is started from the remote control entrypoint (`SharedSessionActionSource::FooterChip`), the inline banners show:
|
||||
- "Remote control active" (instead of "Sharing started")
|
||||
- "Remote control stopped" (instead of "Sharing ended")
|
||||
|
||||
This is threaded via an `is_remote_control: bool` field on `SharedSessionBanners::ActiveShare` and `LastShared`.
|
||||
|
||||
## End-to-End Flow
|
||||
```
|
||||
Start:
|
||||
click start_remote_control_button
|
||||
→ AgentInputFooterAction::StartRemoteControl
|
||||
→ AgentInputFooterEvent::StartRemoteControl
|
||||
→ UseAgentToolbarEvent::StartRemoteControl
|
||||
→ attempt_to_share_session(None, FooterChip, bypass=true)
|
||||
→ terminal manager establishes connection
|
||||
→ on_session_share_started(source=FooterChip)
|
||||
→ skips sharing dialog, copies link, shows toast
|
||||
→ footer re-renders: swaps to stop_remote_control_button
|
||||
|
||||
Stop:
|
||||
click stop_remote_control_button
|
||||
→ AgentInputFooterAction::StopRemoteControl
|
||||
→ AgentInputFooterEvent::StopRemoteControl
|
||||
→ UseAgentToolbarEvent::StopRemoteControl
|
||||
→ stop_sharing_session(FooterChip)
|
||||
→ footer re-renders: swaps back to start_remote_control_button
|
||||
```
|
||||
|
||||
## Risks and Mitigations
|
||||
- **Scrollback guard bypass**: Deliberately allowing no-scrollback shares for remote control. Guard still applies for all other entry points (share modal, context menu, etc.).
|
||||
- **State sync**: Reading `shared_session_status()` at render time means the button reflects state changes from any entry point. Must verify shared session status changes trigger footer re-renders.
|
||||
- **Threading source through share-started flow**: `Event::StartSharingCurrentSession` and the terminal manager callback need to carry the source. Keep it optional so existing callers are unaffected.
|
||||
|
||||
## Testing and Validation
|
||||
- Existing shared session tests pass (protocol unchanged).
|
||||
- Manual: start CLI agent → click "Remote control" → verify link copied + toast → button shows stop state → click stop → session ends + button reverts.
|
||||
- Verify starting share from pane menu causes footer to show stop button.
|
||||
|
||||
## Follow-ups
|
||||
- "Text link to my device" stretch goal.
|
||||
- Rename agent view (non-CLI) share button if desired.
|
||||
@@ -0,0 +1,150 @@
|
||||
# Configurable Toolbar Chips (Header Toolbar)
|
||||
|
||||
**Linear:** [APP-3865](https://linear.app/warpdotdev/issue/APP-3865)
|
||||
**Figma:** none provided
|
||||
|
||||
## Summary
|
||||
|
||||
The app header bar shows a set of panel toggle buttons (tabs panel, tools panel, agent management, code review, notifications mailbox). Users should be able to add/remove, re-arrange, and move these items between the left and right sides of the header, using the same configurator UI pattern as the agent input footer chip editor. This applies to both horizontal and vertical tab modes.
|
||||
|
||||
## Problem
|
||||
|
||||
The toolbar layout is currently hardcoded, outside of customizing which items appear. Users cannot customize item order, or which side items are on. Users with different workflows may want different toolbar layouts — for example, a user who never uses code review shouldn't have to see that button, and a user who frequently uses agent management may want it on the right side near their cursor.
|
||||
|
||||
## Goals
|
||||
|
||||
- Let users reorder toolbar items within a side (left or right).
|
||||
- Let users move toolbar items between left and right sides.
|
||||
- Let users hide toolbar items they don't need.
|
||||
- Let users restore hidden toolbar items.
|
||||
- Integrate with the existing onboarding show/hide mechanism so that onboarding choices and configurator choices stay consistent.
|
||||
- Persist configuration across sessions via settings.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- The search bar remains centered and is not configurable (vertical tabs mode only; not shown in horizontal tabs).
|
||||
- The avatar/profile button remains fixed at the far right and is not configurable.
|
||||
|
||||
## User experience
|
||||
|
||||
### Configurable items
|
||||
|
||||
The following toolbar items are configurable:
|
||||
|
||||
1. **Tabs panel** — toggles the vertical tabs sidebar (hamburger icon)
|
||||
2. **Tools panel** — toggles the left panel containing project explorer, global search, warp drive, conversation history (tool icon)
|
||||
3. **Agent management** — toggles the agent management view (grid icon)
|
||||
4. **Code review** — toggles the code review panel (right panel button)
|
||||
5. **Notifications mailbox** — toggles the notification mailbox (inbox icon)
|
||||
|
||||
Each item is gated on the same feature flags and settings that currently control its visibility (e.g. agent management requires AI enabled + `FeatureFlag::AgentManagementView`, notifications requires `FeatureFlag::HOANotifications`). Items whose prerequisites are not met do not appear in the toolbar or in the configurator.
|
||||
|
||||
### Default layout
|
||||
|
||||
The default layout matches the current hardcoded layout:
|
||||
- **Left:** Tabs panel, Tools panel, Agent management
|
||||
- **Right:** Code review, Notifications mailbox
|
||||
|
||||
### Side placement determines panel open side
|
||||
|
||||
Moving a toolbar item to the left or right side determines which side of the main content area its panel opens on:
|
||||
|
||||
- **Tabs panel:** Opens its sidebar on whichever side its button is placed. Not pinned to the left.
|
||||
- **Tools panel:** Opens on whichever side its button is placed (left or right of main content).
|
||||
- **Code review:** Opens on whichever side its button is placed.
|
||||
- **Agent management:** Replaces the main content area entirely — side placement only affects button position, not where it renders.
|
||||
- **Notifications mailbox:** The popover anchors to its button position — top-left corner under the button's bottom-left when on the left side, top-right under bottom-right when on the right. Notification toasts also appear on the same side as the mailbox button.
|
||||
|
||||
Within a side, panels open in the order their buttons appear in the toolbar. For example, if the left side is configured as `[Code Review, Tabs Panel, Tools Panel]`, then when all three are open, the panels render left-to-right as: Code Review | Tabs Panel | Tools Panel | Main Content.
|
||||
|
||||
### Side-aware UI element flipping
|
||||
|
||||
When a panel or its parent container moves to the opposite side, all associated UI elements flip to avoid rendering off-screen:
|
||||
|
||||
- **Tabs panel detail sidecar:** When the tabs panel is on the left, the hover-detail sidecar opens to the right. When on the right, it opens to the left.
|
||||
- **Conversation list tooltips:** Item hover tooltips open toward the center of the screen (right when panel is on the left, left when on the right).
|
||||
- **Conversation list kebab menu:** The overflow button and its opened menu anchor toward the center.
|
||||
- **Warp Drive item kebab button:** When the tools panel is on the right, the kebab `⋮` button renders as a left-aligned overlay (flush with the left edge) instead of appending to the right end of the row. The opened menu also opens toward the center.
|
||||
- **Warp Drive hover previews and dialogs:** Hover preview sidecars and share/naming dialogs flip to open toward the center.
|
||||
- **Vertical tabs action buttons:** The kebab/close button container appears on the opposite side of the tab group, and the right-click menu anchors accordingly.
|
||||
- **Panel borders and resize handles:** The drag bar and border line render on the edge facing the main content (right edge when panel is on the left, left edge when on the right).
|
||||
|
||||
### Configuration entry points
|
||||
|
||||
1. **Right-click context menu:** Right-clicking on any toolbar item or on the toolbar area between the left items and the search bar / between the search bar and the right items shows a context menu with an "Edit toolbar" option that opens the configurator modal.
|
||||
2. **Settings page:** The configurator is also accessible from the Settings page under Appearance > Tabs, using the same pattern as the agent input footer toolbelt editors (the "Edit agent toolbelt" / "Edit CLI agent toolbelt" buttons accessible under Appearance > Input type). A button like "Edit toolbar" opens the same configurator modal.
|
||||
|
||||
### Configurator modal
|
||||
|
||||
The modal uses the same drag-and-drop `ChipConfigurator` pattern as the agent input footer editor (`app/src/ai/blocklist/agent_view/agent_input_footer/editor.rs`):
|
||||
|
||||
- **Title:** "Edit toolbar"
|
||||
- **Available items** section: shows items not currently placed on either side. Chips can be dragged from here to either drop zone.
|
||||
- **Left side** drop zone: shows items that render on the left side of the header bar. Items can be reordered via drag-and-drop.
|
||||
- **Right side** drop zone: shows items that render on the right side of the header bar. Items can be reordered via drag-and-drop.
|
||||
- Each placed item has an X button to remove it (moves it back to available items).
|
||||
- Items can be dragged between zones (left → right, right → available, available → left, etc.).
|
||||
- **Restore default** link: resets to the default layout. Disabled when already at defaults.
|
||||
- **Cancel** button: discards changes and closes.
|
||||
- **Save changes** button: persists the configuration and closes. Disabled when no changes have been made.
|
||||
|
||||
### Interaction with existing show/hide settings
|
||||
|
||||
The existing settings that control item visibility (`TabSettings::show_code_review_button`, `AISettings::show_agent_notifications`, etc.) remain the source of truth for whether an item is shown. When a user removes an item from the toolbar via the configurator, the corresponding existing setting is toggled off. When a user adds an item back, the setting is toggled on.
|
||||
|
||||
This ensures that:
|
||||
- Onboarding choices are respected: if onboarding hides code review, it won't appear in the toolbar or in the configurator's "available" section until the prerequisite is met.
|
||||
- Settings page toggles remain consistent with the toolbar configuration.
|
||||
|
||||
### Ordering and side placement persistence
|
||||
|
||||
A new setting stores the user's custom ordering and side placement. The setting stores a list of item identifiers for the left side and right side. When the setting is `Default`, the hardcoded default layout is used. When `Custom`, the stored lists determine order and placement.
|
||||
|
||||
Items that are "available" (prerequisites met + existing show/hide setting is on) but not present in either the left or right list are treated as hidden by the user.
|
||||
|
||||
### Edge cases
|
||||
|
||||
- If an item's prerequisite becomes unmet after configuration (e.g. AI is disabled, removing agent management), that item silently disappears from the toolbar. It remains in the stored configuration so it reappears if the prerequisite is met again.
|
||||
- If all items are removed from both sides, the toolbar area remains but is empty (just spacing).
|
||||
- If a panel is open and the user reconfigures its button to the opposite side, the panel should close and reopen on the new side on next toggle.
|
||||
|
||||
## Success criteria
|
||||
|
||||
1. Right-clicking the toolbar area (in both horizontal and vertical tabs mode) opens a context menu with "Edit toolbar".
|
||||
2. The configurator modal opens and displays the correct current layout.
|
||||
3. Items can be dragged between left, right, and available zones, and reordered within zones.
|
||||
4. Clicking X on a placed item moves it to available.
|
||||
5. Saving persists the configuration; reopening the configurator shows the saved layout.
|
||||
6. The toolbar renders items in the configured order and sides.
|
||||
7. "Restore default" resets to the hardcoded default layout.
|
||||
8. Removing an item from the toolbar via the configurator also toggles off the corresponding existing setting (e.g. `show_code_review_button` becomes false).
|
||||
9. Adding an item back toggles the setting on.
|
||||
10. Onboarding settings are respected: items hidden during onboarding are not shown until re-enabled.
|
||||
11. Items gated behind feature flags that are not enabled do not appear in the toolbar or configurator.
|
||||
12. Configuration survives app restart.
|
||||
13. Moving code review to the left side causes the code review panel to open on the left.
|
||||
14. Moving tools panel to the right side causes the tools panel to open on the right.
|
||||
15. Moving tabs panel to the right side causes the vertical tabs sidebar to open on the right.
|
||||
16. Panel ordering within a side is respected (if tabs panel is to the right of tools panel on the left side, the tabs sidebar renders to the right of the tools panel).
|
||||
17. Notification toasts appear on the same side as the mailbox button.
|
||||
18. Warp Drive item hover previews open toward the center of the screen (left when panel is on the right).
|
||||
19. Conversation list tooltips and kebab menus open toward the center.
|
||||
20. Vertical tabs detail sidecar opens toward the center.
|
||||
21. Warp Drive kebab button appears as a left-edge overlay when the panel is on the right side.
|
||||
|
||||
## Validation
|
||||
|
||||
- Manual testing: open the configurator, rearrange items, save, verify the toolbar updates. Restart app and verify persistence.
|
||||
- Verify onboarding integration: complete onboarding with code review disabled, then open the configurator and confirm code review is not in the "available" section until re-enabled in settings.
|
||||
- Verify feature flag gating: with `HOANotifications` disabled, confirm notifications mailbox does not appear in toolbar or configurator.
|
||||
- Verify panel side placement: move code review to the left, verify it opens on the left side of the main content.
|
||||
- Verify panel ordering: configure left as `[Code Review, Tabs Panel]`, open both, verify code review renders to the left of tabs panel.
|
||||
- Unit tests for the new setting type (serialization, default resolution, migration).
|
||||
|
||||
## Feature flag
|
||||
|
||||
The entire feature is gated behind `FeatureFlag::ConfigurableToolbar`, which is enabled by default in all release builds. When disabled, the toolbar renders the default hardcoded layout and the right-click context menu / settings entry point are hidden.
|
||||
|
||||
## Open questions
|
||||
|
||||
None — all questions have been resolved.
|
||||
@@ -0,0 +1,322 @@
|
||||
# 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 composing `VTabs | theme chooser | Main | right panels`.
|
||||
- `app/src/workspace/view.rs:16545` — `render_banner_and_active_tab`: inner layer composing `LeftPanel(tools) | Terminal | RightPanel(code review)`.
|
||||
- `app/src/workspace/view/vertical_tabs.rs:1120` — `render_vertical_tabs_panel`: renders the vertical tabs sidebar with a `Resizable` wrapper and `DragBarSide::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: bool`
|
||||
- `app/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/close
|
||||
- `app/src/workspace/view.rs:6947` — `toggle_right_panel`: code review open/close
|
||||
- `app/src/workspace/view.rs:6669` — `toggle_vertical_tabs_panel`
|
||||
- `app/src/workspace/view.rs:18256` — `ToggleNotificationMailbox` handler
|
||||
|
||||
### Notification toast positioning
|
||||
- `app/src/workspace/view.rs:20265` — mailbox popover anchors to `NOTIFICATIONS_MAILBOX_POSITION_ID` (follows button naturally).
|
||||
- `app/src/workspace/view.rs:20342` — notification toasts anchor to `TAB_BAR_POSITION_ID` at `BottomRight`. 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 via `add_right_side_tab_bar_controls` at 15543.
|
||||
- `app/src/workspace/view.rs:15639` — `add_right_side_tab_bar_controls`
|
||||
|
||||
### Existing chip configurator infrastructure
|
||||
- `app/src/chip_configurator/mod.rs` — `ChipConfigurator` with `LeftRightZones` layout.
|
||||
- `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` — `ToolbarChipSelection` trait, `AgentToolbarChipSelection` enum.
|
||||
- `app/src/terminal/session_settings.rs:277` — `agent_footer_chip_selection` setting.
|
||||
|
||||
### 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`): `Resizable` wrapper with `DragBarSide::Right`. Self-contained rendering in `vertical_tabs.rs`. Open state: `Workspace::vertical_tabs_panel_open`.
|
||||
- **Tools panel** (`LeftPanelView`): rendered via `ChildView::new(&self.left_panel_view)`. Has its own `Resizable` wrapper inside `LeftPanelView`. Open state: `PaneGroup::left_panel_open`.
|
||||
- **Code review** (`RightPanelView`): rendered via `ChildView::new(&self.right_panel_view)`. Has its own `Resizable` wrapper. 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`
|
||||
|
||||
```rust
|
||||
#[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`:
|
||||
|
||||
```rust
|
||||
#[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` (with `DragBarSide` flipped 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:
|
||||
|
||||
```rust
|
||||
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 three `MouseStateHandle`s.
|
||||
- `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 type `A`.
|
||||
|
||||
Each modal view keeps its own `ChipConfigurator`, `MouseStateHandle`s, `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()`: reads `HeaderToolbarChipSelection` from `TabSettings`, builds `ConfigurableItem` list via `ControlItemRenderer::new_with_label_and_icon`.
|
||||
- On `save()`: writes back to `TabSettings::header_toolbar_chip_selection`. Syncs `show_code_review_button` and `show_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_open` to `WorkspaceState`.
|
||||
- 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:
|
||||
|
||||
```rust
|
||||
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` → sync `TabSettings::show_code_review_button`
|
||||
- `NotificationsMailbox` → sync `AISettings::show_agent_notifications`
|
||||
- `ToolsPanel` → 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`): Anchors `BottomLeft`/`TopLeft` when on left, `BottomRight`/`TopRight` when on right.
|
||||
- **Notification toasts** (`view.rs`): Same side-aware anchoring.
|
||||
- **Tabs panel detail sidecar** (`vertical_tabs.rs`): `detail_sidecar_offset_and_max_height` accepts `PanelPosition` and flips `TopRight→TopLeft` / `BottomRight→BottomLeft` (and negates the horizontal gap) when on the right.
|
||||
- **Vertical tabs action buttons** (`vertical_tabs.rs`): Overlay position flips from `TopRight` to `TopLeft`.
|
||||
- **Vertical tabs right-click menu** (`view.rs`): Anchors flip from `BottomLeft`/`TopLeft` to `BottomRight`/`TopRight`.
|
||||
- **Conversation list tooltips** (`conversation_list/item.rs`): `tooltip_opens_right` field on `ItemProps` flips `MiddleRight→MiddleLeft`.
|
||||
- **Conversation list kebab button + menu** (`conversation_list/item.rs`): Button position flips from `TopRight` to `TopLeft`; `MenuDirection` flips.
|
||||
- **Warp Drive item overflow button** (`drive/items/item.rs`): When `overflow_on_left`, the button renders as a `Stack` overlay at `MiddleLeft` (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_stack` flips 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 and `DragBarSide` are driven by `self.panel_position`.
|
||||
- **Vertical tabs panel** (`vertical_tabs.rs`): Border and drag bar side are driven by the `side` parameter.
|
||||
|
||||
## End-to-end flow
|
||||
|
||||
**Toolbar rendering:**
|
||||
1. `render_tab_bar_contents` reads `HeaderToolbarChipSelection` from `TabSettings`.
|
||||
2. For each configured left item (filtered by `is_available`), renders the corresponding button.
|
||||
3. Search bar renders in the middle.
|
||||
4. For each configured right item, renders the corresponding button.
|
||||
|
||||
**Panel rendering:**
|
||||
1. Unified panel composition reads the same config.
|
||||
2. For each left panel item that is open, renders the panel on the left of main content, in order.
|
||||
3. Main content (terminal or agent management) renders in the middle.
|
||||
4. For each right panel item that is open, renders the panel on the right, in order.
|
||||
|
||||
**Configuration flow:**
|
||||
1. User right-clicks toolbar → context menu → "Edit toolbar".
|
||||
2. `WorkspaceAction::OpenHeaderToolbarEditor` opens the modal.
|
||||
3. Modal reads current config, populates `ChipConfigurator`.
|
||||
4. User drags items, clicks save.
|
||||
5. Modal writes `Custom { left, right }` to `TabSettings::header_toolbar_chip_selection`.
|
||||
6. 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:** `HeaderToolbarChipSelection` serialization, 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_open` to panel-type-specific names (e.g. `tools_panel_open`, `code_review_panel_open`).
|
||||
- Consider a dedicated `show_agent_management` boolean 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.
|
||||
Reference in New Issue
Block a user