luxe.
luxe.

A better way to build.

A coding agent for your terminal. Write, review, and ship code with a single, intuitive workflow.

curl -fsSL https://luxe.dev/install.sh | sh

Works on Linux, macOS, and Windows. Learn more.

A feature, written, reviewed and ready to ship in a real session with the waiting sped up.

Guided Review

An agent that explains the work.

Agents write code faster than anyone can read it. So luxe reads it back to you, one chapter at a time, in the order it makes sense. Each chapter says what changed, why, and how it was checked, right next to the code.

It can read a pull request the same way, regardless of who wrote it. luxe walks you through the code so you can review work from a teammate, Dependabot, or another agent in a flash. Comment on a line for the agent to address it, or post a review to GitHub without leaving the app.

A bot's pull request, explained a chapter at a time, with its GitHub discussion alongside. REVIEW Step 1 of 7 src/notification.rs pub struct Notification Parse notification categories into an enum The field the whole change exists for. Everything below is what this line forced. pull request #42 · notify · 6 files · +50 −12 by copilot-swe-agent (bot) · 1 failing · 11 checks passed · changes requested ┃ › @@ -12,4 +12,4 @@ pub struct Notification copilot/category-enum → main · p description ┃ 12 12 pub struct Notification { ┃ 13 - pub category: String, • Adds a Category enum and parses the inbound ┃ 13 + pub category: Category, string once, at the HTTP boundary. @mara-okafor · unresolved · 1 reply • Replaces the router's string comparison with Does anything still construct this from a raw string? an exhaustive match, so a new category fails @copilot-swe-agent: Only the webhook, and it parses first. to compile rather than routing to the digest. • Covers the typo case, which the old shape ┃ 14 14 pub title: String, could not express. ┃ 15 15 pub body: String, What it closes @@ -34,4 +34,8 @@ impl Notification • #318 A typo in a webhook creates a phantom 34 34 impl Notification { category 35 - pub fn label(&self) -> &str { 36 - self.category.as_str() Discussion on GitHub 37 - } 3 threads · 2 unresolved · 1 comment 35 + pub fn label(&self) -> &'static str { • @mara-okafor · src/dispatch/router.rs · 36 + match self.category { outdated 37 + Category::Build => "Build", This match used to fall through to the 38 + Category::Deploy => "Deploy", digest. 39 + Category::Incident => "Incident", • @jon-avery · src/api/webhook.rs · whole file 40 + } · resolved 41 + } Worth a 422 test for an empty category. • @jon-avery · in the conversation — end of file — Could the changelog mention the new 422? 01 / 04 CORE › Make an invalid category unrepresentable A category was a string, so a typo type-checked. This is the type that stops it. M src/notification.rs +8 −4 ┃ pub struct Notification impl Notification A src/notification/category.rs +20 −0 space next · c comment · a ask agent · alt+enter finish · ctrl+b side by side · w wrap · ? help
A bot's pull request, explained a chapter at a time, with its GitHub discussion alongside.
Post a review, complete with a verdict, summary, and comments, right from the app. REVIEW Step 1 of 7 src/notification.rs pub struct Notification Parse notification categories into an enum The field the whole change exists for. Everything below is what this line forced. pull request #42 · notify · 6 files · +50 −12 by copilot-swe-agent (bot) · 1 failing · 11 checks passed · changes requested ┃ › @@ -12,4 +12,4 @@ pub struct Notification copilot/category-enum → main · p description ┃ 12 12 pub struct Notification { ┃ 13 - pub category: String, • Covers the typo case, which the old shape ┃ 13 + pub category: Category, could not express. alt+enter to post Worth a doc comment here: this is parsed once, in the webhook, so nothing downstream should What it closes build a category from a raw string again. • #318 A typo in 󰊤 Post to GitHub category Post this review to acme/notify#42 Discussion on Git  Comment  Approve  Request changes 3 threads · 2 un • @mara-okafor · outdated Summary Summary This match use src/notification.rs digest. line 13 An unknown category now fails the whole webhook with a • @jon-avery · s 422, and older senders still post alert. · resolved Worth a 422 te Can we log and drop those for one release before • @jon-avery · i rejecting them outright? Could the chan 01 / 04 CORE › Make an invalid enter post · ↑/↓ comment · tab verdict · d draft summary · e edit summary · esc cancel A category wa type-checked. This is the type that stops 40 + } it. 41 + } M src/notification.rs +8 −4 — end of file — ┃ pub struct Notification impl Notification A src/notification/category.rs +20 −0 FEEDBACK 3 · 2 not posted ○ on the review An unknown category now fails … ○ notification.rs:13 Worth a doc comment here:… ? on the review Why FromStr here rather than s…
Post a review, complete with a verdict, summary, and comments, right from the app.

