Resolve .reasonix/reasonix.toml only when the root project config is absent. Share selection across loading, local edits, installs and repair; retain project-scope restrictions and reject symlink escapes.
Fixes#6007
The crash files are the runtime's raw output and are not redacted, only
shell.log and host.log are. The notice now follows the same language the
page draws in instead of the machine locale.
A Studio kernel that died of a runtime fatal left no readable trace: its
stderr sat in host.log, which request logging rotated away every few
minutes, and the shell quit without recording how the kernel ended.
Routes that are polled declare themselves with routine(); a successful
request to one logs at Debug, a refused one stays at INFO. The kernel
mirrors runtime crash output into a bounded crash directory the shell
names, the shell quotes the tail into shell.log and the next launch tells
the person once.
Answering the last question of a multi-question ask with nothing left
unanswered commits the batch immediately, skipping the extra confirmation
Enter; an incomplete batch jumps to the first unanswered question instead. The
behaviour is gated on a new top-level `auto_submit` option (default false):
with it on the ask drops its Submit tab, and without it the Submit tab is shown
and still needs its explicit Enter.
`auto_submit` is a user preference: LoadForRoot holds it across the project
merge so a cloned repository cannot turn it on, and neither the annotated
project render nor the project delta carries it. Answering moves on to the next
unanswered question, so answering one the panel was thrown back to skips ahead
to the next gap rather than stopping on the already-answered question that
follows it; only the last question, with nothing left unanswered, commits. With
auto-submit on there is no Submit tab to page onto, so right stops at the last
question rather than landing on a hidden tab that commits.
- config: new top-level auto_submit, user/global only; rendered in the user's annotated TOML and omitted from a project render and the project delta.
- tui: Options.AutoSubmit; nextQuestion advances to the next unanswered question and commits on the last; right stops at the last question instead of a hidden Submit tab.
- program_test.go: the commit, next-unanswered-after-skip-back, skipped-question, hidden-tab, no-option and arrow-stops-at-last cases.
- docs: auto_submit in CONFIG_PATHS.md, reasonix.example.toml, and the Guide shortcut tables (en and zh-CN).
A shell tool result whose whole output is a unified diff — bash running
`git diff`, or `git show` in its default format — is tagged and rendered as a
diff: the TUI draws it through the same rows a ```diff fence produces, ACP
fences it for its client, and the desktop card renders it with DiffView.
The tag is computed once, at the one sink every emitter shares, and is gated by
the opt-in [agent].embedded_diff_detection (default false). Detection is
whole-text and conservative (diff.IsUnifiedDiff): every line must belong to the
diff grammar — including a `git show` / `git log -p` commit header and its
indented message — and a lossy Bound is never tagged, so a diff followed by
prose, a build log, or a truncation note stays prose. The tag is re-derived when
a session is rebuilt from /history, so a reopened session renders the same diff
the live one did.
The desktop DiffView splits a multi-file diff into per-file sections by the same
rule the CLI uses (git's "diff --git", else the "--- "/"+++ " pair) and names
each with its own "path +A -B" header row, so a `git diff` over several files no
longer shows only its first file's name over rows that belong to all of them.
A single-file diff keeps the card headline as its only name, so the fact is not
said twice.
trimDiffPreamble returning "" for a section with no --- line left a ```diff
fence without a --- /+++ pair rendering as an empty block. splitDiffSections now
returns sections untrimmed; the fence path trims each and falls back to the plain
code rail when it carries no file header, while the shell-diff path still drops a
pure preamble. A marked shell diff keeps the card's "⎿" connector on its first
row, like every other tool card's body. /history reads the embedded-diff flag the
session was built with instead of reloading config per request, and the hunk
parser rejects a body that overshoots its declared count.
[cli].diff_formatter names an optional external argv line (run without a shell)
that formats a diff before the CLI/TUI emits it — both a fenced
```diff/```patch block and a writer tool's diff card. The whole diff goes to the
command's stdin and its stdout is re-emitted with non-SGR control sequences
stripped. On failure, timeout, empty output, or an oversized block the built-in
renderer is used. Like [cli].update_channel it is user/global only: a
project-local reasonix.toml cannot run a command on the user's machine.
The run is memoised per (block content, width), because a View re-renders many
times while a fence streams and again on every resize. It runs only on a closed
fence, so a fence still streaming spawns no subprocess per delta, and the command
is handed the terminal width via COLUMNS with its output clamped to it. A
WaitDelay bounds the wait for a formatter's pipes, so one that leaves a child
holding stdout cannot block the render past the timeout. The writer card drops
the "--- /+++ " pair before piping, so the formatter cannot re-emit a raw host
path the card header already names.
This commit also wires [cli].diff_fences, the opt-in toggle that turns the
fenced-diff renderer on.
Studio has no bot surface. On this line the QQ / Feishu / WeChat gateway
is reachable only as `reasonix bot`, a CLI command no Studio user starts,
and it carries about 12.9k lines: internal/bot, internal/botruntime, the
subcommand, its config section and its docs. It is also where the recent
Windows heap-corruption and gateway-wedge fixes had to land.
What goes with it:
- `reasonix bot`, its completion spec and its usage line in three locales.
- The typed [bot] config, its defaults, render block, model-ref and
migration hooks, and the 0.x QQ import.
- GatewayAPI, the control port only the gateway named, and the two
passive inbox variants only it called; neterr.IsTransient and
textutil.FitGraphemeBytes, which only it used.
- The bot entries in repolint's layer and frontend-parity declarations,
the bot race-test target in CI, BOT_GUIDE.md and its images, and the
[bot] example.
What stays, and why:
- The user config is shared with 1.x installs, which still run the
gateway. [bot] now decodes into an opaque table and is written back
unchanged on every save, so a settings change here does not switch
that gateway off or erase its allowlists. Credential variables the
table names are still kept out of subprocess environments.
- surface.Bot stays: usage records on disk carry it, and dropping it
would re-attribute them to another frontend.
- SECURITY.md and the crash-report worker still describe the bot, since
they serve every release line.
Budgets only fall: frontend-parity drops by 20 edges and the reclaimed
headroom is recorded with -update.
The flash model is `deepseek-flash` everywhere: the id it is served under
lost the generation it never needed. An installed entry is renamed rather
than left behind, and the rename moves every key that named the old id with
it — a price or an effort list stranded under the old key stops answering
for the model that replaced it, which reads downstream as a setting the
user edited reverting to its default.
A preset is now what settles which of its models read images. Reading a
name misses every Kimi, Qwen, MiniMax and Claude model these presets
declare, and `deepseek-flash` spells nothing at all; the spelling stays the
last resort for an address nothing has declared, and stays a correctable
suggestion rather than a claim. The connection that existed only to reach an
image-taking model folds into the entry beside it once that entry reaches
the same model on the same key — unless its user configured it, because
folding would answer their edit by deleting where it lived.
A preset changing shape reaches installs holding the old one through one
declared shape per generation and one upgrade that brings a matching entry
forward. The guard is the whole shape, because an entry holding what we
shipped is ours to move and anything else its user curated. That retires
nine hand-written migrations that each recognised one vendor by hand.
Official list prices live once, in `officialRates`, oldest generation first.
A price change appends rather than edits: the superseded rate is what lets
an installed config be recognised as holding one of ours and brought
forward, instead of quoting a retired rate for the rest of its life. `config`
reads them through `billing.CurrentRate` and `SupersededRates` and restates
none of them — a table written twice is one that drifts, which is how a
vendor's USD rate ended up published on one side only.
`make pricecheck` reads each vendor's own page and compares it against that
table, and says how long ago a person last confirmed each one. It reaches
the network, so it stays out of lint and out of CI. A page it cannot read
exits non-zero: unread is its own verdict and never counts as agreement.
Same situation as the session catalog: nothing read it. Its only importer was
the CLI command that rebuilt it, and no product path ever opened the file that
command wrote. Task snapshots and event logs were already the authoritative
data and are read directly.
That leaves two projections, both with real readers and both doing something a
directory scan cannot: history-search maintains an inverted index for BM25,
usage-catalog a precomputed rollup.
Nothing read it. Open, ReconcileDirectory, ListSessions and GetSession had zero
callers outside the package: the sidebar, the CLI picker, the bot and doctor all
list sessions by scanning the directory, which answers from sidecars and never
decodes a transcript. The only entry points were `sessions reindex` and
`doctor sessions`, which rebuilt and reported on a file no one then opened.
Measured before removing: scanning 186 sessions costs 17ms, and the sidebar
reads one workspace directory at a time — on this machine 461 of them averaging
a couple of sessions each, so well under a millisecond each.
What stays is what a scan cannot do. history-search needs an inverted index for
BM25; usage-catalog needs a precomputed rollup. Both have real readers. A
projection earns its place when the authoritative data cannot answer directly
and quickly — listing a directory can, because Turns and Preview already sit in
the sidecar next to each file.
The cost of keeping it was not hypothetical: both sidebar bugs fixed this week
were the projection misreading data that was itself correct. 4400 lines, three
schema migrations and a reconcile loop, for rows nothing displayed.
The memory index added yesterday to replace it goes too — it was written for a
consumer that turned out not to exist. Its comparison harness is in the history
if a projection is ever warranted again.
* fix(session): bind generation write authority and stop false diverged
Problem:
- Mid-turn autosaves classified same-revision reshapes as diverged and
forked recovery branches every ~30s (#8294).
- Process-level "I hold a lease" ownership could authorize stale
controllers after rebind (ABA).
- Empty/missing .jsonl checkpoints with a valid WAL could still enter
conflict paths instead of healing in place.
Root cause:
- checkSnapshotWrite required digest ownership for same-revision rewrites
and had no unforgeable write permit bound to controller generation.
- Release did not wait for in-flight authority-guarded saves.
Fix:
- Introduce SessionWriteAuthority issued by SessionLease with generation
and owner binding; Release waits for active saves.
- Controllers, desktop tabs, CLI, serve, and bot bind authority before
Ready/autosave; missing/stale authority returns typed errors and never
forks recovery.
- Owned same-revision rewrite uses authority or digest ancestry; 0-byte
checkpoints heal from the path's own WAL under the save locks.
- Prefer SaveRewrite for same-revision diverged before recovery fork.
Verification:
- go test ./internal/agent/ -run 'WriteAuthority|SaveSnapshot|SaveConflict|FixedWriter|ZeroByte|Authority'
- go test -race ./internal/agent/ ./internal/control/ focused subsets
- go vet ./internal/agent/ ./internal/control/
Cache-impact: none - no prompt, tool schema, or provider request changes
Documentation-impact: none - internal session save/authority contracts
Fixes#8294
Refs #8342#7982
* fix(session): bound recovery paths and rebind projection lineage
Problem:
- Depth-cap and continuous conflict ticks created digest-keyed recovery
files without bound (#8342).
- Projection sidecars dropped across upgrade/model switch (#8390) and
recovery forks (#8400), snapping the model view to full canonical
history and re-triggering expensive compaction (#8393).
Root cause:
- Isolated recovery paths were content-digest keyed.
- LoadProjectionSidecar treated lineage key mismatch as total invalidation.
- Recovery forks did not inherit a still-valid parent projection.
Fix:
- One fixed recovery file per {lineage, writer}; in-place rewrite with
compact event log and preserved in-flight turn markers.
- Split projectionContentValid from lineage gate; rebind matching content
across key changes and fail closed on prefix mismatch.
- Inherit valid parent projection onto recovery forks; load sidecar on
commitRecoveredSession.
- Dedup conflict diagnostic log entries by path/revision/outcome.
Verification:
- go test ./internal/agent/ -run 'LoadProjection|SaveRecoveryBranchInherits|SaveRecoveryBranchSkips|SaveConflict'
- go test ./internal/control/ -run 'Snapshot|Recovery|Conflict'
Cache-impact: none - projection body and provider-visible prefix unchanged;
rebind only rewrites lineage key; removed full-canonical fallback was a
cache break by construction.
Cache-guard: go test ./internal/agent/ -run 'ProjectionValid|LoadProjectionSidecar|CoveredPrefixHash'
Documentation-impact: none - internal projection/recovery contracts
Co-authored-by: SivanCola <[email protected]>
Co-authored-by: erphenheimer <[email protected]>
Fixes#8342Fixes#8111Fixes#8393
Refs #8390#8400#8444#8430
* fix(catalog): rebuild v3 lineage and reclaim covered recovery copies
Problem:
- 1.24.0/1.24.1 users accumulated covered recovery copies; v1/v2 catalogs
could be polluted and still listed near-duplicates.
- Open paths could keep pointing at covered ancestors when a unique
adopted leaf already covers the group.
- Periodic GC waited a full day before clearing upgrade storms.
Root cause:
- Disposable catalog lacked recovery lineage roles and used v2.sqlite
shared with polluted 1.24.1 data.
- No content-based group classification (covered/adopted/diverged).
- Startup GC used the same long grace as steady-state.
Fix:
- Independent session-catalog/v3.sqlite (v1/v2 left byte-unchanged).
- recovery_group_id / recovery_role / recovery_canonical derived from
ParentID and content coverage, never filename alone.
- Optional SessionMeta fields for Wails; old clients ignore them.
- Retarget Resume to a unique adopted canonical leaf before Controller
rebind/authority bind.
- 15-minute startup grace + 20-minute follow-up, then 6h period; only
covered_copy moves to recoverable .trash after re-checks.
Verification:
- go test ./internal/sessioncatalog/
- go test ./desktop -run 'RecoveryGC|SessionMeta|RecoveryCopy'
Cache-impact: none - catalog is a disposable projection of authoritative
session files; provider-visible prompts unchanged
Documentation-impact: none - catalog stays internal; release notes follow
Refs #8294#8425
* fix(desktop): surface diverged recovery branches and v1.24.2 notes
Problem:
- Topics with multiple independent recovery leaves looked like ordinary
multi-session topics without an explicit choice prompt.
- Release notes needed a clear split between v1.24.1 hygiene and the
v1.24.2 root-cause fix.
Fix:
- Project tree marks topics with multiple diverged recovery branches
(non-destructive; no auto-merge/delete).
- Frontend SessionMeta carries optional recoveryGroupId/role/canonical.
- CHANGELOG documents authority/lineage/GC behavior and distinguishes
v1.24.1 (hide/reclaim copies + Windows flash) from v1.24.2 (stop the
source, bound real conflicts, repair existing directories).
Verification:
- go test ./desktop -run 'SessionCatalog|Recovery'
- go test ./internal/sessioncatalog/ -run 'Lineage'
Cache-impact: none
Documentation-impact: updated - CHANGELOG Unreleased records v1.24.2
recovery root-fix scope for operators and release drafting
* fix(acp): bind session write authority with transcript leases
Problem:
- ACP acquired session leases but did not issue a generation-bound write
authority, leaving saves on the unbound path while other frontends
required authority for ownership decisions.
Fix:
- After session/new, session/load resume, and recovery lease handoff,
bind SessionWriteAuthority on the concrete controller.
- Test fakes without *control.Controller stay unbound.
Verification:
- go build ./internal/acp/
- go test ./internal/acp/ -run 'Lease|Recovery|Switch'
Cache-impact: none
Documentation-impact: none - ACP internal lease handoff only
* Fix session authority and recovery handoffs
Problem: stale controller generations could retain write access, recovery callbacks could publish a path without rebinding authority, and process-wide recovery filenames could overwrite independent transcripts.
Root cause: authority validity was not tied to the lease's live generation, frontend handoffs updated leases and controllers in different phases, and canonical recovery promotion lacked full-lineage content proof.
Fix: make authority admission fail closed, bind controller and lease changes atomically across CLI, serve, ACP, bot, and Desktop, isolate recovery lanes per live session, delay startup GC, preserve legacy recovery filename discovery, and promote catalog canonical leaves only after authoritative content coverage checks.
Verification: focused package suites, full Desktop tests, deterministic authority and recovery regressions, go vet, and race tests for agent, control, sessioncatalog, ACP, serve, CLI, and bot.
* Fix session authority handoff review findings
Problem:
The integrated recovery fix still left stale controller generations writable across several runtime handoffs, reused process-wide recovery lanes, and widened the repolint struct-state baseline.
Root cause:
Lease ownership, controller publication, recovery lineage classification, and catalog cleanup were not committed through one fail-closed authority path on every frontend.
Fix:
Bind generation-scoped write authority before publication across Desktop, ACP, Serve, CLI, and Bot; bound recovery files per live session; verify lineage from transcript content; preserve startup/GC compatibility; extract hotspot helpers and restore immutable agentConfig without changing the lint baseline.
Verification:
go test core affected packages; Desktop go test ./...; focused and surface race suites; Desktop recovery/catalog race; pnpm build; go run ./tools/repolint; git diff --check.
* Fix session recovery lint regressions
Remove two superseded helpers and replace the empty same-revision authority branch with its explicit SaveRewrite ownership contract.
Verification: golangci-lint v2.12.2 reports 0 issues; repolint is clean; agent, control, and CLI tests pass.
* Fix ACP recovery lease retirement
* Deliver the diverged-recovery prompt and correct catalog docs
Problem: the diverged-recovery choice prompt never reached the UI, and the
reference docs still named the previous catalog file.
- session_catalog_runtime.go set node.Status = "diverged_recovery", but
both status whitelists (desktop/tabs.go and projectTreeTopic.ts) accept
only six values, so the status was dropped and the advertised prompt never
surfaced.
- The same block appended a hardcoded English "multiple recovery branches"
label, bypassing the localization used elsewhere in the function and leaking
English into every non-English locale.
- Four docs/* files still documented session-catalog/v2.sqlite as the live
catalog, so anyone clearing a corrupt catalog would delete an orphaned file.
- The CHANGELOG described the 15-minute value as a post-startup wait; the code
uses it as an idle threshold on two early sweeps before falling back to the
24-hour threshold on the 6-hour ticker.
Root cause: the status value and its label were added at the producer without
registering the value with the shared whitelists or the locale dictionaries.
Fix: register diverged_recovery as a first-class topic status. Add the
topicStatusDivergedRecovery constant, admit it in both whitelists, and drop
the hardcoded label so the frontend renders it from the locale dictionaries
(en, zh, zh-TW). Live runtime states take priority in topicStatus() so a
running topic still reports streaming, and the marker does not block
archiving since it is informational and awaits a user decision. Point the four
docs at v3.sqlite, note that v1/v2 stay for coexistence, and restate the GC
grace accurately. Shorten the startRecoveryGC doc comment to satisfy the
repolint budget resynced by #8503.
Verification:
- go test ./internal/agent/ ./internal/control/ ./internal/sessioncatalog/ -race
- cd desktop && go test .
- npm run typecheck (desktop/frontend)
- make lint (golangci-lint 0 issues; repolint clean, baseline unchanged)
- New TestNormalizeTopicStatusKeepsDivergedRecovery fails without the
whitelist change.
* desktop: bump raw bundle budget for diverged-recovery i18n
The trilingual projectTree.status.divergedRecovery labels and
ProjectTopicStatus union addition add 0.1 KiB raw JS+CSS. Ratchet the
budget from 2268.5 to 2268.7 KiB (0.2 KiB headroom). Gzip budgets stay
flat — the strings compress well and aren't on the startup path.
* fix(desktop): restore recovery-copy catalog and hide Windows git flash
Problem:
- Windows desktop cold-start flashed two git.exe consoles because workspace
watch probes used raw exec.Command without HideWindow/CREATE_NO_WINDOW.
- Session catalog v1 lost RecoveryCopy, so the project tree flooded with
redundant recovery "copies" and History could not group or safely clean them.
Root cause:
- workspace_watch gitMetadataDirsForWorkspace bypassed gitcmd.Command.
- Catalog schema/projection never stored recovery_copy; topic stats summed all
recovery branches; sessionMetaFromCatalog dropped RecoveryCopy; background GC
used unguarded DeleteSession.
Fix:
- Route both startup rev-parse probes through gitcmd.Command.
- Add SessionRecord.recoveryCopy + schema v4; open disposable v2.sqlite cache.
- Prove RecoveryCopy via agent.RecoveryBranchCoveredByParent at index time.
- Exclude idle recovery copies from tree/stats; keep open/pinned/adopted ones.
- Propagate RecoveryCopy into History; GC via DeleteRecoveryCopy with recheck.
- Focused regression tests for catalog, tree pagination, GC races, and Windows.
Verification:
- go test ./internal/sessioncatalog/ -count=1
- go test -race ./internal/sessioncatalog/ ./internal/agent/ -run Recovery
- desktop focused recovery/catalog/workspace tests (+ -race)
- go run ./tools/repolint
Cache-impact: none - no prompt, tool schema, or provider request changes
Documentation-impact: updated - session catalog cache path v1→v2 docs
* fix(desktop): make recovery cleanup atomic
Problem:
- Adopted recovery branches were added to ordinary topic turns instead of treated as alternate continuations.
- DeleteRecoveryCopy proved coverage before acquiring the parent and branch removal guards, leaving a logical race before the trash move.
Root cause:
- Catalog SQL only distinguished covered copies from all other rows.
- Desktop reused the generic delete path whose coverage check preceded its destructive lock boundary.
Fix:
- Restore the pre-catalog turn rule: max(sum(normal turns), max(adopted recovery turns)).
- Route recovery-copy cleanup through an atomic agent helper that locks the parent and branch, re-proves coverage, and publishes a recoverable trash entry.
- Preserve open/read-only recovery branches instead of detaching them during bulk cleanup.
Verification:
- go test ./...
- go test -race ./...
- desktop go test ./...
- desktop go test -race ./...
- go vet for root and desktop modules
- Windows amd64 desktop test cross-compile
- go run ./tools/repolint
Cache-impact: none - no prompt, tool schema, provider request, or compaction changes.
Documentation-impact: none - behavior completes the recovery catalog contract documented by the parent commit.
* feat(provider): add exact custom request URLs
Problem:
Custom provider setup mixed base URLs and full request URLs, while legacy chat_url semantics differed across protocols.
Root cause:
The desktop editor reused chat_url for both legacy compatibility behavior and new exact endpoint intent.
Fix:
- add request_url as an explicit exact endpoint field
- preserve legacy chat_url behavior for existing OpenAI, Anthropic, and Responses configs
- simplify the desktop editor to one API address field and migrate only on save
- document and test global and project config round trips
Verification:
- frontend production build and focused editor tests
- focused provider, config, boot, and desktop tests
- desktop go test ./...
* fix(provider): preserve base URL on unchanged endpoint
Problem:
Saving an existing custom provider could rewrite base_url from request_url, changing model discovery and endpoint-derived capabilities even when the user had not changed the API address.
Root cause:
ProviderEditor always derived base_url from the effective request URL instead of preserving the independent stored value for unchanged providers.
Fix:
Preserve the stored base_url when provider kind and effective request URL are unchanged, and derive a new base only for new providers or intentional endpoint/protocol changes. Extract endpoint compatibility logic and provider request URL tests into focused owners, with exact repolint ratchets for the cross-layer contract.
Verification:
- pnpm test:all && pnpm build
- go test ./...
- cd desktop && go test ./...
- go test -race ./internal/provider/openai ./internal/provider/anthropic ./internal/provider/responses
- go vet focused root packages and desktop ./...
- go run ./tools/repolint
* fix(provider): mirror exact URL for legacy OpenAI
Problem:
Older clients ignore request_url and could keep using a stale chat_url after a custom OpenAI-compatible provider was saved with a new exact endpoint.
Root cause:
The desktop editor and save boundary preserved chat_url independently from the authoritative request_url.
Fix:
Mirror the exact OpenAI-compatible request URL into legacy chat_url in both the editor payload and desktop persistence boundary, retain non-OpenAI legacy behavior, and document the downgrade limitation. Add focused downgrade-compatibility coverage and complete the Chinese specification.
Verification:
- pnpm test:all
- pnpm build
- go test ./...
- (cd desktop && go test ./... && go vet ./...)
- go test -race ./internal/provider/openai ./internal/provider/anthropic ./internal/provider/responses
- go vet ./internal/provider/openai ./internal/provider/anthropic ./internal/provider/responses ./internal/config ./internal/boot
- go run ./tools/repolint
- git diff --cached --check
* test(provider): align exact URL copy assertion
Problem:
The provider editor test still asserted the previous English helper copy after the production hint was shortened without changing its meaning.
Root cause:
The final copy-size adjustment and its standalone TSX assertion were not updated together.
Fix:
Assert the current unchanged-URL helper text so the focused editor test covers the shipped copy again.
Verification:
- pnpm exec tsx src/__tests__/provider-editor-model-picker.test.tsx
- pnpm build
- go run ./tools/repolint
- git diff --cached --check
Add the cross-project task catalog, Desktop task center APIs, and CLI
reindex surface on the shared multi-domain SQLite projection stack while
keeping history/usage registration and the hardened session foundation.
Add idempotent usage rollups as a disposable SQLite projection with CLI
doctor/reindex surface, layered on the shared projection lifecycle and
preserving the hardened session/history foundations.
Problem: the isolated Task Catalog branch lacked shell completion metadata and only flushed its shared projection during desktop shutdown, allowing late reconcile goroutines to outlive the shutdown boundary.
Root cause: the task implementation was originally layered on the integrated History branch, so domain-specific lifecycle and documentation details were lost when it was split into an independently reviewable branch.
Fix: register task reindex completions, document the task cache and recovery commands, gate reconcile scheduling during close, wait for scheduled workers, close the shared catalog within the 250ms desktop drain, and account for the measured 1 KiB frontend bundle increase.
Verification: focused Desktop tests, frontend production build and bundle budgets, repository lint, core package tests, and race tests for projectiondb, taskcatalog, taskmonitor, and control passed.
Problem: Operators need a discoverable cache path, diagnostic command, exact-fallback guarantee, and safe rebuild path for usage rollups.
Fix: Link the bilingual Usage catalog guide from CLI and storage references and ratchet repolint to the Usage-only branch shape.
Verification: repolint baseline generation and git diff --check passed.
Problem: Users and operators need a discoverable cache location, diagnostic command, and safe rebuild path for indexed history.
Fix: Link the bilingual history catalog guide from CLI and storage references, document its cache path, and ratchet repolint to the History-only branch shape.
Verification: repolint baseline generation and git diff --check passed.
Problem:
Desktop startup and project-tree queries could synchronously migrate and decode legacy session JSONL, blocking navigation, creation, and shutdown.
Root cause:
Session discovery, metadata repair, controller admission, and plugin startup shared UI-critical paths and broad lifecycle locks.
Fix:
- add a disposable SQLite session catalog with incremental reconciliation, repair checkpoints, corruption recovery, and memory fallback
- expose revisioned cursor pagination and migrate the desktop project tree to lazy loading
- move controller construction outside lifecycle admission and isolate incompatible plugin startup
- add session doctor and reindex commands while preserving authoritative JSONL and metadata
- retain CGO-free builds with platform-specific fallbacks
Verification:
- go test ./...
- cd desktop && go test ./...
- go test -race ./internal/sessioncatalog ./internal/agent
- cd desktop && go test -race .
- go vet ./... in root and desktop
- cd desktop/frontend && pnpm test:all && pnpm build
- CGO_ENABLED=0 cross-builds for supported macOS, Windows, and Linux targets
Problem:
Official DeepSeek setup exposed overlapping protocol entries, unverified web-search controls, and stale vision metadata that could emit image_url parts rejected with HTTP 400.
Root cause:
Provider defaults, capability normalization, request guards, and legacy settings migration did not share one authoritative official-endpoint contract.
Fix:
Make Anthropic Messages the recommended default, retain Responses as the alternate official entry, migrate only untouched legacy OpenAI configs, offer explicit upgrades for customized official configs, gate server web search and image serialization by verified endpoint, and refresh settings and documentation.
Verification:
- cd desktop && go test ./...
- go test ./internal/config ./internal/provider/anthropic
- frontend provider, startup contract, and settings snapshot tests
- pnpm exec tsc --noEmit
- pnpm check:css
- browser verification for DeepSeek and LongCat provider settings
Cache impact:
Changing an untouched legacy provider from OpenAI Chat Completions to Anthropic Messages creates a new provider cache prefix on the first request; subsequent requests reuse the stable Anthropic prefix. System prompts, tool schemas, and message ordering are unchanged.
Rebase and verify the small, independently-testable CLI fixes onto latest
main-v2 so they stop rotting behind required checks.
Problem:
- Several high-value contributor PRs were MERGEABLE but BLOCKED: required
lint/race/test matrix never re-ran against current main-v2.
- Users still hit missing --profile dispatch, machine-session resume,
usage-receipt clutter, Ctrl+Backspace, Insert copy/paste, and long-session
rewind list overflow.
Root cause:
- Fork heads drifted from main-v2; reusing their green checks was unsafe.
- Features are small and orthogonal, so they belong as focused landing work
before the larger integration PRs.
Fix:
- #7621: route bare `--profile` into the interactive session path.
- #7608: resolve `run --resume` against machine session IDs and document QUERY.
- #7587: add `[ui].show_turn_usage` (default true) without disabling accounting.
- #7605: accept Ctrl+Backspace as DeleteWordBackward with Alt+Backspace/Ctrl+W.
- #7262: Ctrl+Insert copies selection without Ctrl+C side effects; Shift+Insert
pastes via the native clipboard path (SSH-safe).
- #7202: store raw user text in checkpoints and window the rewind picker for
long sessions.
Verification:
- go test ./internal/cli ./internal/config ./internal/control -count=1
- focused tests for profile, machine resume, show_turn_usage, word motion,
Insert keys, rewind windowing, and raw checkpoint prompts
Co-authored-by: Hezi-crypto <[email protected]>
Co-authored-by: zhaotian1 <[email protected]>
Co-authored-by: azhonglee <[email protected]>
Co-authored-by: rixzkiye <[email protected]>
Co-authored-by: rongbaichuan <[email protected]>
Co-authored-by: mife <[email protected]>
* fix(mcp): trust and surface project servers
Problem:
Project-provided MCP servers were blocked behind a separate launch authorization flow and were not clearly distinguished from global servers in Desktop or CLI management views.
Root cause:
Project scope was coupled to launch approval, while configured-server lookup and presentation did not preserve project provenance consistently.
Fix:
Treat project MCP configuration as user-authorized by default, preserve workspace-scoped activation and relative working directories, load project config for reconnects, and group project, global, and managed servers consistently across Desktop and CLI.
Verification:
- ./scripts/cache-guard.sh
- go test ./...
- cd desktop && go test ./...
- pnpm build
- pnpm exec tsx src/__tests__/capabilities-panel-actions.test.ts
* fix(mcp): preserve config scopes and precedence
Problem:
Global MCP installs and project declarations shared persistence paths, so Desktop and CLI actions could write the wrong config, store activation against the wrong workspace, or remove same-name declarations across scopes.
Root cause:
Runtime config merging preserved the effective entry but persistence call sites did not consistently carry its owning source or the controller workspace.
Fix:
Write explicit installs to the user-global config, keep project edits in reasonix.toml or .mcp.json, apply deterministic project-over-global precedence, and restore the next same-name fallback after removing an override. Align Desktop, CLI, TUI, imports, Planner-facing runtime state, and documentation with the same rules.
Verification:
- go test ./...
- go vet ./...
- cd desktop && go test ./...
- cd desktop && go vet ./...
- cd desktop/frontend && pnpm exec tsx src/__tests__/capabilities-panel-actions.test.ts
- cd desktop/frontend && pnpm run typecheck
- cd desktop/frontend && pnpm build
- ./scripts/cache-guard.sh
- git diff --check
* test(mcp): satisfy staticcheck in handler
Problem:
The PR lint job rejected the new MCP test handler with staticcheck QF1003.
Root cause:
The handler used an if/else-if chain on the same request method value.
Fix:
Use a tagged switch for initialize and tools/list responses without changing test behavior.
Verification:
- go test ./internal/control
- git diff --check
* feat: trust configured project hooks by default
Problem:
Project hooks required a separate workspace trust action even though configured MCP servers and installed plugin hooks already use low-friction trusted defaults. This added a second confirmation model and left Desktop, CLI, diagnostics, and documentation describing a trust state users should not need to manage.
Root cause:
Hook loading and inspection were gated by a persistent trust.json store, with that state propagated through boot, slash commands, Wails settings, diagnostics, and compatibility payloads.
Fix:
Load project hooks automatically whenever a project root is present, while preserving Safe Mode as the emergency disable path. Remove the trust store and user-facing trust controls, and keep legacy CLI commands, Wails methods, LoadOptions.Trusted, and trusted_project JSON as backward-compatible no-ops or enabled values. Update tests, diagnostics, localization, and documentation to describe automatic loading.
Verification:
- env -u DEEPSEEK_API_KEY go test ./...
- cd desktop && go test ./...
- pnpm typecheck
- pnpm check:css
- pnpm exec tsx src/__tests__/diagnostics-settings.test.tsx
- ./scripts/cache-guard.sh
- Desktop browser validation for project Hooks
- git diff --check
* fix: keep MCP auth cleanup in the active workspace
Problem: The TUI clear-auth action followed the process working directory instead of the active controller workspace, and Desktop CI was blocked by an orphaned helper.
Root cause: Authentication cleanup bypassed the workspace selection used by the MCP manager, while the removed project-trust flow left an unused Desktop path helper behind.
Fix: Centralize TUI MCP workspace selection, add a cross-workspace regression test, remove the unused helper, and document the executable repository-config boundary with Safe Mode recovery guidance in English and Chinese.
Verification: go test ./internal/cli; go vet ./...; go test ./...; cd desktop and run go vet ./..., go build ./..., and go test ./...; ./scripts/cache-guard.sh; git diff --check.
* fix(mcp): preserve advertised cold-cache connect calls
Problem:
A provider could call a cold-cache MCP connect placeholder that had been advertised at request serialization time, but background schema discovery could replace it before dispatch and produce an unknown-tool failure.
Root cause:
Tool calls are resolved against the current registry after the provider returns, while cache-miss discovery intentionally swaps the synthetic connect placeholder for the server's real tools.
Fix:
Recognize a stale synthetic connect call as completed when the same MCP namespace now exposes real tools, and let already-resolved lazy placeholders report the same controlled result. Keep genuine unknown calls unchanged and add regressions at both dispatch and lazy-lifecycle boundaries.
Verification:
- go test ./...
- go test -race ./internal/plugin ./internal/agent
- go vet ./internal/plugin ./internal/agent
- ./scripts/cache-guard.sh
- delayed cold-cache LongCat MCP integration test
* fix: retire automatic Plan Mode
Problem:
Automatic Plan Mode created two different planning experiences, and users who had enabled it kept silently entering Plan Mode after the setting was removed.
Root cause:
The preference was persisted across TOML, desktop, CLI, controller, and classifier paths without a retirement migration or a single explicit-mode contract.
Fix:
Remove the setting and automatic classifier/runtime paths, migrate pre-v5 configs to manual-only behavior, retain safe read compatibility for old TOML and Wails clients, and update docs and metrics.
Verification:
- go test ./internal/config ./internal/control ./internal/boot ./internal/cli
- cd desktop && go test ./...
- pnpm build and pnpm test:typecheck in desktop/frontend
- settings-refresh-snapshot.test.tsx: 43 passed
- ./scripts/cache-guard.sh
- real-browser settings inspection with a clean console
* fix: remove obsolete desktop test helper
Problem:
The Linux desktop CI job failed golangci-lint after the Automatic Plan tests were removed.
Root cause:
A test-only turn runner and its context import were left behind without any remaining callers.
Fix:
Remove the obsolete helper and unused import.
Verification:
- cd desktop && go vet ./...
- cd desktop && go test ./...
* fix: preserve retired auto-plan compatibility
Problem:
Released desktop clients still submit the settings_auto_plan metric, and existing shell automation may still query or disable automatic Plan Mode.
Root cause:
The retirement removed both the worker enum value and the shell command, causing legacy metric batches to fail validation and safe compatibility commands to exit with an error.
Fix:
Keep settings_auto_plan as an input-only legacy metric signal, and retain a hidden config auto-plan shim that reports off, accepts off as a no-op, and rejects enable attempts. Add focused regressions for both contracts.
Verification:
- env -u DEEPSEEK_API_KEY go test ./internal/config ./internal/cli
- npm test in workers/crash-report
- npm run typecheck in workers/crash-report
- ./scripts/cache-guard.sh
- git diff --check
* fix: ignore unknown crash-report metrics
Problem:
A retired or future metric signal could invalidate an otherwise useful metrics batch, while a dedicated legacy allowlist only handled one historical signal.
Root cause:
The worker validated every counter signal with a strict enum before processing the batch.
Fix:
Keep strict validation for known counters, discard bounded unknown signals before storage, and treat an all-unknown batch as a successful no-op.
Verification:
- npm test (workers/crash-report)
- npm run typecheck (workers/crash-report)
- go test ./internal/cli -run TestConfigAutoPlanCompatibilityCommand|TestConfigLoadIgnoresRetiredAutoPlan -count=1
Problem:
Custom providers whose names begin with a digit generated an api_key_env such as 9ROUTER_API_KEY. Credential validation rejected that name, so model discovery and manual provider saves both failed.
Root cause:
The desktop and CLI provider-name normalizers allowed a numeric first character even though Reasonix credential keys require an alphabetic or underscore prefix.
Fix:
Prefix digit-leading generated stems with CUSTOM_ in both surfaces, add regression coverage for 9router, and document the generated name in English and Chinese.
Verification:
- pnpm typecheck && pnpm test (desktop/frontend)
- go test ./internal/config ./internal/cli
* feat(serve): support --port-file/--token-file/--pid-file for supervised headless serve
Extract RunGracefulListener so callers can bind first (--addr 127.0.0.1:0),
record the real port, and keep the auth token out of argv. Groundwork for the
remote-SSH module's detached remote serve bootstrap.
* feat(config): add user-global-only [remote] section for SSH hosts
[[remote.hosts]] entries carry connection/forward/workspace settings and name
credential env vars (passphrase_env/password_env) instead of secrets, mirroring
the provider api_key_env idiom. Like [secrets], loadForRoot pins the section
back to the user-global value after the project merge — with a deep copy,
because TOML decoding mutates slice backing arrays in place and a shallow pin
would alias the project-injected hosts. Remote credential env names flow into
CredentialEnvNames so they are filtered from tool subprocess environments.
Adds RemoteStateDir/RemoteKnownHostsPath and the internal/store name helpers
for the remote-side serve state layout.
* feat(remote): SSH transport kernel — dial, auth, host keys, forwards, SFTP
New internal/remote package group provides the connection layer for the
remote module:
- client.go: supervised connection with a single-goroutine state machine,
keepalive ([email protected]), full-jitter exponential-backoff
reconnect, shared SFTP (generation-checked), and forward re-attach.
- auth.go: OpenSSH-like method order (agent, identity file, default
identities, password, keyboard-interactive); interactively-obtained
secrets are cached in-process so reconnects never re-prompt.
- knownhosts.go: system known_hosts read-only + a Reasonix-managed TOFU
file; key mismatch is a non-promptable hard error.
- dial.go: ProxyJump chains (netclient proxy applies to the first hop only).
- forward/: -L (persistent local listener across reconnects) and -R
(re-registered on each attach) with typed ErrBindBusy/ErrDuplicateForward.
- sftpfs/: list/stat/capped-read-with-text-detection/atomic-write/mkdir/
rename/remove, quarantining the new github.com/pkg/sftp dependency.
- netclient/dialer.go: StreamDialer honoring the user's proxy policy
(SOCKS5 via x/net/proxy, http/https via CONNECT).
- sshconfig.go: ~/.ssh/config import via github.com/kevinburke/ssh_config.
- sshtest/: in-process SSH+SFTP server for tests.
New deps: github.com/pkg/sftp, github.com/kevinburke/ssh_config (both pure
Go). x/* versions kept at main-v2's pins.
Cache-impact: none — no prompt/tool/provider path touched.
Cache-guard: n/a (new leaf subsystem, not in the system-prompt prefix).
* feat(remote): remote serve bootstrap over SSH
internal/remote/bootstrap ensures a detached 'reasonix serve' runs for a
(host, workspace) pair: reuse a live process (the reconnect path), else detect
the remote OS/arch (uname), locate/install/upload reasonix, write a 32-byte
token 0600 via SFTP, launch serve bound to 127.0.0.1:0 with --token-file/
--port-file (token never in argv), poll the port file, and persist ServeState
JSON. Status/Stop/Logs manage the recorded process. Every remote command
operand is POSIX single-quote-escaped (golden test covers '; rm -rf ~').
V1 targets Linux/macOS remotes; cross-platform upload without a matching local
binary is a documented limitation directing to serve_install = npm.
Cache-impact: none — new leaf subsystem, no prompt/tool/provider path.
Cache-guard: n/a.
* feat(cli): reasonix remote command group + /remote slash
Adds the remote subcommand tree (add/list/remove/import/test/connect/open/
status/forward/serve/fs), mirroring the mcp command shape. Host CRUD writes to
the user-global config only (remote hosts are pinned there); connect runs a
foreground supervisor that dials, bootstraps remote serve, forwards the serve
port with the token in a loopback URL, applies configured -L/-R forwards, and
holds the tunnel until Ctrl-C while the remote serve keeps running. Terminal
prompts handle key passphrases, passwords, and TOFU host-key confirmation;
dialing honors the user's proxy settings via netclient. The /remote slash
command shows a read-only host summary. New i18n fields added to all three
catalogs.
Cache-impact: none — CLI surface + i18n only, no prompt/tool/provider path.
Cache-guard: n/a.
* feat(desktop): remote SSH Go bindings (remote_app.go)
25 App bindings behind a narrow remoteKernel interface (fake-injected in
tests): host CRUD (user-global config), Connect/Disconnect/status,
status-driven TOFU host-key confirmation (ConfirmRemoteHostKey resumes a
parked dial), SFTP list/read/write-with-mtime-conflict/mkdir/rename/delete,
port-forward CRUD, and remote serve EnsureServer/Open/Stop/Status/Logs.
desktopRemoteManager wraps internal/remote per host; status/forward/server
transitions bridge to remote:status / remote:forwards / remote:server through
the asyncRuntimeEmitter (never blocking the kernel). The manager is created
lazily on first use and closed in App.shutdown, rejecting any pending host-key
prompt. OpenRemoteWorkspace forwards the serve port and opens the tunneled URL
via Wails BrowserOpenURL; last workspace/host persist to desktop-remote.json
(all-optional fields, backward-compatible).
desktop/go.mod gains pkg/sftp + kevinburke/ssh_config via go mod tidy.
Cache-impact: none — desktop bindings only, no prompt/tool/provider path.
Cache-guard: n/a.
* feat(desktop): remote SSH frontend (host manager, explorer, ports, server)
- lib/types.ts: Remote* TS mirrors + SettingsTab "remote".
- lib/bridge.ts: 25 Remote* AppBindings + browser-dev mock + onRemoteStatus/
onRemoteForwards/onRemoteServer subscribers with a mock fan-out for tests.
- store/remote.ts: zustand store; pending_hostkey status drives the global
fingerprint dialog and resolves on the next non-pending status.
- RemoteHostsPage (Settings > Remote SSH tab): host list with status chip,
connect/disconnect/open, add/edit form, ssh_config import picker.
- RemotePanel: overlay drawer with Files (lazy tree + textarea editor with
mtime-conflict Reload/Overwrite), Ports (live forward rows + add form), and
Server (bootstrap phases + open-in-browser + logs) tabs.
- RemoteHostKeyDialog: global TOFU modal off the store's pendingFingerprint.
- StatusBar remote chip (worst live state, click opens explorer); App.tsx
subscribes remote:* app-wide and renders the panel + dialog.
- i18n keys added to en / zh / zh-TW (key sets stay aligned).
- Editor is a monospace textarea (no CodeMirror/Monaco) per lean-deps.
- Tests: src/__tests__/remote-store.test.ts (added to the test script).
Cache-impact: none — desktop frontend only.
Cache-guard: n/a.
* docs: document the Remote SSH module (SPEC, GUIDE, CONFIG_PATHS, CHANGELOG)
Adds the internal/remote layout + acyclic dependency note to SPEC 2, a bilingual
Remote SSH section to GUIDE.md / GUIDE.zh-CN.md (config, CLI, reconnect/persistence
semantics, host-key TOFU, remote-side state layout, desktop UI), the managed
known_hosts path to CONFIG_PATHS(.zh-CN), and a CHANGELOG entry.
Cache-impact: none — docs only.
Cache-guard: n/a.
* chore: ignore stray .reasonix state under package dirs
A bootstrap test whose RealPath resolved to CWD once wrote a .reasonix/remote/
token under the package dir and it was swept into a commit. The offending file
has been scrubbed from history; this guards against recurrence.
* fix(remote): CI green — errcheck + cross-platform bind-busy and Windows test skips
- client.go: check cl.Wait() return (errcheck lint).
- forward: classify WSAEADDRINUSE as bind-busy on Windows (build-tagged
addrinuse_windows.go), so ErrBindBusy is reported cross-platform.
- sftpfs / bootstrap integration tests skip on Windows: their in-process SFTP
server serves the LOCAL filesystem, so on Windows it serves drive paths and
the POSIX-remote path translation breaks — a harness limitation, not a
product one (V1 remotes are Linux/macOS). Linux/macOS CI covers the flows.
Cache-impact: none — test/lint-only + a Windows errno check.
Cache-guard: n/a.
* test(remote): skip EnsureServe reuse/relaunch/stop tests on Windows at setup
TestEnsureServeReusesLiveProcess and siblings pre-write state via pathsFor()
(POSIX path.Join) before newFakeConn runs, so on Windows the temp-dir remote
home yields a malformed drive path and the setup os.WriteFile fails. Move the
Windows skip to the top of each such test (shared skipOnWindows helper) so it
fires before any pathsFor/os setup, not just inside the harness ctor.
* fix(remote): address review — P1 security / data-integrity / lifecycle
Resolves the 14 findings from code review (all verified against the code):
Security
- Serve token no longer leaks to the remote log: runServe suppresses the
token share line under --port-file (supervised), and the launch script
creates the log 0600 (umask 077 + chmod). (F1)
- ProxyJump no longer sends the target's password to jump hosts: each hop gets
its own credential-free AuthOptions with a per-hop cache (persisted on the
Client so reconnects don't re-prompt). (F2)
- CLI surfaces a misconfigured proxy instead of silently dialing direct; the
https-proxy path now TLS-wraps the connection before CONNECT; desktop passes
the proxy Dialer it previously omitted. (F14)
Data integrity
- `remote fs get` streams the full file via sftpfs.Download instead of the
4 MiB-capped preview reader that silently truncated. (F3)
- Workspace slug carries an FNV hash of the exact path, so /srv/a-b and
/srv/a/b no longer share serve state files. (F10)
- Desktop host edit merges: passphrase_env/password_env/forwards (absent from
the desktop input) are preserved instead of wiped. (F7)
Lifecycle / correctness
- Bootstrap gates on `serve --help` advertising --port-file, not a version
number (the flags ship in this change, so no released version qualifies). (F4)
- Launch uses setsid only when present, so stock macOS (no setsid) works. (F5)
- Desktop no longer uploads the GUI binary as the remote CLI (LocalBinary=""
makes the upload path fail clearly toward npm). (F6)
- Serve tunnel re-binds to the current serve address on every EnsureServer and
is torn down on Stop; per-host serveMu serializes concurrent ensures. (F8)
- Reuse/Stop verify the pid is actually a reasonix serve (ps args), not just
kill -0, so a recycled PID is never mistaken or signalled. (F9)
- Desktop connect retries after failure (failed Start drops the managed host)
and builds a fully-populated host under the lock so a concurrent Disconnect
can always close it. (F11)
- SSH handshake is bounded by a conn deadline (ClientConfig.Timeout does not
cover NewClientConn), so a banner-less host can't hang the dial or Close. (F12)
- ScanSSHConfig returns a non-nil slice so an empty result encodes as [] not
null. (F13)
Tests: per-hop auth isolation, full-file download vs preview cap, slug
collision (/srv/a-b vs /srv/a/b), serve-alive/launch/locate command shape,
desktop edit-merge and non-nil scan. go test ./... green in both modules;
gofmt/vet clean; Windows cross-build green; cache-guard pass.
Cache-impact: none — no prompt/tool/provider/memory path touched.
Cache-guard: ./scripts/cache-guard.sh — all cases pass.
System-prompt-review: not required — no system-prompt-feeding config field changed.
* Harden Remote SSH lifecycle and packaging
Problem:
Remote SSH could misapply ProxyJump credentials, race connection and service lifecycles, interrupt live forwarding during replacement, and ship Desktop builds without the CLI sidecar required by upload installation. Several UI failures were also silent or left stale state behind.
Root cause:
Jump aliases shared endpoint-keyed authentication, bootstrap ownership relied on weak state checks without cross-process serialization, and Desktop operations lacked generation-scoped cancellation. Forward replacement stopped the working listener before the replacement was ready, while packaging and updater contracts did not include the CLI binary.
Fix:
Resolve and authenticate every SSH hop independently, make dialing and Desktop operations context-aware, serialize bootstrap with an atomic remote lock, verify exact process ownership, and make forward replacement failure-atomic. Harden token, SFTP, agent, config, and ssh_config handling; package and update the CLI sidecar on macOS, Windows, and Linux; and surface actionable validation and lifecycle errors in the Desktop UI.
Verification:
- go test ./...
- cd desktop && go test ./...
- go test -race for Remote, CLI, store, and Desktop modules
- go vet ./... for root and Desktop modules
- pnpm test:all and pnpm build
- Windows amd64 cross-compilation for Remote, bootstrap, forwarding, SFTP, and CLI packages
- clean merge snapshot against latest origin/main-v2 with root, Desktop, and frontend checks passing
* Add native Remote SSH workspace windows
Problem:
Remote SSH was buried in settings, and opening a remote workspace handed the authenticated loopback URL to the system browser instead of preserving a desktop workspace experience.
Root cause:
The desktop had no window-level Remote SSH control and no isolated Wails launch mode for a second native workspace window.
Fix:
Add a VS Code-style status entry, host switcher, command actions, and Remote dock. Launch remote workspaces in a lightweight Reasonix process that skips local session ownership, single-instance focus, tray services, and Wails bindings. Pass the loopback URL through a one-shot permission-restricted ticket so the token never appears in process arguments.
Verification:
- go test ./...
- pnpm test:all
- pnpm build
- wails build -clean
- native macOS multi-window smoke test
- git diff --check
* Tidy Remote SSH module metadata
Record the SSH config and SFTP packages as direct dependencies after rebasing onto the latest main-v2 baseline. Remove stale module checksums so the root module remains tidy in CI.
* Fix Remote SSH Windows test assumptions
Load the fixture SSH config by its explicit path while setting both platform home variables, and use the Windows .exe suffix for the desktop CLI fixture. This keeps the Remote SSH coverage portable across the CI matrix.
* 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]>
When REASONIX_HOME is set, the runtime is now fully self-contained: all
configuration, state, cache, skills, commands, hooks, trust, credentials,
migration, and desktop tab/project data live under that directory tree.
Legacy fallback paths and auto-migration are skipped so a source-built
binary can run side-by-side with a stable release without interfering.
Changes:
- config/paths.go: add IsolatedHomeDir(), gate legacyOSSupportDir /
legacyXDGConfigPaths, make userCacheDir() fall back to
REASONIX_HOME/cache when isolated
- config/migrate.go: skip MigrateLegacyIfNeeded when isolated
- config/mcpjson.go: skip legacyConfigPath when isolated
- config/ccswitch.go: skip LoadCCSwitchMCP when isolated
- config/credentials.go: skip ~/.env credential source when isolated
- skill/skill.go: skip OS-home convention dir scan when isolated
- hook/hook.go: skip legacy reasonix home fallback when isolated
- migration/migration.go: skip auto migration entry points when isolated
- outputstyle/outputstyle.go: skip home dir scan when isolated
- desktop/tabs.go: desktopConfigDir / globalWorkspaceRoot now use
config.ReasonixHomeDir() instead of raw os.UserConfigDir()
- desktop/updater.go: defaultUpdateCacheBaseDir uses config.CacheDir()
- CONTRIBUTING.md: document isolated development workflow
- docs/CONFIG_PATHS.md: document full isolation behavior
Closes#5988
Add the new ui.cursor_shape option to the user guide, configuration paths docs, and configuration spec so the formal docs match the PR behavior.
Co-authored-by: SivanCola <[email protected]>