Skip to content
Merged
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,23 @@

## Unreleased

- Fixed the menu cursor disappearing when arrowing onto a disabled row — most visibly Orchestrator → "Show/Hide Session", which is disabled unless a CLI-class orchestrator session exists. Arrow keys already stepped over separators, but not over disabled rows, and the renderer draws those dim and never highlighted: the selection was still there, just invisible, and Enter did nothing. Both kinds of unselectable row are now stepped past. Relatedly, `Up` returns to the menu bar from the topmost row that can actually be selected rather than from literal index 0, which otherwise wrapped to the bottom of the list when the first row was a separator or disabled.

- Fixed codex sessions having no scrollback at all. Both of the paths that could have supplied it produced nothing, for reasons that only look alike from a distance: the capture linkshell already had was gated on the session being on the alternate screen, which codex never enters, and the vt100 fallback it therefore fell through to was empty because codex scrolls inside a `DECSTBM` region — and lines evicted from a restricted region are discarded rather than pushed to scrollback, per the DEC spec and vt100's implementation of it. The transcript is now recovered from the region as it scrolls, for any session kind we capture for, on either screen.
- The recovered transcript also lives in its own buffer rather than sharing `output_lines` with pattern matching and pipe extraction. That buffer is the raw PTY line stream, which for a repainting TUI is mostly repaint fragments, and mixing the two put window-title escapes and pinned composer rows into the scrollback view. Sharing it had a second consequence: the scroll offset was clamped against the line stream, so it could run hundreds of lines past the end of the real transcript into a blank window.
- Fixed the scrollback going blank at the moment you reached the beginning of it, in every session type — the visible window was computed as `total - offset` and collapsed to nothing at the oldest line. Scrolling now stops at the oldest full page.

- Output panes lost their boxes. Each now has a title line (`● alpha · ~/src/linkshell`, filled and bright when focused, hollow and dim when not) and a one-column bar down the left edge that does double duty: `▎` in the accent colour marks the focused pane, `│` in chrome separates a pane from its neighbour, so a split needs neither per-pane borders nor a separate divider. Dropping the bottom border is a row of output back. Content width is unchanged, deliberately — it is what the PTY is sized to and what mouse selection maps against, and those three had been three separate `saturating_sub(2)`s in three files, where a silently 2-column-wider PTY renders fine and mis-maps every drag. They now share one definition. The docked chat and planning panes wear the same chrome; the floating chat overlay keeps its box.

- The status panel moved to a permanent left sidebar, and gained placements: `[general] status_panel` is `"left"` (default), `"bottom"`, `"overlay"` or `"off"`, with `alt-s` hiding and restoring a docked panel. The sidebar costs columns instead of rows — the better trade on a wide terminal — and having the full column height it does not run out of room at four sessions the way the bottom region does. Its rows are restacked vertically, because the horizontal row needs 70 columns and would leave 30 for output; a session with no model shows its working directory instead of an empty `-`. Below `status_panel_width + 60` columns it collapses to a three-column rail of state glyphs rather than starving the output pane: which agent needs you is the panel's real job, and a glyph plus an index still does it.
- Status rows are two lines now: vitals, then model, working directory, and pipe direction. Pipe direction is genuinely new — the old table gave it a fixed 20-column cell that truncated a second peer out of existence. The `│` column separators are gone in favour of fixed-width padding, which removes the whole class of drift that made the columns wander. The `"bottom"` placement past four sessions drops the detail line rather than showing fewer sessions; the `"overlay"` placement claims no layout space at all, so opening it resizes no PTY.

- Added a footer row reporting the focused session — name, model, context, state (with a spinner while it works), elapsed, cost. Together with the tab strip's state glyphs this is what makes the status panel closable: it answers "what am I looking at, is it working, what has it cost" for the pane in front of you, which is what the panel was being kept open to answer. The context counter turns amber past 80% of the window, and renders a bare count with no denominator when the window size was never probed — there is no percentage to colour against, and inventing a threshold is worse than omitting one. Fields are shed as the terminal narrows rather than wrapping, which would cost a row of output.

- The session bar is now a one-row tab strip above the output pane instead of three rows of bordered slot boxes below it, and it names sessions rather than kinds — three shells all reading "shell" was the case it most needed to disambiguate. State moved from the slot's border colour, which a single row has nowhere to put, to a suffix glyph: `!` for WAITING, `✕` for ERROR or dead, `⏸` for paused. That is what keeps "which agent wants me" legible with the status panel closed, which is the point of shrinking the chrome. As the terminal narrows the strip drops names before it drops tabs — inactive tabs first, then all of them — so every session keeps a clickable tab.

- Added a `[theme]` table: `base` selects `classic` (the palette linkshell has always shipped), `dark` (a quieter restyle palette with a single accent colour), or `ansi16` (named ANSI colours only), and any individual colour is overridable as a hex string. With `base` unset the theme is picked from `COLORTERM`: truecolor terminals get `classic`, everything else gets `ansi16`, because a `Color::Rgb` on a 256-colour terminal is quantized to whatever is nearest — which is how a carefully-chosen "dim" ends up indistinguishable from "text". `linkshell doctor` now reports which base resolved and why.