Not every change needs a full tour, though. The preview lets you view the diffs while the agent keeps working. When using luxe fullscreen, the diffs are placed beside the chat for maximum visibility, while smaller spaces see the diffs take its place.

Review the agent's work in one file while it plans work in the next. Ask a question, flag an issue, or steer the agent with ease, then close the diff and return to the chat to stay in the flow. The agent maintains its momentum while you stay informed and in control.

With room to spare, the diff opens beside the conversation. PROJECT M …s/luxe-tui/src/view/dock.rs +48 −9 FILE DIFF 8 of 14 󰉋 luxe 󰘬 feature/context-dock * @@ -140,6 +140,8 @@ fn change_lines 140 140 let seen = app.review.seen.as_ref(); PLAN 4/12 141 - let rows = changes.rows(seen, row_cap); 6 pending · 1 active · 4 done 141 + let all = changes.rows(seen, usize::MAX); ✓ Read the workspace snapshot 142 + let offset = app.dock.changes_scroll.min(all.len()); ✓ Name the sections 142 143 let shared = shared_row_prefix(&all); ✓ Model the snapshot 143 144 if !shared.is_empty() { ✓ Reduce it for the rail 144 145 lines.push(muted_line(format!("under {shared}"), app ◉ Render the Context Plan ↳)); ⊘ Capture responsive fixtures 145 146 } ○ Wire the dock hit regions ○ Document the sections @@ -212,5 +214,6 @@ fn change_file_line 212 214 let styles = &app.theme_styles; FILES 12 changed all ▾ 213 - let ink = if reviewed { styles.dim } else { styles.text state.json ? ↳}; ▾ 󰉋 assets 215 + let open = app.changes.as_ref().is_some_and(|view| view. 󰅴 context-preview.png A ↳path == path); ▾ 󰉋 crates 216 + let ink = if open { styles.selected_bg } else { styles.t ▾ 󰉋 luxe-core/src ↳ext };  workspace.rs +13 −0 M 214 217 Line::from(spans) ▾ 󰉋 luxe-tui/src 215 218 } ▾ 󰉋 view dock.rs +48 −9 M  dashboard.rs +6 −2 M fixture.rs +31 −0 A Why does the dock's change list show directories ctrl+p preview · ctrl+g review instead of files? ELSEWHERE 5 files outside this project The data was always there — DockChanges.files ▾ 󰉋 ~/work/luxe-sdk 󰘬 main carries a path, a status and both counts per file. ▾ 󰉋 src Only the renderer withheld them, reducing every client.ts +18 −3 M file to its directory before drawing. events.ts +64 −0 A ▾ 󰉋 ~/work/luxe-docs 󰘬 guide I put the names back under their directory README.md +2 −2 M headings, middle-eliding the long ones so the basename always survives. MODEL Now the dock's tests, to check nothing else leaned provider 󰅟 lmstudio on the old grouping. model 󰚩 qwen3.8-27b thinking 󰌵 med mode 󱌣 build ⠋ test cargo nextest run -p luxe-tui dock 6s SESSION context  12% of 200k 󱌣 build 38s · esc to interrupt tokens ↑21k ↓5.2k ↺21k cost $0.45 alt+n next file · ctrl+g review this file · esc close review $0.03
With room to spare, the diff opens beside the conversation.
When short on room, it takes the conversation's place until you close it. M crates/luxe-tui/src/view/dock.rs +48 −9 FILE DIFF 8 of 14 @@ -140,6 +140,8 @@ fn change_lines 140 let seen = app.review.seen.as_ref(); 140 let seen = app.review.seen.as_ref(); 141 - let rows = changes.rows(seen, row_cap); 141 + let all = changes.rows(seen, usize::MAX); 142 + let offset = app.dock.changes_scroll.min(all ↳.len()); 142 let shared = shared_row_prefix(&all); 143 let shared = shared_row_prefix(&all); 143 if !shared.is_empty() { 144 if !shared.is_empty() { 144 lines.push(muted_line(format!("under {sh 145 lines.push(muted_line(format!("under {sh ↳ared}"), app)); ↳ared}"), app)); 145 } 146 } @@ -212,5 +214,6 @@ fn change_file_line 212 let styles = &app.theme_styles; 214 let styles = &app.theme_styles; 213 - let ink = if reviewed { styles.dim } else { 215 + let open = app.changes.as_ref().is_some_and( ↳styles.text }; ↳|view| view.path == path); 216 + let ink = if open { styles.selected_bg } els ↳e { styles.text }; 214 Line::from(spans) 217 Line::from(spans) 215 } 218 } alt+n next file · ctrl+b unified diff · alt+w stop wrapping · ctrl+g review this file · esc close 󱌣 build 38s · esc to interrupt 󰉋 luxe 󰘬 feature/context-dock*  12 files · ctrl+p  12.4%/200k 󰌵 med 󰚩 qwen3.8-27b
When short on room, it takes the conversation's place until you close it.

