Skip to content

feat(claude): detach from Claude Code with Ctrl-Q and resume the live session - #56

Merged
Mickael-Roger merged 2 commits into
Mickael-Roger:mainfrom
Banh-Canh:feat/claude-detach
Oct 7, 2026
Merged

Mickael-Roger merged 2 commits into
Mickael-Roger:mainfrom
Banh-Canh:feat/claude-detach

Conversation

@Banh-Canh

Copy link
Copy Markdown
Contributor

Problem

OpenCode survives leaving a workspace because the attach is only a client of a persistent opencode serve. Claude Code has no server: it ran as the exec of the attach, so leaving a workspace killed Claude and whatever turn it was running. There was no way to go back to the dashboard while Claude kept working.

Fix

Claude runs under dtach inside the container. The Claude attach command becomes dtach -A /tmp/opencode-manager-claude.sock -e '^q' -r winch -z claude: the first attach starts the session, later attaches reconnect to it, and Ctrl+Q detaches back to the dashboard while Claude keeps running (its status stays live through the existing hooks). Ctrl+C stays with Claude to interrupt a turn. Ctrl+Q is unbound in Claude Code and typeable on every layout; dtach's default Ctrl+\ needs AltGr on AZERTY.

  • dtach is added to the base image, and Dockerfile.workspace installs it best-effort when the resolved base predates it. Without dtach, attaching runs Claude directly, as before.

Claude attaches are relayed through a host-side pty (internal/termproxy). Three problems only show up with a real terminal:

  1. Ctrl+Q never reached dtach. Claude switches capable terminals (kitty, foot, Ghostty, WezTerm…) to the kitty keyboard protocol and xterm modifyOtherKeys, so Ctrl+Q arrives as ESC[113;5u / ESC[27;5;113~ instead of 0x11. The relay rewrites those encodings (press/repeat, Caps/Num Lock, alternate-key fields) to the raw byte.
  2. The dashboard received escape sequences after a detach. The terminal stayed in the modes Claude enabled. The relay tracks them from Claude's output (kitty flags, modifyOtherKeys, focus/paste/mouse/alt-screen/cursor modes), resets them once detached, and replays them on reattach, since Claude does not renegotiate them.
  3. A reattach showed a blank screen. Claude repaints only what changed, and dtach's same-size redraw signal changes nothing. Once the attach is live, the relay narrows the pty by one column and restores it, so Claude repaints its whole screen.

The relay is used by the TUI (Enter / Ctrl+A) and ocm ws attach --runtime claude. OpenCode and DeepSeek attaches are unchanged. The relay reads stdin through muesli/cancelreader (already used by Bubble Tea), so no goroutine is left holding the terminal after a detach. New dependency: github.com/creack/pty.

Shortcut documentation

  • docs/tui.md: a "Returning to the dashboard" table (OpenCode Ctrl+C, Claude Code Ctrl+Q) and a "Detaching from Claude Code" section.
  • docs/getting-started.md, docs/cli.md (ws attach) and README.md mention the detach keys.
  • AGENTS.md notes for future changes.

Testing

  • gofmt, go vet ./... and go test ./... pass; the branch also builds for darwin/arm64.
  • New unit tests cover the key rewriting table, mode tracking/reset/replay with Claude's real startup sequences (split across reads), an end-to-end run through real ptys (a program enables the kitty protocol, receives Ctrl+Q as 0x11, and the terminal is reset afterwards), the redraw resize, and the attach script with and without dtach.
  • With a real claude under dtach through the relay in a pty harness: Claude enabled the kitty protocol, Ctrl+Q detached with the session still alive, and a reattach repainted the full screen (before the redraw fix: 42 bytes of output and no text).
  • Detaching was tested interactively on Linux with Podman and an AZERTY keyboard.

Notes: no version bump, left to your release tags. Existing workspaces rebuild their image once (the build context changed) to pick up dtach.

🤖 Generated with Claude Code

@Mickael-Roger

Copy link
Copy Markdown
Owner

Blocking issue: the Ctrl+Q translation is not safe when a kitty/modifyOtherKeys CSI sequence is split across terminal reads.

Command.Run calls translateDetachKey independently for each reader.Read result. translateDetachKey deliberately passes an incomplete sequence through unchanged, so a split ESC[113; + 5u reaches dtach as the original escape sequence rather than byte 0x11. In that case Ctrl+Q does not detach.

Please retain incomplete input CSI bytes between reads, translate once the sequence completes, and add an end-to-end regression test that writes the Ctrl+Q sequence in at least two chunks. The existing split sequence unit case currently asserts the failing behavior, so it should be updated as part of the fix.

@Banh-Canh

Copy link
Copy Markdown
Contributor Author

Thanks, fixed: an unterminated CSI sequence at the end of a read is now kept until the next read completes it (or forwarded as is after 50ms if nothing follows, so a lone Esc isn't stuck). The split sequence case is updated, and a new end-to-end test sends Ctrl+Q in two chunks through real ptys. Also rebased on main.

Banh-Canh and others added 2 commits October 7, 2026 07:36
… session

Unlike OpenCode, Claude Code has no server to reconnect to: it ran as the
`exec` of the attach, so leaving a workspace killed Claude and any turn in
progress.

- Run Claude under dtach inside the container (socket
  /tmp/opencode-manager-claude.sock). Ctrl-Q detaches back to the
  dashboard while Claude keeps working; the next attach resumes the same
  session. Ctrl-C stays with Claude to interrupt a turn. Ctrl-Q is unbound
  in Claude Code and typeable on any layout (Ctrl-\, dtach's default,
  needs AltGr on AZERTY).
- Add dtach to the base image, and install it best-effort in the
  workspace image when the base predates it. Without dtach, attaching
  runs Claude directly, as before.
- Relay Claude attaches (TUI and `ocm ws attach`) through a host-side pty
  (internal/termproxy). Claude switches capable terminals to the kitty
  keyboard protocol and modifyOtherKeys, where Ctrl-Q arrives as an
  escape sequence dtach never matches; the relay rewrites it to the raw
  byte. It also resets the keyboard/reporting modes Claude enabled once
  detached, so the dashboard keeps working, replays them on reattach, and
  briefly narrows the pty by one column so Claude repaints its whole
  screen instead of showing a blank one.
- Document the detach keys (TUI guide, getting started, CLI, README).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The relay translated each read on its own, so a kitty or modifyOtherKeys
Ctrl-Q split across reads (ESC[113; then 5u) reached dtach unchanged and
did not detach. Hold back an unterminated control sequence at the end of a
read until the next one completes it, or forward it as is after 50ms, so a
lone Escape key press is not stuck.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Mickael-Roger
Mickael-Roger merged commit ad84667 into Mickael-Roger:main Oct 7, 2026
3 checks passed
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