Repository navigation
Agent: add purposeful motion across chat, composer, menus and settings - #1515
Open
matthewrball wants to merge 30 commits into
Open
matthewrball wants to merge 30 commits into
matthewrball wants to merge 30 commits into
Conversation
Document the motion rules in docs/motion.md. motion.rs gains the duration and easing tokens, a retargetable Tween that the hover plates and the sidebar collapse now share, Shifted (a layout-free paint offset), element-state Animated values, icon swaps and rotating chevrons.
Animated values start at rest, reverse from the value on screen, settle without asking for frames, and snap under reduced motion. A shifted element moves by whole pixels without moving its neighbors.
Menus and popovers fade in over REVEAL while settling 4 px away from the trigger: down below it, up above it, sideways for submenus, following a flipped placement. Each opening replays the entrance, closing stays immediate, and arrow-key highlight moves land at once while pointer hover still eases. Adds HoverBackground::instant and the swap_in content fade to motion.rs.
Switch knobs slide and tracks cross-fade over TOGGLE, reversing from the knob's position; checkbox fills cross-fade and the check scales in. Dialog scrims fade and cards rise 8 px over ENTER and close at once. Tooltips fade in over REVEAL after their delay.
A new screen (Chat, Settings, sign-in, gates) fades in over REVEAL; the first screen draws at once. Settings panes fade on a section change, and on/off pills and the transport segments ease their selected fill.
Task status indicators cross-fade between states, never on first draw; section chevrons rotate, project folder icons cross-fade, and the Settings menu chevron turns as the menu opens.
Rows that arrive live in the open task (the sent message, the reply, thinking, tool cards, errors, the stop notice) fade in with a 6 px rise over 180 ms, timed from view state so a row scrolled away and back never replays it. History, task switches, paging and reconnect rebuilds stay still, as do streaming appends. The send preview rises in and hands off to the real row without a second entrance; the waiting dots fade in. Disclosure chevrons rotate and opened details fade in; a tool call that finishes on screen cross-fades its status. Another task's transcript fades in when it lands, the open spinner waits 300 ms, and notices and the cards above the composer rise in.
A queued chip fades in with the shared 6 px rise when its message is queued and leaves at once when it is sent, edited away or removed.
A multi-line field whose rows change because its text changed (typing, paste, delete, newline, clearing after send, text set from code) eases its height to the new row count over resize_duration with ease-out cubic, retargeting from the height on screen. The first layout, a new wrap width (window resize, the sidebar sliding), a font change and reduced motion snap. While the box is still shorter than its text, the text is laid out for the target height and clipped, so following the cursor never scrolls on the way. Frames are requested only while the height moves. Applies to the composer, Settings additional instructions and the feedback form. The row measure now uses the text style the field paints in: gpui runs measure callbacks outside the element's style, so the composer was sized with the window's default line height and font rather than the chat reading face.
Expand and shrink ease the composer region's height between the height of its text and expanded over PANEL with ease-out cubic. A second click mid-flight turns back from the height on screen. Both ends come from bounds measured the frame before, so the box starts and lands exactly on its resting layout; the field fills the box throughout, so typed text never moves, and the transcript keeps its tail. In the empty state the heading and the privacy line fold away and fade with the composer, so the column does not jump. The maximize and minimize icons cross-fade and scale over TOGGLE, and the button names its action to assistive technology. Reduced motion switches at once, as before.
The composer's main control cross-fades between Send and Stop over TOGGLE: the fills fade and the arrow scales, while the handler, role and accessible name switch on the frame the state changes. The send fill eases between disabled and ready instead of snapping its opacity. A file joining the draft fades and rises 6 px into place over ENTER; removing one is immediate. Reduced motion settles each at once.
Keep the waiting dots' spacing now that they fade in, drop the unused Animated::enter_from, and document swap_in and the menu entrance.
A row that arrives while the transcript follows its tail opens from nothing to its height over 180 ms, so the rows above glide up instead of jumping; scrolled up, it still only fades and rises. A reply that streams meanwhile ends at its true height. The sent bubble rises in from the moment it shows, its status line only fades, and the status shares one slot with the waiting dots, so the real message takes over the bubble's place with nothing jumping. A finished tool call drops its running state at once and fades in the outcome rather than overlapping both labels.
When the empty state gives way to a conversation (the first message, opening a task) or back (New Task), the composer region glides from where it was last drawn to its new place over 200 ms instead of jumping. glide_from moves into motion.rs so the transcript's hand-off and the composer share one paint-offset primitive. Menus the keyboard opens appear at once; pointer openings keep their entrance.
Settings dropdowns open on their current choice even from a click, so the highlighted-item heuristic skipped their entrance. Use the window's last input instead: a pointer opening slides in, a keyboard opening is drawn at rest.
The message being sent and the waiting dots sit in one slot under the transcript, outside the list. When it appears, or its content grows, it now opens over 180 ms with its content pinned to the bottom, so the transcript above glides up rather than jumping. It still shrinks at once, and the row that takes its place opens from the space it leaves, so the hand-off stays still. A task switch or history load shows it at once, and reduced motion settles it immediately.
gpui replays a cached view's last paint with the opacity it was painted at, so fading the whole screen left the cached sidebar stuck at its first, nearly transparent frame after returning from Settings. swap_in now fades a cover in the background color over content that always paints at full opacity. Document the rule for cached views.
The cover was a child of the swapped element, so in a scrolled Settings pane it moved with the content and left a band uncovered. Paint it as a quad over the element's bounds after the content instead: no extra layout node, and swap_in takes any element. Note the sidebar collapse as the one safe fade over a cached view.
The empty state never drew the slot under the transcript, so the snap a blank transcript sets carried over to the first send, and its slot appeared at full height instead of opening. Clear it while the empty state shows, and test it on the whole screen. - Reconnect replays go through one method; its test drives that method instead of toggling the quiet flag by hand. - The copy button hovers with the translucent overlay it had before, so its plate suits tinted bubbles and code blocks. - Docs: dialog cards rise 8 px, notices use ENTER, and a slid element's hitbox moves with its paint.
…ns rise in When a run ended in an error, or a send failed, the slot under the transcript (the waiting dots, or the sent message and its status) vanished at once while the error grew from nothing, so the thread dropped by the slot's height and then rose again. - A run error goes through the same path as every other live row: it ends the wait and opens from the waiting row's space. - A failed send's error opens from the whole slot's height. - The question card rises in like the other cards above the composer. - Docs: covers and fades never block input; rows that replace the slot open from its height.
Choosing Custom amount under Buy credits dropped its field in at once and pushed the pay buttons down; choosing a package snapped them back. Add motion::unfold, which eases a box between nothing and its content's height over PANEL while the content fades, turns back from the height on screen, settles under reduced motion and draws nothing once folded, and use it for the field. Its top padding stands in for the card's gap, which would otherwise jump on its own.
…eing cut Shrinking the expanded composer from the empty state grew the heading's folding box from nothing with its text pinned to the box's top and clipped, so "Work on anything…" showed sliced against the composer's edge for most of the transition. Fold the box as before but leave its content whole: the heading sits at the box's foot and the note at its head, both fading, so the text rides with the composer at its resting gap. The content keeps recording its height, so a fold that starts before the heading was ever measured catches up on the next frame.
The region under the composer eases its height while the composer shrinks back from expanded, but only the composer's own box stretched to fill it. Anything render_composer wraps the box in, such as a new task's folder picker by the prompt (#1353), stretched only while the composer was expanded, so on the first frame of shrinking the box snapped to rest and the heading and the box dropped. Stretch whatever render_composer returns for as long as the composer fills, expanded or on the way there and back, and test a new task's shrink from where it was.
This was referenced Nov 9, 2026
The folding heading is held against the composer and rides above its shrinking box, so in a short or narrow window it reached past the pane's top edge late in the fold and drew faintly over the title bar and window controls. The pane now clips while the composer moves; at rest nothing is clipped, so the slash palette and the glide are untouched.
Brings the motion branch up to master at 6e981a8 and re-applies its motion onto master's current UI. - widgets: default_button eases its hover like primary_button, and keeps master's external_mark and navigate_mark. - Settings: the regrouped sidebar's rows ease their hover through render_nav_item, the pane still fades between sections, and the on/off pill eases to master's default-button colors. - Composer: chip_frame carries the hover transition, so the model chip and the new project chip share it. The Web chip is gone on master, and its accent highlight with it. - A new task's folder picker stretches with the composer for as long as it fills, in the container itself rather than at the two call sites. - Question card: master's compact card keeps the turning chevron, the eased option rows and the rise on entry; its footer buttons take the animated button type. - The runtime error under the empty state uses master's toned banner and keeps its top margin.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Current review stack
Standalone on
master; not part of a stack.Motion across chat, the composer, menus and Settings. It was held until last, since it overlaps most of this week's merged work.
Refreshed onto master
6e981a88by merging master in; no commits were rewritten. The motion now sits on the current UI:chip_frame, so the model chip and the folder chip share it.default_buttoneases on hover like the other buttons.Local checks on this head: app suite 1,470 passed, 0 failed, 3 ignored;
cargo fmt --checkand the three clippy lanes pass on Rust 1.98. The recordings below predate this week's merges: they show the older composer (with the Web chip) and the earlier layout.Current head:
a587cdaf.The original scope and validation are retained below.
Moved from #1509 so the branch lives in this repo. Same commits; earlier discussion is on #1509.
Why
Maple Work changes state in a single frame almost everywhere. Menus pop, dialogs appear, the composer jumps a line at a time, new messages land without any sense of where they came from, and the whole thread jumps when a tool card arrives. This adds short, purposeful motion across the app so each change reads as cause and effect, without slowing anything down.
Every animation is 200 ms or less, eases out, and settles at once under Settings → General → Reduce motion. Transitions that can reverse (hover, switches, chevrons, the composer, the sidebar) turn back from where they are when interrupted; entrances play once. Send, Stop, Escape and keyboard shortcuts act on the frame they arrive. Streaming text, typing and keyboard navigation in menus and lists don't animate, and menus and dialogs close immediately.
The shared rules and tokens are in
apps/maple-agent/docs/motion.md, andui/motion.rsimplements them. This carries #1355 (hover transitions) forward onto current master, so #1355 can close when this lands.What this PR adds
Conversation
Composer and text fields
motion::unfoldmakes the same reveal available to any field a choice shows.Menus, dialogs and navigation
Controls
Tween.Also fixed
Preview
Recorded window-only from a local-only preview build of this branch, with sample tasks and a scripted run (no account or backend). The preview window stayed in the background the whole time, where it drew at about 30 fps, so motion looks slightly steppier here than in the focused app. The preview hook is not committed.
Validation
Run on macOS 26 with the Homebrew Rust 1.96.0 toolchain, at
ad8f7081:cargo test -p maple-agent-app --locked: 1,378 passed, 0 failed, 3 ignored.cargo clippy -p maple-agent-app -- -D warningsfor every feature set CI checks: desktop (--all-targets), headless (--all-targets --no-default-features --features acp,proxy),acponly andproxyonly. All clean. With this local toolchain, two lints already fire on master in files this PR doesn't touch (unexpected_cfgsin the Objective-C macros,clippy::nonminimal_bool), so they were allowed with-A; CI's pinned toolchain runs the real gate.cargo fmt --all --check: clean.GitHub CI on
ad8f7081:agent-lint,agent-test,agent-macosandagent-windowspass.New tests pin the motion clock (
motion::set_test_now). They cover:Visual: I drove every scenario in the GIFs through a local-only preview build of this branch, using sample tasks and a scripted run with no account or backend, and reviewed them frame by frame. That review caught three problems, fixed here before opening the PR:
Review: an independent read-only review, then advisor passes from Codex and Grok 4.7 (Gemini was out of credits) with Claude cross-checking both, found no blockers. Fixed from their notes:
Left as is: a growing field reveals its newest line as the box grows rather than scrolling it into view first; a card that appears above the composer still takes its space at once.
Hands-on testing in builds combined with other open PRs found two problems when shrinking the composer from the empty state:
render_composerwraps the box in now stretches for as long as the composer fills, not only once it's expanded.The repository pre-commit hook isn't installed in this clone (
core.hooksPathis unset), so it didn't run on these commits. I ran the app crate's checks above by hand instead; the other workspace crates are unchanged.Not verified:
Not in this PR
div, so movement is a layout-free paint offset, and press feedback stays an immediate colour change rather than a scale.Open in [code]smith · Need help on this PR? Tag
@codesmith-botwith what you need.