scrollback-first tui_v3 rounds out v2 parity across this session:
- /export: v2-aligned subcommands (clip/file/all/legacy); clip writes to the
real OS clipboard via existing copy() (pbcopy/clip.exe/wl-copy/xclip/xsel);
picker→file prefills an editable filename
- tool-call folding: per-turn fold ported from v2 fold_turns; Ctrl+O toggles;
incremental repaint folds turn N the moment turn N+1 marker streams in
- ask_user: focus-cycle model (↑↓ moves between candidates and the input box,
caret hidden on candidate focus); multi-select is checkbox-only with no
input box; [x]/[ ] ASCII checkboxes instead of Unicode ballot glyphs
- plan card: v2 _refresh_planbar port — 5-row budget, current-step line,
complete/placeholder states with grace periods, mtime-cached reads
- attachments: bracketed-paste, image/file placeholders, multi-line paste
aligned to v2 behaviour
- i18n: zh/en layer merged inline (was a separate tui_v3_i18n.py) so the
frontend ships as one drop-in module
pyproject: add prompt_toolkit + pillow to the `ui` extra.
- Add sys.path.append for llmcore's own directory as fallback
- Remove dead variable _imp_err
- Distinguish missing mykey vs import failure with clear error messages
macOS Terminal.app quantises truecolor to its nearest 256 slot (so
#5e6ad2 lavender lands on slot 62 #5f5fd7, visibly more blue) and
renders \x1b[2m as a heavy multiply instead of a gentle blend, producing
the "blue border + heavy shadow" against iTerm's identical code.
Branch on TERM_PROGRAM: iTerm keeps its truecolor accent and \x1b[2m
dim; Apple_Terminal switches to pinned 256-slot accent (105) and an
explicit mid-gray (244) for dim. Box border (146) was already 256-slot
so it carries unchanged.
Replace _history_lines (pre-rendered) with _blocks (source + kind) so
_repaint_screen can re-render every chip, banner, user panel at the
current width. \x1b[3J wipes scrollback on resize/Ctrl+L so the entire
conversation reflows, not just the visible viewport.
SIGWINCH delivery via self-pipe + select() — PEP 475 auto-retry on
os.read makes signal-driven InterruptedError unreliable under iTerm
split panes, so a dedicated pipe wakes the loop deterministically.
Streaming tracks a single _streaming_block whose source grows monotonically
through safe-position commits; mid-stream resize re-renders both the
committed source and the volatile live_tail.
Eight independent fixes accumulated since the last batch, all landing in
tuiapp_v2.py:
1. Code-block selection offset: _align_md_renders's is_centered heuristic
tripped on any code line indented >=4 spaces (Python's default), stripping
the indent from the source string so the visible selection and the copied
text disagreed. Add wide_lead >= 2*nt0_lead guard — only Rich's H1 centering
(wide_lead ~ width/2) crosses it, code lines never do.
2. Blockquote-continuation selection offset: Rich re-emits `▌ ` on every
wrapped visual line of a quote, but the wide-render carries the marker
only once. Strip the marker as a decoration prefix (counts as indent, not
content) and switch the wrap-accumulator to account for the per-break
eaten space — both pre-existing code-block wraps and blockquotes now map
correctly.
3. Streaming ANSI garbage: while a turn is streaming, the tail text segment
was rendered with `Text(content, style=C_FG)` instead of `Text.from_ansi`.
SGR codes from the model/tool output would render as literal `[31m`
glyphs and only resolve after `m.done` flipped or a resize forced a
remount via the Markdown path. Switch the two streaming fast paths to
Text.from_ansi — O(n) scan, no Markdown parse, colors honored live.
4. continue picker LIMIT removed: tuiapp's own `LIMIT = 20` truncated the
/continue picker even though list_sessions() always builds the full list.
Show everything; header now reads "选择要恢复的会话 (N 条)".
5. Long-paste rescue + user echo elision: Windows Terminal yanks window
focus when its >=5KiB paste-warning dialog pops, leaving focused=None
after the user confirms; Textual then routes the Paste event to the App
bubble instead of InputArea, and the paste appears to do nothing. Add a
GenericAgentTUI._on_paste forwarder that refocuses #input and replays the
event into InputArea._on_paste. Separately, on submit, elide user-side
display of very long pastes (head 10 / tail 5 / `⋯ 省略 N 行 ⋯`); the
agent still receives the full content via expand_placeholders.
6. mykey.py hot reload: load_llm_sessions() already had mtime-gated reload
plumbing, but it only ran on Agent.__init__ / next_llm / list_llms — so
editing mykey.py mid-session required restart. Two triggers added:
on_input_area_submitted now calls load_llm_sessions() before sending
(cheap mtime check; rebuilds on change), and a new /reload-keys command
forces a sweep across every session (resetting llmcore._mykey_mtime
per-iteration so the cache doesn't short-circuit later sessions).
7. Resize / sidebar toggle no longer jumps to bottom: _remount_current_session
used to unconditionally scroll_end after rebuilding. Capture scroll_y +
"was at bottom?" first; stick to bottom only if the user was already there,
otherwise restore the prior scroll_y so resizes don't yank the user away
from what they were reading.
8. Markdown table cells wrap instead of ellipsizing: Rich's Markdown
TableElement passes no `overflow` to Table.add_column, so the default
"ellipsis" hides long cell content with `…` on narrow widths. Patch
TableElement.__rich_console__ on import to pass overflow="fold".
When a single LLM provider goes down mid-task (auth, network, quota,
etc.), the conductor falls completely silent: MixinSession yields
errors as text chunks (not Python exceptions), so the existing
try/except guard in conductor_loop never triggers, and the conductor's
only way to talk to the user (LLM-decided POST /chat tool call) is dead.
Add a tiny watchdog: monitor_conductor_queue now returns the conductor
agent's final done text, and conductor_loop scans the tail-1000-char
window for "!!!Error:" — the prefix llmcore uses for unrecoverable
errors. Tail window (not whole text) avoids false positives from
earlier turns; using a character window (not last-line) matches
ga.do_no_tool's content[-100:] style and tolerates the decoration
(fence + "[Info] Final response to user.") that do_no_tool appends
after the error chunk when its own short window check misses. When
detected, push the matched error line as a system chat message so the
user knows what happened. A dedup check prevents repeated warnings
if multiple events fail in succession.
+16 -4 lines. Only frontends/conductor.py touched. No agent_loop /
llmcore changes; other frontends unaffected.
When setup window starts the bridge, the main window was already created
with URL http://127.0.0.1:14168/ but failed to load (bridge not running yet).
After bridge becomes ready, we now navigate the main window to reload the
bridge URL before showing it, preventing the white screen issue.