From 832e92c7a68f22cdb7038130debb38fa6125b685 Mon Sep 17 00:00:00 2001 From: didericis Date: Tue, 26 May 2026 03:52:12 -0400 Subject: [PATCH 1/3] feat(attach): pass --resume on dashboard re-attach MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Re-entering a running bottle from the dashboard (Enter on the agents pane) now invokes claude with `--resume` so the session picks up the prior conversation history rather than starting a fresh transcript. The first-attach paths (`./cli.py start` and the dashboard's new-agent `n` flow) leave it off — the transcript doesn't exist yet there. `attach_claude` gains a `resume: bool = False` kwarg; `_attach_to_bottle` in the dashboard passes `True`. --- claude_bottle/cli/dashboard.py | 9 +++++++-- claude_bottle/cli/start.py | 16 ++++++++++++---- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/claude_bottle/cli/dashboard.py b/claude_bottle/cli/dashboard.py index 3000f69..cb40743 100644 --- a/claude_bottle/cli/dashboard.py +++ b/claude_bottle/cli/dashboard.py @@ -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 diff --git a/claude_bottle/cli/start.py b/claude_bottle/cli/start.py index 1647b18..c9964b8 100644 --- a/claude_bottle/cli/start.py +++ b/claude_bottle/cli/start.py @@ -92,15 +92,21 @@ 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 `--resume` so claude picks up its prior + conversation history rather than starting a fresh transcript — + the right shape for the dashboard's Enter re-attach (PRD 0020 + chunk 3). 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 +114,8 @@ def attach_claude( claude_args = ["--dangerously-skip-permissions"] if remote_control: claude_args.append("--remote-control") + if resume: + claude_args.append("--resume") return bottle.exec_claude(claude_args, tty=True) From 14d5c783706f0e917ba1e0c856ff49d608323c6e Mon Sep 17 00:00:00 2001 From: didericis Date: Tue, 26 May 2026 03:56:56 -0400 Subject: [PATCH 2/3] fix(attach): use --continue (no picker) instead of --resume `--resume` alone surfaces claude's session picker even when only one session exists. `--continue` jumps to the most recent session non-interactively, which is the actual behavior the dashboard's Enter re-attach wants for typical bottle-with-one-session cases. --- claude_bottle/cli/start.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/claude_bottle/cli/start.py b/claude_bottle/cli/start.py index c9964b8..87c5b31 100644 --- a/claude_bottle/cli/start.py +++ b/claude_bottle/cli/start.py @@ -97,11 +97,12 @@ def attach_claude( """Run claude inside `bottle` as an interactive session. Blocks until the session ends; returns the claude process's exit code. - `resume=True` adds `--resume` so claude picks up its prior - conversation history rather than starting a fresh transcript — + `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). First-attach paths (`./cli.py start`, the dashboard's - new-agent flow) leave it False. + 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, which calls it from inside a `curses.endwin → … → @@ -115,7 +116,9 @@ def attach_claude( if remote_control: claude_args.append("--remote-control") if resume: - claude_args.append("--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) From ae6d11f09d40dcf3a17adfae5c2ec275eee776a1 Mon Sep 17 00:00:00 2001 From: didericis Date: Tue, 26 May 2026 04:18:26 -0400 Subject: [PATCH 3/3] fix(dashboard): use os._exit on quit so bottles survive the dashboard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `bottles` dict held `@contextmanager`-wrapped launch contexts. On normal Python interpreter shutdown those context managers' generators got GC'd, which raised GeneratorExit at the yield point and ran the `finally` block — invoking each bottle's teardown and tearing down the compose project. Net effect: `q` WAS implicitly stopping every dashboard-launched bottle even though the keypress handler just `return`'d. `os._exit(0)` skips all Python-level cleanup (GC, atexit, etc.), so the docker compose projects survive the dashboard exit untouched. Curses gets explicit `endwin()` first because the brutal exit skips curses.wrapper's normal terminal restoration. Matches PRD 0020's resolved-question answer (`q` does NOT tear down bottles; teardown is always explicit via `x` or `./cli.py cleanup`). --- claude_bottle/cli/dashboard.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/claude_bottle/cli/dashboard.py b/claude_bottle/cli/dashboard.py index cb40743..04faefd 100644 --- a/claude_bottle/cli/dashboard.py +++ b/claude_bottle/cli/dashboard.py @@ -860,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, @@ -940,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