Skip to content

Remote mode: wire client adoption into connections, trim what only works locally - #182

Merged
fylorn merged 8 commits into
devfrom
feat/remote-mode-ui
Sep 24, 2026
Merged

fylorn merged 8 commits into
devfrom
feat/remote-mode-ui

Conversation

@fylorn

@fylorn fylorn commented Sep 24, 2026

Copy link
Copy Markdown
Contributor

Stacked branch. It contains #181 (feat/connections) and #180 (feat/adopt-in-app) merged in, so until those two land this diff includes their commits. Merge order: #180 → #181 → this PR. GitHub will reduce this PR to its own changes (the merge commit cabdc8d and commit 47de817) once #180 and #181 are in dev. The P2 handshake branch (feat/control-handshake) is stacked on #181 separately; I did not touch control.rs, connector.rs, the supervisor or key handling.

Wiring adoption into connections (replaces #181's TODO hooks)

  • Switch-confirm count (switch_preflight): the adopted clients whose configured endpoint is this machine (127.0.0.1 / localhost / [::1]) are counted from the app's own view of the files (clients::adopted_pointing_at_local). It no longer asks the local core, so it still answers when that core is stopped.
  • "Also point these clients at ": after the switch is committed, connection::switch runs clients::retarget_adopted(&control, &server_host) against the new core, which issues each client's key. Only clients still pointing at this machine are changed. switch_connection now returns { view, retargeted: { synced, failed } | null }. If any failed, the dialog stays open and lists each client with its reason. If all succeeded, a toast names them. A failure does not undo the switch.
  • clients::gateway_host(state) returns the host of the active remote profile while remote, and 127.0.0.1 locally. The port comes from the core's configured gateway port. Adopt, copy endpoint, the keys page and the menu bar's copy-address all go through it.
  • New command retarget_clients: the one-click repoint on the Clients page. It does nothing in local mode.

Remote-mode trimming (mockup ⑧ and the plan's remote-mode table)

  • Clients and MCP pages carry a fixed note: they check and change this Mac's client files and point them at <server>'s gateway; clients on the server are not affected.

  • Clients still pointing at the local gateway are marked on their row ("指向本机网关 127.0.0.1:8788", in the "未生效" state). A line above the table has a 改为指向 button, and per-client results show under it.

  • Settings has two groups in remote mode:

    • 这台 Mac 上的应用: connections, language, appearance, menu bar, launch at login, updates, notices, about, uninstall.
    • 的配置: listen and retention, plus a note that the remote control listener and key can only be changed on the server.

    The diagnostics bundle is hidden. About shows the connection and the server's core version instead of the local core binary. Uninstall says the server is not affected; it only restores this Mac's clients and data. Local mode keeps its current layout and order.

  • ChatGPT sign-in offers only the device code while remote, with a one-line reason. Z.ai is unchanged.

  • ${变量名} in upstream headers and the "system proxy" option in the proxy selects say they use the server's environment.

  • The sidebar gateway address and the connection test result show the dialled host + the server's gateway port (clientGateway), not the server's bind address (0.0.0.0:8788).

  • The local-only environment variables that user_env loads apply only when the app starts the local core, which is already local-only, so nothing changed there.

  • The plan also mentions an "open data folder" action, but the app has none, so there is nothing to hide.

Copy is in src/connection/remote.i18n.tsx (zh + en). Remote state comes from the connection view #181 already emits, through useRemote().

Checks

  • cargo fmt --check, cargo clippy --all-targets -D warnings, cargo test, pnpm typecheck, pnpm test
  • New unit tests: loopback detection (Rust and TS), only clients pointing at this machine count, and remote-mode status of a client left on the local gateway.
  • Checked in the isolated preview harness with a mocked remote connection. Clients page: note, flagged rows, and one-click repoint with one mocked failure, reported per client. Settings: the two groups, no diagnostics, About rows. MCP: the note. Local-mode settings are unchanged.
  • The core P4 codes (control.remote.*, config.remote_*) are not in the pinned core yet, so their translations are left to the P2 branch.

🤖 Generated with Claude Code

fylorn and others added 8 commits September 24, 2026 23:23
… core

Client adoption, MCP editing, the static scan and the file watcher change
files on this machine, so they now run in the desktop app. The tw-adopt and
tw-scan crates move here from core (src-tauri/crates/, one workspace with the
app; the unused tw-config dependency is dropped).

- New Tauri commands replace the control-plane endpoints: list_clients,
  plan_adopt, adopt_client, plan_restore, restore_client, diagnose_client,
  prepare_client_key, mcp_targets, plan_mcp, apply_mcp, scan_clients,
  gateway_base, plus delete_key and rotate_key (the adopted-client check and
  the key sync need this machine's files). restore_all no longer asks core.
- Core is still asked for two things only: the gateway port (Overview) and
  keys (Keys, ClientKey). The gateway host is a single function so remote
  mode can swap it; retarget_adopted() repoints every adopted client.
- The watcher runs in the app: `local-event` (clients_changed / scan_alert)
  to the webview, scan alerts into the notice bus; the first scan is a
  baseline. Core's own ClientsChanged/ScanAlert are ignored.
- Types for these commands live in src-tauri/src/wire.rs and generate
  src/generated/lite-api.ts; names identical to tw-api.ts are imported.
- Message codes the app emits itself are listed in src-tauri/msg-codes.txt
  (generated from source); translations are checked against it plus core's.
- The webview endpoint whitelist drops the moved endpoints, and DeleteKey,
  RotateKey and ClientKey now go only through Rust commands.

Works against the pinned core v0.46.0 (its endpoints just go unused).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ransport stubbed)

The connection-management shell from the remote-core plan (P5, part 1):

- Connection list persisted by the app itself (connections.json in the data
  dir): built-in, undeletable "This Mac" plus remote entries. Keys live in the
  OS keychain (keyring crate), never in the file and never sent back to the UI.
- One connector seam (connection::connector): test/connect returns ServerInfo
  or a ConnectError (unreachable / closed / wrong key / version mismatch /
  timeout). The remote handshake is a single stub, connector::handshake, that
  P2 fills in with the Noise handshake; until then it answers "not available
  yet" after the TCP connect.
- ControlClient is retargetable; every clone follows the current connection.
  In remote mode the local core stops once in-flight requests finish and the
  watchdog does not restart it; switching back starts it. Never falls back to
  local on its own.
- Launch screen waits at most ~8 s, then the window opens in a "not
  connected" state (data pages disabled, Settings usable, retry with backoff,
  edit, switch to this Mac). Version-mismatch page. Mid-session disconnect
  shows a banner, makes the content read-only, raises one notification and
  reconciles through the existing core-state/events_dropped path.
- Launch guard: two launches in a row that never got ready, or holding
  Option/Alt at launch, show a small connection picker first.
- Sidebar switcher, Settings > Connection section, add/edit dialog with
  test / save / save and switch, switch confirmation with the "also point
  these clients at the server" option (clients::retarget_adopted_clients is
  a TODO hook until adoption moves into lite).
