32 Commits

Author SHA1 Message Date
Jonas H
15b20a8af1 Put every app key behind one prefix
The keybindings had no red thread because there were three focus states
and every key had to ask "is this mine or the child's" — hence ctrl-q,
because plain q was forwarded. Replace the whole model: unprefixed keys
belong to the embedded claude, always, and everything cloak owns sits
behind ctrl-space (CT_PREFIX to change), tmux-style, in one table that
the which-key popup, the footer hint and the dispatch all read.

Esc is the only key with a rule of its own, and it is about reachability
rather than modes: it closes the topmost overlay, and with nothing open
it goes to the child, so interrupt and Esc-Esc rewind keep working. View
state — filters, the lane the feed shows — is a setting, not a mode, and
is deliberately not escapable.

That removes the focus model entirely, which lets the two feeds collapse
into one: the feed renders whichever lane feed_lane names, at full
width, and the stream picker chooses it. Subagents used to live in a
modal popup holding a second draw_feed with its own cache and scroll
model, rendering the same thing twice. The sessions panel loses its half
of the screen the same way. Both pickers become bottom strips with the
feed readable above them, so walking the list previews each row — which
is the view-without-resuming that /resume cannot do, and frees enter to
attach the pane.

Adds a find bar with in-place highlighting, scrolling to the matching
line rather than the containing entry, and drops the two keys that
served the old layout.
2026-09-10 14:16:34 +02:00
Jonas H
e9e224f856 Frame only the CLI's own errors, not tool errors
A failed tool was never the case worth growing the pane for: its
tool_result reaches the feed with the next request, so the feed above
already shows it. Only an error the CLI produced itself is invisible
there. Framing both meant tool errors bled through and held the pane open.

The `⎿` gutter is now the whole test — it means a tool is talking, so the
row is skipped. That leaves the un-guttered `● API Error: …` shape, which
also needs no lookup of the row above: a CLI error names itself.

Also expire an error at the newest prompt row. ERROR_SCAN alone was far
too coarse, because the CLI's reply to an error is only two rows: the
error stayed inside the window and held the pane through the next turn.
2026-09-10 12:21:42 +02:00
Jonas H
ae454a3d2d Fullscreen the pane while an editor holds it
ctrl-g opens the prompt in $EDITOR, which takes the child's pty over via
the alternate screen — something Claude Code never does itself. The
compact frame looks for the input box's two rules, so an editor could
only ever be cropped by it. Follow the alternate screen instead: give
the pane the whole screen for as long as the editor lasts, then put it
back. Edge-triggered, so ctrl-f still wins.
2026-09-07 11:52:43 +02:00
Jonas H
043c99cdeb Frame Claude Code's errors into the compact pane
The compact pane is prompt-only because the feed above shows the context.
An error is the exception: the feed cannot show it. A tool_result only
reaches us with the next request, which a turn that died never sends, and
an `API Error` row is Claude Code's own text, never in the stream at all.

So the frame walks further up to take the report in, and the scheduled
ctrl-l wipe is cancelled while one is framed — Ink redraws only the live
frame, so wiping would delete the error 400ms after it appeared.

Detection is text shape, not colour: the tool error renders palette 211
and the API error 220, both theme values. ERROR_SCAN is the only bound,
which is what makes it self-limiting — the error drifts out of the window
as the next turn prints output and the pane shrinks back on its own.
2026-09-07 10:20:52 +02:00
Jonas H
6bb7e7d424 ai titles for sessions 2026-09-07 08:07:43 +02:00
Jonas H
b9d7d2c969 Scroll the fullscreen pane's own scrollback
Claude Code grabs no mouse and stays off the alternate screen, so a plain
terminal answers the wheel with its own scrollback and the child never hears
about it. The fullscreen pane is that terminal, so it does the same job: the
view is a stable row index into wezterm-term's scrollback, which pins the rows
you scrolled to while the child keeps writing.