- Fixed the menu bar's Down arrow doing nothing past the first press: it was wired to "open the submenu", which selects item 0 unconditionally, so every press re-selected the same row and the items below the first were unreachable by keyboard. Up now walks back up and pops out to the section row from the first item. This is what made the planning backend picker (Agenda → Planning Model, the second item) look like it did not exist.
- Fixed the planning pane's context meter reporting `~0k` for any thread under 1000 tokens — `used / 1000` in integer arithmetic — which read as a meter that was not measuring anything rather than one reporting a small number. Counts below 1k now keep their digits.
- The context meter also now reports what a turn actually sent. Tool results are consumed inside a turn and never persisted (deliberately — it is what keeps a thread portable between a 32k local model and a 200k hosted one), so a turn that read half the repository left a transcript of a few hundred tokens and a meter that barely moved. Each reply carries the peak request size back, shown as `~868/131k (peak 5.4k)`. The first number is still what the *next* turn starts from; the peak belongs to the turn that produced it and is not restored when a thread is reopened.
Expand Down
39 changes: 37 additions & 2 deletions docs/config-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,43 @@ use these defaults.
## `[general]`

`max_ipc_message_bytes = 0`, `scroll_buffer_lines = 2000`,
`tick_interval_ms = 100`, `ipc_state_override_timeout_secs = 60`, and
`menu_key = "ctrl+space"`.
`tick_interval_ms = 100`, `ipc_state_override_timeout_secs = 60`,
`menu_key = "ctrl+space"`, `status_panel = "left"`, and
`status_panel_width = 28`.

