Right side panel
mnml’s chrome wraps the editor on three sides: the file rail and activity bar on the left (toggled by Ctrl+B), the statusline + bufferline at top and bottom, and — since 2026-06-28 — a right side panel that mirrors the rail on the opposite edge. The right panel is the home for panes that read better as a vertical sidebar than as a horizontal split: the Outline (outline.show) and the project-wide Diagnostics list (lsp.diagnostics). When the panel is visible, those two commands route their pane into the panel instead of carving a split out of the editor body.

The panel is opt-in. It opens with Ctrl+Shift+B (the natural mirror of Ctrl+B), it can be sized by dragging its left edge, it persists across restarts via session.json, and it has a one-cell × close button on its header so you can evict the hosted pane without closing the column itself.
Opening and closing
Section titled “Opening and closing”| Gesture | What it does |
|---|---|
Ctrl+Shift+B | Toggle the panel. Fires view.toggle_right_panel. |
:set rightpanel | Open the panel (idempotent — already-open stays open). |
:set rightpanel! | Toggle (vim-style invrightpanel is the long form; :set rp! is the short form). |
:set norightpanel | Close the panel. |
| Click the palette-bar’s right-panel chip | Toggle. Same command. |
Ctrl+Shift+B is VS Code’s “Run Build Task” chord. mnml has no build-task concept yet, so the chord is repurposed for the panel toggle; if a task runner lands later, the binding may need revisiting (Ctrl+Alt+B is the next pick). The palette command (view.toggle_right_panel) calls this out in its title so a porter sees the conflict.
The panel’s visibility, width, and currently-hosted pane id all round-trip through <workspace>/.mnml/session.json, so restart mnml and the panel comes back exactly as you left it.
Hosted panes (v3 + v4 tab strip)
Section titled “Hosted panes (v3 + v4 tab strip)”![open lib.rs, open the panel, outline.show + lsp.diagnostics both host as TABS, click between them or use <leader>t[ / <leader>t] to cycle, × closes the active tab, panel stays open](/_astro/right-panel-v2-hosting.DA9WLCTH_1c8jXH.webp)
Three commands route their pane into the panel as a tab when it’s open:
outline.show— the LSP Outline pane (symbols sidebar for the active file). Default chord<leader>lo(vim) or palette. Tab label shows<filename> ⌥<symbol-count>.lsp.diagnostics— the workspace-wide Problems list. Default chord<leader>le(vim) or palette. Tab label showsproblems ✗<errors> ⚠<warnings>.ai.chat/ai.ask/ai.explain/ … — the AI chat pane (v4, 2026-06-28). Tab label shows<title> ●/✦/✗/…(state-marker). When the panel column is narrower than 40 cells, a one-line hint at the top of the chat body suggests dragging the edge wider.
When the panel is closed, these commands fall back to their pre-panel behavior: Outline splits horizontally above the active editor; Diagnostics opens a vertical split below the focused leaf; AI opens as a horizontal split next to the editor. When the panel is open, the pane is pushed into app.right_panel_panes as a new tab — the editor body keeps its full width, and the panel renders the active tab inside its column.
The tab strip caps at 3 simultaneous tabs. Pushing a 4th displaces the oldest (FIFO) with a toast so you know what happened. Tabs you don’t see scroll off the right edge — the label truncates with … so long titles still fit.
Short-form chip labels at narrow widths
Section titled “Short-form chip labels at narrow widths”Each chip gets a per-chip budget (roughly column_width / n_tabs - gap). When the full title fits, the chip paints it verbatim. When it would truncate past the count or status glyph — the part that actually changes over time — the chip falls back to a short form that keeps the live information instead:
| Pane | Full label | Short form when tight |
|---|---|---|
| Outline | main.rs ⌥42 | main.rs (file stem only — truncated to budget if even that doesn’t fit) |
| Diagnostics | problems ✗2 ⚠1 | ✗2⚠1 (or ✗2 / ⚠1 / ✓ depending on counts) |
| Tests | tests Done ✓15 ✗0 | ✓15 (passes), ✗1 (any failures), … (running) |
| Grep | grep:query (24) | que… 24 at budget ≥ 5, (24) at the tightest |
| AI | AI: explain — done | AI ✦ at budget ≥ 4, ✦ / ● / … / ✗ alone otherwise |
Status glyphs across all five: … running / asking, ● live, ✦ done, ✗ failed, ✓ passed. The point is that when you can’t see the noun, you can still see the state — three Diagnostics tabs read ✗2 / ✗1 / ✓ at a glance even when each chip is only 4 cells wide.
Switching, closing, opening
Section titled “Switching, closing, opening”| Gesture | What it does |
|---|---|
<leader>t] | Switch to the next tab in the strip |
<leader>t[ | Previous tab |
<leader>tx / Ctrl+Alt+W | Close the active tab (view.right_panel_close_tab) |
| Click a tab chip | Switch to that tab |
Click the × button | Close the active tab |
Right-click the × button | Same context menu as right-clicking the active tab |
| Right-click an inactive tab | Menu: Switch to this tab / Close tab / Close other tabs / Close all tabs / Hide side panel |
| Right-click the active tab | Menu: Close tab / Close other tabs / Close all tabs / Hide side panel |
Click :outline.show / :lsp.diagnostics / :ai.chat / :find.grep / :test.run in the empty state | Fires that command directly — mouse path to populate the panel |
Ctrl+Alt+W deliberately steers clear of vim NORMAL’s Ctrl+W window prefix (which would be eaten by the vim handler before reaching the global keymap) and isn’t a VS Code chord for anything else, so it’s safe in both input modes. The Close other tabs and Close all tabs items show up only when there are at least two tabs — the menu prunes them when there’s nothing to close.

Empty state
Section titled “Empty state”With the panel open but no pane hosted, the body paints a faint hint listing every command that routes into the panel when visible:
right panel
Nothing here yet.
:outline.show :lsp.diagnostics :ai.chat :find.grep :test.run
Hide: Ctrl+Shift+B or :set norpThe header reads right panel in lowercase — same vocabulary the palette title, the tooltips, whichkey, the context menu, and the toast all use, so a bold modifier alone is enough hierarchy. The body hint is comment-colored on the panel’s slightly-darker background. All five ex commands are real and clickable — click the row and that command fires immediately. Or copy-and-paste them into the cmdline.
Header and close button
Section titled “Header and close button”When a pane is hosted, the panel’s header shows the tab strip. The far-right cell is a clickable × (or x under [ui] ascii_icons = true) — left-click evicts the active hosted tab but keeps the panel open. Right-click on the × opens the same context menu the active tab would (Close tab / Close other tabs / Close all tabs / Hide side panel).
The × paints in two visually distinct states so you can tell at a glance which tab it acts on:
- Active tab is the rightmost chip — the cells between the chip’s right edge and the
×fill with the chip’s active background (bg2) and the×itself paints infgonbg2. The bridge makes the×read as the chip’s close button. - Active tab is NOT rightmost (e.g. you cycled to a tab that’s followed by another) — no bridge paints, and the
×switches tocommentonbg_dark(the inactive-chip styling). It’s still the same action — close the active tab — but the visual signals “mode-dependent” instead of “local to the chip next to me”, so you don’t read it as a close-this-inactive-chip button.
This is the eviction split mnml makes for the panel:
| Gesture | Panel | Hosted pane |
|---|---|---|
Click the header’s × | Stays open (empty state) | Evicted from the panel (still in app.panes) |
Ctrl+Shift+B (toggle off) | Closes | Evicted from the panel |
:set norightpanel | Closes | Evicted from the panel |
view.toggle_right_panel from the palette | Toggles | Evicted on close |
Either close path nulls right_panel_pane_id. Re-opening the panel returns to the empty-state hint — your hosted pane isn’t auto-restored, because the design assumes you’ll re-fire outline.show or lsp.diagnostics for whatever you want next.
Multiple tabs (v3)
Section titled “Multiple tabs (v3)”
A walkthrough of the v3 tab-strip: open the panel (empty state), fire outline.show (Outline becomes tab #1), fire lsp.diagnostics (Diagnostics joins as tab #2 — strip now shows both chips), cycle with <leader>t[ (Outline active) and <leader>t] (Diagnostics active), then click the × button twice — first close evicts Diagnostics and the Outline takes over as the active tab; second close empties the panel back to the hint state. The panel column stays open the whole time; only the hosted panes come and go.
AI chat host (v4)
Section titled “AI chat host (v4)”
ai.chat / ai.ask / ai.explain / ai.fix / ai.refactor / ai.write_tests all route into the side panel as a tab when it’s open (instead of splitting the editor body). The tab label carries the prompt title (AI: explain, AI: ask…, …) and a one-glyph state marker: ● asking, ✦ streaming, ✓ done, ✗ error. Body shows the prompt block at the top followed by the streaming answer; the standard AI-pane bindings (a apply, r re-ask, x cancel) work the same as in a split. When the panel column is narrower than 40 cells, the body paints a AI chat reads better at 40+ cells — drag edge wider hint above the prompt — a softer nudge than the Outline / Diagnostics “too narrow” hint at 16 cells, because the chat can still render at narrow widths, it just reads worse.
The recording uses [ai] backend = "cli" (mnml’s default), which shells claude -p — no ANTHROPIC_API_KEY needed.
Sizing
Section titled “Sizing”The panel takes a fixed cell width carved off the right of the workspace area before the rail’s left split happens, so the rail and the panel size independently. Default width comes from [ui] right_panel_width in TOML (auto-default if unset).
Drag-resize
Section titled “Drag-resize”The panel’s left edge has a 2-row visible grip (┃ / | in ASCII mode) centered vertically. The hit zone is 3 cells wide × 4 rows tall — bigger than the visible grip so trackpad users don’t miss. Click the grip and drag horizontally to resize; release commits the width to app.right_panel_width and the next session save.
Width is clamped at render time: the panel can’t shrink the editor’s middle column below 20 cells, and the panel itself can’t go below 8 cells.
”Too narrow” hint
Section titled “”Too narrow” hint”Below 16 cells the panel’s body would render an unusable squeeze of the Outline or Diagnostics — gutter plus a fragment of each line, no room for the symbol names. Instead of painting that, the panel shows:
OUTLINE
too narrow — drag edge widerThe header still renders so you can see what’s hosted. The body’s hint stays until you drag the grip back past 16 cells or close the panel.
Layout interaction
Section titled “Layout interaction”The panel column is independent of the split tree. Splits inside the editor body don’t propagate into the panel — a vertical split is still a vertical split, just within a slightly-narrower middle column. The hosted Outline / Diagnostics pane lives in app.panes like any other pane, but its layout cell is the panel column instead of a leaf in app.layouts.
Practically, this means:
- The bufferline (when visible) spans the editor’s middle column only, not the panel.
- Per-leaf tab strips (when splits exist) stay within their splits — the panel column has its own one-line header instead.
- Focus moves into the panel the same way it moves into other panes — clicking inside it sets
app.activeto the hosted pane id;Escfrom inside the pane returns focus to the previous editor leaf. - The drag-resize grip is checked before the left rail’s grip in the layout-rect dispatcher, so dragging never accidentally moves the wrong column.
Configuration
Section titled “Configuration”Two TOML keys cover the panel’s defaults:
[ui]right_panel_visible = false # open the panel on startupright_panel_width = 32 # initial width in cellsBoth also round-trip through <workspace>/.mnml/session.json — the workspace’s last-saved width and visibility override the config defaults on re-open. Delete the session file (rm .mnml/session.json) to fall back to the TOML defaults.
The hosted-tab list is persisted across restarts. session.json stores each tab by KIND ("outline" / "diagnostics"), so re-opening the workspace re-fires those commands in order and the panel comes back exactly as you left it. AI chat tabs are deliberately skipped (live state + auth context can shift between sessions); re-fire :ai.ask etc. to restore one.
What’s still ahead
Section titled “What’s still ahead”- Pluggable hosts. Only Outline, Diagnostics, and AI route into the panel today. Other panes (test output, grep, browser) are candidates for v5.
- Overflow chevrons. The 3-tab cap displaces FIFO with a toast; lifting the cap needs left/right scroll chevrons on the tab strip (same shape as the bufferline).
Source
Section titled “Source”src/app/mod.rs—right_panel_visible,right_panel_width,right_panel_panes,right_panel_active_idx,right_panel_push,close_right_panel_hosted_panes,RIGHT_PANEL_MAX_TABS.src/ui/mod.rs— the layout split that carves the column (right_panel_area), the tab-strip painter, the header /×/ too-narrow / empty-state painters, the drag-grip indicator.src/app/ex_commands.rs—:set rightpanel/:set rightpanel!/:set norightpanel.src/command.rs—view.toggle_right_panel(Ctrl+Shift+B),view.right_panel_{next,prev,close}_tab.src/app/lsp.rs—open_outline_paneandopen_diagnostics_pane(route into the panel when visible).src/app/ai.rs—ask_ai(routes AI chat into the panel when visible).src/app/mod.rs::open_diagnostics_pane— same routing forlsp.diagnostics.src/app/session.rs— visibility + width round-trip.
- LSP — the symbols, outline, and diagnostics surfaces that the panel hosts
- Activity bar — the chrome on the opposite edge (rail + activity strip)
- Dock widgets — the corner-pinned mini-panels above the editor body
- Settings & configuration —
[ui]keys includingright_panel_visible/right_panel_width - Chord chains — how
Ctrl+Shift+Band the rest of the keymap resolve