contract(issue#4): author ratatoskr.tui shell + amend issue #3 cli
Issue #4: Textual TUI shell — the interactive primary presenter (design-brief §1, §5). Single chat-pane App[int] subclass + sync run_tui(args) entry. Composes existing sessions + sse_client modules (no forked API-consumption code, per design-brief §8b). Six FN blocks: run_tui, RatatoskrApp class + on_mount + on_unmount, on_input_submitted, _stream_turn_worker, _render_event_to_log, action_interrupt, action_quit, _cancel_via_sse. Nine hard invariants codifying: - INV-001: lazy-import boundary so cli.py STILL doesn't import textual at module scope (issue #3's INV-001 carried forward) - INV-002: session-identity-always-visible footer (`<agent> · …<tail8>`) with explicit `<unknown>` carve-out for --session without --agent - INV-003: two-stage Ctrl-C state machine (idle/streaming/cancelling) per design-brief §8c - INV-005: markdown default-on with --raw opt-out; deliberately produces streaming-deltas + post-Done markdown re-render (accepted v1 trade-off, Static-then-commit refactor deferred) - INV-007: one AsyncClient per app lifetime - INV-008: mid-session errors → idle (don't exit); only initial session-create errors exit Concurrent in-place amendment of issue #3's contract: - --send becomes optional; when omitted, send_content=None is the TUI-mode marker - --raw flag added to ParsedArgs - main dispatches via lazy `from ratatoskr.tui import run_tui` when send_content is None - _parse_args + main TESTS sections updated (no_send_marks_tui_mode replaces usage_no_send; new raw_flag_default_false / raw_flag_set / no_send_dispatches_to_tui) Volva paraphrase round on issue #4: 5 findings, all amended. (1) INV-002 `<unknown>` carve-out wording. (2) Idle "Ctrl-C twice to exit" hint kept per design-brief §8c's conservative-by-design rationale; INV-003 spells out the intentional one-press-from-idle discrepancy. (3) Markdown double-render trade-off made explicit in INV-005. (4) Submit-during-streaming now writes `[busy] turn in flight; input ignored` (visible notice, not silent swallow). (5) `{!r:.200}` format spec kept with explanatory inline comment. Both contracts drift-check clean. prd: pinned to issue #4 body SHA b1e73e7d2e3dd453 at 2026-05-21T06:21:37+00:00.
This commit is contained in:
@@ -201,14 +201,19 @@ STEPS:
|
||||
RETURN 11
|
||||
ON SystemExit as exc:
|
||||
RETURN int(exc.code) if exc.code is not None else 0
|
||||
2. [sequential, flexibility=prescriptive] RETURN asyncio.run(_amain(args))
|
||||
2. [branch, flexibility=prescriptive] IF args.send_content is None:
|
||||
# TUI mode (issue #4 amendment) — lazy import preserves INV-001 (no textual in cli at module scope)
|
||||
FROM ratatoskr.tui IMPORT run_tui
|
||||
RETURN run_tui(args)
|
||||
ELSE:
|
||||
RETURN asyncio.run(_amain(args))
|
||||
TESTS:
|
||||
happy_returns_amain_exit_code [happy,tracer]: argv specifies a complete --send invocation; monkeypatch _amain to return 0 → main returns 0
|
||||
usage_error_no_send [error]: argv=[] → main returns 10; stderr has "[usage_error]"; _amain never called
|
||||
usage_error_both_session_and_new [error]: argv has both --session and --new → returns 10; stderr "[usage_error]"
|
||||
auth_error_missing_key [error]: argv specifies --send/--new/--agent but neither --api-key nor WORLDTREE_API_KEY is set → returns 11; stderr "[auth_error]"; _amain never called
|
||||
no_argv_uses_sys_argv [trace]: argv=None → _parse_args is called with sys.argv[1:] (monkeypatched argparse capture confirms)
|
||||
help_exits_cleanly [happy]: argv=["--help"] → main returns 0 (or whatever code argparse exits with); _amain never called; help text was printed to stdout by argparse
|
||||
no_send_dispatches_to_tui [happy]: argv omits --send → main lazy-imports run_tui and calls it (NOT _amain); send_content marker is None (issue #4 amendment)
|
||||
```
|
||||
|
||||
```contract
|
||||
@@ -245,16 +250,19 @@ ERROR_ROUTING:
|
||||
state_recovery: none (no resources acquired before _parse_args)
|
||||
STEPS:
|
||||
1. [setup, flexibility=prescriptive] Construct argparse.ArgumentParser:
|
||||
--send <content> (required, str, non-empty)
|
||||
--send <content> (str, OPTIONAL — issue #4 amendment: omitted → TUI mode marker; if passed, must be non-empty)
|
||||
--session <id> (str, optional)
|
||||
--new (bool flag, default False)
|
||||
--agent <id> (str, optional — required-with-validation in step 3)
|
||||
--api-key <key> (str, optional — env fallback in step 4)
|
||||
--server <url> (str, optional — env fallback + default in step 5)
|
||||
--raw (bool flag, default False — issue #4 amendment: TUI markdown opt-out)
|
||||
2. [sequential, flexibility=prescriptive] Parse argv:
|
||||
TRY: ns = parser.parse_args(argv if argv is not None else sys.argv[1:])
|
||||
ON SystemExit:
|
||||
Re-raise as UsageError with argparse's captured message
|
||||
2a. [branch, flexibility=prescriptive] IF ns.send is not None AND not ns.send:
|
||||
RAISE UsageError("--send content must be non-empty") # empty-string --send still invalid
|
||||
3. [branch, flexibility=prescriptive] Validate session/new/agent triad per INV-004:
|
||||
IF ns.session and ns.new: RAISE UsageError("--session and --new are mutually exclusive")
|
||||
IF NOT ns.session AND NOT ns.new: RAISE UsageError("pass exactly one of --session or --new")
|
||||
@@ -266,12 +274,13 @@ STEPS:
|
||||
5. [sequential, flexibility=prescriptive] Resolve server_url per INV-006:
|
||||
server_url = ns.server or os.environ.get("WORLDTREE_API_URL") or "http://localhost:8000"
|
||||
6. [cleanup] RETURN ParsedArgs(
|
||||
send_content=ns.send,
|
||||
send_content=ns.send, # may be None (TUI marker, issue #4 amendment)
|
||||
session_id=ns.session,
|
||||
new=ns.new,
|
||||
agent_id=ns.agent,
|
||||
api_key=api_key,
|
||||
server_url=server_url,
|
||||
raw=ns.raw, # issue #4 amendment
|
||||
)
|
||||
TESTS:
|
||||
happy_new [happy,tracer]: argv=["--send", "hi", "--new", "--agent", "mimir", "--api-key", "k"] → ParsedArgs(send_content="hi", session_id=None, new=True, agent_id="mimir", api_key="k", server_url="http://localhost:8000")
|
||||
@@ -281,7 +290,9 @@ TESTS:
|
||||
server_default [trace]: argv omits --server and WORLDTREE_API_URL is unset → ParsedArgs.server_url=="http://localhost:8000"
|
||||
server_env_fallback [trace]: monkeypatch WORLDTREE_API_URL="http://t.local:9000"; argv omits --server → ParsedArgs.server_url=="http://t.local:9000"
|
||||
server_flag_beats_env [trace]: monkeypatch WORLDTREE_API_URL="env"; argv has --server "flag" → ParsedArgs.server_url=="flag"
|
||||
usage_no_send [error]: argv=["--new", "--agent", "mimir", "--api-key", "k"] → UsageError (argparse required-flag)
|
||||
no_send_marks_tui_mode [happy]: argv=["--new", "--agent", "mimir", "--api-key", "k"] → ParsedArgs.send_content=None (TUI marker; issue #4 amendment — was UsageError pre-#4)
|
||||
raw_flag_default_false [trace]: --raw absent → ParsedArgs.raw==False (issue #4 amendment)
|
||||
raw_flag_set [trace]: --raw → ParsedArgs.raw==True (issue #4 amendment)
|
||||
usage_both_session_and_new [adversarial]: argv has both --session and --new → UsageError("mutually exclusive")
|
||||
usage_neither_session_nor_new [adversarial]: argv has --send but neither --session nor --new → UsageError("pass exactly one")
|
||||
usage_new_without_agent [adversarial]: argv=["--send","hi","--new","--api-key","k"] → UsageError("--agent is required when --new")
|
||||
|
||||
Reference in New Issue
Block a user