References

Context with a keystroke.

Type @ and grab what you mean: a file, a skill, an issue or pull request, an MCP server, or even something from your own sources, like Jira or Asana.

Mentioning context inserts a pointer, not a paste. The agent gets the path or the issue number and reads it when it actually needs to, so a big file never floods the conversation.

An issue and a skill, picked with @, used to perform a quick fix.

Everything else lives in the Command Palette. It only lists what can run where you are, puts the commands you used last at the top, and tells you what each one does before you run it.

In the composer, you can start with a / and the same commands open right above it. Select one to run it, or fill it in and add arguments, like /diff pr 142.

Every shortcut is listed in Help, and you can rebind any of them as you see fit.

Type a few characters to find what you need and how to use it. PROJECT 󰉋 todo 󰘬 main * FILES 3 changed +25 −2 all ▾ ▸ 󰉋 .luxe ▾ 󰉋 src 󰍉 Command palette  cli.js Search: rev  filter.js store.js ❯ Review changes /diff ctrl+g Review changes ▾ 󰉋 test Second opinion /review /diff · ctrl+g  filter.test. Preview /preview ctrl+p  AGENTS.md Opens a Guided Review: a walkthrough of the package.json current revision, read one chapter at a time. ctrl+p preview · ctr Used alone, /diff creates a review for the working tree, or opens it if one is already pinned. You can optionally name a target to create a fresh review for it. Usage /diff the review open, or the working tree /diff turn the last turn /diff base <ref> /diff commit <oid> /diff pr <number or URL> /diff refresh rebuild the open review enter run · ↑/↓ choose · esc close 1/3 MODEL anthropic 󰚩 Claude Sonnet 5.5 thinking 󰌵 low mode 󱌣 build shift+tab for plan mode · / or ctrl+k for commands · @ to reference files, skills, and more 󱌣 build SESSION context  0% of 1.0M
Type a few characters to find what you need and how to use it.
A single keystroke can get you to the same commands even faster from the composer. PROJECT 󰉋 todo 󰘬 main * FILES 3 changed +25 −2 all ▾ ▸ 󰉋 .luxe ▾ 󰉋 src  cli.js +5 −1 M  filter.js +12 −0 M store.js ▾ 󰉋 test  filter.test.js +8 −1 M  AGENTS.md package.json ctrl+p preview · ctrl+g review 󰞷 Commands ❯ GitHub /github Pull requests and issues on GitHub, with each Preview /preview one's status and conversation. Pull request /pr Worktree /worktree MODEL anthropic 󰚩 Claude Sonnet 5.5 thinking 󰌵 low enter run · tab add arguments · ↑/↓ choose · esc close mode 󱌣 build 󱌣 build SESSION /git context  0% of 1.0M
A single keystroke can get you to the same commands even faster from the composer.

