From 6aaa7f2d02ccea999d3b8ba39da3b434b4cbe9f4 Mon Sep 17 00:00:00 2001 From: grobomo Date: Sun, 5 Apr 2026 23:32:45 -0500 Subject: [PATCH] T007: Add --stop flag to self-close tab without launching new session Saves SESSION_STATE.md then kills current tab's shell process tree. Use case: stop hook can call `new_session.py --stop` to cleanly self-close when no more work remains. --- TODO.md | 2 +- new_session.py | 49 ++++++++++++++++++++--------- scripts/test/test-T007-stop-flag.sh | 35 ++++++++++++++++++--- 3 files changed, 67 insertions(+), 19 deletions(-) diff --git a/TODO.md b/TODO.md index 4bbc0b8..415e0a1 100644 --- a/TODO.md +++ b/TODO.md @@ -100,7 +100,7 @@ directory before launching the interactive session bypasses the dialog. - [x] T004: Fix pretrust v2 — trust is stored in `~/.claude.json` projects[path].hasTrustDialogAccepted, write it directly (PR #17 merged) - [x] T005: Verified — pretrust-test tab launched WITHOUT trust dialog. Feature works end-to-end. - [x] T006: Parent trust walk — check parent dirs before writing per-project entries, update CLAUDE.md docs (70 tests) -- [ ] T007: Add `--stop` flag to new_session.py — kills current tab without launching a new one +- [x] T007: Add `--stop` flag to new_session.py — kills current tab without launching a new one (PR #19) ## Rename: context-reset → new-session (007) diff --git a/new_session.py b/new_session.py index 0297a34..d86bef4 100644 --- a/new_session.py +++ b/new_session.py @@ -1047,6 +1047,8 @@ def main(): help="Auto-close terminal tab (Windows: sets WT closeOnExit=always temporarily)") parser.add_argument("--timeout", type=int, default=45, help="Phase 2 verification timeout in seconds") parser.add_argument("--dry-run", action="store_true") + parser.add_argument("--stop", action="store_true", + help="Kill current tab without launching a new one (self-close)") args = parser.parse_args() # Exclusive OS-level file lock: prevents concurrent resets on the same project. @@ -1108,9 +1110,41 @@ def main(): return log(f"Cross-project reset: saving state in {project_dir}, launching in {launch_dir}") - prompt = args.prompt or build_prompt(launch_dir) launch_name = os.path.basename(launch_dir) + def _remove_lock(): + nonlocal _lock_fh + try: + if _lock_fh: + _lock_fh.close() # Closing file handle releases OS lock + _lock_fh = None + if os.path.exists(lock_file): + os.remove(lock_file) + except Exception: + pass + + if args.stop: + log(f"=== Stop mode: closing current tab for {launch_name} ===") + # Save session state before dying + context = extract_session_context(project_dir) + if context: + state_path = os.path.join(project_dir, "SESSION_STATE.md") + with open(state_path, "w", encoding="utf-8") as f: + f.write(f"# Session State (auto-generated by context-reset)\n\n") + f.write(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n") + f.write(f"## Last Session Conversation\n\n{context}\n") + log(f"Saved session state to {state_path}") + shell_pid = find_shell_pid() + if shell_pid: + log(f"Killing current tab (shell PID {shell_pid})") + kill_old_tab(shell_pid, close_tab=args.close_tab) + else: + log("WARNING: could not find shell PID to kill") + _remove_lock() + return + + # Build launch command (needed for dry-run and normal mode) + prompt = args.prompt or build_prompt(launch_dir) log(f"=== Context reset started for {launch_name} ===") log(f"Project dir (state): {project_dir}") if launch_dir != project_dir: @@ -1119,24 +1153,11 @@ def main(): log(f"Prompt: {prompt[:80]}...") log(f"Close old tab: {not args.no_close}") - # Tab title: always the folder name (short, stable) tab_title = launch_name tab_color = get_tab_color(launch_dir) log(f"Tab: title='{tab_title}', color={tab_color}") - cmd = build_launch_cmd(launch_dir, prompt, tab_title, tab_color) - def _remove_lock(): - nonlocal _lock_fh - try: - if _lock_fh: - _lock_fh.close() # Closing file handle releases OS lock - _lock_fh = None - if os.path.exists(lock_file): - os.remove(lock_file) - except Exception: - pass - if args.dry_run: log(f"DRY RUN - command: {cmd}") shell_pid = find_shell_pid() diff --git a/scripts/test/test-T007-stop-flag.sh b/scripts/test/test-T007-stop-flag.sh index 8943722..ca9c984 100644 --- a/scripts/test/test-T007-stop-flag.sh +++ b/scripts/test/test-T007-stop-flag.sh @@ -1,6 +1,33 @@ #!/usr/bin/env bash -# Test: --stop flag kills current tab without launching new one -# Placeholder — will be implemented with T007 +# Test: --stop flag argument parsing and stop-mode logic set -e -echo "test-T007-stop-flag: SKIPPED (not yet implemented)" -exit 0 + +cd "$(dirname "$0")/../.." + +python3 -c " +import sys, os, argparse +sys.path.insert(0, '.') +import new_session + +# Test 1: argparse accepts --stop +parser = argparse.ArgumentParser() +parser.add_argument('--stop', action='store_true') +parser.add_argument('--project-dir', default='.') +args = parser.parse_args(['--stop', '--project-dir', '.']) +assert args.stop is True, '--stop flag not parsed' +print('PASS: --stop flag accepted by argparse') + +# Test 2: --stop is in new_session.main's parser (check source) +import inspect +src = inspect.getsource(new_session.main) +assert \"'--stop'\" in src or '\"--stop\"' in src, '--stop not in main parser' +print('PASS: --stop flag defined in main()') + +# Test 3: stop mode saves SESSION_STATE.md (check the code path exists) +assert 'args.stop' in src, 'args.stop not referenced in main' +assert 'Stop mode' in src, 'Stop mode log message not in main' +assert 'extract_session_context' in src, 'extract_session_context not called in stop mode' +print('PASS: stop mode code path exists with state saving') +" + +echo "test-T007-stop-flag: ALL PASSED"