282 lines
16 KiB
Markdown
282 lines
16 KiB
Markdown
# 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.
|