Added
- Default session model — Settings → Agent can pick the model and thinking level every new session starts on, whichever workflow mode it runs in. The choice lives in Orbit's own
~/.orbit-pi/session-defaults.json(pi's global settings file is untouched), rides the spawn flags for a session's first turn andset_model/set_thinking_levelRPC for every later session in a live process, and leaves pi's own default alone when unset. The thinking options are the levels pi itself derives for the selected model, so the control tracks the model rather than the live session. Resumed sessions keep the model in their file; the composer chip still changes model / thinking per session. - Zed design tokens —
theme/tokens.rsports Zed's sizing system: dynamic spacing with Compact / Default / Comfortable density, text / headline / icon / button sizes, list, popover, context-menu, modal, tooltip, input and scrollbar metrics, the corner-radius scale, elevation shadows withelevation_1/2/3, and motion durations. Tokens equal Zed's default pixels at Orbit's defaults and follow the UI font size and Spacing Density settings.
Changed
- Context menus (sidebar session / workspace, Explorer, Git sync / branch / file) now follow Zed's context-menu metrics: 200px minimum width, 23px entries, 14px text and icons, Base04 / Base06 insets,
ListSeparatorspacing, an 8px radius with Zed's lighter popover shadow, and an 8px window-edge margin. Their sizes now scale with the UI font size. - Tooltips use Zed's tooltip metrics: offset off the cursor, 14px text, and wrapping at 288px instead of overflowing on one line.
- Extension dialogs use Zed's modal layout:
ModalHeader/ section / footer insets, a Small headline, a 32px input field, 32px confirm buttons, and the four-layer modal shadow. - The remaining floating modals — provider usage / API key / provider editor (Settings), the update dialog, and the custom-UI card — use Zed's
ModalSurface(elevation_3): the four-layer shadow, an 8px radius, andModalHeader/ section /ModalFooterinsets. The legacy heavierpopover_shadowhelper is gone. - Sidebar session and workspace rows use Zed's list-item tokens: a 4px (
rounded_sm) hover/selection radius,TextSizefor title / label / metadata text, andDynamicSpacingfor their padding and gaps. - Transcript chrome follows the token scales: message-row padding, user-bubble radius / padding / text, tool and question card shells and headers, tool error strips, and the usage footer metric and breakdown card now read
TextSize,DynamicSpacing,Radius, andBufferLineHeightinstead of ad-hoc px. - Settings chrome follows the token scales: the page header, section labels, grouped boards, and
setting_rowlayout useTextSize/DynamicSpacing/Radius, the row separators use the 1px hairline token, and keycap chips size toButtonSize::Defaultheight. Setting cards and toolbars migrate next. - Spacing is unified on
DynamicSpacing. The ad-hocTheme::spacehelper — which scaled with density but ignored the UI font size — is removed, and its ~185 call sites (mostly the Git panel, Usage page, and Settings) now resolve through the token scale, so they follow the UI font size too. - UI type is unified on
TextSize. ~370.text_size(theme.ui_px(…))sites snap onto the 10 / 12 / 14 / 16 scale (XSmall/Small/Default/Large); at most a 1px shift each. Sub-10px badge glyphs and 17px+ display headings, which the UI scale doesn't cover, keep their sizing. - Corner radii are unified on
Radius. gpui's fixedrounded_sm/md/lg/xlhelpers and every on-scalerounded(px(N))(2/4/6/8/12) now resolve through the theme, so card, chip, and input corners also follow the UI font size. - One-shot transitions adopt
AnimationDuration(Fast, 150ms). Looping affordances (spinner, shimmer, streaming) and feedback timers keep their own cadences, which the three-value token scale doesn't cover. - The settings page is fully on the token scales. Its side nav, headers, sections, rows, toolbars, select popups, cards, and modals now read
DynamicSpacing/TextSize/Radius/ButtonSize; the last gpui spacing utilities and raw px literals are gone, leaving only bespoke geometry (avatar / dot / toggle sizes, fixed panel and modal widths). - The Usage page (and its chart / heatmap / table / filters / tooltip modules) is on the same scales: all spacing, type, and on-scale radii resolve through the tokens; only chart and table geometry keeps its own px.
- Every input box follows the
InputFieldtokens. The branch picker's create-branch field (previously frameless) now usesinput_field_frame, and the Ask panel's custom-answer field usesDynamicSpacingoffsets; all other single-line inputs already routed throughinput_field_frame/picker_search_frame. The main composer, file editor, and inline tree rename stay bespoke components. - Every search box is the picker search row (
picker_search_frame), matching the command palette: the settings / Git / Usage searches and the skills filter were boxedInputFields and now share the palette's flat 36px row, so all searches read identically. The Git panel's issue / pull-request search grows to fill its filter bar and shrinks when tight (min 160px) instead of a fixed 200px, so the Pulls tab no longer overflows with its extra state chip. - The session-details title (rename) input follows the
InputFieldtokens: its block usesDynamicSpacing/input::gap/input_label/input_field_frame, and the Generate-title and Update buttons size toButtonSize::Large(32px) so the row lines up with the 32px field. - Picker ↑/↓ now scroll the focused row into view. The command palette, workspace picker, and branch picker set a positive
ScrollHandleoffset, but gpui stores it negative once scrolled down (the model selector already negated it) — so the list never actually scrolled and the highlight walked off-screen, looking like ↑/↓ did nothing. They negate it now, and the palette also scrolls against its actual (window-capped) list height instead of the uncappedlist_max_h. Two command-palette tests cover the ↑/↓ move and its scroll-into-view; the scroll test fails on the old sign. - The model picker had the same ↑/↓-doesn't-stick symptom for four reasons, all now fixed. (1) Its open-time "pin the highlight to the active model" re-ran on every render for the first 400 ms (the hover-suppression window), so a ↓ pressed right after opening was silently snapped back; the pin now runs only on a genuine open / catalog change. (2) Its deferred scroll re-targeted the active model for up to 250 ms after open; it now follows the current highlight. (3) pi re-reports the whole catalog on every
get_state, andset_catalogre-armed the pin and re-scrolled to the active model each time — so any periodic sync undid an ↑/↓ while the popup was open. An unchangedset_catalogis now a no-op. (4) The pin stayed armed until the popup's *first* render, so a ↑/↓ arriving before that frame (or right after a scope change / catalog refresh re-armed it) was undone by it; a deliberate ↑/↓ now cancels the pin. Covered end to end as well: a test opens the realOrbitAppwith the realbind_keys, asserts the popup's filter actually holds focus, and steps the highlight through several presses. - Picker rows no longer move the keyboard highlight on hover: the command palette, workspace picker, and extension-dialog option rows used
on_hoverto follow the pointer, andon_hoverre-fires whenever hit-testing changes — so scrolling a row under a stationary pointer hijacked the highlight. They useon_mouse_movenow, so only real pointer movement moves it. - Single-line inputs no longer wrap. Every
ComposerInputcapped atwith_max_lines(1)— the session-details name, the Git branch field, the provider / model / plugin / skill filters, the usage searches, the extension dialog, and the find bar — now also setswith_wrap(false), so a long value is clipped at the edge instead of wrapping into a second row and painting the editor's vertical scrollbar. Non-wrapping text also scrolls horizontally to follow the caret, so typing past the field's width still shows what you type (clipped to the text area, so the code editor's gutter stays put). - The Git panel's tab strip is on the §5 button language: each tab is a
button_frame(ButtonSize::Medium)— the same 28px height,Base08padding,Base04gap,button::RADIUS(4px), and Default label as the branch chip and the panel's filter chips — so the tabs and the chips beside them read as one system. The tab strip and branch row areBase40tall, and the whole panel's spacing resolves throughDynamicSpacing(one sharedtab_barfor Changes / History / Graph / Issues / Pulls). - The sidebar's New Task, Search, and Usage controls share one
button_frame(ButtonSize::Large): the hand-rolled 28px Search / Usage rows now match the New Task button's height,Base08padding,button::RADIUS(4px), and Default label, while keeping their ghost treatment (no fill or border, hover only) — one evenly sized stack. - The model selector's option rows keep the picker's two-line token height. A capped list is a flex column, so without
flex_noneevery row shrinks toward thepicker_entryminimum (31px) instead ofpicker::two_line_entry_height(50px), leaving the 28px leading provider / thinking chip with almost no vertical padding. The no-match empty row, its provider-header gap, and the row's hover highlight are on the same tokens /on_mouse_moveas the other pickers.
Fixed
- The model and thinking chip pickers keep keyboard focus when opened from their composer chip. The composer box's own mouse-up handler (which focuses the input) is an ancestor of the chip and ran after it in gpui's bubble order, so ↑/↓/Enter/Escape went to the composer instead of the popup. The composer box now leaves focus alone while any of its popovers is open, and a click inside the model popup no longer bubbles out to it.
- The top-bar provider-quota popover no longer clips its last provider card when several accounts are connected: the card list scrolls inside the popover's height cap instead of stretching past it. The list now carries its own max height rather than living in a
flex_1body — inside the deferred, anchored popover the available height is zero, where a flexible child collapses and the overflow is clipped.