Skip to content

feat(web): session detach/undock + beta instance isolation (port 5000)#103

Open
Ark0N wants to merge 3 commits into
masterfrom
beta/session-detach
Open

feat(web): session detach/undock + beta instance isolation (port 5000)#103
Ark0N wants to merge 3 commits into
masterfrom
beta/session-detach

Conversation

@Ark0N
Copy link
Copy Markdown
Owner

@Ark0N Ark0N commented Jun 6, 2026

Beta branch — opened to run CI; not necessarily for immediate merge.

Session detach / undock

  • GET /session/:id serves the SPA in "solo mode", reusing the existing client (terminal, local-echo overlay, reconnect) — no duplicated terminal code. One PTY already fans out to N SSE/WS clients, so a detached window is just another live client; no server fan-out work was needed (the brief's "critical question" was already solved).
  • Per-tab ⧉ pop-out icon; detached tabs show a badge and focus the popup on click; closing the popup re-docks. Cross-window state via BroadcastChannel + a WindowProxy poll, surviving dashboard reload (roll-call). app.detachSession(id) is a single idempotent entry point (future gesture hook). <base href="/"> so relative assets resolve under /session/:id.

Beta instance isolation (run alongside prod)

  • Default port 3000 → 5000.
  • New src/config/instance.ts: CODEMAN_INSTANCE (default beta) derives both the data dir (~/.codeman-beta) and tmux socket (tmux -L codeman-beta). Every ~/.codeman path now routes through dataPath()/getDataDir(). Overridable via CODEMAN_INSTANCE / CODEMAN_DATA_DIR / CODEMAN_TMUX_SOCKET. Prevents a second instance from discovering and attaching PTYs to the first instance's live tmux sessions.

Verification

  • tsc / eslint / prettier / lockfile clean.
  • Playwright E2E (detach → popup → badge → focus-on-click → redock-on-close → solo chrome → "session unavailable").
  • Default isolation confirmed to see zero real sessions.

🤖 Generated with Claude Code

Ark0N and others added 3 commits June 6, 2026 03:53
Detach a session tab into its own browser window and back.

Detach/undock:
- GET /session/:id serves the SPA in "solo mode", reusing the existing
  client (terminal, local-echo overlay, reconnect) so no terminal code is
  duplicated. One PTY already fans out to N SSE/WS clients, so a detached
  window is just another live client — no server fan-out work was needed.
- A pop-out icon per tab; detached tabs show a badge and focus the popup on
  click; closing the popup re-docks. Cross-window state via BroadcastChannel
  plus a WindowProxy poll, and survives a dashboard reload (roll-call).
  app.detachSession(id) is a single idempotent entry point (future gesture
  hook). <base href="/"> so relative assets resolve under /session/:id.

Beta-branch isolation (so it can run alongside a prod Codeman):
- Default port 3000 -> 5000.
- New src/config/instance.ts derives the data dir and tmux socket from
  CODEMAN_INSTANCE (default "beta"): ~/.codeman-beta + tmux -L codeman-beta.
  Every ~/.codeman path now goes through dataPath()/getDataDir() (state,
  mux-sessions, settings, push keys, lifecycle log, screenshots, certs,
  linked-cases, subagent window state). Overridable via CODEMAN_INSTANCE /
  CODEMAN_DATA_DIR / CODEMAN_TMUX_SOCKET. Prevents a second instance from
  discovering and attaching PTYs to the first instance's live tmux sessions.

Verified: tsc / eslint / prettier / lockfile clean; Playwright E2E (27 checks)
for detach/solo/redock; default isolation confirmed to see zero real sessions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…instance

The instance-isolation sweep routed every ~/.codeman write through dataPath()
except the legacy ~/.claudeman → ~/.codeman migration in the StateStore
constructor, which stayed hardcoded. Gate the whole legacy block on the default
(prod) instance so a named instance (e.g. CODEMAN_INSTANCE=beta) never reads or
renames into the shared ~/.codeman / ~/codeman-cases layout. Prod behavior is
unchanged (CODEMAN_INSTANCE empty → migration still runs).

Note: swapping newDir to getDataDir() was rejected — its mkdirSync side-effect
would make !existsSync(newDir) false and silently disable the migration.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Finding 2: unify the pop-out icon and tab-click paths via _raiseDetached().
  After a dashboard reload (no owned WindowProxy ref), clicking the pop-out icon
  no longer re-runs window.open() — which reloaded the live popup's terminal —
  and instead raises it via the channel, matching the tab-click behavior.
- Finding 3: debounce channel-driven redock. A popup *reload* emits
  redocked->detached in quick succession; a 1.5s grace lets the re-announce
  cancel the redock so the dashboard badge no longer blips on popup refresh.
- Finding 4: periodic liveness reconcile. A popup hard-killed without a
  'pagehide' (crash / OS kill) while the dashboard holds no ref would leave its
  tab stuck "detached". The dashboard now re-roll-calls every 5s and re-docks
  any channel-only tab that stays silent.

Frontend-only; validated with node --check (app.js is outside the ts/lint/prettier gates).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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