Luxe

The interface

Sidebar, statusline, mouse, steering, and the model picker.

The Context Dock (sidebar) and the statusline are two forms of one workspace- context surface. In auto they coordinate so the same value shows in only one at a time:

{
  "dock": { "mode": "auto", "position": "left" },
  "statusline": { "visibility": "auto" }
}
  • dock.positionleft (default) or right. The dock is a full-height column spanning the whole viewport; the conversation, composer, and statusline share the remaining width beside it.
  • dock.modeauto (default; shown side-by-side on wide terminals ≥136 cols), shown, or hidden. On narrower terminals an explicit toggle overlays the dock or replaces the conversation.

The dock is a purely informational panel — it has no keyboard section navigation or collapse. Scroll it with the mouse wheel when its content is taller than the panel; its two actions are clickable (the MODEL row opens the model picker, Review changes opens Guided Review) and are also reachable via /model and /diff.

  • statusline.visibilityauto (default), shown, or hidden. In auto the statusline is shown unless the full-height sidebar is beside the conversation, so it disappears on wide terminals (the sidebar carries everything) and returns as a compact fallback on narrower ones. When the statusline is hidden the composer bottom-aligns for a clean, minimal look.

The full-height sidebar always carries its own MODEL section (model/context/tokens/cost/ thinking) at the bottom — its visibility is decoupled from the statusline. In auto you see exactly one copy: beside the sidebar the statusline auto-hides, so the telemetry lives in the sidebar; on narrower terminals the sidebar is gone and the compact statusline carries it. Forcing the statusline on with /statusline show while the sidebar is beside the chat shows both — the sidebar keeps its MODEL footer instead of emptying it. (Overlay/replacement docks still defer to the visible statusline to avoid a redundant second copy in those compact modes.)

Toggle either surface at runtime — no restart needed:

ActionKeyCommand palette / slash
Toggle sidebarAlt+D/dock · /dock show|hide|auto
Swap sidebar side/dock left|right · “Swap Context Dock side”
Toggle statuslineAlt+S/statusline · /statusline show|hide|auto

While a turn runs, the row just above the prompt carries a sweeping bar shaded in the theme accent — the conversation’s own footer, so “still working” sits where you left the output — followed by the turn’s elapsed time and the live interrupt chord (1:23 · Ctrl+Esc to interrupt). The bar replaces the older spinner-plus-”working” label: the motion already says it. Running tool cards keep their own spinner and name, and the row is empty when nothing is running.

Mouse

Luxe captures the mouse by default. The wheel scrolls whatever is under the pointer — the conversation, the Context Dock, or, in Guided Review, the diff versus the file rail — and clicks act on what you click: a dashboard menu row launches it, a Guided Review findings-inspector row selects that finding, with more click targets arriving over time. The keyboard remains a complete interface; the mouse only augments it.

A notch moves the transcript three lines ("conversation": { "scroll_lines": 5 } to change it, 1–20), and the scroll indicator is a control, not decoration: press anywhere on it to jump there, drag the thumb to scroll continuously — the pointer keeps steering even once it leaves the one-cell lane — and let go to stop. Dragging to the bottom re-arms follow mode, so new output keeps arriving in view. PageUp/PageDown still move a full viewport and Ctrl+End returns to the bottom.

Select and copy with the mouse, no modifier needed. Capture takes the terminal’s own click-drag selection away, so Luxe implements it: drag across the transcript or the composer to select, and releasing copies to the system clipboard (the copy-on-select behavior a terminal would have given you — the composer confirms it with a ✓ copied … title). A plain click on a transcript message highlights the whole message and copies that — the highlight and the clipboard never disagree — and clicking it again clears the highlight without copying. Dragging past the top or bottom of the transcript scrolls it. Esc clears the selection, and typing replaces a composer selection; Shift+arrows select from the keyboard and Ctrl+A selects the whole draft.

Because the release already copied, you rarely need a copy key — but Ctrl+Insert, Ctrl+Shift+C, and Alt+y all copy the selection explicitly. Your terminal’s copy shortcut only reaches Luxe if the terminal passes it through. Most terminals consume it to copy their selection, which a captured drag never creates. Two ways to line them up:

  • Make the terminal’s copy key fall through when it has nothing of its own to copy. In Ghostty that’s the performable: prefix — e.g. keybind = performable:control+insert=copy_to_clipboard — after which a desktop copy shortcut that lowers to Ctrl+Insert (Omarchy’s Super+C does exactly this, via sendshortcut CTRL, Insert) copies Luxe’s selection.
  • Or hold Shift (Option on macOS) while dragging: the drag goes to the terminal, which makes a real terminal selection that its own copy key can grab. /mouse off disables capture entirely and gives every mouse interaction back to the terminal.

