1. wheel: scrolling up now clears follow (the real bug — with follow=true every
draw snapped back to the bottom, so the wheel appeared dead on both Linux
and Windows); crossterm already normalizes xterm (Linux) and ConPTY
(Windows) wheel events to ScrollUp/ScrollDown, and ScrollLeft/ScrollRight
are handled too
2. expansion becomes three-state (collapsed → partial → full → collapsed):
the first expand shows the first 20 lines with a '… 还有 N 行(再次点击/Tab
展开全部)' hint; the second expand reveals everything. Applies to thinking,
tool-execution output and tool-result content. ToolCall args stay
single-level human-readable.
tests: three-state cycle, full-flag propagation
1. scrolling: ↑/↓ scroll the messages when the input is empty (otherwise the
caret moves), Ctrl+U scrolls up line-by-line, PageUp/PageDown page, wheel
scrolls; follow-to-bottom restored on reaching the end
2. markdown: hand-rolled renderer (headers, bold/italic, inline code, fenced
code blocks, lists, quotes, links, rules) applied to assistant/user text
3. ordering/refresh: messages are now committed into the transcript in real
time at MessageEnd (assistant + tool results), so earlier turns never
vanish mid-run and the latest content stays at the bottom; the live view
shows only the streaming partial plus running tool executions (finished
ones are presented by their tool_result messages, with duration)
4/5. expanded tool calls show human-readable arguments (command:/path:/edits
as key-value lines) instead of raw JSON; tool results show duration
new tests: markdown renderer, format_args_human, commit_partial, live
running-only execs, tool-duration propagation
StreamEvent::Error is encoded by the agent loop as an assistant message with
stop_reason=error and error_message, but the UI only rendered the header and
the (empty) content, hiding the actual message (e.g. a provider HTTP 400
detail). Now:
- UiBlock::Error renders error_message in red, for both committed and live
assistant messages
- on job Done, the error is also surfaced into the status bar and a note,
even when the job itself reported no error (it was encoded as a message)
- regression test: an error message renders its concrete text
- Braille spinner driven by a per-frame counter, shown in both the status
bar (bottom) and at the bottom of the message area while a job runs
- phase detection (RunState::working_phase): running tool > thinking >
streaming > generic, so the hint reads e.g. "⠹ shell: cargo test…"
- testable via the injected clock (tests/state_tests.rs)
- ratatui + crossterm chat UI: streaming assistant text, multi-line input,
bottom status bar (model / protocol / session id / context usage)
- thinking blocks collapse to a summary with duration + estimated tokens;
tool calls collapse to a concrete summary (path / command) + duration;
click or Tab expands the details
- config modal (/config): provider, OpenAI protocol, baseUrl, apiKey, model,
contextWindow — persisted to ~/.focus/config.json; session list (/sessions)
on the harness JSONL tree; /new, /compact, /help
- auto-compaction (80% threshold, no confirmation) + dynamic system prompt
with usage info run in a background job (fake-provider-testable)
- Esc aborts a run (abandons the job thread; bounded by the provider timeout)
- tests: config codec, summaries/durations/wrap/caret, UI state machine with
an injected clock, job flow with a fake provider, session persistence