Skip to content

Commit 93a1f6a

Browse files
committed
feat: add --continue/-c flag and exit hint for session resume
- 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 <session-id>' — 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
1 parent 2027b8c commit 93a1f6a

4 files changed

Lines changed: 162 additions & 8 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ wheels/
1010
# Virtual environments
1111
.venv
1212

13+
# macOS
14+
.DS_Store
15+
1316
# Hugo docs site (website/)
1417
website/public/
1518
website/resources/

src/tau_coding/cli.py

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -190,8 +190,16 @@ def main(
190190
] = PrintOutputMode.text,
191191
resume: Annotated[
192192
str | None,
193-
typer.Option("--resume", help="Resume a session id in TUI mode."),
193+
typer.Option("--resume", "-r", help="Resume a session id in TUI mode."),
194194
] = None,
195+
continue_session: Annotated[
196+
bool,
197+
typer.Option(
198+
"--continue",
199+
"-c",
200+
help="Continue the most recent session in this directory.",
201+
),
202+
] = False,
195203
new_session: Annotated[
196204
bool,
197205
typer.Option("--new-session", help="Create a new session in TUI mode (default)."),
@@ -239,6 +247,18 @@ def main(
239247
if ctx.invoked_subcommand is not None:
240248
return
241249

250+
resolve_cwd = cwd or Path.cwd()
251+
252+
if resume is not None and continue_session:
253+
raise typer.BadParameter("--resume and --continue cannot be used together")
254+
255+
if continue_session:
256+
if new_session:
257+
raise typer.BadParameter("--continue and --new-session cannot be used together")
258+
latest = SessionManager().latest_session_for_cwd(resolve_cwd)
259+
if latest is not None:
260+
resume = latest.id
261+
242262
if resume is not None and new_session:
243263
raise typer.BadParameter("--resume and --new-session cannot be used together")
244264

@@ -289,10 +309,10 @@ def main(
289309
if prompt_option is None:
290310
notice = _startup_update_notice()
291311
try:
292-
anyio.run(
312+
session_id_used = anyio.run(
293313
run_openai_tui,
294314
model,
295-
cwd or Path.cwd(),
315+
resolve_cwd,
296316
resume,
297317
new_session,
298318
provider,
@@ -305,6 +325,7 @@ def main(
305325
)
306326
except (RuntimeError, ValueError) as exc:
307327
raise typer.BadParameter(str(exc)) from exc
328+
_print_resume_hint(session_id_used)
308329
raise typer.Exit()
309330

310331
prompt = prompt_option
@@ -334,6 +355,17 @@ def main(
334355
raise typer.Exit(1)
335356

336357

358+
def _print_resume_hint(session_id: str | None) -> None:
359+
"""Print a hint showing how to resume the just-ended session."""
360+
if session_id is None:
361+
return
362+
manager = SessionManager()
363+
record = manager.get_session(session_id)
364+
if record is None or record.updated_at <= record.created_at:
365+
return
366+
typer.echo(f"To continue this session: tau -c | tau --resume {session_id}")
367+
368+
337369
async def run_openai_tui(
338370
model: str | None,
339371
cwd: Path,
@@ -346,8 +378,8 @@ async def run_openai_tui(
346378
extension_paths: tuple[Path, ...] = (),
347379
extensions_enabled: bool = True,
348380
project_extensions_enabled: bool = False,
349-
) -> None:
350-
"""Run the Textual TUI with the default OpenAI-compatible provider."""
381+
) -> str | None:
382+
"""Run the Textual TUI, returning the session id that was active on exit."""
351383
release_notes_notice = startup_release_notes_notice(_current_version())
352384
startup_notices = [
353385
notice
@@ -357,7 +389,7 @@ async def run_openai_tui(
357389
)
358390
if notice is not None
359391
]
360-
await run_tui_app(
392+
return await run_tui_app(
361393
model=model,
362394
cwd=cwd,
363395
session_id=session_id,

src/tau_coding/tui/app.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5775,8 +5775,12 @@ async def run_tui_app(
57755775
extension_paths: tuple[Path, ...] = (),
57765776
extensions_enabled: bool = True,
57775777
project_extensions_enabled: bool = False,
5778-
) -> None:
5779-
"""Create the default provider/session and run the Textual app."""
5778+
) -> str | None:
5779+
"""Create the default provider/session and run the Textual app.
5780+
5781+
Returns the session id that was active on exit, or ``None``
5782+
if the session was not created.
5783+
"""
57805784
if new_session and session_id is not None:
57815785
raise RuntimeError("--resume and --new-session cannot be used together")
57825786

@@ -5821,6 +5825,7 @@ async def run_tui_app(
58215825
provider = LoginRequiredProvider(startup_message)
58225826
runtime_provider_config = None
58235827
session: CodingSession | None = None
5828+
result_id: str | None = None
58245829
try:
58255830
index_on_first_persist = False
58265831
if record is None:
@@ -5861,9 +5866,11 @@ async def run_tui_app(
58615866
initial_prompt=initial_prompt,
58625867
)
58635868
await app.run_async()
5869+
result_id = getattr(session, "_config", None) and session._config.session_id
58645870
finally:
58655871
if session is not None:
58665872
close_session = getattr(session, "aclose", None)
58675873
if close_session is not None:
58685874
await close_session()
58695875
await provider.aclose()
5876+
return result_id

tests/test_cli.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ async def fake_run_openai_tui(
202202
provider_name: str | None,
203203
auto_compact_token_threshold: int | None,
204204
initial_prompt: str | None,
205+
resume_picker: bool = False,
205206
update_notice: object | None = None,
206207
*extra: object,
207208
) -> None:
@@ -241,6 +242,7 @@ async def fake_run_openai_tui(
241242
provider_name: str | None,
242243
auto_compact_token_threshold: int | None,
243244
initial_prompt: str | None,
245+
resume_picker: bool = False,
244246
update_notice: object | None = None,
245247
*extra: object,
246248
) -> None:
@@ -628,6 +630,16 @@ async def fake_run_openai_print_mode(
628630
def test_default_tui_invokes_tui_runner_with_flags(
629631
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
630632
) -> None:
633+
from tau_coding.paths import TauPaths
634+
635+
paths = TauPaths(home=tmp_path / ".tau", agents_home=tmp_path / ".agents")
636+
manager = SessionManager(paths)
637+
manager.create_session(
638+
cwd=tmp_path,
639+
model="fake",
640+
session_id="session-1",
641+
)
642+
631643
calls: list[tuple[str | None, Path, str | None, bool, str | None, int | None, str | None]] = []
632644

633645
async def fake_run_openai_tui(
@@ -638,6 +650,7 @@ async def fake_run_openai_tui(
638650
provider_name: str | None,
639651
auto_compact_token_threshold: int | None,
640652
initial_prompt: str | None,
653+
resume_picker: bool = False,
641654
update_notice: object | None = None,
642655
*extra: object,
643656
) -> None:
@@ -655,6 +668,7 @@ async def fake_run_openai_tui(
655668
)
656669

657670
monkeypatch.setattr(cli, "_startup_update_notice", lambda: None)
671+
monkeypatch.setattr(cli, "SessionManager", lambda *args, **kwargs: manager)
658672
monkeypatch.setattr(cli, "run_openai_tui", fake_run_openai_tui)
659673

660674
result = CliRunner().invoke(
@@ -677,6 +691,104 @@ async def fake_run_openai_tui(
677691
assert calls == [("fake", tmp_path, "session-1", False, "local", 1000, None)]
678692

679693

694+
def test_continue_flag_resolves_latest_session(
695+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
696+
) -> None:
697+
"""`--continue`/`-c` resolves the latest session for the cwd."""
698+
from tau_coding.paths import TauPaths
699+
700+
paths = TauPaths(home=tmp_path / ".tau", agents_home=tmp_path / ".agents")
701+
manager = SessionManager(paths)
702+
manager.create_session(cwd=tmp_path, model="fake", session_id="latest-session")
703+
704+
calls: list[str | None] = []
705+
706+
async def fake_run_openai_tui(
707+
model: str | None,
708+
cwd: Path,
709+
session_id: str | None,
710+
new_session: bool,
711+
provider_name: str | None,
712+
auto_compact_token_threshold: int | None,
713+
initial_prompt: str | None,
714+
resume_picker: bool = False,
715+
update_notice: object | None = None,
716+
) -> str | None:
717+
del update_notice, resume_picker, auto_compact_token_threshold, initial_prompt
718+
calls.append(session_id)
719+
return session_id
720+
721+
monkeypatch.setattr(cli, "_startup_update_notice", lambda: None)
722+
monkeypatch.setattr(cli, "SessionManager", lambda *args, **kwargs: manager)
723+
monkeypatch.setattr(cli, "run_openai_tui", fake_run_openai_tui)
724+
725+
result = CliRunner().invoke(app, ["--cwd", str(tmp_path), "-c"])
726+
727+
assert result.exit_code == 0
728+
assert calls == ["latest-session"]
729+
730+
731+
def test_continue_flag_creates_new_session_when_none_exists(
732+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
733+
) -> None:
734+
"""`--continue` with no previous sessions falls through to a new session."""
735+
calls: list[str | None] = []
736+
737+
async def fake_run_openai_tui(
738+
model: str | None,
739+
cwd: Path,
740+
session_id: str | None,
741+
new_session: bool,
742+
provider_name: str | None,
743+
auto_compact_token_threshold: int | None,
744+
initial_prompt: str | None,
745+
resume_picker: bool = False,
746+
update_notice: object | None = None,
747+
) -> str | None:
748+
del update_notice, resume_picker, auto_compact_token_threshold, initial_prompt
749+
calls.append(session_id)
750+
return session_id
751+
752+
monkeypatch.setattr(cli, "_startup_update_notice", lambda: None)
753+
monkeypatch.setattr(cli, "run_openai_tui", fake_run_openai_tui)
754+
755+
result = CliRunner().invoke(app, ["--cwd", str(tmp_path), "-c"])
756+
757+
assert result.exit_code == 0
758+
assert calls == [None]
759+
760+
761+
def test_resume_flag_conflicts_with_continue(tmp_path: Path) -> None:
762+
"""`--resume` and `--continue` cannot be combined."""
763+
result = CliRunner().invoke(
764+
app,
765+
["--cwd", str(tmp_path), "--resume", "session-1", "-c"],
766+
)
767+
768+
assert result.exit_code != 0
769+
assert "--resume and --continue cannot be used together" in _strip_ansi(result.output)
770+
771+
772+
def test_print_resume_hint_shown_when_session_has_content(
773+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, capsys: pytest.CaptureFixture[str]
774+
) -> None:
775+
"""Exit hint is printed when the session has conversation entries."""
776+
from tau_coding.paths import TauPaths
777+
778+
paths = TauPaths(home=tmp_path / ".tau", agents_home=tmp_path / ".agents")
779+
manager = SessionManager(paths)
780+
session_id = "used-session"
781+
manager.create_session(cwd=tmp_path, model="fake", session_id=session_id)
782+
manager.touch_session(session_id)
783+
784+
monkeypatch.setattr(cli, "_startup_update_notice", lambda: None)
785+
monkeypatch.setattr(cli, "SessionManager", lambda *args, **kwargs: manager)
786+
787+
cli._print_resume_hint(session_id)
788+
captured = capsys.readouterr()
789+
assert f"To continue this session: tau -c | tau --resume {session_id}" in captured.out
790+
791+
680792
def test_default_tui_rejects_resume_with_new_session(tmp_path: Path) -> None:
681793
result = CliRunner().invoke(
682794
app,

0 commit comments

Comments
 (0)