From 93a1f6ab5feef835dfe74d76320e13a2ab31d290 Mon Sep 17 00:00:00 2001 From: jheronimus Date: Fri, 10 Jul 2026 03:31:52 +0300 Subject: [PATCH] feat: add --continue/-c flag and exit hint for session resume MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add --continue/-c flag that resumes the most recent session in cwd via SessionManager.latest_session_for_cwd(); falls through to new session if none exists - Add -r alias for the existing --resume flag - Allow positional prompt after -c/-r to pre-fill in TUI (e.g. tau -c 'finish the auth module') - Print exit hint after TUI: 'To continue this session: tau -c | tau --resume ' — only when session has conversation (updated_at > created_at), following Pi/Codex convention - run_tui_app returns the active session_id for the hint - Remove dead resume_picker plumbing (nothing ever sets it) - Add .DS_Store to gitignore --- .gitignore | 3 + src/tau_coding/cli.py | 44 +++++++++++++-- src/tau_coding/tui/app.py | 11 +++- tests/test_cli.py | 112 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 162 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index 9c5d03fc5..24a2d850f 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,9 @@ wheels/ # Virtual environments .venv +# macOS +.DS_Store + # Hugo docs site (website/) website/public/ website/resources/ diff --git a/src/tau_coding/cli.py b/src/tau_coding/cli.py index a1f8eb7ad..d14d30d06 100644 --- a/src/tau_coding/cli.py +++ b/src/tau_coding/cli.py @@ -190,8 +190,16 @@ def main( ] = PrintOutputMode.text, resume: Annotated[ str | None, - typer.Option("--resume", help="Resume a session id in TUI mode."), + typer.Option("--resume", "-r", help="Resume a session id in TUI mode."), ] = None, + continue_session: Annotated[ + bool, + typer.Option( + "--continue", + "-c", + help="Continue the most recent session in this directory.", + ), + ] = False, new_session: Annotated[ bool, typer.Option("--new-session", help="Create a new session in TUI mode (default)."), @@ -239,6 +247,18 @@ def main( if ctx.invoked_subcommand is not None: return + resolve_cwd = cwd or Path.cwd() + + if resume is not None and continue_session: + raise typer.BadParameter("--resume and --continue cannot be used together") + + if continue_session: + if new_session: + raise typer.BadParameter("--continue and --new-session cannot be used together") + latest = SessionManager().latest_session_for_cwd(resolve_cwd) + if latest is not None: + resume = latest.id + if resume is not None and new_session: raise typer.BadParameter("--resume and --new-session cannot be used together") @@ -289,10 +309,10 @@ def main( if prompt_option is None: notice = _startup_update_notice() try: - anyio.run( + session_id_used = anyio.run( run_openai_tui, model, - cwd or Path.cwd(), + resolve_cwd, resume, new_session, provider, @@ -305,6 +325,7 @@ def main( ) except (RuntimeError, ValueError) as exc: raise typer.BadParameter(str(exc)) from exc + _print_resume_hint(session_id_used) raise typer.Exit() prompt = prompt_option @@ -334,6 +355,17 @@ def main( raise typer.Exit(1) +def _print_resume_hint(session_id: str | None) -> None: + """Print a hint showing how to resume the just-ended session.""" + if session_id is None: + return + manager = SessionManager() + record = manager.get_session(session_id) + if record is None or record.updated_at <= record.created_at: + return + typer.echo(f"To continue this session: tau -c | tau --resume {session_id}") + + async def run_openai_tui( model: str | None, cwd: Path, @@ -346,8 +378,8 @@ async def run_openai_tui( extension_paths: tuple[Path, ...] = (), extensions_enabled: bool = True, project_extensions_enabled: bool = False, -) -> None: - """Run the Textual TUI with the default OpenAI-compatible provider.""" +) -> str | None: + """Run the Textual TUI, returning the session id that was active on exit.""" release_notes_notice = startup_release_notes_notice(_current_version()) startup_notices = [ notice @@ -357,7 +389,7 @@ async def run_openai_tui( ) if notice is not None ] - await run_tui_app( + return await run_tui_app( model=model, cwd=cwd, session_id=session_id, diff --git a/src/tau_coding/tui/app.py b/src/tau_coding/tui/app.py index f2199da72..705229b60 100644 --- a/src/tau_coding/tui/app.py +++ b/src/tau_coding/tui/app.py @@ -5775,8 +5775,12 @@ async def run_tui_app( extension_paths: tuple[Path, ...] = (), extensions_enabled: bool = True, project_extensions_enabled: bool = False, -) -> None: - """Create the default provider/session and run the Textual app.""" +) -> str | None: + """Create the default provider/session and run the Textual app. + + Returns the session id that was active on exit, or ``None`` + if the session was not created. + """ if new_session and session_id is not None: raise RuntimeError("--resume and --new-session cannot be used together") @@ -5821,6 +5825,7 @@ async def run_tui_app( provider = LoginRequiredProvider(startup_message) runtime_provider_config = None session: CodingSession | None = None + result_id: str | None = None try: index_on_first_persist = False if record is None: @@ -5861,9 +5866,11 @@ async def run_tui_app( initial_prompt=initial_prompt, ) await app.run_async() + result_id = getattr(session, "_config", None) and session._config.session_id finally: if session is not None: close_session = getattr(session, "aclose", None) if close_session is not None: await close_session() await provider.aclose() + return result_id diff --git a/tests/test_cli.py b/tests/test_cli.py index 5696b39a2..05caf1d34 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -202,6 +202,7 @@ async def fake_run_openai_tui( provider_name: str | None, auto_compact_token_threshold: int | None, initial_prompt: str | None, + resume_picker: bool = False, update_notice: object | None = None, *extra: object, ) -> None: @@ -241,6 +242,7 @@ async def fake_run_openai_tui( provider_name: str | None, auto_compact_token_threshold: int | None, initial_prompt: str | None, + resume_picker: bool = False, update_notice: object | None = None, *extra: object, ) -> None: @@ -628,6 +630,16 @@ async def fake_run_openai_print_mode( def test_default_tui_invokes_tui_runner_with_flags( monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: + from tau_coding.paths import TauPaths + + paths = TauPaths(home=tmp_path / ".tau", agents_home=tmp_path / ".agents") + manager = SessionManager(paths) + manager.create_session( + cwd=tmp_path, + model="fake", + session_id="session-1", + ) + calls: list[tuple[str | None, Path, str | None, bool, str | None, int | None, str | None]] = [] async def fake_run_openai_tui( @@ -638,6 +650,7 @@ async def fake_run_openai_tui( provider_name: str | None, auto_compact_token_threshold: int | None, initial_prompt: str | None, + resume_picker: bool = False, update_notice: object | None = None, *extra: object, ) -> None: @@ -655,6 +668,7 @@ async def fake_run_openai_tui( ) monkeypatch.setattr(cli, "_startup_update_notice", lambda: None) + monkeypatch.setattr(cli, "SessionManager", lambda *args, **kwargs: manager) monkeypatch.setattr(cli, "run_openai_tui", fake_run_openai_tui) result = CliRunner().invoke( @@ -677,6 +691,104 @@ async def fake_run_openai_tui( assert calls == [("fake", tmp_path, "session-1", False, "local", 1000, None)] +def test_continue_flag_resolves_latest_session( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + """`--continue`/`-c` resolves the latest session for the cwd.""" + from tau_coding.paths import TauPaths + + paths = TauPaths(home=tmp_path / ".tau", agents_home=tmp_path / ".agents") + manager = SessionManager(paths) + manager.create_session(cwd=tmp_path, model="fake", session_id="latest-session") + + calls: list[str | None] = [] + + async def fake_run_openai_tui( + model: str | None, + cwd: Path, + session_id: str | None, + new_session: bool, + provider_name: str | None, + auto_compact_token_threshold: int | None, + initial_prompt: str | None, + resume_picker: bool = False, + update_notice: object | None = None, + ) -> str | None: + del update_notice, resume_picker, auto_compact_token_threshold, initial_prompt + calls.append(session_id) + return session_id + + monkeypatch.setattr(cli, "_startup_update_notice", lambda: None) + monkeypatch.setattr(cli, "SessionManager", lambda *args, **kwargs: manager) + monkeypatch.setattr(cli, "run_openai_tui", fake_run_openai_tui) + + result = CliRunner().invoke(app, ["--cwd", str(tmp_path), "-c"]) + + assert result.exit_code == 0 + assert calls == ["latest-session"] + + +def test_continue_flag_creates_new_session_when_none_exists( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + """`--continue` with no previous sessions falls through to a new session.""" + calls: list[str | None] = [] + + async def fake_run_openai_tui( + model: str | None, + cwd: Path, + session_id: str | None, + new_session: bool, + provider_name: str | None, + auto_compact_token_threshold: int | None, + initial_prompt: str | None, + resume_picker: bool = False, + update_notice: object | None = None, + ) -> str | None: + del update_notice, resume_picker, auto_compact_token_threshold, initial_prompt + calls.append(session_id) + return session_id + + monkeypatch.setattr(cli, "_startup_update_notice", lambda: None) + monkeypatch.setattr(cli, "run_openai_tui", fake_run_openai_tui) + + result = CliRunner().invoke(app, ["--cwd", str(tmp_path), "-c"]) + + assert result.exit_code == 0 + assert calls == [None] + + +def test_resume_flag_conflicts_with_continue(tmp_path: Path) -> None: + """`--resume` and `--continue` cannot be combined.""" + result = CliRunner().invoke( + app, + ["--cwd", str(tmp_path), "--resume", "session-1", "-c"], + ) + + assert result.exit_code != 0 + assert "--resume and --continue cannot be used together" in _strip_ansi(result.output) + + +def test_print_resume_hint_shown_when_session_has_content( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + """Exit hint is printed when the session has conversation entries.""" + from tau_coding.paths import TauPaths + + paths = TauPaths(home=tmp_path / ".tau", agents_home=tmp_path / ".agents") + manager = SessionManager(paths) + session_id = "used-session" + manager.create_session(cwd=tmp_path, model="fake", session_id=session_id) + manager.touch_session(session_id) + + monkeypatch.setattr(cli, "_startup_update_notice", lambda: None) + monkeypatch.setattr(cli, "SessionManager", lambda *args, **kwargs: manager) + + cli._print_resume_hint(session_id) + captured = capsys.readouterr() + assert f"To continue this session: tau -c | tau --resume {session_id}" in captured.out + + def test_default_tui_rejects_resume_with_new_session(tmp_path: Path) -> None: result = CliRunner().invoke( app,