* fix(tui): repaint composer after macOS input
* feat(tui): add compact conversation history
* fix(tui): repaint messages after streaming and scroll
* fix(tui): polish history focus and scrollbar styling
GAnet 0.5.0 ships macOS sidecars, so the SOP no longer stops on non-Windows
desktops. The flow itself is unchanged; the edits are the platform facts GA
needs to follow it on a Mac:
- default component location moves to ~/.genericagent/components/GAnet on
every platform; a checkout under the GA root's temp/ is wiped by the temp
rewind mechanism
- launcher is ganet.cmd on Windows and ganet.sh on macOS; the bound Python
shim is ga_python.cmd / ga_python.sh
- sidecar data lives in %LOCALAPPDATA%\GenericAgent\GAnet or
~/Library/Application Support/GenericAgent/GAnet; login autostart is the
HKCU Run entry or a LaunchAgent; both are removed by remove_environment()
- release entries for macOS use platform "darwin", and verify_release checks
Mach-O as well as PE
- on macOS, setup and upgrades request the screen-recording permission at the
end and report it in screen_access; it is advisory and never blocks
- macOS needs no sudo; git/python placeholders without Xcode CLT must not be
invoked
* feat(worldline): integrate checkpoint-tree rewind into tui_v2
- tuiapp_v2.py: /rewind durable inline picker + /worldline three-pane
checkpoint tree (tree UI inlined; colors follow the v2 theme) with a
per-node diff viewport; conv-only /rewind restores via the persistent store.
- worldline.py (new): UI-agnostic backend — RewindStore (persistent
checkpoint tree + content-addressed blobs), reconcile, restore_plan,
node_diff, native-log projection.
- continue_cmd.py: gated worldline support — list_sessions(rewind_root=),
continue_inplace/copy(allow_empty=); default-off, other UIs byte-identical.
- tui_v3.py: status line shows concrete model id (unrelated minor tweak).
* feat(worldline): /rewind reuses the restore-mode picker (conv/code/both)
After picking a turn, /rewind now opens the same RestoreModeScreen as
/worldline to choose conversation / code / both, then restores via the
persistent store — making /rewind a true lite view of /worldline (linear
list, no tree/diff) over the identical restore backend.
* feat(worldline): sync working memory (history_info/key_info) on rewind
树节点除 conv 外,再存 history_info 增量(与 conv 同构)+ key_info 快照。
restore_plan 重建二者,_rw_sync_working_memory 在 rewind 时就地写回
handler.history_info / working['key_info'],使注入的 WORKING MEMORY 与
回退后的对话一致,消除旧 rewind「历史回退、纪要串味」缺陷。
向后兼容:老树无 WM 记录 → path_has_wm=False → 恢复时跳过 WM 同步,
不动现场纪要。单测 21/21 + 真机(363580)端到端验证(call_013→014
v2 痕迹 ×3→×0)通过。
* fix(worldline): reconcile against the log, only on /continue; rebuild working memory on absorb
- reconcile now takes the log-parsed full history, never the compressed
backend.history. Compressed memory is a lossy derivative; reconciling the
full tree against it made the tree adopt a front-trimmed suffix as a new
trunk (false divergence). Contract pinned in the docstring.
- drop in-session reconcile from /rewind and /worldline: the tree is kept
current every turn by _rw_commit, and live history there is compressed.
reconcile now runs only on /continue, where backend.history is the freshly
loaded full log.
- reconcile rebuilds history_info/key_info from the absorbed log so resumed
and external-UI sessions also carry working memory on rewind.
* fix(worldline): feed commit the full log; derive deltas from the tree's real length
_rw_commit fed the compressed backend.history, so once a long session front-trims,
commit's absolute hist_len delta drops turns. Now:
- _rw_commit feeds the log-parsed full history (fallback to backend.history).
- commit derives the delta start from len(rebuild_history(parent)) /
len(rebuild_hist_info(parent)) instead of the stored hist_len, so a poisoned
count can't drop turns and a previously-lost turn self-heals on the next commit
(the log is append-only, always a superset of the tree).
* chore(worldline): mark in-memory _do_rewind as the non-persistent degraded fallback
It chops the compressed live history (can't reach front-trimmed turns, keeps
truncated remnants, restores neither files nor working memory). Only used when
there is no checkpoint tree; the durable tree-rebuild path is always preferred.
* fix(worldline): derive tree working memory from the log, not live handler state
/continue resets agent.history (handler.history_info) to empty. _rw_commit
previously fed that live list as hist_info, so after a continue the per-turn WM
delta misaligned with the tree's log-derived WM and new turns' summaries were
dropped. commit now derives hist_info/key_info from the history it stores (the
full log) when not given, matching reconcile; _rw_commit stops passing the live
list. Tree WM is uniformly log-derived and survives the continue reset.
* feat(worldline): restore working memory on /continue (TUI-only, derived from the log)
continue_cmd clears handler.history_info on restore; the TUI's continue path now
derives history_info from the loaded log (same _derive_hist_info as the tree) and
writes it back to agent.history, so a continued session keeps its working memory,
consistent with the tree and live sessions. Scoped to tuiapp_v2 only — shared
continue_cmd is untouched, so telegram/discord/streamlit frontends are unaffected.
* refactor(continue): restore working memory on /continue via opt-in restore_wm
Move the WM restore from the TUI into continue_cmd as an opt-in. _load_history_into
derives history_info from the loaded log and writes it back to agent.history when
restore_wm=True; continue_inplace/continue_copy thread the flag and the worldline
TUI passes True. Default False keeps every other frontend (telegram/discord/
streamlit/shared chat) byte-for-byte unchanged. _derive_hist_info is self-contained
(no worldline dependency), same extraction口径 as the tree.
* perf(worldline): only re-parse the log in _rw_commit when front-trim is detected
_rw_commit re-parsed the whole log every segment to feed commit a full history;
for very long sessions (multi-MB logs) that adds a multi-second lag per turn. Now
it uses the fast in-memory backend.history by default and only falls back to
parsing the log when front-trim is detected — RewindStore.first_question() vs the
live history's first question (front-trim pops the first question, so they diverge).
Common case (no trim) pays nothing; correctness under trim is preserved.
* feat(worldline): conv/code bridge nodes + align _rw_commit to native log
- worldline: commit_bridge() + restore_plan() three-way (both/conv/code) with a new rw_tag field. conv bridges fork at parent(target); code bridges mount at parent(old_head) as a sibling of the current conversation node (same conversation, older code).
- tuiapp_v2: _rw_commit now reads the native log (parse_native_log) instead of the compressible live backend.history; stat-cached _rw_log_history; title from the real log delta; skip commit when there is no new complete pair. Cursor points at the bridge for conv/code; rw_tag rendered after the title.
- continue_cmd: add parse_native_log() public wrapper (complete Prompt->Response pairs only; [] for native-but-empty, None for non-native).
* fix(worldline): realpath 去重 key + 节点行数增删 + 剥离 project-mode
- key()/cwd 改用 realpath 解析 junction/symlink,同一物理文件只产生一个 tracked
key(修 workspace 下 @提及绝对路径与相对写入产生双 key、回退误覆写当前内容)
- 新增 node_line_delta:按 node_diff 计每节点相对父节点的 +新增/-删除 行数,
供 /rewind、worldline 显示代码变动量
- 重建历史时剥离 project-mode 注入块与自动注入标记(worldline._msg_user_text +
continue_cmd._derive_hist_info),标题/提问只留用户原话
* feat(worldline/rewind): 面板与 rewind UI 精简重设计
worldline 面板:
- 右栏改用原生 OptionList(整项高亮/滑窗跟踪/点击映射,取代手写 ClickableList)
- 左右栏比例 3:2;详情面板精简(删掉与状态条/diff 重复的操作提示、时间、回退说明)
- 状态条重写为完整动宾短语,Enter 随聚焦切文案;右栏聚焦高亮改用更亮的 alt_bg
(对齐左树,修此前聚焦态反而更淡的问题)
/rewind:
- 三列:用户 prompt · 上次时间 · 代码 +新增/-删除(绿/红,无改动显示「(无改动)」);
倒序、聚焦底部、超长 prompt 省略号截断
- 延迟折叠(defer_collapse):选中先弹模式窗,确认才回退并清卡片;Esc 取消保留列表、
焦点回到列表,不再留误导性「✓ 已选中」
其他:
- Ctrl+D 关会话时释放日志锁(abort + release_current),之后可原地 /continue
* fix(worldline): 回填输入框的 prefill 剥离 project-mode 注入
restore_plan 的 prefill/_prompt_of 走未清洗的 user_msg_text(树里的 user 消息取自
含注入的 native 日志),把 --- [PROJECT MODE:...] --- 一起塞进输入框。统一过
_strip_project_mode(幂等)。回归:temp/test_rewind_prefill_no_projectmode.py 9/9。
* refactor(tui): 删除被取代的旧 rewind 面板与死代码
- 删旧 /rewind 整簇:RewindScreen 类 + _on_rewind_pick + _rw_* 9 个 helper
(已被内联 choice 卡 + RestoreModeScreen / RewindTreeScreen 取代,无 live 入口)
- 删未用 import next_same_depth/CheckpointTree、只写不读的 _diff_for/assistant_len
- 净 -276 行;import tuiapp_v2 通过、worldline 回归全绿
* fix(worldline): apply_code 回退失败不再静默,回传错误由 UI 提示
apply_code 直接写用户工作区文件,中途失败(权限/磁盘)会留下半回退状态。原先
restore_plan 吞成 changed=[],UI 显示「无变更」,用户毫不知情——全 PR 里失败半径
最大却唯一被静默的点。改为 _apply_code_safe 回传 (changed, code_error),
_rw_restore_node 据此追加「⚠️ 代码回退未完成…请检查」告警。
* fix(continue): preserve session workspace on in-place restore
In-place /continue retargets agent.log_path to the restored file, so the
reset bind `_bind_workspace(None)` ran during restore was persisting
session_ws_set(path, "") — erasing that session's own workspace mapping to
"" (read back as "explicitly off") before we read it, which also short-
circuited the log-scan fallback. The continued session never re-entered its
workspace.
Add a `persist` flag to `_bind_workspace`; the restore-time reset passes
persist=False so it only refreshes in-memory state. The session→workspace
map is now written solely by explicit /workspace, /workspace off, and a
successful restore.
* fix(tui_v3): support Home/End keys for line start/end
PTK delivers Home/End as raw VT sequences in KeyPress.data, which
_esc_repl swallowed (only arrows were decoded). Map \x1b[H/[F/[1~/[4~
and SS3 \x1bOH/\x1bOF to internal bytes 0x07/0x14, extend _ESC_RE to
match SS3 as whole sequences, and add jump-to-line-start/end handlers
in _keys. Home no longer collides with Ctrl+A select-all.
* feat(worldline): integrate checkpoint-tree rewind into tui_v2
- tuiapp_v2.py: /rewind durable inline picker + /worldline three-pane
checkpoint tree (tree UI inlined; colors follow the v2 theme) with a
per-node diff viewport; conv-only /rewind restores via the persistent store.
- worldline.py (new): UI-agnostic backend — RewindStore (persistent
checkpoint tree + content-addressed blobs), reconcile, restore_plan,
node_diff, native-log projection.
- continue_cmd.py: gated worldline support — list_sessions(rewind_root=),
continue_inplace/copy(allow_empty=); default-off, other UIs byte-identical.
- tui_v3.py: status line shows concrete model id (unrelated minor tweak).
* feat(worldline): /rewind reuses the restore-mode picker (conv/code/both)
After picking a turn, /rewind now opens the same RestoreModeScreen as
/worldline to choose conversation / code / both, then restores via the
persistent store — making /rewind a true lite view of /worldline (linear
list, no tree/diff) over the identical restore backend.
In-place /continue retargets agent.log_path to the restored file, so the
reset bind `_bind_workspace(None)` ran during restore was persisting
session_ws_set(path, "") — erasing that session's own workspace mapping to
"" (read back as "explicitly off") before we read it, which also short-
circuited the log-scan fallback. The continued session never re-entered its
workspace.
Add a `persist` flag to `_bind_workspace`; the restore-time reset passes
persist=False so it only refreshes in-memory state. The session→workspace
map is now written solely by explicit /workspace, /workspace off, and a
successful restore.
The tool-cards change (#593) tightened _md_line_has_box_drawing to exempt
pure-horizontal '─' runs as markdown hr. But a SIMPLE-box Markdown table's
only box glyph is its header rule, so that exemption dropped whole tables
out of the passthrough copy path and into the wide-render aligner, which
maps selection cell-columns to character indices — misaligning copy of any
table with CJK (wide) cells.
Remove the pure-horizontal exemption so a table's header rule is detected
again and the table run stays passthrough. Card gutters (└─ / │ / └) keep
their own prefix-based exemptions, so tool cards are unaffected. The only
cost is a real markdown hr copying as a dash run — the pre-#593 behavior.
Ctrl+C / selection copy fell back to Textual's OSC 52 escape on Windows/Linux.
Some terminals (notably VSCode's integrated terminal) decode the OSC 52 base64
payload as latin-1 instead of UTF-8, so non-ASCII copied text lands in the
clipboard as mojibake (e.g. 需要 -> éè¦). Classic Windows conhost ignores
OSC 52 clipboard writes entirely, so copy was a silent no-op there (incl.
Windows Server).
Prefer the OS-native clipboard (_copy_to_clipboard: win32 CF_UNICODETEXT /
pbcopy / wl-copy / xclip), which writes real Unicode and bypasses the terminal.
Keep OSC 52 only as a fallback, and force it under SSH (SSH_CONNECTION /
SSH_TTY) where the native call would write the remote host's clipboard instead
of the user's local one.
- _term_write(): route OSC/control sequences through Textual's driver writer
queue instead of direct sys.__stdout__. A title/OSC write landing mid-frame
during streaming used to split Textual's own escape sequences, rendering as
flashing ANSI garbage; serializing on the same writer queue lands it cleanly.
- sidebar: wrap the session list in a VerticalScroll (#sidebar-scroll) so it
scrolls when overflowing (scrollbar-gutter reserved to avoid row reflow);
inner #sidebar Static keeps padding so the click hit-test math is unchanged.
Keyboard session-switch scrolls the active session into view.
* fix(tui): stop CRLF stdout rendering as blank lines (v2)
Windows child stdout is `\r\n`; the streaming path fed it straight to
`Text.from_ansi`, which treats `\r` as a carriage return and erases each
line's text, so a `[Stdout]` block rendered as a run of blank lines until
the turn finished (the done-state Markdown render strips `\r`). Normalise
`\r\n` -> `\n` before `from_ansi`; lone `\r` is kept so progress-bar
overwrites still work.
* fix(tui): stop leaking empty per-session task_dir signal folders
Both v2 and v3 eagerly `os.makedirs` the per-session `_intervene` /
`_keyinfo` signal dir, but the only writer (`_intervene`) already creates
it lazily — so every session that never used intervene left a stale empty
`temp/_tui_v2_<pid>_<id>` / `temp/_tui_v3_<pid>` behind (85+ accumulated).
`consume_file` tolerates a missing dir, so the eager create is redundant.
- Set only the task_dir *path*; let the writer create it on first inject.
- Remove empty dirs on session close (v2 `_cmd_close`), on graceful exit
(v2 `on_unmount`, v3 `_cleanup`), and sweep stale ones left by crashes
at startup (v2 `on_mount`, v3 `main`). `os.rmdir` only removes empty
dirs, so a still-pending `_intervene` is never clobbered.
* fix(tui): repair /continue search box cursor & up-nav loop (v2)
The SearchablePicker swallowed Right while the search Input had focus
(always treating it as "select"), so the caret could never move right in
the query. And Up could leave the list but never return: from the search
box Down entered the list, yet Up at the top row was stuck.
- Right commits the highlight only when the caret is already at end of the
query; otherwise it moves the caret within the search text.
- Up on the first list row returns focus to the search box (scoped to
/continue — no-op for ChoiceLists outside a SearchablePicker), clearing
the highlight so the next Down re-enters at the first row, not the second.
- Up from the search box wraps around to the bottom of the list.
/continue previously pushed plan_scan_baseline past every restored
message, so plan_state.is_active() saw an empty slice and hid the card —
even when the prior session's plan.md still had `[ ]` items the user
wanted to keep working on. `working['in_plan_mode']` isn't replayed by
the log restore either, so the card never came back.
Make the baseline conditional: default to 0 so the scanner walks the
full restored history; only push past it when the restored plan.md is
already all-done, suppressing the stale ✓ card. Resolved-path missing
or empty items naturally fall back to "no card".
/stop now also refills the input box with the just-sent user message,
so an interrupted send is one keystroke from edit-and-resend. Only
when the box is empty (don't clobber a half-typed follow-up). Agent
history is untouched — a resend duplicates the turn in LLM context;
/rewind 1 is the manual escape.
Behaviour matrix (/continue):
unfinished plan → baseline=0, scanner finds path, card resumes
completed plan → baseline=len(msgs), card stays hidden
plan.md deleted → resolve_path None, card stays hidden
fresh enter_plan_mode after continue → working flag re-arms, normal
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".
- Per-fold click-to-expand: assistant messages render as a sequence of
text/fold-header/fold-body widgets; FoldHeader is a clickable Static
that toggles m._toggled_folds and remounts just that message
- Ctrl+O is authoritative: clears all per-fold overrides so the state
is uniformly all-collapsed or all-expanded
- Streaming spinner: dedicated tiny Static (`m._spinner_widget`) after
segment widgets, driven by an independent 100ms timer. Bypasses
Markdown completely so unclosed code fences / paragraph whitespace
trimming can't eat it
- Hide `**LLM Running (Turn N) ...**` markers at render time only;
fold_turns still uses them for splitting
- Ctrl+B sidebar toggle: invalidate width-keyed cache and reuse the
resize debounce path so the chat reflows reliably after toggling
- #input min-width: 10 — guards Textual TextArea.render_lines against
`range() arg 3 must not be zero` when sidebar is shown on a very
narrow window and #main collapses to ≤ 0 cols
- Pasting non-image files (.docx/.pdf/.py …) was folded as `[Image #N]`
because clipboard helper returned any file path. Split into image vs
file by extension; non-images now fold as `[File #N]` and expand to
the path on submit, same flow as images.
- Backspace/Delete used to nibble placeholders one char at a time.
Override `action_delete_left/right`: when the caret sits flush against
a `[Pasted text #N +X lines]` / `[Image #N]` / `[File #N]` placeholder
on the matching side, remove the whole span and drop its dict entry.
- Ctrl+C inside the input was clearing the whole field. Root cause:
`screen.get_selected_text()` doesn't see TextArea's internal selection,
so the handler fell through to the input-reset branch. Fixed by
preferring the focused InputArea's `selected_text` before screen-level
drag selection.
- Input text/history/pastes were globally shared across agent sessions
(single InputArea widget). Moved the four state slots
(text/history/pastes/paste_counter) onto `AgentSession`, with a
`_swap_input_for_session` snapshot/restore on session switch.
* fix(tui): debounce v2 layout reflow on resize
Same-size resize events are deduped, layout reflow is debounced to 80ms,
and message remount is deferred to the next frame so #messages.content_region
reports the post-resize width instead of the pre-resize one.
* feat(tui): rebind v2 keys (Ctrl+C dual, Ctrl+↑↓ sessions, Ctrl+O fold) + help overlay
- Ctrl+C is the single stop/quit key: stops a running task; otherwise
clears non-empty input; otherwise arms quit (press again within 2s to
exit). Removes Ctrl+S, Ctrl+Q, and InputArea's prior Ctrl+C
copy-or-clear binding. Any non-Ctrl+C key disarms the pending quit.
- Ctrl+O toggles turn fold (was Ctrl+F, which collides with terminal
app search shortcuts on macOS). Topbar gains a small "▾ fold"
indicator when fold mode is on.
- Ctrl+Up / Ctrl+Down switches sessions (was Ctrl+Left / Ctrl+Right)
to match the vertical session-list mental model.
- Esc is the single one-press cancel for ChoiceList — drops the prior
two-step "Left to leave, Left to cancel" flow. Esc also continues to
close the command palette and now closes the help overlay.
- Adds a Ctrl+? help overlay listing every keybinding. Bound on both
ctrl+? and ctrl+question_mark to survive terminal reporting quirks.
- Newline keeps Ctrl+J / Ctrl+Enter / Shift+Enter as cross-terminal
fallbacks (Ctrl+J is the portable one for macOS Terminal.app); drops
Alt+Enter.
- Bottom bar simplifies to four essentials + Ctrl+? and / hints, and
shows a transient "再按 Ctrl+C 退出" while quit is armed.
* fix(tui): switch help shortcut from Ctrl+? to Ctrl+/
Ctrl+? (= Ctrl+Shift+/) is unreliable across terminals — many emulators
either swallow it or report it as something Textual can't parse. Ctrl+/
is the conventional VS Code / command-line help shortcut and ships with
the legacy Ctrl+_ fallback (ASCII 0x1F is what older terminals send for
Ctrl+/). All three forms are bound (ctrl+slash, ctrl+/, ctrl+underscore)
so the binding survives terminal-reporting differences.
Bottom hint bar collapses to a single left-aligned line carrying the
command-palette and help hints alongside the action keys, since the
prior right column repeated information the left column already had room
for. The help overlay header reads "快捷键帮助" instead of "快捷键".
* fix(tui): ctrl+c dual-press with non-empty input, centered help modal
- Ctrl+C with non-empty input now clears AND arms quit; a second Ctrl+C
within 2s exits. Previously typing in the input let one Ctrl+C exit
silently after the clear, which was easy to trigger by accident.
- Help is now a ModalScreen pushed over the main screen so opening it
no longer shifts the input box. Centered with a lighter background
(#21262d on #30363d border) to read as a lifted panel.
- Esc-cancelling a ChoiceList removes the prompt cleanly with no
"已取消" output — the canceled prompt simply disappears from the
chat, matching expectations for in-flight tool prompts.
* fix(tui): keep Ctrl+C copy semantics and update choice hints
When there is selected text, Ctrl+C now copies the selection and returns
without triggering stop, input clear, or quit-arming. This preserves the
TUI selection/copy path after Ctrl+C became the global stop/quit key.
Choice prompts now advertise the current Esc-cancel behavior instead of
the removed Left-arrow flow, and stale comments/docs mentioning the old
left-arrow return path were cleaned up.
PR #360 disabled Textual mouse support on darwin (run(mouse=False)) to
keep terminal-native selection working. Side effect: ?1006 SGR mouse was
never enabled, while macOS Terminal.app / iTerm2 default ?1007 (alternate
scroll mode) translated wheel events into Up/Down keys — which then
triggered the newly added input-history navigation.
Re-enable mouse globally and explicitly disable ?1007 on mount so wheel
events flow through the SGR channel without spawning duplicate arrow
keys. Keyboard Up/Down history navigation is unaffected. The ?1007
disable is a no-op on Linux / Windows Terminal where it defaults off.
When an assistant message already has a mounted body widget, streaming updates only refreshed the widget content. Scroll the messages container after those in-place updates so both streaming chunks and final responses remain visible at the bottom.
Handle paste events directly for both short and multi-line input instead of mixing TextArea's default paste path with the custom placeholder path. This keeps single-line paste from being inserted twice while preserving the existing multi-line placeholder behavior.