Skip to content

Tcode Chat UI v2 - #1000

Draft
Tryanks wants to merge 227 commits into
mainfrom
ui-v2
Draft

Tryanks wants to merge 227 commits into
mainfrom
ui-v2

Conversation

@Tryanks

@Tryanks Tryanks commented Oct 26, 2026 •

Copy link
Copy Markdown
Owner

No description provided.

@Tryanks Tryanks changed the title Tcode UI v2 Tcode UI v2 — the thousandth Oct 26, 2026
@Tryanks Tryanks changed the title Tcode UI v2 — the thousandth Tcode UI v2 Oct 26, 2026
@Tryanks Tryanks changed the title Tcode UI v2 ✨ Tcode UI v2 Oct 26, 2026
@Tryanks Tryanks changed the title ✨ Tcode UI v2 Tcode UI v2 Oct 26, 2026
@Tryanks Tryanks changed the title Tcode UI v2 Tcode Chat UI v2 Oct 26, 2026
Placeholder commit so the draft PR exists before the first motion change
lands. The motion coverage inventory lives in tmp/audit/motion/ (ignored)
until the first batch is implemented.
A closed dialog is marked closing and stays mounted, inert and occluding,
until its exit has played; scrim, opacity and lift follow one Presence
sample per opening. Layers count the open dialogs below, so a dialog
opened as another leaves does not jump when the leaving one is dropped.
Adds crate::motion, the shared appear/lift helper.
GPUI unmounts a hidden tooltip at once, so only the entry can animate.
Dropped before the branch merges.
Every job is gated on the pull request not being a draft, and
ready_for_review triggers the run once it stops being one.
…ined under it

