Skip to content

fix(remote): route remote repository reads and mutations through typed commands - #548

Merged
Ziinc merged 2 commits into
mainfrom
claude/prd-gaps-remote-adapter-routing
Sep 27, 2026
Merged

Ziinc merged 2 commits into
mainfrom
claude/prd-gaps-remote-adapter-routing

Conversation

@Ziinc

@Ziinc Ziinc commented Sep 27, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Covers remote-development PRD acceptance criteria 3 (normal review UI), 4 (core mutations over the typed command layer) and 7 (safe reconnect).

  • Remote (SSH) repositories now send the desktop's workspace, file and commit reads and mutations through the typed remote command layer. Operations that have no typed command fail with a structured unsupported: error. They no longer run a local Tauri command against a path that exists only on the remote host.
  • Fixes remote CreateCommit. Before, TS sent no idempotency_key, and the Rust enum requires one, so the request failed at the IPC boundary. The verify-before-retry check also treated any commit with the same message as already applied. That gave false positives on repeated messages such as "wip".
  • Fidelity fixes:
    • RebaseWorkspace used a hardcoded "main".
    • SplitCommit ignored commit, widened hunks to whole files and dropped the message.
    • MoveWorkspaceChanges dropped files and hunks.
  • Adds the typed reads that were missing: WorkspaceDiff (the remote review diff now includes committed files), CommitDiff, CommitFileDiff and SearchFiles.

Changes

