51 Commits
Author SHA1 Message Date
Lancy cfb483ae11 feat(config): support nested project config fallback
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
2026-10-19 11:36:17 +08:00
yhh 46efe8eba4 feat(studio): web search model setting in model preferences 2026-10-10 08:36:00 -07:00
yhh c77b9fd284 docs(host): say which log files are redacted; show the exit notice in the page's language
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.
2026-10-09 17:38:16 -07:00
yhh 9e600e1750 fix(host): keep routine polling out of the INFO log and leave a crash trail when the kernel dies
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.
2026-10-09 17:04:16 -07:00
Stephen R. van den Berg 5b609815c5 feat(cli): auto-submit a fully answered ask batch, gated on auto_submit
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).
2026-09-30 22:42:33 +02:00
Stephen R. van den Berg 519a4acc11 feat(cli): render a shell result whose whole output is a diff
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.
2026-09-29 22:45:17 +02:00
Stephen R. van den Berg 4dfab0faf5 feat(cli): format diffs with an external command via [cli].diff_formatter
[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.
2026-09-29 22:45:17 +02:00
esengine f352dfbecc chore: remove the IM bot gateway
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.
2026-09-17 02:02:48 -07:00
esengine e6db271d9f feat(config): the DeepSeek catalog is one declaration, and its rates are one table
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.
2026-09-13 18:47:33 -07:00
esengine 8c29942890 refactor: the task catalog is removed
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.
2026-08-21 20:57:57 -07:00
esengine 1c5c4adc70 refactor: the session catalog is removed
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.
2026-08-21 20:36:44 -07:00
SivanCola 3dc808ede6 fix(session): v1.24.2 session write authority and recovery root fix / 根治会话写权限与 recovery 误判 (#8457)
* 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 #8342
Fixes #8111
Fixes #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.
2026-08-12 14:18:55 +08:00
SivanCola 6382d24ee1 Fix v1.24.1 recovery catalog and Windows startup / 修复 v1.24.1 恢复会话目录与 Windows 启动闪窗 (#8425)
* 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.
2026-08-11 17:48:10 +00:00
SivanCola 54a46607f5 merge(main-v2): integrate provider URLs and turn labels 2026-08-11 20:17:25 +08:00
SivanCola 64f70f3fad feat(provider): exact custom request URLs / 自定义供应商使用精确请求地址 (#8386)
* 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
2026-08-11 20:06:17 +08:00
SivanCola 0d41f2ca46 merge: adopt task catalog from #8219
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.
2026-08-11 01:52:01 +08:00
SivanCola 40b15fd327 merge: adopt usage catalog from #8218
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.
2026-08-11 01:50:15 +08:00
SivanCola 3739f12a7d fix(tasks): complete catalog lifecycle contracts
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.
2026-08-10 20:06:02 +08:00
SivanCola d9ca43ac5e docs(stats): document usage catalog recovery
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.
2026-08-10 19:58:55 +08:00
SivanCola 98e30ee1a7 docs(history): document catalog recovery
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.
2026-08-10 19:45:27 +08:00
SivanCola bf19574d3a feat(desktop): add resilient session catalog
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
2026-08-10 17:16:26 +08:00
SivanCola 89617b2119 feat(provider): recommend DeepSeek Anthropic access
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.
2026-08-08 15:04:54 +08:00
918d8b1a9a fix(cli): land independent CLI feedback PRs on main-v2 (#7656)
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]>
2026-08-06 11:56:17 +08:00
SivanCola 2c1df5b4f9 Trust configured MCP and Hooks by default / 默认信任已配置的 MCP 与 Hooks (#6889)
* 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
2026-07-24 16:00:36 +08:00
SivanCola 77fd1a47be fix: retire automatic Plan Mode / 退役自动计划模式 (#6734)
* 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
2026-07-20 21:51:51 +08:00
SivanCola d8d3e5e09c Fix digit-leading custom provider credentials (#6701)
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
2026-07-19 23:53:01 +08:00
SivanCola 65fcd46525 Add a Remote SSH module (remote serve + tunnel) / 新增远程 SSH 模块(远端 serve + 隧道) (#6673)
* 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.
2026-07-19 21:48:20 +08:00
SivanColaandbillycat 1bd5f04d2a Polish CLI composer and responsive status UI / 优化 CLI 输入框与响应式状态栏 (#6591)
* 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]>
2026-07-17 16:27:09 +08:00
SivanCola f5179b4de4 fix: harden release-critical provider and credential paths
Merge cumulative Anthropic-compatible usage across stream events, keep saved credentials outside model-controlled tool environments and read paths, and make desktop session teardown waits concurrent with semantic timing coverage.
2026-07-16 00:50:02 +08:00
SivanCola 19ae765a20 Make todo progress guard adaptive 2026-07-15 11:22:59 +08:00
SivanCola 1a45a16400 fix(agent): enforce serial todo progress 2026-07-15 10:42:57 +08:00
SivanCola 1cc80b8100 Merge remote-tracking branch 'origin/main-v2' into pr6004-isolated-runtime-config-override
# Conflicts:
#	docs/CONFIG_PATHS.md
2026-07-05 23:23:45 +08:00
SivanCola 88598c8fc3 docs: clarify state root paths 2026-07-05 22:58:26 +08:00
SivanCola 98a1fb29a0 feat: add task contracts and stabilize credential path 2026-07-05 22:24:48 +08:00
HaoyueQin a238ace545 feat: support REASONIX_HOME for fully isolated runtime environments
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
2026-07-05 17:00:29 +08:00
ying-hua 052708f6f4 Support provider extra_body 2026-07-02 18:33:27 +08:00
SivanCola 0b3f501db0 fix(provider): support full custom chat URLs 2026-06-30 17:41:52 +08:00
SivanandSivanCola 5f6315c079 Document TUI cursor shape config
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]>
2026-06-30 11:26:05 +08:00
SivanCola 3ee319d763 Merge pull request #5348 from myipanta/fix/4708-custom-provider-key-collision
fix: generate unique apiKeyEnv for non-ASCII custom provider names
2026-06-28 03:11:42 +08:00
SivanCola 4f1769a05f Unify custom provider key env derivation
Co-authored-by: SivanCola <[email protected]>
2026-06-28 03:04:16 +08:00
SivanCola a11719a643 Improve custom provider key env defaults
Co-authored-by: SivanCola <[email protected]>
2026-06-28 02:52:22 +08:00
SivanCola 6f059b850a feat(migration): import legacy sessions from explicit roots 2026-06-27 01:59:16 +08:00
Sivan 1d564bd4d5 Scope project dotenv expansion 2026-06-23 13:06:08 +08:00
Sivan 2dd6fc6ddf Fix provider credential follow-ups 2026-06-23 12:27:23 +08:00
SivanandBrunswickTabby 7e7c7eda09 Use global env for provider credentials
Co-authored-by: BrunswickTabby <[email protected]>
2026-06-23 11:38:06 +08:00
Sivan 605aeed4a3 fix(config): backfill MCP servers into global config 2026-06-17 12:00:55 +08:00
lab1 1cb651046d Address config path migration review 2026-06-16 22:16:33 +08:00
SivanCola e786f400b9 Document migration rescue command 2026-06-16 21:52:48 +08:00
Sivan d6d18e1c55 Clarify API key credential storage 2026-06-16 19:44:13 +08:00
Sivan a148c43d2c Add credential store backends 2026-06-16 19:27:08 +08:00