`status_panel` is `"left"` (a permanent sidebar), `"bottom"` (the always-on
region below the output), `"overlay"` (alt-s only, claims no layout space) or
`"off"`. `status_panel_width` is the sidebar's width in columns, clamped to
16–60; below that width plus 60 columns of terminal the sidebar collapses to a
narrow rail. See [Status panel](panes-and-navigation.md#status-panel).

## `[theme]`

Every colour the UI draws with. `base` picks a palette; any individual field
overrides it with a `#rrggbb` hex string.

```toml
[theme]
base = "dark" # "classic" | "dark" | "ansi16"
accent = "#5fb3d4"
```

- `classic` — the palette linkshell has always shipped.
- `dark` — the restyle palette: desaturated chrome, one accent colour used for
focus and nothing else.
- `ansi16` — named ANSI colours only, so a 16- or 256-colour terminal renders
*your* colour scheme instead of a quantized approximation of a truecolor one.

`base` unset auto-detects: `classic` when `COLORTERM` contains `truecolor` or
`24bit`, `ansi16` otherwise. `TERM` is not consulted — it reads
`xterm-256color` on nearly everything, truecolor-capable or not. Run
`linkshell doctor` to see which base resolved and why.

Overridable fields: `bg`, `surface`, `chrome`, `text`, `text_dim`,
`text_bright`, `accent`, `warn`, `err`, `ok`, `info`, `ctx`, `cost`, `pipe`,
`on_accent`, `sel_bg`, and the per-agent `kind_claude`, `kind_codex`,
`kind_opencode`, `kind_ohmypi`, `kind_aider`, `kind_shell`, `kind_custom`,
`kind_orch`. An unparseable value is reported on stderr and ignored.

## `[socket]`

Expand Down
137 changes: 124 additions & 13 deletions docs/panes-and-navigation.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Panes, Navigation & Keybindings

- [Split panes](#split-panes)
- [Tab strip](#tab-strip)
- [Footer](#footer)
- [Scrollback](#scrollback)
- [Status panel](#status-panel)
- [Keybindings](#keybindings)
Expand All @@ -9,8 +11,22 @@
## Split panes

Split any pane side by side (`alt-\`) or top/bottom (`alt--`), repeatedly and in
any direction, for arbitrary tiled layouts. The session bar is centered and
reflows based on how many sessions are open.
any direction, for arbitrary tiled layouts.

Panes have no boxes. Each carries a title line and a one-column bar down its
left edge, and that bar does double duty — `▎` in the accent colour marks the
focused pane, `│` in chrome separates a pane from its neighbour — so a split
needs neither per-pane borders nor a separate divider:

```
▎ ● alpha · ~/src/linkshell │ ○ beta · /tmp
▎ $ cargo test │ $
```

A filled dot and a bright name mean focused; hollow and dim mean not. A pane
scrolled off the live tail says so in its title (`↑12`). The docked chat and
planning panes wear the same chrome; the floating chat overlay keeps its box,
because a floating thing needs an edge to read as floating.

| Key | Action |
|-----|--------|
Expand All @@ -20,29 +36,123 @@ reflows based on how many sessions are open.
| `alt-r` | Rotate the focused pane's split direction |
| `alt-o` | Focus next pane |

## Tab strip

One row above the output pane names every visible session:

```
1 alpha 2 beta! 3 gamma✕
```

The active tab is highlighted; a tab underlined is showing in some other split
pane. The suffix glyph is the session's state — `!` for WAITING, `✕` for ERROR
or a dead session, `⏸` for paused, nothing otherwise — so "which agent wants
me" is legible without the status panel open. Click a tab to focus it.

As the terminal narrows the strip drops names before it drops tabs: first the
inactive tabs fall back to bare indices, then all of them do. Every session
keeps a tab.

## Footer

The bottom row reports the focused session:

```
claude · opus-4-8 18.0k/180k ⣾ THINKING 0:42 $0.31 alt-h help
```

The name is coloured by state — accent while working, amber on WAITING, red
on ERROR — and a dead session offers `alt-r restart`. The context counter
turns amber past 80% of the window; when the window size is unknown (a local
model that was never probed) it renders a bare count with no denominator,
because there is no percentage to colour against. A paused or dead session
greys the whole row: its numbers are frozen.

Fields are dropped as the terminal narrows rather than wrapping, which would
cost a row of output. The hint goes first, then cost, then elapsed; the
session's name and state are the last to go.

## Scrollback

`alt-shift-PageUp/PageDown` (and `alt-shift-↑/↓`) scroll every session type the
same way. Shells use the terminal's native scrollback; full-screen TUIs (claude,
codex, opencode) scroll through linkshell's captured line history, shown dimmed.
The view holds position while new output streams in — typing returns you to the
live tail.
same way. The view holds position while new output streams in — typing returns
you to the live tail.

Shells use the terminal's native scrollback. Agent TUIs (claude, codex) scroll
through a transcript linkshell recovers from the screen as it scrolls, shown
dimmed, because vt100 has no scrollback to offer them — for two different
reasons. Claude runs on the alternate screen, which has none by design. Codex
stays on the normal screen but scrolls inside a `DECSTBM` region, and lines
evicted from a restricted region are discarded rather than retained, per the
DEC spec. Scrolling stops at the oldest full page.

Mouse text selection works everywhere: drag to select, auto-copies to clipboard.

## Status panel

Each session gets one row:
A permanent sidebar down the left, on by default. `alt-s` tucks it away and
brings it back.

```
Status │▎ ● alpha · ~/src/linkshell
● alpha 1m32s │▎ $ cargo test
READY 18.0k/180k │▎
opus-4-8 $0.31 │▎
│▎
! beta 12s │▎
WAITING 2.1k │▎
sonnet-5 $0.04 │▎
2 sess $0.35 │
```

Each session is a stacked block: name and elapsed, state and context, then
model and cost. A session with no model (a shell) shows its working directory
instead — the thing that actually distinguishes it from the shell in the next
pane. Clicking a block focuses that session.

The sidebar costs **columns instead of rows**, which is the better trade on a
wide terminal, and it has the full column height — so unlike the bottom
region it doesn't run out of room at four sessions.

### When the terminal is narrow

Below `status_panel_width + 60` columns the sidebar collapses to a
three-column rail rather than starving the output pane:

```
1 🟠 →2 THINKING 1m 32s │ ~450 tok │ ~$0.02
●1 ▎ ● alpha · ~/src/linkshell
!2 ▎ $ cargo test
✕3 ▎
```

State still reaches you — which agent needs you is the sidebar's real job, and
a glyph plus an index still does it.

### Placement

```toml
[general]
status_panel = "left" # "left" (default) | "bottom" | "overlay" | "off"
status_panel_width = 28 # columns for the sidebar
```

`→2` means this session has an active pipe to session 2. The arrow goes bold for
one second when the pipe fires. Token counts and cost come from the JSONL logs
written by Claude and Codex — not from screen scraping. Shell and custom
sessions show `—`. On Pro/Max subscriptions, linkshell detects the subscription
and shows real token counts while skipping meaningless cost.
- **`left`** — the sidebar above.
- **`bottom`** — the always-on region below the output, in two-line rows.
Its height is capped at a third of the terminal, so past four sessions it
drops the detail line to keep every session visible.
- **`overlay`** — not docked; `alt-s` opens it centered over the output, which
costs no layout space and resizes no PTY.
- **`off`** — never shown, and `alt-s` does nothing.

In `left` and `bottom`, `alt-s` hides and restores the panel. Note that this
resizes the sessions' PTYs, as any docked panel must; `overlay` is the
placement that never does.

Token counts and cost come from the JSONL logs written by Claude and Codex —
not from screen scraping. Shell and custom sessions show `—`. On Pro/Max
subscriptions, linkshell detects the subscription and shows real token counts
while skipping meaningless cost.

## Keybindings

Expand All @@ -51,6 +161,7 @@ and shows real token counts while skipping meaningless cost.
| `alt-n` | New session dialog |
| `alt-c` | Open command bar |
| `alt-t` | Toggle agent chat pane |
| `alt-s` | Show/hide the status panel |
| `alt-h` | Toggle help |
| `alt-x` | Kill active session |
| `alt-d` | Detach (sessions keep running) |
Expand Down
Loading
Loading