Git and GitHub

As good as it gits.

Browse and select an issue with /github, or pick one up with luxe --issue 123. luxe reads the issue, makes a worktree and a branch for it, and starts a session there. Your own checkout stays exactly how you left it.

Once the work is reviewed, a pull request is a keystroke away, drafted from the review you just read with Closes #123 already in it. From there, you can keep track of checks, conflicts, and what people are saying. The agent can help you make requested changes and draft replies to comments.

Need to handle a bigger change? Try opening it as a stack, one pull request per chapter, and let /stack restack them when the bottom one moves.

What needs to happen before the pull request can be merged, and shortcuts to get started. 󰊤 GitHub · acme/notify 󰛐 Review requested 3 󰀓 Mine 2  Assigned issues 4 󰓂 Pull requests 47  Issues 23 / is:open is:pr author:@me 2 shown #142 Route digests by category nora-quinn · main ← digest-routing · +96 −31 in 7 files · 25m ago 󰓂 #142 Route digests by category 25m nora-quinn ✗ 1 failing · changes requested open ● backend ● notifications 󰓂 #139 Batch digest sends per tenant 3d  Overview 󰆁 Conversation 7 nora-quinn · +140 −22 draft · ◔ running Status 3 incomplete ✗ Checks 1 of 12 failing: test (ubuntu-latest) ✗ Review changes requested by jon-avery · 2 threads open ◔ Reviewers awaiting lena-hart and acme/platform ✓ Conflicts none with main Reviewers 1 of 5 approved ✗ jon-avery changes requested · 3h ago ✓ mara-okafor approved · 1d ago – coderabbitai commented · bot · 1d ago ◔ lena-hart requested · 1d ago ◔ acme/platform requested · team · 1d ago Description Digests went to every subscriber whatever the category, so an incident page and a weekly product note landed in the same email. enter review · → details · tab section · / search · s state · c reference · o open · esc close 1/2
What needs to happen before the pull request can be merged, and shortcuts to get started.
Review the discussion and draft replies with the agent's help. 󰊤 GitHub · acme/notify 󰛐 Review requested 3 󰀓 Mine 2  Assigned issues 4 󰓂 Pull requests 47  Issues 23 / is:open is:pr author:@me 2 shown #142 Route digests by category nora-quinn · main ← digest-routing · +96 −31 in 7 files · 25m ago ❯ 󰓂 #142 Route digests by category 25m nora-quinn ✗ 1 failing · changes requested open ● backend ● notifications 󰓂 #139 Batch digest sends per tenant 3d  Overview 󰆁 Conversation 7 nora-quinn · +140 −22 draft · ◔ running rules is right, but it is decided in two places; and on-call-only for incidents needs a test, since that is the one a mistake here would page the wrong people over. ▾ @jon-avery · src/dispatch/router.rs:88 · unresolved @jon-avery · 3h ago This decides the fallback again — the scheduler already does it. If the two ever disagree, a tenant without rules gets incidents on the daily send. Can this read the schedule the scheduler picked instead? Your reply · unsent Good catch — they could drift, and the failure mode is the one you describe: a tenant without routing rules would get incidents on the daily send if the router and the scheduler ever disagreed about the fallback. enter collapse · r reply · d draft a reply · b hide bots · tab overview · ← list 1/2
Review the discussion and draft replies with the agent's help.
  • Review any pull request

    /diff pr 142 reads a pull request at the commit GitHub reported. Comment on lines, approve, or request changes without leaving the app.

  • Answer review threads

    Comments on your pull request show up on the lines they're about. Hand one to the agent, then reply and resolve it from the same screen.

  • Merge from the terminal

    /pr lists what's in the way: checks, reviews, conflicts. Work with the agent to deal with it, and merge with a final confirmation when you're ready.

Models and Providers

Plays well with others.

luxe knows more than 8,000 models from over 200 providers, through the models.dev catalog, and connects to two dozen of those providers by name. Use a frontier model, a local one through LM Studio or Ollama, or any OpenAI-compatible endpoint. Sign in with a ChatGPT or Copilot subscription, or bring an API key. /model lets you switch mid-session.