The ctrl-l transcript wipe is cancelled while fullscreen — there the pane is
not prompt-only, and its transcript is the thing being scrolled.
2026-08-27 14:34:48 +02:00
Jonas H
73871cb1dc Run every model at the 1M context window
The window is a beta header, so nothing on disk records it. Observing it on
the wire and replaying it on resume had to survive a session rename, a hot
reload, a WebSearch sub-request and a process restart — every gap fell back
to the short window.

Force it instead: Models::arg is the single decision point, and every spawn
and resume goes through it. A model is still only suffixed when the installed
claude ships that variant, so haiku stays haiku.
2026-08-27 13:06:21 +02:00
Jonas H
a5a6092579 Detect the unmarked @ path listing in the pane
Claude Code marks a fuzzy `@` hit with `+ `, but a path that leaves the
project (`@../de`) switches it to a plain directory listing whose rows
carry no marker — the selected row differs only in colour, which the
pane's text-only read of the screen drops. So the compact frame ended one
row under the input box and showed the first hit alone.

Recognise a bare path row by shape (a whitespace-free token holding a
`/`) and, since a user statusLine can look exactly like that, confirm the
list over two rows: the row under the rule must be a menu row or blank
(it is shown either way, so a one-hit menu needs no detection), and at
least one further row must match too.
2026-08-27 12:44:06 +02:00
Jonas H
1f483ad3d1 Hot-reload onto a rebuilt binary with ctrl-r
Restarting to pick up a code change costs the three things this app
exists to hold: the proxy port, the embedded claude pane, and the live
feed. So ctrl-r does not restart — it execs the binary now on disk into
this same process. execve keeps the pid, the open fds and the child
processes, so the listener socket, the pty and its claude child all
simply carry on; the feed travels in a JSON snapshot.

Build nothing, watch nothing. A build is the user's business, and a
running instance must not decide on its own when to become different
code. Rebuild outside, then press ctrl-r in each instance.

Drain before the exec. It destroys the tokio tasks relaying in-flight
responses, so the proxy stops accepting and finishes what it has first.
The socket stays open throughout (App::listener_fd is a dup), so
requests made during the swap queue in the kernel backlog and are served
by the new image — verified end to end: nothing refused, nothing cut.

Treat the snapshot as advisory. It is written by the old binary and read
by the new one, whose types usually just changed — that is the normal
case, not the edge case. The fd numbers stay in plain fields and the
feed is decoded per session behind a sanitiser, so a schema change costs
the feed and never the port or the pane.
2026-08-27 11:46:15 +02:00
Jonas H
8907766b5b Strip nF escapes so rustfmt output loses its B
rustfmt and `git diff` close every coloured run with `ESC ( B` (designate G0
as ASCII) and write it after the newline, ahead of the SGR reset. With no arm
for nF escapes the parser fell through to "two-char escape", ate `ESC (` and
left the `B` as text, so each diff line in the feed read `B+ added line`.

Intermediates 0x20-0x2f now run to a final 0x30-0x7e, and an unterminated
sequence stops where the CSI arm would.
2026-08-27 10:33:18 +02:00
Jonas H
ba6b18e7d9 Lane server tools, lift task notes, parse ANSI
Three streams of data were being lost or mangled in the feed.

WebSearch is not purely client-side: it issues a nested /v1/messages that
declares Anthropic's hosted web_search under the parent session id and with
no agent-id header. Read as a turn start it pushed a fake user prompt,
clobbered the main lane's system/tools signatures and downgraded a [1m]
session to the short window on the next resume. Requests are now classified
three ways (Turn / ServerTool / Side) from the tools array shape alone, and a
nested call gets its own lane, readable only in the A popup. Its result and
citations arrive complete in the stream and are echoed back in no later
request body, so they are attached as the stream delivers them.

