Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/core/STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ Codemux is past Linux MVP and shipping cross-platform binaries. The workspace sh

Landed on `main` after the `v0.7.9` tag (unreleased) are three changes: **OpenCode conversation sync across cloud-push** (issue #16), a **project-favicon cache-bust fix**, and a **dev-only Tauri mock runtime**. (1) **OpenCode conversation sync** — the OpenCode counterpart to PR #15's Claude conversation sync. Pushing a workspace with an active OpenCode pane now continues that conversation on the remote (and pull-back continues it on the laptop), verified monotonic across 3+ cycles. Because OpenCode keeps every conversation in one SQLite DB (`~/.local/share/opencode/opencode.db`) that can't be rsynced without clobbering the host's other history, the sync uses OpenCode's **own `export`/`import` CLI** instead of a bespoke `rusqlite` row extractor (deliberately *not* the issue's original plan — the schema is large and fast-moving). `opencode export <id>` produces a portable bundle; `opencode import` on the receiving side preserves the session id, associates the session with the cwd it runs from, and is idempotent — so only the one synced session is touched and the host's unrelated OpenCode sessions are never clobbered (the explicit acceptance criterion). New module `src-tauri/src/ssh/opencode_db_sync.rs` (push `sync_opencode_session` + pull `pull_opencode_session`), wired into `commands/hosts.rs` alongside the Claude JSONL sync; `terminal/daemon_backed.rs` gained a generalized `build_agent_relaunch_command` that builds `opencode --session <id>` / `--continue` (the Claude-only relaunch synthesis is now per-agent), plus an `opencode` session adapter for local app-restart parity. Verified end-to-end against a Docker SSH host (`scripts/e2e/opencode-sync-e2e.sh` + env-gated `src-tauri/tests/opencode_sync_roundtrip.rs`): a session grew 4→644 messages across a host continuation, preserved over 3 push/pull cycles, with the unrelated host session held intact throughout. See `docs/features/opencode-conversation-sync.md`. (2) **Project-favicon cache-bust fix** (PR #71) — re-saving a project image (or re-opening the picker) now appends a changing `&v=` token to the derived favicon-service URL via `resolveImageUrl(input, cacheBust)`, so a site that changed its favicon visibly refreshes instead of the WebView serving the same stale cached bytes forever; direct/data URLs are passed through untouched. Touches `src/lib/project-image.ts`, `src/components/ui/project-avatar.tsx`, `src/components/layout/sidebar-project-group.tsx`, `src/components/overlays/project-image-dialog.tsx`. (3) **Dev-only Tauri mock runtime** (PR #82) — a `src/dev/` shim (`tauri-mock.ts` + `mock-fixtures.ts`, ~1k lines) installs a faithful `window.__TAURI_INTERNALS__` so the real React UI boots in a plain browser tab under `npm run dev` (Vite at `localhost:1420`) with seed data and no desktop window or Rust backend, enabling the Codemux browser-pane screenshot workflow for UI work. Dual-guarded in `main.tsx` (`import.meta.env.DEV` tree-shakes it out of production; `!("__TAURI_INTERNALS__" in window)` keeps it dormant under `npm run tauri:dev`). See `docs/features/dev-mock-runtime.md`.

Also landed on `main` after the `v0.7.9` tag (unreleased): **headless-daemon worktree provisioning parity** (issue #78). The daemon's `worktree_create` MCP tool now provisions a new worktree the same way the desktop does: gitignored include files (`.env` & co, `.codemuxinclude` → defaults) are copied from the parent repo synchronously before the tool returns, and the project's `.codemux/config.json` setup commands run on a background thread through a new UI-free `crate::scripts::run_setup_commands` core (shared with the desktop's `run_setup_scripts_with_config`, now a thin Tauri-event wrapper over it) with the full `CODEMUX_ROOT_PATH`/`CODEMUX_WORKSPACE_PATH`/`CODEMUX_BRANCH`/`CODEMUX_PORT` (+ NAME/ID) env and the same deterministic `allocate_workspace_port` port. The tool response gains a `setup` summary (`{port, includes_copied, setup_commands, setup_running}`); setup progress/failures log to the daemon's stderr, and a failing setup script never fails the tool call. Combined with the earlier daemon-side fetch-before-branch in `remote/git.rs` (issue #76 parity), remote-created branches start at the freshly-fetched `origin/<base>` tip with graceful offline/local-only fallback. Verified by 3 new unit tests in `remote/tools/mod.rs`, a full-daemon HTTP integration test (`http_worktree_create_provisions_like_desktop` in `src-tauri/tests/codemux_remote_serve_mcp.rs`, stale-clone scenario), and a live Docker e2e against the real binary. See `docs/features/setup-teardown.md` § "Headless Daemon Parity", `docs/features/remote-hosts.md`.

Shipped in `v0.7.9` is **"Operate a remote workspace in place" (Open on host)** — the no-pull remote-operation capability (issue #64). The Workspaces overview's host-backed sibling row gains an **"Open on host"** action (enabled when the host is configured locally) that creates a local *attach-in-place* workspace: `WorkspaceSnapshot` gains `remote_cwd` (the workspace's real on-host directory) + `attach_only` (operated in place, no local files), `create_remote_attach_workspace` builds a ready single-terminal workspace with `host_id` set and **nothing copied under `~/.codemux/` locally**, and the daemon-backed terminal path (`remote_spawn_cwd`) spawns into `remote_cwd` over the existing SSH-tunneled pty-daemon so commands run on the host with live streaming. Persistence is real: `ssh::tunnel::build_remote_command` now **reuses a still-running daemon** (via a `<socket>.pid` liveness probe) or **spawns it detached** (`setsid`/`nohup`, stdio redirected) instead of `exec`-ing it in the SSH foreground, so closing the app leaves the host process running and reopening re-tunnels + `client.list()`-reattaches the live sessions (a strict improvement for the push flow too). The command (`workspace_open_on_host`) resolves the host-backed sync row → local host → `origin_path`, is idempotent, and is excluded from `reconcile_from_snapshot` so it never creates a duplicate cloud row; the overview dedupes the sibling card against the open in-place view and renders an "on host" badge with detach-only close. See `docs/features/remote-in-place.md`.

Shipped in `v0.7.9` is a **multi-device robustness + remote-persistence pass** layered on top of repo-unit sync. (1) **SSH tunnel health is now surfaced in the UI**: the `TunnelStatus` the supervisor already computed (`connected`/`pending`/`reconnecting`/`circuit_open`) is bridged to the frontend via a new `tunnel-status-changed` event + `spawn_tunnel_status_forwarder` (self-terminating per supervisor), a zustand `tunnel-status-store` fed by an app-root `useTunnelStatusEvents` hook, and a sidebar pill — amber **"Reconnecting…"** on a sleep/wake or WiFi flap, red **"Connection lost — re-push"** once the circuit breaker trips — so a dropped tunnel no longer looks like a frozen workspace. (2) **Host persistence**: auto-upgrade no longer kills host-side agents — `hosts_upgrade` probes the daemon's `live_terminals` (via `codemux-remote serve status`) and **defers** the systemd-unit restart when sessions are live (`UpgradeOutcome::Skipped`); separately, the local pty-daemon now **idle-reaps** itself after 1h with zero sessions (hard re-check under lock so it can never reap a live session). (3) **Workspaces-sync robustness**: project-first remote pull with a real protected root (new local-only `default_branch` column + `resolve_default_branch`/`ensure_origin_head` + `workspaces_adopt_project` "Pull project" action), serialized adopts via a per-row creation lock, client-side `dedupe_sibling_rows` collapse of cross-device duplicate cards, daemon-side `collapse_main_for_uid`/`normalize_main_workspaces` (one repo root per project), uid-keyed collision-safe host paths (`<basename>-<short-uid>`), and a non-destructive `workspaces_reconcile_copy` action for legacy divergent copies. (4) **OpenFlow comm-log fix**: the daemon-backed agent spawn path (default since persistent agents) now tees cleaned PTY output to the communication log via the shared `comm_log_entry_for_chunk` helper, so daemon-spawned OpenFlow agents stop producing an empty log that blinded stuck-detection. See `docs/features/remote-hosts.md`, `docs/features/persistent-agents.md`, `docs/features/workspaces-sync.md`, `docs/features/workspaces-overview.md`, `docs/features/openflow.md`, `docs/plans/repo-unit-sync.md`.
Expand Down
2 changes: 1 addition & 1 deletion docs/features/remote-hosts.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ Headless tool surface (advertised via `tools/list`):
| Tool | Purpose |
|---|---|
| `workspace_create` | Register a new workspace. |
| `worktree_create` | `git worktree add` a branch under `~/.codemux/worktrees/<repo>/<branch>` (desktop layout) and register it with `kind = worktree` sharing the parent repo's `project_uid`. |
| `worktree_create` | `git worktree add` a branch under `~/.codemux/worktrees/<repo>/<branch>` (desktop layout) and register it with `kind = worktree` sharing the parent repo's `project_uid`. Desktop parity (issue #78): fetches `base` from origin first so new branches start at the remote tip (best-effort, 10s cap, offline falls back to local refs), copies gitignored include files (`.env` & co, `.codemuxinclude` → defaults) from the parent repo before returning, and runs the project's `.codemux/config.json` setup commands on a background thread with `CODEMUX_ROOT_PATH`/`CODEMUX_WORKSPACE_PATH`/`CODEMUX_BRANCH`/`CODEMUX_PORT` (+ NAME/ID) set — `CODEMUX_PORT` comes from the same deterministic `allocate_workspace_port` hash as the desktop. The response's `setup` field reports `{port, includes_copied, setup_commands, setup_running}`; setup progress/failures go to the daemon's stderr (journal). The Settings-UI script fallback is desktop-only (it lives in the desktop SQLite DB), so headless config is file-based only. |
| `workspace_list` | All workspaces, newest first. |
| `workspace_info` | One workspace by id. |
| `workspace_update` | Mutate name/branch/notes. |
Expand Down
21 changes: 20 additions & 1 deletion docs/features/setup-teardown.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,24 @@ Setup can be re-run on an existing workspace:

Re-run executes the full pipeline: `.codemuxinclude` file copy, then setup commands.

### Headless Daemon Parity (remote hosts)

The headless `codemux-remote serve` daemon runs the same pipeline when an
agent creates a worktree via the `worktree_create` MCP tool (issue #78):

- Worktree includes copy runs synchronously before the tool returns.
- Setup commands run on a background thread via the shared
`crate::scripts::run_setup_commands` core — identical commands,
identical `CODEMUX_*` env, same deterministic `CODEMUX_PORT`.
- Progress goes to the daemon's stderr (journal) instead of Tauri
events; the tool response carries a `setup` summary
(`{port, includes_copied, setup_commands, setup_running}`).
- Config is file-based only (`.codemux/config.json` in worktree → repo
root). The Settings-UI fallback lives in the desktop SQLite DB and
does not exist on a headless host.
- A failing setup script never fails the tool call — the workspace is
still created and registered, like the desktop.

### Run Command Behavior

- `Ctrl+Shift+G` or command palette "Run Dev Command"
Expand Down Expand Up @@ -146,8 +164,9 @@ The "Configure" button opens Settings > Projects. Dismiss persists per-project.

- `src-tauri/src/config/workspace_config.rs` — config struct, reader, git root resolver, `read_effective_config`
- `src-tauri/src/database.rs` — `ProjectScripts` struct, DB get/set methods
- `src-tauri/src/scripts.rs` — setup/teardown/run execution, `.codemuxinclude` processing, port allocation
- `src-tauri/src/scripts.rs` — setup/teardown/run execution, `.codemuxinclude` processing, port allocation; `run_setup_commands` is the UI-free core shared with the headless daemon
- `src-tauri/src/commands/workspace.rs` — lifecycle hooks, `run_project_dev_command`, Tauri commands
- `src-tauri/src/remote/tools/mod.rs` — headless `worktree_create` provisioning (`provision_worktree_workspace`)
- `src-tauri/src/control.rs` — `rerun_setup` socket command
- `src-tauri/src/cli.rs` — `codemux workspace rerun-setup` CLI command
- `src-tauri/src/commands/database.rs` — `get_project_scripts`, `set_project_scripts` commands
Expand Down
2 changes: 2 additions & 0 deletions docs/features/worktree-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ config/master.key

Files are copied (not symlinked), preserving directory structure. Runs before setup scripts so copied files are available during setup.

The same bootstrap (includes copy + setup scripts + env vars below) also runs when a worktree is created on a remote host through the headless daemon's `worktree_create` MCP tool — see `docs/features/setup-teardown.md` § "Headless Daemon Parity".

## Environment Variables

Setup and teardown scripts receive:
Expand Down
2 changes: 1 addition & 1 deletion docs/plans/mcp-on-remote.md
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,7 @@ Again: **do not build this in v1.** The point of listing it is to lock in the fo
- `pty.rs` — minimal portable-pty wrapper, per-terminal ring buffer (1 MiB cap).
- `server.rs` — axum HTTP routes: `GET /health`, `GET /tools/list`, `POST /tools/call`.
- `mcp.rs` — JSON-RPC 2.0 stdio MCP server (`initialize`, `tools/list`, `tools/call`) forwarding to the local daemon over HTTP.
- `tools/mod.rs` — 12 headless tools: workspace_{create,list,info,update,close}, worktree_create (added `v0.7.5`, backed by `remote/git.rs`), terminal_{spawn,write,read,list,close}, app_status.
- `tools/mod.rs` — 12 headless tools: workspace_{create,list,info,update,close}, worktree_create (added `v0.7.5`, backed by `remote/git.rs`; since issue #78 it also provisions like the desktop — fetch-before-branch, worktree-includes copy, background `.codemux/config.json` setup commands with `CODEMUX_*` env + deterministic port, summarized in the response's `setup` field), terminal_{spawn,write,read,list,close}, app_status.
- **CLI subcommands on `codemux-remote`**: `serve` (long-running daemon with graceful SIGTERM/SIGINT shutdown), `serve status`, `serve stop`, `mcp` (stdio MCP for an agent CLI).
- **Tests**: 26 unit tests in the `remote` module + 8 end-to-end integration tests in `src-tauri/tests/codemux_remote_serve_mcp.rs` covering the full HTTP roundtrip, the MCP stdio roundtrip (the headline test), and edge cases (auth required, singleton check, status JSON, missing-daemon error path, PTY echo roundtrip).
- **Example systemd user unit** at `scripts/codemux-remote.service.example` with install instructions.
Expand Down
32 changes: 27 additions & 5 deletions src-tauri/src/commands/mcp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -288,11 +288,33 @@ mod tests {
.await
.unwrap();

// Exactly one `codemux` row — the always-on hardcoded one.
let codemux_rows: Vec<_> =
result.iter().filter(|s| s.name == "codemux").collect();
assert_eq!(codemux_rows.len(), 1);
assert_eq!(codemux_rows[0].primary_source(), McpConfigSource::Codemux);
// The project-scope `codemux` entry must be suppressed: no
// codemux row may carry a project-level source. (Scoped to
// project sources rather than a global count because
// `list_mcp_servers` also scans real user-level config paths —
// on a developer machine where `codemux-remote serve` has
// auto-registered itself into e.g. `~/.claude.json`, a
// user-level codemux row legitimately coexists with the
// hardcoded self row.)
assert!(
!result.iter().any(|s| {
s.name == "codemux"
&& s.sources.iter().any(|src| matches!(
src,
McpConfigSource::ClaudeProject | McpConfigSource::CodemuxProject
))
}),
"project-scope codemux entry leaked into {:?}",
result
);
// The always-on hardcoded self row is present exactly once.
assert_eq!(
result
.iter()
.filter(|s| s.primary_source() == McpConfigSource::Codemux)
.count(),
1
);
// `demo` from the same file still shows up.
assert!(result.iter().any(|s| s.name == "demo"));
}
Expand Down
Loading
Loading