diff --git a/docs/core/STATUS.md b/docs/core/STATUS.md index 1597c64..1110201 100644 --- a/docs/core/STATUS.md +++ b/docs/core/STATUS.md @@ -8,11 +8,13 @@ ## Current Headline -Codemux is past Linux MVP and shipping cross-platform binaries. The workspace shell, terminal management, git integration, presets, settings sync, and most ADE features are real and daily-drivable on both Linux and Windows. Latest released version is `v0.7.9`. +Codemux is past Linux MVP and shipping cross-platform binaries. The workspace shell, terminal management, git integration, presets, settings sync, and most ADE features are real and daily-drivable on both Linux and Windows. Latest released version is `v0.8.0`. -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 ` 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 ` / `--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`. +Shipped in `v0.8.0` 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 ` 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 ` / `--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/` 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`. +Landed on `main` after the `v0.8.0` tag (unreleased): the **Tauri/React performance & architecture epic (issue #81 — all nine sub-issues closed)**. Terminal: the already-installed xterm.js **WebGL renderer addon is now actually loaded** (GPU glyph rendering with `onContextLoss` + no-WebGL2 DOM-renderer fallback, issue #72), and **PTY producer back-pressure is engaged on the live render path** (issue #73 — see "Workspace & terminals" below). Main thread: the blocking `workspace.rs` (issue #74) and `files.rs` (issue #79) commands are now `async` + `spawn_blocking`, so workspace/worktree creation, directory listing, file search/read/write, and clipboard-image saves no longer stall the GTK main thread. Agent chat: thread-scoped runtime events — including the `content_delta` token stream — arrive over a **per-thread Tauri `Channel`** (`attach_agent_chat_output`/`detach_agent_chat_output` + `AgentChatChannelRegistry`, issue #75) instead of the global event bus, and the transcript is **virtualized** with `react-virtuoso` (issue #77) so the DOM stays bounded (~10–20 mounted rows) for arbitrarily long sessions. Git: new workspace branches **fetch `origin/` before branching** (issue #76, shared with the daemon). Plus opt-in **run-start rollback checkpoints** (issue #80, Settings → Agent — see "Agent Chat" below) and the headless-daemon worktree provisioning parity described next (issue #78; containerized clean-host e2e at `scripts/e2e/daemon-worktree-setup-e2e.sh`). + +Also landed on `main` after the `v0.8.0` 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/` 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 `.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`. @@ -24,7 +26,7 @@ The same `v0.7.8` window added **Docker-published container ports** to the sideb Shipped in `v0.7.7` is a **cross-device pull/adopt/close hardening pass** — two audits (one deep code audit, one live end-to-end SSH round-trip against a loopback host) surfaced a cluster of failure-mode/lifecycle bugs in the remote-host push/pull/adopt + inventory-poll pipeline, all off the happy path. The fixes: (1) `workspaces_adopt_synced` now checks `pull_outcome.ok` — a failed rsync/SSH pull previously returned a success toast over an empty shell that the idempotency guard then refused to retry; on failure it now removes the shell, reverts the row to a re-pullable sibling (`unlink_workspace_sync_from_local`), and returns a real `Err`. (2) Both close paths (`close_workspace` **and** `close_workspace_with_worktree`) now tear down the SSH tunnel supervisor + cached client (`forget_workspace_client` + `shutdown_supervisor`); previously closing a pushed workspace leaked the `TunnelSupervisor` task, the bound local socket, and the remote pty-daemon for the app's lifetime. (3) All three adopt paths (single-dir, worktree-repo-rsync, clone) now stamp `project_uid` (`set_workspace_project_identity`) so adopted workspaces converge with their siblings in the overview instead of pushing a `None` that wiped the daemon-derived uid. (4) The inventory poller undeletes a remote-discovered tombstone on reappear (and dedupes duplicate ids within one envelope) instead of INSERTing a fresh row that churned the cloud row. (5) `pull_workspace_back` classifies a missing remote path as `RemoteNotFound` via a stdout sentinel (`CMX_REMOTE_DIR_OK/MISSING`) instead of a fragile `exit status: 7` string-match. (6) `provision_workspace_mcp_config` resolves the remote `$HOME` and emits an absolute `.mcp.json` command path (the systemd `%h` specifier agent CLIs can't expand silently broke remote MCP auto-discovery). Plus shell-injection hardening on `ssh_upload_executable`/`ssh_write_file` (via tilde-aware `shell_escape`), a tunnel-socket hash widened 12→16 hex chars (48→64-bit, kills a birthday-collision risk), a stale-link pull guard (`detect_same_branch_project_conflict` now intersects against live workspace ids so a closed-but-stale sync row no longer phantom-blocks Pull), and a sidebar fix so the workspace-row X button no longer overlays + swallows clicks on the linked-issue badge. A new env-gated (`CODEMUX_E2E_SSH_HOST`) integration harness, `src-tauri/tests/codemux_ssh_roundtrip.rs`, encodes the `RemoteNotFound`, `.mcp.json`-absolute, and tilde-expansion regressions. See `docs/features/workspaces-sync.md`, `docs/features/remote-hosts.md`. -The `v0.7.5` release rounds out the remote-workspace story and hardens the terminal. **Agent-created host workspaces now pull with their files and git intact**: a workspace an agent built directly on a `codemux-remote` host (e.g. via `git worktree add` + `workspace_create`) previously pulled to the desktop as an empty worktree because the pull rsynced from the conventional `~/.codemux/worktrees//` path rather than the workspace's real on-host `path`. The fix threads the daemon's actual path through a new local-only `origin_path` column on `workspaces_sync`, recreates linked worktrees of local-only repos via a whole-repo rsync + `git worktree prune`/`add` (so a worktree's cross-machine-broken `.git` gitfile is rebuilt locally), and stops the cloud round-trip from clobbering locally-derived `project_uid`/`workspace_kind` with `COALESCE(server, local)`. The same work added a **`worktree_create` tool to the headless daemon catalog (now 12 tools, up from 11)** so an agent on a host can make a canonical-layout worktree directly instead of improvising one. See `docs/plans/remote-workspace-pull-fix.md`. The terminal gained **PTY output flow-control / backpressure** plumbing (a daemon-side `SetFlowPaused` request + HIGH/LOW write-queue watermarks) — landed but currently inert on the live render path (see "Workspace & terminals" below) — plus split panes now inherit the workspace cwd, and the **standalone "Diff Viewer" new-tab (+) and command-palette entry points were removed** (the Changes-panel file-click flow is the only way to open a diff tab; the diff tab kind and all diff infrastructure stay). +The `v0.7.5` release rounds out the remote-workspace story and hardens the terminal. **Agent-created host workspaces now pull with their files and git intact**: a workspace an agent built directly on a `codemux-remote` host (e.g. via `git worktree add` + `workspace_create`) previously pulled to the desktop as an empty worktree because the pull rsynced from the conventional `~/.codemux/worktrees//` path rather than the workspace's real on-host `path`. The fix threads the daemon's actual path through a new local-only `origin_path` column on `workspaces_sync`, recreates linked worktrees of local-only repos via a whole-repo rsync + `git worktree prune`/`add` (so a worktree's cross-machine-broken `.git` gitfile is rebuilt locally), and stops the cloud round-trip from clobbering locally-derived `project_uid`/`workspace_kind` with `COALESCE(server, local)`. The same work added a **`worktree_create` tool to the headless daemon catalog (now 12 tools, up from 11)** so an agent on a host can make a canonical-layout worktree directly instead of improvising one. See `docs/plans/remote-workspace-pull-fix.md`. The terminal gained **PTY output flow-control / backpressure** plumbing (a daemon-side `SetFlowPaused` request + HIGH/LOW write-queue watermarks) — inert on the live render path at the time, later wired live by issue #73 (see "Workspace & terminals" below) — plus split panes now inherit the workspace cwd, and the **standalone "Diff Viewer" new-tab (+) and command-palette entry points were removed** (the Changes-panel file-click flow is the only way to open a diff tab; the diff tab kind and all diff infrastructure stay). `v0.7.4` shipped **first-class project identity + `main`/`worktree` workspace kind** (landed on `main` before the tag). "Project" was previously implicit (path-derived `basename(project_root)`, with `main`-vs-`worktree` inferred from `worktree_path == null`), so a repo cloned at different paths on different devices/hosts had no stable identity and agent-/host-created workspaces appeared anonymous. A new `src-tauri/src/project_identity.rs` computes identity **deterministically** — `project_uid = UUIDv5(canonical git remote ?? absolute project_root)` — so every checkout of the same remote converges on the same uid with zero coordination (no replicated projects table). `derive_kind` classifies `main` (`.git` is a directory) vs `worktree` (`.git` is a file). The headless daemon (`remote/workspace.rs`) stamps `project_uid`/`project_name`/`kind`/`repo_remote` at create plus an idempotent boot sweep; the `hosts_inventory` poller threads them into `workspaces_sync` (and finally populates `project_remote` for remote-discovered rows, closing a v1 gap); the desktop stamps `WorkspaceSnapshot.project_uid`/`workspace_kind` in `set_workspace_project_root`; and the cloud `codemux_workspaces` schema now round-trips all three fields (server-authoritative on pull). The overview groups by `project_uid` and renders a `main`/`worktree` badge, and the pull-conflict guard matches on exact `project_uid`. See `docs/plans/project-identity.md`, `docs/features/workspaces-sync.md`, `docs/features/workspaces-overview.md`. @@ -52,7 +54,7 @@ The repo structure is clean and domain-split: - Hidden-pane terminal pause to eliminate cross-workspace typing lag - Tab bar with terminal, browser, editor, and diff tab types - Pane splits, resize, drag-swap, close — split panes inherit the workspace cwd (`v0.7.5`) -- **PTY output flow-control / backpressure** plumbing (`v0.7.5`): a daemon-side `SetFlowPaused` wire request + per-session `flow_paused` gate, and renderer HIGH (16 MiB) / LOW (4 MiB) write-queue watermarks that can `pause_pty_output`/`resume_pty_output`. The daemon side and fail-safes are real, but only the disabled `terminal-cache.ts` lineage calls pause/resume — the live `TerminalPane` path does not, so backpressure is **inert in production today** (the live freeze fix is the throttled write pump, not PTY pausing). See `docs/features/terminal.md`. +- **PTY producer back-pressure — engaged on the live path** (issue #73; daemon-side plumbing since `v0.7.5`): the live `TerminalPane`'s write pump tracks queued bytes and calls `pause_pty_output` above the HIGH watermark (16 MiB) / `resume_pty_output` below LOW (4 MiB, hysteresis), so a fast producer (`yes`, a verbose build, a runaway agent) blocks on the kernel PTY buffer instead of ballooning the renderer queue or overflowing the `pending_output` ring. Both spawn paths honor the pause: daemon-backed sessions via the `SetFlowPaused` wire request, in-process sessions via a per-session `flow_paused: Arc` polled by `batched_reader_loop` (previously a deliberate no-op). Self-heals everywhere — the flag clears on attach/detach, a 10 s `FLOW_MAX_PARK` backstop force-resumes a wedged pause, and the renderer resumes on unmount. The throttled write pump remains the consumer-side complement. See `docs/features/terminal.md`. - **Terminal workspace-switch performance**: the live terminal path is the per-mount lifecycle in `src/components/terminal/TerminalPane.tsx` (xterm constructed on mount, disposed on unmount — only the active workspace/surface is ever rendered). Switching no longer freezes because all xterm writes (disk-scrollback restore + the `attach_pty_output` reattach replay + live output) drain through a throttled, byte-budgeted write pump (`src/components/terminal/terminal-write-pump.ts`) that yields between batches, and the wasted serialize of alt-screen TUI buffers (Claude Code, lazygit, vim, btop) is skipped on unmount. The earlier persistent-xterm cache (`terminal-cache.ts`, shipped `14735bf`) was rolled back in `2baa42f`; it is retained but **disabled / not wired** (see its banner) pending a possible future flag-gated revival, so `useTerminalCacheGc` / `useTerminalThemeSync` are no-ops today. - **Session persistence**: terminal scrollback save/restore across restarts (Windows-only backend backstop in `scrollback::flush_cache_to_disk`), adapter-based resume for CLI tools (Claude Code `--resume`/`--continue` via hook-captured session IDs) - **Persistent PTY daemon** (`pty_daemon::server` + `client` + `supervisor` + `manifest`): every shell spawn now routes through a detached `codemux pty-daemon` subprocess so agents survive app close. On relaunch the supervisor adopts the running daemon and reattaches live sessions. **Default-on**, no setting; `CODEMUX_DISABLE_PTY_DAEMON=1` is the only escape hatch. Graceful fallback to the in-process portable-pty path on every error site, plus a 3-failures-in-60s crash circuit breaker that disables the daemon path for the rest of the process lifetime. Unix only; Windows still uses the in-process path until the named-pipe IPC is wired. **Since `v0.7.9`**: the daemon **idle-reaps** itself (manifest + socket removed, process exits) after 1h with zero live sessions (hard re-check under the lock so it can never reap a live session), and the daemon-backed reader now tees cleaned PTY output to the OpenFlow comm log via the shared `comm_log_entry_for_chunk` helper (fixing empty comm logs for daemon-spawned OpenFlow agents).