Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ wheels/
# Virtual environments
.venv

# macOS
.DS_Store

# Hugo docs site (website/)
website/public/
website/resources/
Expand Down
44 changes: 38 additions & 6 deletions src/tau_coding/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)."),
Expand Down Expand Up @@ -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")

Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand All @@ -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,
Expand Down
11 changes: 9 additions & 2 deletions src/tau_coding/tui/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -5736,8 +5736,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")

Expand Down Expand Up @@ -5772,6 +5776,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:
Expand Down Expand Up @@ -5811,9 +5816,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
112 changes: 112 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,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:
Expand Down Expand Up @@ -240,6 +241,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:
Expand Down Expand Up @@ -618,6 +620,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(
Expand All @@ -628,6 +640,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:
Expand All @@ -645,6 +658,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(
Expand All @@ -667,6 +681,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,
Expand Down