Merge pull request 'feat(attach): --continue on re-attach + keep bottles on dashboard quit' (#47) from reattach-resume-flag into main
test / unit (push) Successful in 17s
test / integration (push) Successful in 1m8s

This commit was merged in pull request #47.
This commit is contained in:
2026-05-26 14:04:32 -04:00
2 changed files with 48 additions and 7 deletions
+33 -3
View File
@@ -696,10 +696,15 @@ def _attach_to_bottle(
slug: str,
) -> str:
"""Handoff: curses.endwin → attach claude → curses refresh.
Returns the post-attach status-line message."""
Re-entry into a running bottle from the dashboard always
passes `--resume` so claude picks up its prior conversation
rather than starting a fresh transcript — the first attach
happens via `_new_agent_flow` which sets up the transcript
in the first place. Returns the post-attach status-line
message."""
curses.endwin()
try:
exit_code = attach_claude(bottle, remote_control=False)
exit_code = attach_claude(bottle, remote_control=False, resume=True)
except BaseException:
stdscr.refresh()
raise
@@ -855,6 +860,30 @@ def _try_init_green() -> int:
return 0
def _quit_without_teardown(bottles: dict) -> None:
"""Exit the dashboard process WITHOUT triggering Python's normal
cleanup of the `bottles` dict's context managers.
The dict holds `@contextmanager`-decorated objects whose
underlying generators have implicit close-on-GC behavior:
when Python's interpreter shutdown collects them, each
generator's `finally` block runs, which invokes that bottle's
teardown (`docker compose down`). PRD 0020 explicitly DOESN'T
want that — quitting the dashboard should leave running
bottles running. `os._exit` skips all Python-level cleanup
(GC, atexit, stdio flush, etc.), so the docker compose
projects survive the dashboard exit untouched.
The `bottles` arg is accepted for the explicit
documentation-of-intent — we're choosing not to close
these. Curses gets its terminal restored via the explicit
`endwin` below since `os._exit` doesn't run
curses.wrapper's finally."""
del bottles # nothing to do with it; the os._exit is the point
curses.endwin()
os._exit(0)
# PRD 0019 chunk 3: which pane the j/k/arrow keys move through.
# Tab toggles. The proposals pane is the default focus — proposal
# action keys (a/m/r/Enter) require it; agent-scoped keys (e/p,
@@ -935,7 +964,8 @@ def _main_loop(stdscr: "curses._CursesWindow") -> None:
status_line = ""
if key in (ord("q"), 27): # q or ESC
return
_quit_without_teardown(bottles)
return # unreachable; _quit_without_teardown os._exit's
if key == 9: # Tab
focus = PANE_AGENTS if focus == PANE_PROPOSALS else PANE_PROPOSALS
continue
+15 -4
View File
@@ -92,15 +92,22 @@ def prepare_with_preflight(
def attach_claude(
bottle: Bottle, *, remote_control: bool = False,
bottle: Bottle, *, remote_control: bool = False, resume: bool = False,
) -> int:
"""Run claude inside `bottle` as an interactive session. Blocks
until the session ends; returns the claude process's exit code.
`resume=True` adds `--continue` so claude picks up its most
recent session non-interactively (no session-picker prompt) —
the right shape for the dashboard's Enter re-attach (PRD 0020
chunk 3), where a bottle typically has exactly one session.
First-attach paths (`./cli.py start`, the dashboard's new-agent
flow) leave it False.
Used as the inner step of `./cli.py start` (one-shot) and by the
dashboard (PRD 0020), which calls it from inside a `curses.endwin
→ … → stdscr.refresh()` handoff so the curses surface gets out
of the terminal's way while claude has it."""
dashboard, which calls it from inside a `curses.endwin → … →
stdscr.refresh()` handoff so the curses surface gets out of the
terminal's way while claude has it."""
info(
"attaching interactive claude session "
"(Ctrl-D or 'exit' to leave; container will be removed)"
@@ -108,6 +115,10 @@ def attach_claude(
claude_args = ["--dangerously-skip-permissions"]
if remote_control:
claude_args.append("--remote-control")
if resume:
# `--continue` jumps straight to the most recent session
# without showing the picker `--resume` would surface.
claude_args.append("--continue")
return bottle.exec_claude(claude_args, tty=True)