The search row comes first and takes an Input's frame (input height,
radius_input, background with the input border, small text in the
placeholder colour, leading search icon); its shortcut is plain text via
Kbd::appearance(false). It still opens the palette. The machine control
moves under it and loses its outline.
The popover's exit draws the content again; keeping the closed menu lets
it reuse the entity instead of building and focusing a fresh one.
Sampled through the shared appear helper (Presence over SETTLE). gpui-base
mounts popover content only while open, so the leaving content is drawn
where it last landed, inert and occluding, until the presence is absent.
A dismissed menu stays mounted, inert and without its outside-press
handler, until its presence is absent. Tests that asserted the menu gone
the frame after dismissal now let the exit settle first.
The exit drew the content under a separate Positioner, so element state
inside it (a list's scroll offset, an input) reset for the exit frames.
It now renders gpui_base::Popup with the same id and a "content" child,
the tree base's Popover draws while open, and Popup positions it from its
own anchor state, so the recorded bounds and on_position hook are gone.
The exit renders through the same base dialog with the same element ids,
so scroll and input state hold for the exit; it is inert through the
base's switches (not topmost, no Escape, no backdrop press), an occluding
scrim and a shield over the card. Each opening keeps the layer it opened
at inside its own keyed slot, so layers that repeat while one dialog
leaves under another do not share element state. Closing a focused dialog
with no opener to return to blurs, since the leaving dialog is still
rendered.
gpui-base's ScrollbarMotion defaults every duration to zero and leaves the
product timing to the styled layer; Tcode projects SETTLE into it.
A wide or compact toast fades and lifts in and out over SETTLE; a dismissed
one stays mounted under a pointer cover until its exit ends instead of
holding full opacity and vanishing. A notice pushed again under its id keeps
its entity, so a runtime operation's progress updates neither replay the
entry nor cut short a reversal. The detail panel grows with MotionReveal.

The sidebar swipe test taps the row's lower edge: the undo notice is still
rising into place over the row centre it used to tap.
widgets::Collapsible drives gpui-base's reveal hook with a SETTLE transition
on the open state, and unmounts content once a section is fully shut. Used by
plugin details, Traverse status details, the PR panel's agent tools and the
off-diff comment list.
…resetting the list

ListState::reset in gpui-fast 0.1.10 clears logical_scroll_top, so every re-read that
changed the row count put the reader back at the top.
…s at once

The native navigation callbacks write the failure outside any GPUI update; nothing
re-rendered the panel until an unrelated notify.
…tion

reset_action takes any ElementId so the per-child reset can keep its indexed id.
…er uses SETTLE and its ticks follow the fill
Tryanks and others added 30 commits November 2, 2026 17:39
ListScroll moves a gpui list a step each frame from where it was to its
target as the list measures both that frame, landing exactly there
(following the tail for the end); a target past two viewports is first
brought within them. ListState scrolls at once and gpui-base has no
animated scroll. Anything else moving the list between steps (the
reader's wheel, pan or scrollbar, which reach the list through
gpui-base without its scroll handler) stops it where it was taken; rows
spliced under it settle it at the target; reduced motion goes at once.
Uses: the jump to the latest, which fades as the glide starts, and a
palette or anchor jump to a turn of the thread on screen.

The far-history paging test lets the jump play over its frames before
it checks the reader is back on the tail.

Co-Authored-By: Claude <noreply@anthropic.com>
ImageSource::Custom holds an Arc'd loader, so the tests that gate a
decode use an AtomicBool rather than an Rc<Cell>; and an inline image's
element takes the window it samples its fade with.

Co-Authored-By: Claude <noreply@anthropic.com>
A message moving under a still pointer as the rows settle took the hover
and notified the chat, as if the timeline had changed; only rows that
reveal something now take the hover, and a move redraws the window.

Co-Authored-By: Claude <noreply@anthropic.com>
The executor's clock is web_time's Instant on wasm32, so the glide's
start uses the same type there; tcode-web failed to check without it.

Co-Authored-By: Claude <noreply@anthropic.com>
…ns to the other axis

Reflow::reflowed marks a frame as a reflow whatever the width did, for an
owner that picks its layout from a width measured the frame before (the
composer's control row folds a frame after it narrows): every part that
moved glides from where it was. ResizeGlide records both sides of each
panel's drawn size and turn() lays the group out along the other axis, so
a group whose panels change sides with what they show (a terminal split
across or down) glides along whichever it now is.
… the control row come and go in place

The approval, grant, question, fallback, trigger-menu, goal and queue panels
above the card fold in and away, drawn as they were while they leave, and
the checkout row under it; the next approval or grant cross-fades over the
one answered. Terminal-context, review-comment and reference chips, the
image thumbnails (which fade in as they decode) and queued rows are keyed:
one that goes narrows or folds away, the rest move over, and rows a queue
drag passes glide. The draft field's height glides as it grows, shrinks
and clears. The desktop Send widens into spinner, Steer and Stop part by
part; the compact control cross-fades between Send, Queue and Stop and its
caption folds in. A row too narrow for its controls narrows the traits,
permission and divider away and the overflow in, and glides the meter and
mic to their places on the frame it folds. Question cards: the answer area
folds open and shut, questions cross-fade, choices fill, Done folds in,
hiding trades the card for its pill. Goal status glyphs cross-fade and
glide colour; the steer chooser rises in and its choices fill as the
finger crosses; the mic's colour and halo glide; model and traits labels
cross-fade (the traits chip's width glides); the context ring sweeps. The
model and permission panes' local 150ms fade is gone: the popover's own
appear runs enter and exit. Panels keep their motion per thread, so one
thread's never folds away over another's. Index-keyed chip and thumbnail
ids are keyed by identity.

Tests that draw one frame per step reduce motion; ones that click a menu
or resize the row let the frames the app draws pass first.
…nd selection action come and go

The body is one resizable group whether or not the active terminal is
split: splitting opens the second pane out of nothing as the first gives
way, and closing the split shrinks it away; with motion reduced the group
lays out afresh at once. The restart button folds along the toolbar, a
tab's bell dot folds in and away, "Add to context" fades over a selection,
and the phone key bar folds in with the keyboard.
…ctions come and go in place

The list is one keyed column, so an agent moving from Running to Awaiting
settle or Done glides there; the summary, captions and done rows fold in
and away under a turning chevron. The empty state and the list
cross-fade. A row's badges narrow in and away, its status cross-fades
between spinner and dot with the colour gliding, its result preview folds
in, and its hover actions fade in over the elapsed time (ListHover).
…up as one goes

The menu stays mounted while there is a selection, so it rises in as it
opens and fades out, inert, as a pan or a command closes it. Items are
keyed: Select All, once used, narrows away and the pill closes up.
…crolls a row into view gradually

A gpui list lays out only the rows in view and places each one itself, so a
row whose index changed was drawn at its new place at once, and keyed's
per-frame measurement cannot follow rows a list does not lay out. ListFlip
asks the list where it draws each row in view just before its owner
replaces the rows, and the row at its new index glides from there on the
FLIP core. ListScroll::reveal glides a list just far enough that a row is in
view, where scroll_to_reveal_item would put it at once.

Co-Authored-By: Claude <noreply@anthropic.com>
…bell and time widen in and narrow away

The flat, grouped and Arrange lists glide a row moved by a pin, a section
change, a sort or a drag from where it was (ListFlip), replacing the flat
list's spring over computed tops. A carried row keeps its place hidden
under its ghost instead of turning into a gap, so nothing shifts as a drag
starts. An empty section's drop target and Arrange's empty-Pinned row fold
in and out. Flat and Grouped cross-fade as the layout switches, each list
keeping its own scroll. The Follow-up bell and a grouped row's time widen
into the row and narrow away, so the title gives way gradually. Hover
reveals come from one ListHover, and a pinned row is scrolled into view
gradually.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
…; hovers, avatars, jumps and dialog heights glide

- PR numbers, reaction chips and the reviewer/label rows are keyed_across:
  a chip widens in or narrows away carrying its gap, the empty caption
  gives way to the first chip.
- The PR list's rows, stack captions and host notices, the merge stack
  notes and the off-diff card's rows are keyed: they fold in and out and a
  reorder glides.
- Row signals and the draft label fold across instead of appearing at
  full width.
- Row menu reveal and the add-reaction chip follow a ListHover; the
  hover card's and off-diff rows' fills glide.
- Avatars fade in over their initial once they decode (image_arrival).
- Jumps to a file, a thread or the composer glide (ListScroll).
- The picker, merge and stack dialogs glide their height per read phase;
  the phone's primary action row folds; the file column widens in.
…s take the new mode's tints once a theme change settles
…k Log folds its rows in and out

gpui's list has no insert, remove or row-height motion. RowGlide and the
timeline's arrival stamps move into motion::list_rows as ListRows: an owner
stamps rows that arrive, gives rows that leave their last value and the
survivor they fold against, and draws both through RowGlide. The timeline
keeps only its stamp policy. Expanding a Work Log folds its activities in
under the header; collapsing it folds them out, inert, where they stood
(between a live run's header and its live window), and the answer below
moves with the fold.

Co-Authored-By: Claude <noreply@anthropic.com>
A live run's newest activities are keyed windows: as the run moves on,
the oldest folds out, rebuilt from the run while it goes, and the new one
folds in, so what follows it moves up instead of jumping.

a_subagent_row_opens_its_thread_beside_the_lead pressed a Work Log row on
the frame the Work Log opened, where no reader can see it; it now presses
once the row has folded in (driver fixed: the app draws the fold first).

Co-Authored-By: Claude <noreply@anthropic.com>
…teer and an image read do

gpui-base has no shared-element transition, and an element drawn under
another parent appears there at once, clipped by it. motion::shared keeps
where each shared element was last drawn and in which site; drawn in
another site the next frame, it is laid out in its new place and drawn on
top, clipped to the view's scope, travelling from where it was.

A steer the agent takes leaves the trailer: its pending place folds away,
the message travels to its place in the turn while its chip cross-fades
from Steering and its dashed border fades, and the turn's status travels
below it. A tool call that completes with the pictures it read travels
from its run to below the Images tiles. A destination not drawn (out of
the list's rendered window) has nowhere to travel to: the pending place
folds away showing the message as it was.

The trailer's status and steers share a column: a steer list ended with a
negative margin left the status's gap below it.

Co-Authored-By: Claude <noreply@anthropic.com>
…de ListFlip

list_rows::changes diffs the row keys before and after a replacement: the
rows that arrived, inserted or appended, and the rows that left with the
survivor each folds against. With ListFlip moving the rows that stayed, a
list re-sorted, filtered and grown in one change (the sidebar) glides,
folds in and folds out together. The timeline's collapsed Work Log finds
its leaving rows the same way.

Co-Authored-By: Claude <noreply@anthropic.com>
A pane holds a lease on its thread's timeline in the store; closing a
leased thread keeps its timeline (and its replica among the kept ones)
until the lease is released. A pane whose thread closes beside others
keeps its chat bound to it while the chat fades under the waiting hint,
then rebinds and releases; a pane taken out, faded or not, releases with
it. Nothing about the fade's length reaches the store.

Co-Authored-By: Claude <noreply@anthropic.com>
A Mermaid picture's colours cannot be mixed as a token's are, so the
picture drawn for the new mode, requested on the glide's first frame,
cross-fades over the old one instead of replacing it in one frame.
Reduced motion still swaps at once.

Co-Authored-By: Claude <noreply@anthropic.com>
The timeline's rows sit in a gpui-fast list whose scroll layer keeps rows
it painted without drawing them again. A row whose hover changes is
rendered again, and its reveal, sampled while it moves, asks for the frames
that follow from inside the list, which keeps the layer drawing the row:
checked with frames drawn as the display draws them, without a refresh.

Co-Authored-By: Claude <noreply@anthropic.com>
Clippy's type-complexity lint refused the tuple list_rows::changes
returned; it returns Changes { arrived, left }.

Co-Authored-By: Claude <noreply@anthropic.com>
…rame

A leaving row is drawn somewhere new (against its survivor), so its
RowGlide has no measurement on the frame it starts leaving; laid out at
nothing, the app showed the rows gone for a frame before they folded
(seen in the app: gpui-fast lays a list row out once per frame, where the
test platform's measuring pass hid it). Glide::Leave lays an unmeasured
row out as tall as it is.

Co-Authored-By: Claude <noreply@anthropic.com>
The badge's own width glide ran over the numbers' keyed fold, so a newly
linked number widened the badge on two easings at once. The number that
arrives is the honest owner of the widening; the header's glide goes.

Co-Authored-By: Claude <noreply@anthropic.com>
…l floats, docks and slides gradually, and the layout glides across the breakpoint

Attaching, switching and detaching keep the workspace left on screen,
inert and with its link closed, beneath the one rising over it
(motion::Scene), and let it go once covered. The right panel floats and
docks with its width gliding while its docked place opens or closes
beneath it, slides in from the window's edge as it opens floating and
out as it closes, and is held at the width it is being given until the
group has it. On Machines the sidebar's column narrows away and back;
the collapsed sidebar's hover overlay slides in and out. Turning compact
or wide glides the sidebar and conversation from where one layout drew
them (motion::reflow).

Co-Authored-By: Claude <noreply@anthropic.com>
… gone

Co-Authored-By: Claude <noreply@anthropic.com>
…w jump glides the seams

Swapping or moving a thread between two panes carries each shown
conversation from where it was drawn into its new pane over SETTLE,
drawn over the column on the opaque canvas at its pane's size; the
keyboard rules are unchanged. A window that jumps to another size
(maximised, fullscreen, turned, or a frame-to-frame change of 96px or
more) glides the column's seams from where they were drawn to where
the new size puts them; a dragged edge is followed at once.

Co-Authored-By: Claude <noreply@anthropic.com>
…ws glide their hover fill, a highlighted list row glides into view, and the composer's hairline glides with the keyboard

- TabBar items come and go through keyed_in (underline across, segmented by
  flex share, rail down); the selection mark is painted from this frame's
  item places, blended between the tabs it leaves and the one it goes to.
  TabItem::trailing is a builder so a closing tab is drawn with its control.
- material::menu_item_frame takes an ItemFill and selectable_surface draws a
  hover_layer: hover_fill sampled when the row renders, under its content.
- VirtualList::reveal on measured rows glides through ListScroll.
- The shell keyboard-pane test reads hairlines once the glide has rested;
  the trigger-menu arrow test lets each glide finish before looking.
…ement holds a port no :0 bind is given

a_restarted_machine_is_rejoined_and_buffered_writes_are_delivered failed
once at transport.rs:830 (the restarted TraverseHost::start unwrap) in a
full workspace run and passed alone, so it is deleted (Principle 10). It
chose its fixed port by binding 127.0.0.1:0 and dropping the socket, so the
port sat in the ephemeral range: once the first machine shut down, any
concurrent :0 bind in the suite could be handed that port before the
restart rebound it. Nothing else proves restart -> rejoin, the replayed
subscription, a write buffered while the machine was down being delivered,
or the identity file surviving the restart.

a_restarted_machine_rejoins_and_delivers_writes_buffered_while_it_was_down
binds below every platform's ephemeral range, as the app's DEFAULT_PORT
does, accepts any HostClosed reconnect (retry history is the client
recovery unit tests' contract) and awaits the rejoin as the replayed
subscription's snapshot plus the buffered command's ack.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant