* fix(cli): preserve UTF-8 when copying from WSL
* fix(cli): preserve native WSL clipboard backends
Problem: WSL sessions lost working Wayland and X11 clipboard writes when Windows PowerShell was unavailable.
Root cause: WSL detection unconditionally replaced every Linux clipboard backend with the Windows bridge.
Fix: Prefer wl-copy, xclip and xsel, and reserve the UTF-8 PowerShell bridge for the Windows fallback. Never invoke raw clip.exe.
Verification: Reproduced all five native-backend failures before the fix. Native backend, process stdin, command failure and OSC 52 fallback tests pass with the race detector.
* fix(cli): bound WSL clipboard command waits
Problem: A stalled clipboard subprocess could prevent the asynchronous copy operation from reaching its OSC 52 fallback.
Root cause: The WSL command had neither a cancellation deadline nor a bounded pipe-drain wait.
Fix: Apply a five-second context deadline, cancel the process tree through the shared proc helper, and bound inherited output-pipe draining to one second.
Verification: Full CLI tests, focused race tests including deterministic cancellation after child startup, go vet, repolint and golangci-lint all pass. Native WSL clipboard round-trip validation remains external.
---------
Co-authored-by: SivanCola <[email protected]>
Problem:
- The updated connector path left rewriteTranscriptBlock unused, and the current linter rejected a HasPrefix/TrimPrefix pair.
Root cause:
- Connector rewrites now preserve copy metadata through rewriteConnectorBlock, making the old text-only helper obsolete.
- main-v2 enables the modernize string-prefix rule.
Fix:
- Remove the unused helper and use strings.CutPrefix while retaining the same copy-span indentation behavior.
Verification:
- make lint
- go test ./internal/cli -count=1
- go test -race ./internal/cli -count=1
- go vet ./internal/cli
- go run ./tools/repolint
Refs #9244
Problem:
- Character-based gutter stripping silently removed legitimate lines beginning with │, ▎, or ⎿.
- Multi-line tool and reasoning connectors retained one leading space on the first line and five on continuation lines.
Root cause:
- Selection copy inferred decoration from the copied characters after provenance had been discarded.
- Connector continuation padding was not represented by the single-glyph heuristic.
Fix:
- Extend the existing copy-only span rendition to mark renderer-owned gutters explicitly while keeping display fallback byte-faithful.
- Preserve copy renditions for live tool blocks and regenerate semantic Markdown and reasoning spans on demand.
- Move selection-copy ownership into a focused module and cover rendered, replayed, fallback, literal-glyph, connector, and CJK UTF-8 paths.
Verification:
- go test ./internal/cli -count=1
- focused copy tests under go test -race
- go vet ./internal/cli
- go run ./tools/repolint
Refs #9244
* refactor(go): adopt modern stdlib idioms across kernel and desktop
Mechanical sweep with golangci-lint's modernize/intrange/copyloopvar/
usestdlibvars fixers plus hand-fixes for what they could not rewrite:
slices.Contains, min/max, slices.Backward, WaitGroup.Go, reflect.TypeFor,
maps.Copy, strings.Cut/CutPrefix/SplitSeq, strings.Builder, range-over-int.
Net -549 lines with no behavior change.
Three loops in session.go stay index-based: slices.Backward yields element
copies, so the fixer's rewrite silently dropped writes to DecisionReceipts
and ToolCalls (caught by TestDelivery* and TestDependentSameBatch*). All 49
slices.Backward sites were audited for that shape; only these three write
through the index, and they carry a nolint with the reason.
CaptureAfter's scan loop had an empty body and never ran any logic, so the
fixer could not express it. Drop it and the accessor it was the only caller
of. omitzero is deliberately untouched: turning omitempty into omitzero
changes the JSON written for nested structs, which is a wire-format change
to session files and the extension protocol.
* build(lint): gate the modern-idiom linters
Nothing stopped the codebase from drifting back to pre-1.22 idioms. Both
modules are now clean for modernize, intrange, copyloopvar, and
usestdlibvars, so enable them rather than leaving the sweep as a one-off.
omitzero stays disabled with the reason inline; usetesting, perfsprint, and
errorlint still have residue and land separately.
* fix(cli): transcript scroll state, wrap cache, and mouse re-enable
Problem
- Opening approval/chooser panels shrank the viewport and flipped AtBottom
false, which permanently disabled tail-follow until a manual return to
bottom (#6430). Long sessions with frequent panels looked "broken" (#6978).
- Every streaming commit re-wrapped the entire joined transcript, so long
sessions paid full O(history) cost on each keystroke of agent output.
- Windows ConPTY dropped mouse tracking after long turns/resize/focus loss;
wheel then became Up/Down history cycling (#7583). /mouse toggle twice was
the only recovery.
- Composer cursor coordinates were not clamped to terminal bounds after VS
Code fullscreen/resize storms (#6282, #7236).
Root cause
- Tail-follow relied on a pre-Update AtBottom snapshot, not an explicit
followTail/userScrolled mode; modal height changes were misread as user
scroll. Wrap cache did not exist. Mouse mode was set only in View() without
re-emitting enable sequences. Cursor position was raw offset math.
Fix
- Explicit scrollFollowMode: wheel/PgUp/scrollbar leave follow; empty Enter,
Ctrl+End, forceGotoBottom restore it. Height-only modal changes keep
followTail and re-pin when appropriate.
- Per-block wrap cache with append-only extension; full rebuild on width
change, mid-history rewrite, or dirty reflow.
- Rate-limited mouse enable on resize, FocusMsg, and TurnDone.
- clampCursorToTerminal on both alt-screen and native-scrollback paths.
Verification
- go test ./internal/cli -count=1
- TestModalOpenDoesNotDisableTailFollow
- TestUserScrollBreaksAndEmptyEnterRestoresFollow
- TestWrapCacheAppendOnlyMatchesFullRebuild
- TestLongTranscriptAppendKeepsFollowAndDoesNotFullRebuildEachTime (2k blocks)
- TestMouseReenableRateLimit, TestClampCursorToTerminal
Refs #6282#6430#6978#7028#7180#7236#7537#7583#6563
* fix(cli): suffix wrap invalidation, drag scroll sync, trailing mouse re-enable
Problem
- streamAnswer set transcriptDirty → forceFullWrap rebuilt the entire wrap
cache on every flushed paragraph, the long-session hot path #7706 claimed
to remove.
- Scrollbar drag only synced scrollMode on MouseRelease, so agent events
mid-drag still saw followTail and GotoBottom.
- Mouse re-enable rate limit dropped in-window requests with no trailing
retry; the last resize in a storm could leave ConPTY without tracking.
- Lint: SA9003 empty branch, unused resetThenEnableMouseTracking/modalOpen.
Root cause
- Collapsed "dirty" with "full rebuild"; gesture state lagged YOffset; leading
edge only rate limit.
Fix
- invalidateWrapFrom / rewriteTranscriptBlock for live rewrites; Update only
force-full-wraps on width change or history shrink; suffix path re-wraps
from wrapBlockCount; feedViewportContent uses SetContentLines.
- dragScrollbar and edge autoScroll sync scrollMode on every YOffset change.
- maybeReenableMouse sets pending + arms trailing timer (mouseReenableMsg);
controllable mouseNowFn for tests without sleep.
- batchCmds collapses nil; mouse re-enable only on real size change.
Verification
- go test ./internal/cli -count=1
- TestStreamAnswerSuffixInvalidationNotFullRebuild
- TestScrollbarDragMotionSyncsBeforeRelease
- TestMouseReenableTrailingEdge (fake clock)
- BenchmarkStreamAnswerSuffixWrap hist=500/1000/2000 (~const allocs/op)
Refs #6430#6978#7583#6282#7236
* fix(cli): clear Integration C lint and docs-impact blockers
- Fill empty answerIdx branch with t.Fatal; use wrappedContentString
- Combine fakeNow declare/assign; drop unused mouseReenableSeq generation
- Trailing mouse timer is single-armed pending only (no stale seq)
Verification: go test ./internal/cli -count=1; staticcheck ./internal/cli
Give every sub-agent child its own progress state and a fixed transcript
slot keyed by its namespaced call ID, independent of the single live
toolStreamID, so concurrent children never cross-stream. By default the
block shows phase, running elapsed, and recent activity; /verbose (Ctrl+O)
adds bounded 4 KiB reasoning/text tails with a truncation marker. Terminal
children fold to a one-line summary; Termux native scrollback queues a
status line on phase changes and terminal only (verbose previews at most
once per 2s per child). Adds localized phase labels and progress formats.
Problem:
Selecting rendered math from a resumed session or replayed branch copied only the Unicode display glyph instead of the original LaTeX source.
Root cause:
The copy transcript path marker-rendered live Markdown blocks but reused the already-rendered text for replay bundles, so historical assistant formulas had no source spans.
Fix:
Share replay traversal between visible and clipboard renderers, inject the marker-aware assistant renderer for copy operations, and assign unique prefixes across replayed assistant messages. Add coverage for ordinary and local recovery history.
Verification:
- go test ./internal/cli -count=1
- go test -race ./internal/cli -run 'TestSelectedText|TestReplaySections' -count=1
- go test -p 2 ./...
- go vet ./...
- cd desktop && go test ./...
- pnpm test:typecheck
- pnpm exec tsx src/__tests__/message-selection-copy.test.ts
- pnpm build
Problem:
CI reported SA4006 because the no-marker branch assigned the loop position immediately before breaking.
Fix:
Remove the dead assignment while preserving the branch's existing output and control flow.
Verification:
- go test ./internal/cli -count=1
- go test -race ./internal/cli -run 'TestSelectedText|TestLanguageCommandRefreshesCurrentController|TestCurrencyCommandPersistsAndRefreshesCurrentController' -count=1
- go vet ./internal/cli
- golangci-lint v2.12.2: 0 issues
Co-authored-by: SivanCola <[email protected]>
Problem:
Desktop copy could discard prose around a single formula, while the TUI reused display coordinates against a separately wrapped raw transcript. The original commit also dropped the /currency dispatcher and discarded the /language rebuild command.
Root cause:
The desktop path performed global string substitution instead of respecting DOM range boundaries. The TUI assumed rendered Unicode and LaTeX source had identical line and column geometry.
Fix:
Reconstruct desktop selections from ordered KaTeX DOM ranges and MathML annotations. Re-render semantic CLI Markdown only when copy is requested, attach zero-width source markers to the identical visible rendition, and map selected display cells back to formula spans. Restore both runtime-setting dispatchers and add focused regressions for partial, mixed, duplicate, and wrapped selections.
Verification:
- go test ./internal/cli -run 'TestSelectedText|TestLanguageCommandRefreshesCurrentController|TestCurrencyCommandPersistsAndRefreshesCurrentController|TestRenderInlineMath|TestCopyToClipboard' -count=1
- go vet ./internal/cli
- pnpm exec tsx src/__tests__/message-selection-copy.test.ts
- pnpm typecheck
- pnpm test:typecheck
- git diff --check
Co-authored-by: SivanCola <[email protected]>
Desktop (Wails):
- MarkdownRenderer annotates .katex/.katex-display/.katex-html with
data-latex-source attribute from rehype-katex MathML annotation
- messageSelectionCopy handler reads data-latex-source and replaces
KaTeX rendered text with the original $...$ / ... LaTeX markup
- Auto-locks to the full formula when any part is selected
- Collects all display blocks in the range for multi-row formulas
CLI (TUI):
- mathNode retains both value (Unicode display) and source (raw LaTeX)
- mdRenderer gains rawMath mode and RenderRaw() method
- renderAssistantMarkdownRaw builds a parallel transcript with
raw LaTeX preserving $...$ delimiters
- selectedText() uses rawWrappedLines when available to restore
original LaTeX on clipboard copy
Fixes: Ctrl+C copy of LaTeX math expressions in both desktop and CLI
* fix(cli): restore terminal interactions and transcript hierarchy
Problem:
- Captured mouse mode could copy selections but could not right-click paste clipboard text.
- Composer drag selections were visible but did not reliably reach the system clipboard.
- Non-Git workspaces reserved or misaligned an empty footer data band.
- Assistant answers lacked a distinct identity and spacing from reasoning and turn receipts.
Root cause:
- Mouse handling had no text-only clipboard read path, composer selection release stopped at local state, and footer/transcript rendering assumed fixed visual bands.
Fix:
- Route right-click clipboard text through the canonical paste event and auto-copy composer drag selections.
- Make optional footer telemetry compact and left-aligned outside Git repositories.
- Render assistant markdown with a Reasonix identity, stable indentation, replay support, and semantic spacing.
- Document the interaction changes and add focused regression coverage.
Verification:
- go test ./internal/cli
- go test ./...
- git diff --check
* fix(cli): clarify right-click paste over SSH
Problem: Captured right-click paste attempted to read the remote host clipboard during SSH sessions and the documentation implied it could access the user's local clipboard.
Root cause: The right-click path called the native clipboard reader without applying the remote-session boundary already used by clipboard copy.
Fix: Detect SSH sessions before reading clipboard text, preserve the composer, show a localized terminal-paste hint, and document local versus remote behavior.
Verification: go test ./internal/cli; go test ./...; go vet ./...; git diff --check.
* test(cli): isolate local clipboard cases from SSH
Problem: Four local clipboard interaction tests inherited SSH environment variables and failed when the suite ran on a remote development host.
Root cause: The tests stubbed native clipboard operations without explicitly selecting a local clipboard session.
Fix: Add a shared test helper that clears all supported SSH markers and use it in each local paste and drag-copy case.
Verification: Normal and simulated-SSH go test ./internal/cli -count=1; go test ./...; go vet ./...; git diff --check.
* feat(cli): polish composer and responsive status UI
* fix(cli): remove obsolete status helpers
* chore: credit status footer co-contributor
Attribution:
Credit @billycat for the full-data TUI status footer restoration and height-accounting coverage contributed in #6587.
Relationship:
This PR carries that behavior forward in the redesigned responsive footer. This attribution-only commit does not change the runtime tree.
Verification:
Confirmed this commit has the same tree as its parent.
Co-authored-by: billycat <[email protected]>
* polish(cli): refine receipt and themed footer
Problem:
The wide footer overemphasized per-turn metadata, separated effort from its model context, and used faint or hard-coded colours that did not preserve the same hierarchy across dark and light terminals.
Fix:
Use a single receipt boundary with softer separators, group model/effort/work together, add a theme-specific subtle contrast token, and render explicit effort through each theme's semantic info colour. Keep dark, light, and no-colour geometry identical.
Verification:
- go test ./internal/cli -count=1
- DEEPSEEK_API_KEY= go test ./... -count=1
- git diff --check
* polish(cli): tint composer borders with theme accent
Problem: The composer top and bottom borders stayed neutral, so the input area did not reflect the active CLI theme.
Root cause: refreshCLIStyles bound the composer border foreground to the neutral border token.
Fix: Bind both composer borders to the active accent and cover every dark and light theme style plus NO_COLOR.
Verification: Focused composer theme test and the full internal/cli suite pass with an isolated user home.
* fix(cli): localize persistent footer labels
Problem: Chinese and Traditional Chinese sessions still showed English metric labels in the persistent footer.
Root cause: The new footer embedded MODEL, EFFORT, WORK, CACHE, CTX, COMPACT, JOBS, and BAL as literals outside the i18n catalog.
Fix: Add localized footer labels, use them at every metric call site, and calculate model width from the active label.
Verification: i18n catalog tests and the full internal/cli suite pass with an isolated user home.
* fix(cli): spell out footer shortcuts
Problem: The persistent footer used terminal shorthand for Shift+Tab and Ctrl+Y, which was difficult to recognize at a glance.
Root cause: The compact locale strings preferred symbolic key notation without a responsive fallback for the extra width of readable names.
Fix: Spell out both shortcuts in every locale, wrap semantic groups on medium terminals, and hide optional shortcut help only when a complete key-name group cannot fit.
Verification: The i18n catalog and full internal/cli test suites pass with an isolated user home.
* fix(cli): localize work mode values
Problem:
Chinese CLI sessions showed an English work-mode value such as '模式 balanced', unlike the localized desktop UI.
Root cause:
The CLI used one helper both for stable command identifiers and user-facing profile labels, so English command names leaked into localized status and notices.
Fix:
Separate stable work-mode names from localized display labels, add English, Simplified Chinese, and Traditional Chinese catalog entries, and keep slash-command completion values unchanged.
Verification:
- go test ./internal/i18n -count=1
- go test ./internal/cli -run 'Test(StatusFooter|WorkMode|RuntimeProfileDisplay|RenderWorkModes)' -count=1
- git diff --check
* fix(cli): use platform image paste shortcut in tests
Problem: The image-paste selection regression test always sent Ctrl+V, so the Windows CI job could not enter its Alt+V image-paste path.
Root cause: The test ignored the platform mapping already enforced by imagePasteShortcut.
Fix: Select Ctrl+V on macOS/Linux and Alt+V on Windows, and make the test name and diagnostics platform-neutral.
Verification: The isolated internal/config, internal/i18n, and internal/cli suites passed; the Windows/amd64 internal/cli test binary also cross-compiled successfully.
* docs(cli): document responsive composer controls
Documentation: Explain footer states and telemetry, theme-aware composer behavior, platform-specific text and image paste, and the stable mode shortcuts in the English and Chinese CLI guides.
Website: Mirror the operation and shortcut guidance on the public docs page and refresh the CLI homepage summary.
Changelog: Record the responsive composer, localized footer, clipboard split, and theme behavior under Unreleased.
Verification: The site test suite and Astro static build passed, and git diff --check reported no whitespace errors.
---------
Co-authored-by: billycat <[email protected]>
Releasing a left-drag selection in the TUI transcript now copies it to
the clipboard automatically (native terminal convention), with a
transient status-line "copied to clipboard" notice shared by the
mouse, right-click, and Ctrl+C copy paths.
Add "/mouse" to release mouse capture back to the terminal on demand,
restoring its native click-drag selection and right-click context
menu at the cost of in-app drag-select, the scrollbar, and
wheel-scroll while it's off. REASONIX_DISABLE_MOUSE=1 starts every
session with it off.
Clarify that OSC 52 clipboard copy depends on terminal support while avoiding remote-host platform clipboard writes in SSH/tmux flows.
Co-authored-by: SivanCola <[email protected]>
Remove the atotto/clipboard dependency from copyToClipboard and the
two-tier strategy that tried the platform clipboard (xclip/wl-copy/pbcopy)
first and fell back to OSC 52 on failure.
Over SSH, the platform tool writes to the remote server's clipboard — the
fallback never triggered locally because clipboardWriteAll returned nil
success. OSC 52 works in all environments (local, SSH, tmux, modern
terminals) and is the correct single path.
- transcript.go: remove clipboardWriteAll var, simplify copyToClipboard
- transcript_test.go: replace two-tier tests with direct OSC 52 test
- chat_tui_test.go: remove clipboard stubs from Ctrl+C tests
copyToClipboard discarded the atotto error and never reached the system
clipboard inside tmux or over SSH, where xclip/xsel/wl-copy cannot see the
display server. Fall back to OSC 52 so tmux and modern terminals forward
the copy to the host clipboard.
The fallback returns tea.SetClipboard(text)() — the *message*, run here —
not the command: a tea.Cmd returned as a tea.Msg is not recognized by the
runtime's dispatch and is dropped, so it would emit nothing. The message
(setClipboardMsg) is handled by the runtime, which writes the OSC 52
sequence to the terminal.
Closes#2716.
Co-authored-by: reasonix <[email protected]>
* feat(cli): add transcript buffer + viewport plumbing for alt-screen migration
Inline rendering can't repaint cleanly when the terminal shrinks — bubbletea
v2's inline renderer has no width-change full-reset path, so a narrowed frame
leaves ghost borders. The chat TUI will move to alt-screen + an in-app
viewport to fix it.
First stage, no visible change: maintain the committed transcript and a
sized viewport alongside the existing tea.Println scrollback path. View()
still renders inline; the viewport is populated and sized but not yet shown.
* feat(cli): render chat transcript in an alt-screen viewport
Switch the chat TUI from inline rendering to alt-screen with an in-app
scrollable viewport. Alt-screen owns the grid and repaints the whole frame,
so terminal resize no longer strands ghost borders — the bug inline mode
couldn't fix (bubbletea v2's inline renderer has no width-change full reset).
- View renders the viewport (transcript) above the pinned bottom region and
requests AltScreen + mouse; the viewport pads to fill the screen.
- Update wraps the model loop to keep the viewport sized and fed, tail-
following newest output only while the user is at the bottom.
- Wheel + PgUp/PgDn scroll the transcript (history is no longer terminal-
native scrollback).
- Drop the inline-only machinery: tea.Println chunking, the forceRepaint /
repaintToggle resize nudge, chunkLines, scrollChunkHeight.
* feat(cli): scrollbar + mouse text selection with Ctrl+C copy
Restore the read-back affordances alt-screen took away, in-app:
- A scrollbar column on the right of the transcript (thumb tracks scroll
position; blank when content fits).
- Left-drag selects transcript text and reverse-highlights it across lines;
selection is keyed by absolute content line so scrolling never moves it.
- Ctrl+C copies the selection to the system clipboard (terminal convention:
copy when something is selected, otherwise the existing interrupt/quit).
Selection clears on the next keystroke or plain click.
Transcript rendering, selection, and clipboard live in transcript.go to keep
chat_tui.go from growing further.
* fix(desktop): bind vite dev server to IPv4
Unset host makes Vite listen on ::1; the Wails dev proxy dials [::1] and
fails on Windows hosts where IPv6 loopback is filtered, so the webview shows
a 502. Pin server.host to 127.0.0.1.
* fix(cli): correct alt-screen transcript rendering, scroll, and selection
Follow-ups after testing the alt-screen viewport across terminals:
- Wrap the transcript with lipgloss instead of ansi.Hardwrap so styled spans
that span a wrap point are closed at the line end and reopened next line;
Hardwrap left them open and bled the attribute into the padding and the
following row on stricter terminals.
- Pad the status rows to full width so they overwrite the whole line (an
unpadded status left stale cells from the prior frame on the right).
- Force a full repaint whenever the viewport offset moves (wheel, PgUp/PgDn,
edge auto-scroll, tail-follow) — some terminals mishandle the renderer's
scroll/insert-line optimization and strand rows.
- Tame edge-drag auto-scroll: one line per tick, stop at the boundary so a
held edge can't run the selection to the end.
- Cache the wrapped lines and drop the per-row lipgloss pad in renderTranscript
so a drag (which re-renders on every mouse move) stays cheap.
clipboard moves to a direct dependency (used by Ctrl+C copy).
---------
Co-authored-by: reasonix <[email protected]>