Rust (core/remote.rs, cli/mod.rs, tauri.conf.json)

  • CreateCommit has a new optional base_change_id: the working-copy change id before the commit. retry_after_reconnect fills it with a WorkspaceChangeMarker read before it sends the mutation. WorkspaceChangeMarker now also returns working_copy_change_id. The verification check has three outcomes:
    • AlreadyApplied only when that exact change is no longer the working copy and carries the message.
    • NotApplied while it is still the working copy.
    • Ambiguous otherwise, or when no marker is available.
  • DeleteWorkspace verification treated every workspace as gone, because it compared numeric ids as strings. RenameWorkspace verification read a field that does not exist, so it was always ambiguous. Both are fixed.
  • RebaseWorkspace now uses jj::get_default_branch.
  • SplitCommit passes the message through. It refuses a commit other than @ and refuses hunk selections, so neither is silently widened.
  • MoveWorkspaceChanges now forwards files and hunks.
  • CreateWorkspace carries the local metadata JSON (title, description, moved files, sparse patterns). UpdateWorkspace carries title.
  • Adds 4 read kinds, so KIND_NAMES goes from 40 to 44.
  • CLI config: commits did not declare --path, and conflicts did not declare --target, --value or --idempotency-key. The VM-side parser rejects undeclared flags, so remote split with hunks and remote conflict resolve were broken over SSH. Those flags are now declared, along with the new flags.
    TypeScript
  • repository-adapter.ts:
    • remoteMutation always uses dispatchMutationOverSsh(transport.endpoint, …) for SSH repos. It does not add callers of dispatchMutation (compatible with refactor(remote): remove Sprite-exec managed transport #543).
    • newIdempotencyKey generates one key per user action.
    • assertLocalOperation / RemoteOperationUnsupportedError guard local-only operations.
    • resolveRemoteLocation maps a workspace path to its workspace id.
    • New read wrappers.
  • New repository-adapter-mutations.ts: create/delete/move/rename/push/update/retarget workspace, restore file, split, resolve, move/abandon/describe commit, and get commit description.
  • api.ts / api-extra.ts call these adapters. Operations with no typed command are guarded:
    • merge, commits-ahead, check-and-rebase, home rebase and dry run
    • restore-all and snapshot
    • pull, switch branch, archive, schedule
    • bookmark conflict
    • undo, revert, shift timestamps
    • start resolve session
  • The file-browser draft review for a remote repo is kept in memory. Before, it wrote a local .treq/local.db under the remote path.
  • remote-capabilities.ts adds mergeWorkspace, marked unsupported for remote with a reason. The Dashboard merge-preview entry point checks it, and RemoteCapabilityNotice lists it. This is a small UI change: remote repos show one more line in the capability notice.
  • remote-dispatch.ts: removes the duplicate CreateCommit union member that made idempotency_key optional, and adds the new fields and kinds.

Testing

  • cargo test --lib --features tauri-test -- remote cli::tests: 206 passed, 3 failed. The 3 failures also happen without this change and come from this container:
    • change_marker_… and bookmark_track_… need a jj binary on PATH.
    • skips_project_skills_when_cwd_is_not_writable fails because the tests run as root.
  • New Rust tests:
    • CreateCommit marker check: repeated-message false positive, the tentative working copy, ambiguous cases, baseline fill-in, and a real-repo before/after commit.
    • Split: real repo checks the message is kept and a non-@ commit is refused; hunk selections are refused.
    • Move: moves only the selected files in a real repo.
    • Rebase: a real repo with a non-main default branch. This test fails with the old hardcoded "main".
    • Delete and rename verification.
    • CLI: every typed request only emits flags declared in tauri.conf.json, and every request round-trips through cli_args → parse_remote_command_request.
  • cargo clippy --locked --all-targets --all-features -- -D warnings: clean. cargo fmt --check: clean.
  • npx vitest run --config vitest.unit.config.ts: 137 files, 694 tests, all passed. This includes the new src/lib/repository-adapter.test.ts, which checks:
    • routing for reads and mutations
    • a fresh key on each CreateCommit
    • an ambiguous result makes the call fail
    • unsupported operations never call invoke
    • local repos still use invoke
  • npx tsc --noEmit, eslint and oxlint on the touched files, and biome format: clean. The remaining eslint warnings in Dashboard.tsx and api-extra.ts were already there.
  • NAPI integration tests were not run. test/integration/remote-workspace-ui.test.tsx opens a saved remote repo with no endpoint, which uses the local transport, so these paths are not exercised there. Its capability-notice assertions use substring matches.

Not in this PR

  • Typed commands for merge, merge preview (jj_get_commits_ahead), check-and-rebase, home-branch rebase, restore-all/snapshot, pull, switch branch, archive/schedule, bookmark-conflict resolution, undo/revert, shifting timestamps and the resolve-conflicts session. These now fail with an unsupported: error rather than running locally. Only merge is disabled up front in the UI through capabilities.
  • Hunk-level split. There is no core hunk split, so the remote refuses hunk selections.
  • Remote file-browser draft reviews are kept only for the session. There is no typed command for that table yet.
  • File browser directory listing (list_directory, ls_workspace_with_status) is still local-only.
  • A saved remote repository with no endpoint gets transport: local, so its calls still go to local commands. The integration tests depend on this behaviour, and fixing it belongs with the transport cleanup.
  • The local set_workspace_target_branch Tauri command also hardcodes "main". Only the remote path is fixed here.
    🤖 Generated with Claude Code
    https://claude.ai/code/session_01MbYSPHBRmDYwPJpHG33Y4t
    Generated by Claude Code

…d commands

Remote (SSH) repositories now send every workspace, file, and commit read or
mutation the desktop UI performs through the typed remote command layer.
Operations with no typed command fail with a structured "unsupported" error
instead of running local Tauri commands against a remote path.

- CreateCommit always sends an idempotency key, and its verify-before-retry
  check uses the pre-commit working-copy change id instead of matching the
  message, so repeated messages such as "wip" no longer read as applied.
- RebaseWorkspace uses the repository default branch, SplitCommit honours
  the commit and message and refuses hunk selections, and
  MoveWorkspaceChanges keeps files and hunks.
- New typed reads: WorkspaceDiff, CommitDiff, CommitFileDiff, SearchFiles.
- Every flag the typed commands emit is declared in the CLI config, and
  every request round-trips through its CLI arguments.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MbYSPHBRmDYwPJpHG33Y4t

@Ziinc Ziinc left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inline notes on why the main files changed.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CreateCommit check at verification_for now keys on the pre-commit working-copy change id, which retry_after_reconnect reads before it sends the mutation.

  • jj_commit rewrites @ in place, so the change id survives and becomes the committed change. That tells "this commit landed" apart from "an older commit happens to say wip".
  • The marker is stored in the request itself, so the VM's idempotency record and its stale-claim recovery use the same check.
  • The fidelity fixes (rebase default branch, split guard) refuse unsupported inputs instead of widening them, because a silent widening changes what the user committed.

Generated by Claude Code

Comment thread src-tauri/tauri.conf.json

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The remote VM parses argv with this CLI config, and it rejects undeclared flags.

  • commits --path (split hunks) and conflicts --target/--value/--idempotency-key were emitted by cli_args but never declared, so those commands could only fail over SSH.
  • The new every_remote_cli_flag_is_declared_in_tauri_config test in cli/tests.rs keeps the two in sync from now on.

Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remoteMutation replaces dispatchMutation(repo.endpoint, …), which fell back to a local dispatch when the endpoint was null.

  • It uses dispatchMutationOverSsh(transport.endpoint, …) for SSH repos, matching the shape refactor(remote): remove Sprite-exec managed transport #543 leaves behind.
  • assertLocalOperation is the safety net for operations with no typed command: it raises a structured unsupported: error instead of running a local command against a remote path.
  • resolveRemoteLocation lets the existing workspace-path APIs (jjSplit, jjRestoreFile, readFile) find the workspace id without changing their call sites.

Generated by Claude Code

Comment thread src/lib/api-extra.ts

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The file-browser draft review (loadFileBrowserReview) is kept in memory for remote repos.

  • The local command would open or create .treq/local.db under a path that exists only on the remote host.
  • No typed command exposes that table yet. Keeping the draft for the session keeps commenting usable without writing to the wrong machine.

Generated by Claude Code

Resolve repository-adapter conflicts with the Sprite transport removal
(#543): keep the SSH-narrowed activeForPath, drop the managed-Sprite
import, and keep main's transportCreateCommit tests with the idempotency
key the PR now sends. Refuse SplitCommit hunk selections before the
idempotency store opens the repo, so the refusal does not depend on the
repo path being writable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012PGXpwptLRetsjFcHkL8x5
@Ziinc
Ziinc merged commit 50c2808 into main Sep 27, 2026
23 checks passed
@Ziinc
Ziinc deleted the claude/prd-gaps-remote-adapter-routing branch September 27, 2026 13:38

This branch was successfully deployed

1 active deployment
preview — 4116aad5 Deployed Sep 27, 2026 by Ziinc via build #1345
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants