Luxe

Configuration

The config file, credentials, and the UI settings.

Luxe looks for configuration in:

  • ~/.config/luxe/auth.json (0600 permissions) — API keys and provider settings
  • Environment variables — LUXE_API_KEY, LUXE_PROVIDER, etc.
  • CLI flags — --api-key, --provider, --model

Auth File Format

luxe auth login writes this for you; the format is documented because it’s yours to edit. A top-level key is a provider’s credential, and the four reserved keys (providers, models, default_provider, default_model) keep their configuration meaning:

{
  "openai":         { "type": "api_key", "key": "sk-..." },
  "anthropic":      { "type": "api_key", "key": "!op read op://vault/anthropic/credential" },
  "github-copilot": { "type": "oauth", "access": "tid=...", "refresh": "gho_...",
                      "expires": 1785964482483 },

  "providers": { "lmstudio": { "base_url": "http://127.0.0.1:1234/v1" } },
  "default_provider": "openai",
  "default_model": "gpt-5.6"
}

A key may be a literal, $ENV_VAR, ${ENV_VAR}, or !command (run at request time, not while listing credentials). The older nested form still works and is still read:

{
  "providers": {
    "openai_compat": { "api_key": "sk-...", "base_url": "https://api.openai.com/v1" },
    "anthropic": { "api_key": "sk-ant-..." }
  },
  "default_provider": "openai_compat",
  "default_model": "gpt-4o"
}

Luxe writes the file 0600 and never rewrites a key it doesn’t understand — unknown entries and hand-written blocks survive luxe auth login and /logout.

UI configuration

TUI preferences load from ~/.config/luxe/config.json, with project overrides in .luxe/config.json. Both halves of the statusline are configurable by rendered order — the left from the outer edge toward the center, the right from the center outward:

{
  "$schema": "./config.schema.json",
  "theme": "nord",
  "statusline": {
    "visibility": "auto",
    "left": ["branch", "cwd"],
    "right": ["cost", "tokens", "context", "thinking", "model"],
    "thinking_colors": true
  }
}

Those are the defaults. mode (NORMAL/INSERT/VISUAL), lsp, and task are opt-in — add them to left if you want them. Language-server state is otherwise a question you ask with /lsp, and the Context Dock carries the richer version of everything else.

/theme changes this from inside the app, with a live preview — see The interface.

theme selects a built-in theme — twelve ship by default: luxe and luxe-light (Luxe’s own, and where a fresh install lands), plus dark, light, nord, dracula, gruvbox, tokyo-night, kanagawa, rose-pine, catppuccin, and everforest — or point it at a custom ~/.config/luxe/themes/<name>.json. An unknown name falls back to luxe.

luxe is a warm graphite canvas with a champagne-brass accent and jewel-tone semantics; luxe-light is the same hues on warm parchment. Both hold a contrast floor of 4.5:1 for every accent and 7:1 for body text, asserted in the test suite rather than eyeballed. If you run Omarchy and its current desktop theme matches one Luxe ships, that one is used instead — matching your desktop beats showing off.

A colors map overrides individual theme tokens without forking a whole theme — handy for tweaking one or two colors on top of a built-in. Keys are token names, values are #rrggbb, an ANSI index 0-255, or an empty string for the terminal default; unknown or unparseable entries are ignored. For example, to recolor the diff line backgrounds (diffAddBg/diffDelBg, subtle green/red bars behind added/removed lines) and the deeper wash that marks the changed span inside a replaced line (diffAddEmphasisBg/diffDelEmphasisBg, derived from the row tint when unset, and deliberately light — the strike and the bold lead, the wash only locates):

{
  "colors": {
    "diffAddBg": "#12331f",
    "diffDelBg": "#3a1620",
    "diffAddEmphasisBg": "#1f5c33",
    "diffDelEmphasisBg": "#6d2233"
  }
}

The bar is flat and minimal: muted text with lightly tinted icons and plain spacing between modules — no separators, arrows, or filled blocks. The model name is the one brighter, bold right-edge anchor; context turns yellow past 70% and red past 90%.

