auto resume
This commit is contained in:
75
CLAUDE.md
75
CLAUDE.md
@@ -68,8 +68,10 @@ src/markdown.rs wraps tui-markdown: renders GFM tables itself (box-drawing,
|
||||
width-fitted wrapped columns) and strips heading `#` markers —
|
||||
the pinned tui-markdown 0.3.5 does neither
|
||||
src/sessions.rs on-disk session history: background scanner thread keeps
|
||||
App::disk_sessions fresh (~1/s poll, labels re-read only on
|
||||
mtime change); load_view/load_history rebuild a feed Session
|
||||
App::disk_sessions fresh (~1/s poll, `read_meta` re-read only on
|
||||
mtime change — one pass yields the label *and* the session's
|
||||
last main-chain model, which `App::resume_model` turns into the
|
||||
`--model` a resume spawns with); load_view/load_history rebuild a feed Session
|
||||
from a JSONL transcript (lazily, on first view); build_tree
|
||||
parses uuid/parentUuid chains into a TurnTree (one node per
|
||||
real user prompt; rewinds leave fork points); materialize
|
||||
@@ -140,7 +142,22 @@ UI thread redraws on its own tick (no channel; just the mutex).
|
||||
transcript — but a second ctrl-↓ within 3s forces it (liveness is
|
||||
unknowable: an idle claude sends no traffic; `EmbedUi::past_embeds` skips
|
||||
the guard for sessions whose instance we killed ourselves). `--session-id`
|
||||
cannot be combined with `--resume` (CLI rejects it without `--fork-session`).
|
||||
cannot be combined with `--resume` (CLI rejects it without `--fork-session`);
|
||||
`--model` can, and every resume passes it.
|
||||
- **A resume continues on the session's own model**, not the CLI default:
|
||||
`App::resume_model` reads the model Claude Code recorded for the session's
|
||||
last main-chain assistant message (`DiskSession::model`, filled by the
|
||||
scanner's `read_meta` — subagent `isSidechain` records run their own model
|
||||
and `<synthetic>` error records carry no model, so both are skipped) and
|
||||
`app::model_arg_for_id` maps that id to a `--model` argument: a known alias
|
||||
(`sonnet`, `opus`, … from `App::model_choices`) wins over the dated snapshot
|
||||
id, so a retired snapshot can't pin the pane; an id with no alias inside is
|
||||
passed through verbatim (`--model` takes full names too). The transcript is
|
||||
authoritative, so a mid-session `/model` switch is honoured. `[1m]`
|
||||
(1M-context) picks are the one thing it cannot see — the transcript records
|
||||
the same base id either way — so `Session::spawn_model` remembers the exact
|
||||
argument the pane was spawned with in-process and wins **only** while it
|
||||
still names the model the transcript reports.
|
||||
- **Turn tree / branching** (lazygit/yazi-style, all in the sessions panel):
|
||||
`space` (or `→`/`l`) expands the selected session's turn tree — one row per
|
||||
real user prompt, abandoned rewind branches indented `⑂` under their fork
|
||||
@@ -189,14 +206,22 @@ UI thread redraws on its own tick (no channel; just the mutex).
|
||||
below the bottom rule for a menu row, not just the row directly under it: the
|
||||
list can start after a blank/header row and only the highlighted item carries
|
||||
a glyph (unselected file rows are plain names), so checking one row collapsed
|
||||
the pane whenever that row wasn't the selected item. The framed region drives the pane height too:
|
||||
the pane whenever that row wasn't the selected item. Above the top rule the
|
||||
frame also swallows an **active task panel** (`text_is_task_row` /
|
||||
`task_block_top`): Claude Code parks the `N tasks (…)` header + `✔ ◼ ◻` rows
|
||||
(and its `… +N pending` overflow line) directly above the input box, so
|
||||
walking up over that block — tolerating one blank line and single wrapped /
|
||||
activity rows, capped at `MAX_TASK_BLOCK` — makes task status visible with no
|
||||
extra app state. Priority when the pane can't hold everything: the panel is
|
||||
dropped first (`CompactFrame::ess_top`, the one-context-row frame) so the
|
||||
line you're typing and an open menu never fall off screen. The framed region drives the pane height too:
|
||||
`compact_rows` (called from `ui::draw`) measures box-height + tail so the
|
||||
pane auto-expands as the prompt gains lines or a menu opens and shrinks back
|
||||
when idle (floor `MIN_COMPACT_INNER`, cap = screen − 6); `PTY_PAD` keeps the
|
||||
PTY taller than the visible window so the child can still draw the rows we
|
||||
crop. **The Compact PTY is sized to the *whole screen height*, not the
|
||||
visible pane** (`ui::draw` passes `f.area().height` to `resize` only for
|
||||
Compact): Compact's height is derived by measuring what Ink has already
|
||||
crop. **A cropped PTY is sized to the *whole screen height*, not the
|
||||
visible pane** (`ui::draw` passes `f.area().height` to `resize` for every
|
||||
view except Full): their height is derived by measuring what Ink has already
|
||||
drawn, and Ink only ever draws as many rows as the PTY reports, so tying the
|
||||
PTY to the (small) visible height is a feedback loop — an `@`/`/` menu or a
|
||||
big paste that suddenly needs many more rows than the current PTY+pad never
|
||||
@@ -212,9 +237,20 @@ UI thread redraws on its own tick (no channel; just the mutex).
|
||||
returns `None` on a transient mid-repaint (box border caught missing) so the
|
||||
last height is kept. Without this the height oscillates every frame during a
|
||||
subagent turn or `@`/`/` menu filtering, and each change resizes the PTY →
|
||||
Ink repaints → flicker. `PaneView::Interactive` (the tap-grown AskUserQuestion / ExitPlanMode
|
||||
pane, whose selection box renders *above* the input) top-anchors from row 2
|
||||
instead so the prompt stays visible. `PaneView::Full` (fullscreen) renders
|
||||
Ink repaints → flicker. `PaneView::Interactive` (the tap-grown
|
||||
AskUserQuestion / ExitPlanMode pane, whose selection box renders *above* the
|
||||
input) is **measured the same way, never estimated**: `interactive_frame`
|
||||
anchors on the rule above the header-chip row (`← ☐ Header ✔ Submit →`),
|
||||
else the second-to-last rule, and runs to the last non-blank row;
|
||||
`EmbeddedTerm::interactive_rows` feeds that height through the same
|
||||
hysteresis. `App::ask_question_rows` (the row guess from the tool JSON) is
|
||||
only the fallback for the frames before Ink has drawn the box — it can't know
|
||||
how far the question text wraps, which is what used to crop the first
|
||||
paragraph. Because the Interactive PTY is now screen-tall, Claude Code lays
|
||||
the prompt out in full instead of switching to its own truncated form.
|
||||
`interactive_view_range` top-anchors on that frame and slides down only far
|
||||
enough to keep the `❯` option on screen when the prompt overflows the pane.
|
||||
`PaneView::Full` (fullscreen) renders
|
||||
the child's screen verbatim from row 0 with the PTY sized exactly to the
|
||||
pane. Permission-prompt boxes (rounded borders, not rules, and not in the
|
||||
API stream) aren't expanded in the compact pane — consistent with the
|
||||
@@ -235,7 +271,14 @@ UI thread redraws on its own tick (no channel; just the mutex).
|
||||
straight out of the installed `claude` ELF (single self-contained binary
|
||||
with the JS bundle embedded). No API call, never runs claude — just resolves
|
||||
`claude` on PATH and greps its bytes for the longest lowercase-token array
|
||||
anchored by `opus`+`sonnet`. Tab/BackTab cycle sessions (`p` no longer
|
||||
anchored by `opus`+`sonnet`. The list then gets the **1M-context variants**
|
||||
appended (`sonnet[1m]` etc., Claude Code's `--model` spelling for the long
|
||||
context window): `term::long_context_tokens` collects every quoted
|
||||
`"<token>[1m]"` literal in the same byte scan, and only aliases that really
|
||||
have one are offered (today `opus`/`sonnet`/`fable` — *not* `haiku` or
|
||||
`mythos`), so the suffix is never assumed. `[1m]` needs no shell quoting:
|
||||
the pane spawns via `CommandBuilder` argv, not a shell.
|
||||
Tab/BackTab cycle sessions (`p` no longer
|
||||
mirrors BackTab). v visual range, b branch, Esc unwinds (visual → tree →
|
||||
quit). n/N jump the feed scroll to the next/previous user prompt
|
||||
(`App::prompt_jump`, applied in `draw` where entry heights are cached). The
|
||||
@@ -275,7 +318,15 @@ UI thread redraws on its own tick (no channel; just the mutex).
|
||||
`127.0.0.1:<port>/v1/messages` without auth — a relayed 401 from Anthropic
|
||||
proves the round-trip. The TUI can't run in a non-tty. `CT_UPSTREAM` points
|
||||
the proxy at an alternative upstream (e.g. a local fake SSE server) for
|
||||
fully offline end-to-end tests with zero API usage.
|
||||
fully offline end-to-end tests with zero API usage. That fake server is
|
||||
`dev/fake_upstream.py`: it answers every request with canned SSE, so a **real
|
||||
`claude` child** can be made to render its client-side tool UIs on demand
|
||||
(`dev/.fake_scenario` = `ask | plan | todo | taskupdate | text`, switchable
|
||||
mid-run) — this is how the pane's frame detector is developed against what
|
||||
Ink actually draws. Drive it through tmux (`.claude/skills/tui-verify`) and
|
||||
obey that skill's safety rule: **never `pkill`/`killall`**, tear down only
|
||||
your own named tmux session. The child writes real task files under
|
||||
`~/.claude/tasks/<its-session-id>/`; delete that directory afterwards.
|
||||
|
||||
## Not yet handled (known MVP limits)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user