luxe gets along with the rest of your setup, too. Files open in $EDITOR on a keypress, Herdr's sidebar knows when it's working or waiting on you, and the MCP servers you already run can come along for the ride.

Every model you can reach, in one searchable list, with your configured providers first. 󰚩 Select model Search: sonnet 5 ❯ Claude Sonnet 5 anthropic/claude-sonnet-5 (1.0M ctx · reasoning) Claude Sonnet 5 foundry-anthropic/claude-sonnet-5 (1.0M ctx · reasoning) ● Claude Sonnet 5.5 anthropic/claude-sonnet-5-5 (1.0M ctx · reasoning · catalog) Claude Sonnet 5 github-copilot/claude-sonnet-5 (1.0M ctx · reasoning · catalog) Claude Sonnet 5.5 github-copilot/claude-sonnet-5.5 (1.0M ctx · reasoning · catalog) Claude Sonnet 4.5 foundry-anthropic/claude-sonnet-4-5 (200k ctx · reasoning) Claude Sonnet 4.5 anthropic/claude-sonnet-4-5-20250929 (200k ctx · reasoning · catalog) Claude Sonnet 4.5 (latest) anthropic/claude-sonnet-4-5 (200k ctx · reasoning · catalog) enter switch · ↑/↓ choose · esc close 1/8
Every model you can reach, in one searchable list, with your configured providers first.

The toolset

The right tools for every job.

Dedicated tools make actions more legible, especially at agent speeds. Limitations, formatting issues, and inflexible design can make tools less attractive to models, though, resulting in tasks being frequently shelled out in massive chains. So, the bar every tool in luxe has to clear is straightforward: It has to beat the shell command it replaces. Here's how each one does.

The model gets a real language server instead of grep, a debugger it can actually drive, structural search that understands syntax, git with output that fits, a to-do list that survives compaction, and a way to stop and ask you about the important stuff. For other agents, most of those are plugins that live somewhere else. They're available here out of the box, and /tools shows what's available in a given request.

/tools shows you what the model can reach and how to make adjustments.  Tools · what the agent can reach right now In every request (12) activate_tools bash edit git glob grep question read skill test todo write Available on demand (11) — loaded when the agent calls activate_tools ast_grep debug impact lsp_diagnostics lsp_navigation module_report read_symbol screenshot symbol_search web_fetch web_search Turn the agent's tools on or off in /settings, on the Tools page. Use luxe --with-tool <name> or luxe --no-tool <name> to turn one on or off for a single session.
/tools shows you what the model can reach and how to make adjustments.

Extending luxe

Make it yours.

Skills teach the agent how you work. Hooks can gate, rewrite or log anything the agent does. MCP servers hand the model new tools, mention sources link extra context, and the RPC protocol drives everything luxe does, Guided Review and GitHub included, so you can build a whole new front end on top.

An extension is a file, or a program that runs externally with the permissions you already have, so installing one is as simple as copying a folder.
Check out the docs to learn more.

~/.config/luxe/

  • config.jsonhow it looks, which tools exist
  • keybindings.jsonyour keybinds, your workflow
  • themes/
  • nord.jsondress it up however you like
  • skills/
  • review/a SKILL.md and any notes it needs
  • hooks.jsongate, rewrite or log tool calls
  • mcp.jsonservers the model can call
Everything is a file you can read, diff and share.

Look and Feel

Right at home, in every home.

luxe comes with plenty of themes to suit your style, including community favorites like Catppuccin, Nord, Tokyo Night, GitHub, One Dark, and more. Or, you can stick with its own signature theme, which holds every accent to a contrast floor so every color stays readable.

Want to customize further? A theme is a JSON file in ~/.config/luxe/themes that names only the colors it changes, so a new one is a few lines to write and share.

For users on Omarchy, luxe wears your desktop's theme and changes with it when you switch.

Get excited.

curl -fsSL https://luxe.dev/install.sh | sh

Then run luxe in a project. The first launch walks you through signing in.