Luxe writes to the desktop clipboard: wl-copy on Wayland (so a Wayland-native paste — a compositor keybinding, another app — sees it), otherwise the platform clipboard (X11/macOS/Windows), held by one long-lived owner for the life of the process so an X11 selection doesn’t evaporate the moment it is set. Only if none of those accept it does Luxe fall back to OSC 52, which is what makes copy work over ssh or inside a multiplexer (tmux needs set -g set-clipboard on).

Hovering highlights. Pointer motion moves the same highlight the arrow keys set on every list surface — the command palette, / and @ flyouts, the model, thinking, session, and project pickers, permission and question prompts, and the dashboard menu — so clicking is just “confirm what’s under the pointer”. Clickable chrome (the dock’s Review-changes and model rows, Guided Review’s footer shortcuts) lights up under the pointer too.

ActionCommand
Toggle mouse/mouse · “Toggle mouse support”
Enable / disable/mouse on · /mouse off

Set "mouse": false in config.json to start with capture off; /mouse on re-enables it for the session.

Steering a running turn

Hitting Enter while the agent is working doesn’t start a competing turn and doesn’t silently queue behind one — it steers. The message is delivered to the running turn at its next checkpoint (between tool iterations), so the model sees it without losing the work in flight. The turn clock and token counters keep running; only a promoted message starts a new turn.

A steer sent during the turn’s final response isn’t stranded either: rather than ending, the turn takes it in and answers it. And because a reply with no tool calls is what ends a turn, the call that follows a mid-task steer is told as much — address the interruption, then pick the interrupted work back up — so a steer gets an answer without quietly abandoning the task. Whether to stop is still the model’s call, and a steer that asks it to stop is honored.

Alt+Enter (or tui.editor.queue_message, Alt+q) queues the message as a separate follow-up turn instead, promoted one at a time, in order, as each turn ends.

Until a submission actually takes effect it is not in the transcript — dropped there it would land wherever the stream happened to be and then scroll away under the turn’s own output. It waits instead in a muted strip pinned just above the prompt box, marked steering or queued #1, and joins the conversation at the point it really lands: when the turn takes the steering in, or when the follow-up becomes a turn of its own. Cancelling drops what is still waiting and records it in the transcript as never sent, so nothing you typed disappears without saying so.

Model & thinking level

Switch the active model or its reasoning level mid-conversation — no restart, and the current session carries on:

ActionCommand palette / slash
Change model/model · “Change model”
Set thinking level/thinking · “Set thinking level”
Change theme/theme · “Change theme”
New session/new · dashboard n

/model opens a searchable picker over every model Luxe knows for the providers it can route to — your configured ones first, then the rest of the catalog. Selecting one switches the active model in place, routing to that provider’s adapter (so you can jump across providers, e.g. a local model to Claude, without relaunching), and the context window, output cap, reasoning levels, and pricing all move with it. The list is rebuilt each time the picker opens, so editing models.json mid-session takes effect immediately.

See Model metadata for where that metadata comes from.

/theme lists every theme Luxe ships and previews as you move: arrowing onto a name repaints the whole app in it immediately, because a palette is not something you can judge from its name. Enter keeps it and writes it to your config so it survives a relaunch; Esc puts back the one you started with and records nothing. Your per-token colors overrides ride along in the preview, so what you are judging is what you would actually get. Writing the config is surgical — the theme key changes and nothing else in the file is touched.

/thinking lists the reasoning levels (Off … Ultra) gated by the active model: a level it can’t honor is grayed out, labeled not supported, skipped by the cursor, and inert under a click, with the model named on the frame — so the list only offers levels that survive the request. (A level set from config or --thinking is still clamped to the nearest supported one, and the statusline shows the clamped value.) --thinking <level> sets the starting level at launch (handy for headless runs, e.g. --headless --provider anthropic --thinking high). /new starts a genuinely fresh session (the sidebar resets); use the dashboard’s find/recent/grep or -c/--resume to return to a prior one.

Reasoning is preserved correctly across tool calls. Both Anthropic (signed thinking blocks) and the OpenAI Responses API (encrypted reasoning items, replayed statelessly via store:false + include: reasoning.encrypted_content) require a model’s reasoning to be sent back — verbatim and in its original order — alongside the tool calls it produced, or the API rejects the follow-up request. Luxe captures each reasoning block (text + its provider attestation) and replays it unmodified, in the exact order the model emitted it, so extended-thinking-with-tools works on both providers without the “must start with a thinking block” / “function_call without its required reasoning item” 400s. Reasoning is never replayed as plain text on the chat-completions path.