packages/cua-driver/docs/cursor-modifier-badge-restructuring-plan.md
Status: Implemented in PR #2677; validation complete
Base: main at b249df857994ebaebf9f02248e8312b3bd2a5dd2
Scope: Move delivery and target modifiers from around the agent pointer into the session badge while preserving action animations beside the pointer.
Cua Driver should present two kinds of cursor information in two clear places:
The badge remains lightweight. The session name fades using the existing two-second hold and 400-millisecond fade. Modifier chips appear only while their action is active, followed by a 400-millisecond trailing fade. When the name is hidden, the badge contracts to a compact session orb and active modifier chips. Hovering the pointer restores the full session name where hover observation is supported.
This is visual feedback only. It is not an authorization indicator, a security boundary, or evidence that an action succeeded.
The work is complete when:
Use one pill rather than adding a second persistent status capsule.
| Badge content | Visibility |
|---|---|
| Session orb and name | Existing reveal, two-second hold, 400-millisecond fade; hover restores full opacity |
| Delivery and target chips | Full opacity while modifiers are active; 400-millisecond trailing fade after they clear |
| Badge chrome | Visible while either the name or modifier chips are visible |
Starting an action must not restart the session-name timer. Otherwise a long session name would flash on every tool call. Modifier changes reveal or update the chips only.
When the name has faded, the pill contracts to the session orb and active chips. If no public session label exists, modifier-bearing actions still get this compact session-colored capsule. This preserves context without requiring a display name.
The badge owns six small vector glyphs:
They are rendered through the shared Skia path at the live backing scale. Delivery uses a filled chip treatment. Target uses an outlined chip treatment. Shape, not color alone, communicates the difference.
Do not reuse theme-authored modifier frames inside the badge:
.cua-theme artifacts.The badge and its modifier glyphs therefore remain outside the dotLottie theme contract.
Keep:
BADGE_HEIGHT = 28BADGE_MAX_WIDTH = 188BADGE_CURSOR_GAP = 25Use 18-point modifier chips with a four-point gap. The text budget becomes dynamic:
The label must ellipsize to its measured width and never wrap. The current font layout can wrap a wide 28-character label outside the pill, so this restructuring must fix that existing defect before reducing the text budget.
This change deliberately replaces the recently introduced v1 theme contract rather than carrying an unused compatibility layer.
Use:
cua.cursor-theme/22cua-driver-actions-v23CUATHEM3The v2 source manifest contains only actions. A modifiers field is rejected with a focused migration message. The compiled CompiledTheme structure drops its modifier map entirely.
Existing v1 .cua-theme files are rejected with a clear instruction to rebuild the source as v2. No decoder shim, aliases, ignored fields, or transitional profile are added.
Regenerate cua.default.lottie and cua.default.cua-theme without modifier animations. The new source hash and artifact are checked in together and verified reproducibly.
Refactor cursor-overlay/src/session_badge.rs so geometry is computed before painting.
Add shared inputs and outputs similar to:
pub struct SessionBadgeInput<'a> {
pub label: Option<&'a str>,
pub delivery: Option<DeliveryModifier>,
pub target: Option<TargetModifier>,
pub cursor: (f32, f32),
pub backing_scale: f32,
pub label_alpha: f32,
pub chip_alpha: f32,
pub clip: Option<(f32, f32)>,
}
pub struct SessionBadgeLayout {
pub rect: Rect,
pub orb: Rect,
pub label: Option<BadgeLabelLayout>,
pub delivery_chip: Option<BadgeChip>,
pub target_chip: Option<BadgeChip>,
pub pill_alpha: f32,
pub label_alpha: f32,
pub chip_alpha: f32,
}
pub fn session_badge_layout(
input: SessionBadgeInput<'_>,
) -> Option<SessionBadgeLayout>;
pub fn paint_session_badge(
pixmap: &mut Pixmap,
layout: &SessionBadgeLayout,
session_fill: [u8; 4],
alpha_scale: f32,
);
pub const fn session_badge_extents() -> BadgeExtents;
The shared layout function owns:
Platform adapters must derive their dirty regions from session_badge_extents() instead of copying badge widths.
Add cursor-overlay/src/badge_glyphs.rs with host-owned vector geometry and typed mapping from DeliveryModifier and TargetModifier.
Glyphs must:
Extend RenderStateCore with a short-lived modifier latch so chips can fade after CursorVisualState returns to idle:
pub badge_modifiers:
Option<(Option<DeliveryModifier>, Option<TargetModifier>)>,
pub badge_modifier_clear_secs: Option<f64>,
Add shared methods for:
SessionBadgeInputBeginAction updates the modifier latch without revealing the session name. tick_idle starts the trailing fade when semantic modifiers clear. Idle cursor opacity still multiplies the entire badge, so the badge cannot outlive the cursor.
In theme_artifact.rs, stop compositing delivery and target animation layers with the pointer transform. Continue painting the selected action animation exactly as before.
Remove modifier animation storage and compositing from the compiled artifact.
Use the common renderer and layout without platform-specific badge geometry. The existing full-screen overlay and hardware-pointer hover polling remain unchanged.
Derive the layered-window dirty envelope from shared badge extents. Fix the current stale invariant where CURSOR_PAD = 192 no longer covers BADGE_MAX_WIDTH + 8.
Run the invariant test in CI instead of compiling it without executing it.
Derive horizontal and vertical tile margins from shared badge extents. The current vertical tile margin has little spare room below the badge and should not depend on a copied constant.
Use shared badge visibility rather than session_label.is_some() when deciding whether the tile must include badge space.
Continue using the shared renderer. Modifier chips and timed visibility work identically. Hover reveal remains unavailable through the stock protocol because the client cannot observe another client's global pointer.
Remove pointer-relative modifier drawing. Render the same two badge chip slots in the Shell badge and mirror the shared timing constants.
Keep the D-Bus action payload unchanged. Bump the bundled helper version and its Rust-side version pin together. Add parity fixtures so glyph identity, ordering, and timing cannot drift silently.
cursor-overlay unit tests in Linux CI.SessionBadgeInput, SessionBadgeLayout, and session_badge_extents.paint_compiled_theme_with_tint.modifiers from the source manifest and CompiledTheme.RenderStateCore badge rendering.modifiers section| Platform | Required evidence |
|---|---|
| macOS | Lume install from exact source SHA; full action sequence; hover reveal; edge placement; video |
| Windows | GitHub Actions plus interactive VM when available; layered-window edges; two-chip layout; video |
| Linux X11 | GitHub Actions or VM; tile bounds at all edges; two-chip layout; video |
| Native Wayland | Shared-renderer chips and timing; bottom-edge clipping; documented hover limitation; video |
| GNOME helper | Version handshake; label and chip timing; empty-label capsule; parity recording |
Use semantic and pixel oracles. A successful tool result alone is not visual proof.
Cursor review media must come from the overlay-enabled
agent_cursor_showcase_test. The shared behavior matrix intentionally launches
its daemons with --no-overlay so cursor pixels cannot contaminate its action
oracles; those recordings prove tool behavior, not cursor rendering.
Update:
packages/cua-driver/docs/cursor-themes.mdtools/cursor-gallery/README.mdDocument clearly:
| Risk | Mitigation |
|---|---|
| Chips are too small at 1x | Review exact production pixels before removing pointer modifiers; increase chip size only within the unchanged badge envelope |
| Long labels collide with two chips | Measure and ellipsize text; never wrap |
| Badge becomes too noisy | Do not reveal the name on action start; chips exist only during actions plus one shared trailing fade |
| Existing v1 custom themes stop loading | Provide clear rebuild guidance in the CLI and upgrade documentation |
| Platform geometry drifts | Centralize layout and extents; adapters consume shared outputs |
| GNOME renderer drifts | Share protocol semantics and fixtures; version the helper; verify recordings |
| Modifier chips are mistaken for permission status | Avoid security language and colors; explicitly document that they are informational |
| Rollback crosses incompatible artifact versions | Keep the v2 source archive reproducible and document that driver and theme artifacts must move together |
The implementation was validated from the PR source with no failed behavioral cells:
| Platform | Result |
|---|---|
| macOS Lume VM | 151 pass-equivalent cells: 143 delivered and 8 expected refusals |
| Windows GitHub runner | 83 pass-equivalent cells: 61 delivered and 22 expected refusals |
| Linux X11 GitHub runner | 82 pass-equivalent cells: 47 delivered and 35 expected refusals |
| Native Wayland GitHub runner | 82 pass-equivalent cells: 47 delivered and 35 expected refusals |
| Native cursor showcase | Overlay-enabled pixel oracle and move, click, type, scroll, and drag recording passed on Windows, X11, and Wayland |
| GNOME helper | API version handshake, metadata, label and chip timing, compact layout, and Rust-to-helper parity tests passed |
The PR also passes the Linux and Windows Rust suites, portable contract parity on macOS, Linux, and Windows, Nix builds and tests, generated contract checks, documentation synchronization, link checks, and publish-bundle validation.
Native review recordings are attached to the PR for macOS, Windows, X11, and Wayland. A live GNOME Shell recording was not captured in this PR. The GNOME renderer is verified through its versioned helper contract and automated parity tests, while the other Linux recording exercises the shared renderer used by native Wayland.
The first two phases are additive and should remain even if the visual move is rejected because they fix test, padding, and label-layout defects.
The behavioral switch can be rolled back by restoring the v1 default source and artifact together with modifier layer compositing. Because the contract is intentionally breaking, the driver and installed theme artifact version must move together.
The GNOME helper and Rust helper-version pin must always roll forward or back together.
The plan recommends:
The breaking theme-contract choice is approved. The remaining visual decisions are reviewed against exact production pixels.