An Agent call returns its tool_result immediately ("async agent launched"),
so the real completion is a <task-notification> injected into the parent's
next user turn. Those are lifted out of the prompt: the report moves onto the
Agent entry it answers, the usage totals onto the lane, and the status
becomes one glyph-led note line. A finished agent used to keep reading as
running.

Tool output we do not control carries SGR codes. A self-contained parser maps
them to styles instead of leaving [1m as literal text; filled blocks keep
their own colours and take only the attributes.

Also: a non-2xx upstream response now surfaces as an error entry instead of a
silent stall, tool renderers cover the file/shell/task/prompt/web families,
and the fake upstream answers the nested hosted-tool request so both search
paths run offline.
2026-08-27 10:24:05 +02:00
Jonas H
c7eddf3bde subagent handling + 1m context resume 2026-08-26 11:17:43 +02:00
Jonas H
a7d7940be3 auto resume 2026-08-17 07:47:47 +02:00
Jonas H
fc9c05ce97 fake upstream
+1
2026-08-17 07:47:35 +02:00
Jonas H
b56ea650ab claude expand fixes and line numbers 2026-07-09 08:49:53 +02:00
Jonas H
27b0f791aa table markdown rendering improvement 2026-06-29 07:24:20 +02:00
Jonas H
9c5356c6ba new session fixes 2026-06-26 13:10:10 +02:00
Jonas H
5669d30c27 dynamic prompt framing 2026-06-26 10:31:56 +02:00
Jonas H
f97828d7d1 less filter 2026-06-19 12:14:31 +02:00
Jonas H
f0e5c202f5 UX improvements 2026-06-17 12:53:18 +02:00
Jonas H
8ffd258341 multiline paste and cursor fix 2026-06-16 11:45:26 +02:00
Jonas H
10279a49a4 ui updates 2026-06-16 11:04:50 +02:00
Jonas H
a72eafc42a claude cloak rename 2026-06-15 10:31:35 +02:00
Jonas H
37cf183377 Add session history browser, turn-tree branching, model picker, mouse capture, and feed cache
Sessions panel now shows on-disk JSONL history for the current directory
(background scanner, ~1/s, deduped against live sessions). space/→ expands
a session into a lazygit-style turn tree; j/k walks sessions and turns; v
anchors a visual range; b materialises a new fully-decoupled branch file.
ctrl-↓ is the commit point for attach/resume/spawn; live external sessions
get a 3-second liveness guard (past_embeds skips the guard for sessions we
killed ourselves).

n opens a model-picker popup and spawns a fresh --session-id session;
model aliases are auto-discovered by scanning the installed claude binary
(no API call, no exec). ctrl-f toggles fullscreen for the embedded pane.

Mouse is now captured: wheel always scrolls the feed, left-drag selects
screen text copied on release via OSC 52 (like Claude Code). Native
selection needs shift held.

FeedCache stores per-entry rendered lines + wrapped heights, rebuilt only
when the content fingerprint changes — work while holding the app mutex is
proportional to what changed, and feed scroll now works past u16::MAX lines.
Kind::User entries (deep-blue ❯ prefix) surface user prompts from request
bodies via record_user_prompt.

src/markdown.rs added: GFM table rendering (box-drawing, width-fitted
columns) and heading # marker stripping, compensating tui-markdown 0.3.5
gaps without upgrading.

The proxy tap now runs on a dedicated tokio task via a bounded mpsc channel
(try_send; drop-on-full) so it never delays forwarded bytes. Listener is
bound before the TUI starts so multi-instance port coexistence works from
launch; CT_UPSTREAM added for fully offline testing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 11:27:34 +02:00
Jonas H
d34496da0b Document embedded terminal module in CLAUDE.md
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 09:11:00 +02:00
Jonas H
255c67de3f Fix session key extraction for Claude Code 2.1.x user_id format
metadata.user_id is now a JSON-ish blob containing "session_id":"<uuid>"
(verified against the 2.1.173 binary: user_id is built from an object
with device_id/account_uuid/session_id). split("session_") was matching
inside "session_id":" and producing mangled keys like `id":"a78…`,
which broke the embed-session match — so the pane never grew for
interactive prompts. New session_key() handles both the JSON blob and
the legacy user_…_session_<uuid> format, with tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 09:08:28 +02:00
Jonas H
ee429be917 Crop claude chrome rows; size ask-tool pane from option count
Render window is now content-anchored instead of the raw screen top:
ends BOTTOM_CROP(2) rows above the last non-blank row (hides the
"? for shortcuts"/mode hint rows) and starts no higher than row 2
(hides "✻ Worked for 1s"; the streaming spinner stays visible since it
sits next to the input box). The PTY gets PTY_PAD(4) extra rows so
Claude Code still has room to draw what we crop. Compact pane shrinks
12 → 8 rows.

AskUserQuestion pane height is now estimated from the tool input
(max over questions of: question text + 2 rows per option incl. the
implicit "Other" + submit/hint rows + tab header), replacing the flat
75% — which remains the fallback for ExitPlanMode/parse failures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 09:04:19 +02:00
Jonas H
b05706a51e Keep the claude pane prompt-only: compact height + auto transcript wipe
The pane was showing Claude Code's own transcript (tool calls, diffs) —
redundant with the feed above. Two measures:

- Default pane height is now compact (12 rows): room for the input box
  and status, (almost) none for transcript. Interactive grow to 75% is
  unchanged.
- When a turn of the embedded session finishes streaming (Tap drop),
  schedule a ctrl-l into the PTY 400ms later: Claude Code clears the
  screen but keeps the conversation, leaving just the prompt UI.

Residual: while a response is streaming, a few transcript rows can
still scroll through the compact pane until the turn-end wipe fires.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 08:52:30 +02:00
Jonas H
a770052673 Grow interactive prompt pane to 75% of the screen
Claude Code clips AskUserQuestion option lists to the rows available;
60% was still cutting options off. Also relax the upper clamp so small
terminals give the prompt as much as possible (6 rows reserved).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 08:44:26 +02:00
Jonas H
4f9331e274 Replace alt-key scheme with directional focus (ctrl-arrows + F2)
Alt-modified keys never reach the app on keyboard layouts where Alt
composes characters (e.g. dk_mac_fixed: alt-c = ©, alt-q = @), so the
modifier-split model was unusable. New scheme, layout-proof:

- F2          show/hide the claude pane
- ctrl-down   focus the claude pane (spawns/shows it if hidden)
- ctrl-up     focus the feed
- claude focused: every other key goes to claude (bold cyan border)
- feed focused: original plain keybindings unchanged
- CT_DEBUG_KEYS=1 surfaces raw key events in the status bar

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 08:36:47 +02:00
Jonas H
60df921dbb Add embedded claude pane (option B spike): PTY + wezterm-term module
- src/term.rs: self-contained module spawning `claude --session-id <uuid>`
  with ANTHROPIC_BASE_URL pointed at our proxy, inside a portable-pty;
  wezterm-term models the screen (and answers terminal queries), a small
  renderer paints cells into the ratatui buffer.
- Toggle with alt-c; off by default, hiding keeps the session alive.
- Modifier-split input: plain keys go to claude, alt-keys drive the feed
  (alt-q quit, alt-j/k scroll, alt-f filter, alt-n/p session).
- Dynamic pane height: the tap flags AskUserQuestion/ExitPlanMode in the
  embedded session (matched via --session-id) before Claude Code renders
  the prompt → pane grows 35%→60%; shrinks when the tool_result echoes
  back in the next request.
- wezterm-term isn't on crates.io: pinned git rev of the monorepo.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 08:16:53 +02:00
Jonas H
3f49e11b21 Initial commit: pass-through proxy TUI for Claude Code streams
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 07:59:05 +02:00