- Menubar/tray "Connection" submenu on all platforms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e clients list

The menu bar's copy-address action still read gateway_base from the /clients
endpoint, which core is dropping. It now uses the same function as the
clients and keys pages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On macOS the keychain prompts for the login password for this item (again
after each update of an unsigned app), which is not worth it for a key that
can be copied from the server again. There is now one code path on every
platform: connection-keys.json in the data directory, created 0600 from the
start (temp file with the mode, then rename), in a directory created private
(0700 on unix, protected DACL on Windows; ported from core's private_dir).

Kept apart from connections.json so the frequent list writes never touch the
secret and the code that reads the list cannot see it. Keys still never go
to the webview. The keyring crate and all keychain copy are gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…m into connections

Client adoption now lives in the app, so the two TODO hooks from the
connections work get their real implementations:

- switch_preflight counts the adopted clients still pointing at this
  machine's gateway from the app's own view of the files, so it answers
  even while the local core is stopped.
- Switching with "also point these clients at <server>" runs
  clients::retarget_adopted against the new core after the switch is
  committed (keys are issued by that core) and returns what was and was
  not repointed (switch_connection now returns { view, retargeted }).
- clients::gateway_host is the host of the active remote connection, so
  adopting, copying an endpoint and the menu bar write the server's
  gateway; 127.0.0.1 locally.
- retarget_clients repoints the adopted clients still pointing at this
  machine once connected to a remote core (the Clients page button).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s locally

Following mockup ⑧ and the remote-mode table in the plan:

- Clients and MCP pages carry a fixed note that they change this Mac's
  files, pointing clients at the server's gateway; the server's clients
  are not affected.
- Adopted clients still pointing at the local gateway are flagged on their
  row ("points to the local gateway") and above the table, with a one-click
  "Point at <server>"; what was and was not repointed is listed per client.
  The switch-confirm dialog shows the same report when some failed.
- Settings split into "the app on this Mac" and "configuration on <server>"
  (listen, retention, and a note that the remote control listener and key
  can only be changed on the server). The diagnostics bundle is hidden; About
  names the connection and the server's core version; uninstall says the
  server is not affected. Local mode keeps its layout.
- ChatGPT sign-in offers only the device code while remote (the browser
  callback can only reach the machine running core); Z.ai is unchanged.
- ${NAME} in upstream headers and the "system proxy" choice say they are
  the server's environment.
- The sidebar and the test result show the gateway clients should use: the
  host the app dialled plus the server's gateway port, not its bind address.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…te-mode-ui

# Conflicts:
#	src-tauri/Cargo.lock
#	src-tauri/src/lib.rs
#	src/connection/connection.i18n.tsx
@fylorn
fylorn merged commit fa32a3c into dev Sep 24, 2026
4 checks passed
@fylorn
fylorn deleted the feat/remote-mode-ui branch September 24, 2026 17:45
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.

1 participant