Left modules: branch (Git branch with a yellow * when the worktree is dirty), cwd (compact path such as ~/…/personal/luxe), lsp, task, activity (spinner/tool/ elapsed), and mode (NORMAL/INSERT/VISUAL — opt-in; Luxe isn’t a modal editor, so it is off by default). Right modules: cost, tokens, context, thinking, and model. Reorder entries to move modules or omit entries to hide them. The default left set is branch, cwd, lsp, task.

branch and cwd are shown by default (the lualine convention) and stay pinned to the statusline even while the Context Dock also shows identity — they’re the always-present, glanceable copy, and the only one when the dock is hidden or in a narrow layout. Omit a module from an explicit left to hide it. The branch module prefers the live workspace snapshot, so its dirty indicator tracks edits as the agent works.

thinking_colors is off by default (the thinking module stays muted). When enabled, it tints the level with theme tokens: muted off, normal minimal, yellow low, green medium, blue high, purple xhigh, and magenta ultra. The bar is inset one column each side so it lines up with the prompt box rather than hugging the terminal edge. Run /reload after editing the file to apply these settings without restarting Luxe.

The startup dashboard shows the luxe. wordmark, drawn in the theme’s own accent so it re-colors with whatever theme you run. "banner_animation": false holds it still; "dashboard": false skips the surface entirely and goes straight to a session. The logotype comes in three sizes and takes the largest that fits, so a terminal too narrow or too short gets a smaller drawing rather than a clipped one. See the logotype below for the drawing itself.

glyphs chooses how much fidelity the interface asks for:

{ "glyphs": "auto" }
settingiconsneeds
nerdMaterial Design icons — the look Luxe ships witha Nerd Font
unicodeGeometric Shapes and Arrows (◆ ▸ ● ▪ ✓)nothing beyond UTF-8
asciiplain punctuation (/ + ~ = x)nothing at all

Where a substitute would say less than the words already do, the icon is left out rather than approximated: a tool card below nerd carries no glyph at all, because the tool’s name is right there in bold and the card’s color and wash already report how the call went.

auto (the default) resolves it for you, and only from evidence: kitty bundles its own symbol font, so it gets the icons outright; elsewhere fontconfig is asked whether any installed font actually provides them. Where that question has no answer — macOS, Windows, a container without fc-listauto chooses unicode, on the grounds that plain shapes for someone who has a patched font is a smaller disappointment than a screen of . Set the key explicitly to overrule it.

Every setting is fully usable: the icons are decoration, and the words next to them carry the meaning. ascii also swaps the drawn logotype for a letter-spaced L U X E, since a terminal that wants ASCII shouldn’t be sent block elements.

The logotype

The dashboard mark is drawn with three glyphs — the upper half block, the lower half block, and the full block. Every terminal with built-in box drawing synthesizes those against the cell box rather than scaling a font outline into them, so it lands identically whether you have a patched Nerd Font or the plainest monospace your distribution shipped. Nothing to install, nothing to configure.

There is a second drawing, and it is prettier:

{ "banner": "powerline" }

powerline is the chamfered logotype, its corners cut at 45° with the Powerline Extra triangles (U+E0B8U+E0BE). Where a terminal draws those at exactly half a cell it is the better mark. Where it does not, it is visibly wrong — measured against a calibrated probe, ghostty, kitty and WezTerm draw all four correctly, while alacritty 0.17 inks two of them across 62% of the cell, hanging a bright one-pixel whisker off the letter at every font size.

Luxe does not guess which you have, and does not offer a degraded version of a drawing made for something else. blocks is the default because it is right everywhere; powerline is there because if your terminal can draw it, you should be able to have it.

The mark also carries two lights, which banner_animation turns off together: a slow gradient drifting across the letters over about twenty seconds, and a quicker highlight crossing in 1.6 seconds and resting for five. Together they cost about 0.8% of a core while the dashboard is on screen — roughly a sixth of what the streaming animation costs during an ordinary turn.

The conversation canvas is configurable too:

{
  "conversation": {
    "alignment": "full",
    "growth": "bottom",
    "prose_max_width": 120,
    "wide_block_max_width": 140,
    "mode_border": false,
    "thinking": "shown",
    "scroll_lines": 3,
    "activity_bar": "bars",
    "diff_layout": "auto",
    "stream_reveal": "smooth"
  }
}

alignment controls how conversation content uses the terminal width: full (default) fills the pane, since a wide terminal was a deliberate choice; left anchors prose and code to the left gutter under readable width caps; center splits the slack evenly. Under left/center, prose wraps at prose_max_width (72–120, default 120) while code, tables, and tool output use wide_block_max_width (72–140); full ignores both.

growth controls how the transcript fills the viewport when a conversation is shorter than the visible area: bottom (default) anchors messages just above the input and grows upward, chat-style; top anchors them under the header and grows downward. Either way, a long conversation scrolls normally with the newest message at the bottom. User turns are shown as a subtle full-width background band (no icon); each message gets a blank line of breathing room above it.

A tool call is a card in the same language, read left to right: a nerd-font icon for that tool (± edit, a terminal for bash, spectacles for read, .* for grep — a spinner while it runs), the tool name in bold, then what it acted on — the command, the path, the pattern — in plain body text, and finally the duration in the muted/comment color, because how long it took is the least of it. The outcome is carried by color rather than by a tick: the name and a subtle wash behind the card go green when it lands and red when it fails (a cancelled or interrupted call keeps a /!, since it gets no wash).

Under that header, the cards whose payload is the point carry a body. A write shows the file it wrote — syntax-highlighted for that language, with a muted line-number gutter, because every line of a new file being green says nothing you can act on — and its header says what it did to the file (Created · 1.2 KB, Overwrote · 340 B). An edit shows its diff. A bash shows its command, but only when the command spans lines: a one-liner is already on the header in full, and bash cards come in bursts. Everything else stays a single line until you ask.

A body is capped at ten lines, and says exactly what that left out: ⋯ 20 more lines · 30 total · Ctrl+O to expand. The counts are live, and the chord is read from your keymap, so a rebind rewrites the hint. Ctrl+O opens every truncated card at once and closes them again; selecting one card (Alt+K/Alt+J, or a click) and pressing Enter/Space opens just that one. Opening a card that has no body of its own — a read, a grep — shows the raw call and a bounded window onto its output: bounded because the toggle is transcript-wide, so one keystroke can’t turn the conversation into a wall of file contents. When the shortfall is the payload’s rather than the budget’s, the footer says so instead of offering a key that wouldn’t help.

diff_layout chooses how an edit/write card draws its diff: auto (the default) renders it side by side when the card is wide enough to give each column a readable width — about 130 terminal columns, dock open or not — and falls back to unified below that; unified always stacks one column; split prefers side by side wherever it fits. Side by side pairs each removal with its replacement across a rule that holds one screen column on every row, wraps both sides rather than clipping them (a card can’t scroll sideways), and tints changed rows so an absent counterpart is visibly absent. A one-sided change — an all-additions block — stays unified whatever the preference: two columns for it is half a card of empty wash. Guided Review keeps its own review.diff_layout.

Either layout marks what changed inside a replaced line, so a one-token edit reads as one token rather than two rewritten lines you have to compare by eye. A word-level diff between a removal and its replacement finds every span that differs, not just the one stretch between their common ends: turning takes a string like '2' into takes a number like 2 marks two spans on each side and leaves the like between them alone. Each span wears a deeper wash of its own row’s color — struck through where content left, bold where it arrived. The wash is what makes the eye land on it; the mark says which way it went, and survives a theme or terminal with no background to give. Indentation is never marked, and two lines with nothing left in common come back unmarked, since their own color already says the line was replaced.

mode_border (default false) keeps the composer/prompt border in the theme’s base color at all times. Set it to true to bring back the vim-style cue that tints the border by editor mode (NORMAL/INSERT/VISUAL) — only visible when the statusline is hidden.

thinking controls how model reasoning is shown in the transcript, rendered as muted italic text (the thinkingText theme token): shown (default) prints it inline in full; collapsed shows a one-line peek you expand on demand — select the block (arrow keys or a click) and press Enter/Space, exactly like a tool card; hidden omits it entirely. Reasoning is always recorded on the session regardless of this setting, so switching from hidden to shown reveals it on the next reload, and it is never replayed back to the model. It streams as the model writes it — reasoning appears token by token, before the answer it precedes, on every provider that emits it (Anthropic thinking blocks, the OpenAI Responses API’s reasoning summaries, and the reasoning_content/reasoning deltas that DeepSeek-lineage, LM Studio, OpenRouter, and Together servers send).

stream_reveal (default smooth) paces streamed reasoning and prose onto the screen instead of painting each arrival the instant it lands, with the newest few characters fading up out of the background. It exists because a provider’s delivery is nothing like its production: measured on the wire, one API batches server-side and sends a single ~75-character delta every ~470 ms — two visible jumps a second, however fast the model really is — while a local model sends 4 characters every 16 ms like clockwork. The reveal estimates the rate text is actually arriving at and plays it back at that rate, so the same words land evenly. The buffer it holds is measured in arrivals, so the lag scales itself to how lumpy the provider is: about 390 ms of trailing text on the batched one, ~17 ms on a local model, and no change at all to when the first character appears.

Everything else lands immediately: tool cards, your own messages, system lines. A card can’t appear above prose that hasn’t finished arriving, so it waits for it — typically ~150 ms, bounded, and never cumulative, because a finished message drains to a fixed deadline. Anything that needs you shows the rest at once: a permission prompt, a question, an error, an interrupt, your next keystroke, or scrolling away from the bottom. Set off to paint every arrival the moment it lands.

scroll_lines (1–20, default 3) is how far one mouse-wheel notch moves the transcript.

activity_bar picks the look of the indicator that runs above the composer while a turn is in flight — a comet shaded in the theme accent that sweeps the rail, runs off the end, waits a beat, and comes back the way it came. Its cells dim and thin as they trail away, so the far tail converges on the rail in both color and shape and disappears outright — and the rail itself is lit by the comet rather than drawn at a fixed weight, so the bar’s ends melt into the canvas and only the stretch the comet is passing through reads as a rail at all. bars (default) draws its units with block elements (▌▍▎▏), which every terminal font provides itself at exact cell fractions, so the gap between units is part of the glyph and no font substitution can distort it; dots (●•·) is softer and lighter. progress (▰▱, slanted) and rect (▮▯, upright) are handsomer if your font actually contains them — many programming fonts don’t, and the substitute is often drawn at double width, which makes the units bleed into each other; the same caveat applies to braille. off keeps the elapsed time and the interrupt hint and drops the motion.

Guided Review has its own block:

{
  "review": {
    "word_wrap": false,
    "context_files": ["docs/DESIGN.md"]
  }
}

context_files names extra project-relative planning files a review reads for context, on top of PLAN.md and AGENTS.md when they exist. They ground the chapter narrative in what the change was for; each must resolve inside the opened project.

word_wrap (default false) controls how long unified-diff lines are shown: false horizontally scrolls them (/), true soft-wraps them under the gutter with a continuation glyph so nothing is hidden off-screen. Toggle it live with w while reviewing (the footer shows w wrap / w scroll).

diff_layout chooses how a diff is drawn: auto (the default) renders side by side whenever the diff pane can give each column a readable width and falls back to unified below that, unified always stacks one column, split prefers side by side wherever it fits. Ctrl+B flips it for the session and the footer names what the key switches to (or the constraint, Ctrl+B needs 100 cols, when the pane is too narrow). Side by side is a view of the same rows, so scrolling, hunk selection, comments, search and clicks behave identically in both — including w: a wrapped pair takes as many visual lines as its longer side needs and pads the shorter one, so the columns stay aligned. Without wrap, long lines clip and / scroll both columns together (Home resets).