diff --git a/features/steps/tui_session_export_import_steps.py b/features/steps/tui_session_export_import_steps.py index 95e08eb8d..cdf1a97d9 100644 --- a/features/steps/tui_session_export_import_steps.py +++ b/features/steps/tui_session_export_import_steps.py @@ -399,3 +399,41 @@ def step_tui_file_exists(context: Context, path: str) -> None: # Cleanup with contextlib.suppress(OSError): os.unlink(path) + + +# --------------------------------------------------------------------------- +# Domain model: as_export_plain_text() +# --------------------------------------------------------------------------- + + +@given("a session with two messages for plain text export") +def step_session_two_messages_plain(context: Context) -> None: + msgs = [ + _make_message(MessageRole.USER, "Hello from user", 0), + _make_message(MessageRole.ASSISTANT, "Hello from assistant", 1), + ] + context.txt_session = _make_session(messages=msgs) + + +@given("a session with no messages for plain text export") +def step_session_no_messages_plain(context: Context) -> None: + context.txt_session = _make_session() + + +@when("I call as_export_plain_text on the session") +def step_call_as_export_plain_text(context: Context) -> None: + context.txt_output = context.txt_session.as_export_plain_text() + + +@then('the plain text output should start with "{prefix}"') +def step_txt_starts_with(context: Context, prefix: str) -> None: + assert context.txt_output.startswith(prefix), ( + f"Expected plain text to start with {prefix!r}, got: {context.txt_output[:80]!r}" + ) + + +@then('the plain text output should contain "{text}"') +def step_txt_contains(context: Context, text: str) -> None: + assert text in context.txt_output, ( + f"Expected {text!r} in plain text output.\nGot:\n{context.txt_output[:500]}" + ) diff --git a/features/tui_session_export_import.feature b/features/tui_session_export_import.feature index 5add50a8e..e77ba174a 100644 --- a/features/tui_session_export_import.feature +++ b/features/tui_session_export_import.feature @@ -110,3 +110,35 @@ Feature: TUI session export/import (JSON + Markdown) And there is an invalid JSON file for TUI import When I call TUI handle with "session import " for the import file Then the TUI handle result should contain "Invalid JSON" + + Scenario: TUI session export with --format txt returns success + Given a TUI command router with mocked session service for export + When I call TUI handle with "session export --format txt" for the current session + Then the TUI handle result should contain "Session exported (TXT)" + + Scenario: TUI session export with --format txt to file writes plain text + Given a TUI command router with mocked session service for export + When I call TUI handle with "session export --format txt /tmp/tui_test_export.txt" for the current session + Then the TUI handle result should contain "Session exported to" + And the tui exported file "/tmp/tui_test_export.txt" should exist + + Scenario: Plain text export contains session header and messages + Given a session with two messages for plain text export + When I call as_export_plain_text on the session + Then the plain text output should start with "Session:" + And the plain text output should contain "USER" + And the plain text output should contain "ASSISTANT" + And the plain text output should contain "Hello from user" + And the plain text output should contain "Hello from assistant" + + Scenario: Plain text export with no messages contains placeholder + Given a session with no messages for plain text export + When I call as_export_plain_text on the session + Then the plain text output should start with "Session:" + And the plain text output should contain "(no messages)" + + Scenario: TUI session export error message includes txt format + Given a TUI command router with mocked session service for export + When I call TUI handle with "session export --format xml" for the current session + Then the TUI handle result should contain "Invalid format" + And the TUI handle result should contain "txt" diff --git a/src/cleveragents/domain/models/core/session.py b/src/cleveragents/domain/models/core/session.py index 05c6b97ce..e90c9756e 100644 --- a/src/cleveragents/domain/models/core/session.py +++ b/src/cleveragents/domain/models/core/session.py @@ -467,6 +467,54 @@ class Session(BaseModel): lines.append("") return "\n".join(lines) + def as_export_plain_text(self) -> str: + """Return a plain text transcript of the session. + + This is a **lossy** export intended for sharing and reading in plain + text editors. It does not contain enough information to fully restore + a session via ``import_session`` -- use :meth:`as_export_dict` for that. + + The output format is:: + + Session: + Actor: + Namespace: + Created: + Updated: + + ---------------------------------------- + + [] (): + + + ---------------------------------------- + """ + separator = "-" * 40 + lines: list[str] = [] + lines.append(f"Session: {self.session_id}") + lines.append(f"Actor: {self.actor_name or '(none)'}") + lines.append(f"Namespace: {self.namespace}") + lines.append(f"Created: {self.created_at.isoformat()}") + lines.append(f"Updated: {self.updated_at.isoformat()}") + if self.linked_plan_ids: + plan_refs = ", ".join(self.linked_plan_ids) + lines.append(f"Linked Plans: {plan_refs}") + lines.append("") + lines.append(separator) + lines.append("") + if not self.messages: + lines.append("(no messages)") + lines.append("") + else: + for msg in self.messages: + ts = msg.timestamp.strftime("%Y-%m-%d %H:%M:%S") + lines.append(f"[{msg.sequence}] {msg.role.value.upper()} ({ts}):") + lines.append(msg.content) + lines.append("") + lines.append(separator) + lines.append("") + return "\n".join(lines) + model_config = ConfigDict( str_strip_whitespace=True, validate_assignment=True, diff --git a/src/cleveragents/tui/commands.py b/src/cleveragents/tui/commands.py index 34d4d22bb..82b70c6f1 100644 --- a/src/cleveragents/tui/commands.py +++ b/src/cleveragents/tui/commands.py @@ -95,8 +95,8 @@ class TuiCommandRouter: path = tokens[i] i += 1 - if fmt not in ("json", "md"): - return f"Invalid format: {fmt!r}. Use 'json' or 'md'." + if fmt not in ("json", "md", "txt"): + return f"Invalid format: {fmt!r}. Use 'json', 'md', or 'txt'." try: container = self._resolve_container() @@ -124,6 +124,27 @@ class TuiCommandRouter: ] session.messages = messages content = session.as_export_markdown() + elif fmt == "txt": + from cleveragents.domain.models.core.session import ( + MessageRole, + SessionMessage, + ) + + session = service.get(session_id) + messages = [ + SessionMessage( + message_id=m["message_id"], + role=MessageRole(m["role"]), + content=m["content"], + sequence=m["sequence"], + timestamp=m["timestamp"], + metadata=m.get("metadata", {}), + tool_call_id=m.get("tool_call_id"), + ) + for m in export_data.get("messages", []) + ] + session.messages = messages + content = session.as_export_plain_text() else: content = json.dumps(export_data, indent=2, default=str)