Skip to content

Commit b028d01

Browse files
committed
fix(tui): adapt the shared coding TUI to the merged session foundation
The TUI branch predates the review rewrite of #82, so it was written against a session module that no longer looks the same: * Sessions moved from `nooa.sessions` to `nooa_cli.sessions`. * `SessionStore.create()` renamed its `host` argument to `origin`, so the TUI now records `origin="tui"` alongside the ACP host's `origin="acp"`. * `SessionResumed` and `SessionCleared` were dropped from the module when it was descoped, and are restored here. Both are transient `EventBase` types, so they stay out of `SESSION_EVENT_TYPES`, which is the set persisted with the session. `turn_count` also changed meaning: it now counts user turns only, which is what the live `record_user` path always did. The old summary query counted agent messages too, so the two disagreed. The test asserting the previous value is updated rather than the merged behavior. One unrelated fix: the bang-shell cwd assertion compared an unresolved temp path, which fails on macOS through the /var symlink. Signed-off-by: Paul Furgale <pfurgale@nvidia.com>
1 parent 0f7216a commit b028d01

13 files changed

Lines changed: 60 additions & 29 deletions

packages/nooa-cli/src/nooa_cli/sessions/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
from nooa_cli.sessions.events import (
66
SESSION_EVENT_TYPES,
7+
SessionCleared,
8+
SessionResumed,
79
SessionStarted,
810
SessionTitleUpdated,
911
SessionUserMessage,
@@ -20,9 +22,11 @@
2022
__all__ = [
2123
"InvalidSessionIdError",
2224
"SESSION_EVENT_TYPES",
25+
"SessionCleared",
2326
"SessionHandle",
2427
"SessionInfo",
2528
"SessionNotFoundError",
29+
"SessionResumed",
2630
"SessionStarted",
2731
"SessionStore",
2832
"SessionTitleUpdated",

packages/nooa-cli/src/nooa_cli/sessions/events.py

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
22
# SPDX-License-Identifier: Apache-2.0
3-
"""Durable metadata for interactive coding-agent sessions."""
3+
"""Durable metadata and transient lifecycle events for coding-agent sessions."""
44

5-
from typing import ClassVar
5+
from typing import Annotated, ClassVar
66

7-
from nooa.context_blocks import Metadata
7+
from pydantic import Field
8+
9+
from nooa.context_blocks import EventBase, Metadata
810
from nooa.context_blocks.roles import Role
911

1012

@@ -36,6 +38,33 @@ class SessionUserMessage(Metadata):
3638
content: str = ""
3739

3840

41+
class SessionResumed(EventBase): # type: ignore[misc]
42+
"""An interactive agent has been restored or initialized for a session."""
43+
44+
_role: ClassVar[Role] = Role.RUNTIME_EVENT
45+
handler_aliases: ClassVar[tuple[str, ...]] = ("TuiSessionResumed",)
46+
47+
session_id: Annotated[str, Field(description="The resumed or started session ID")]
48+
restored: Annotated[
49+
bool,
50+
Field(description="Whether a snapshot was restored into the agent"),
51+
]
52+
53+
54+
class SessionCleared(EventBase): # type: ignore[misc]
55+
"""An interactive agent's working state has been reset."""
56+
57+
_role: ClassVar[Role] = Role.RUNTIME_EVENT
58+
handler_aliases: ClassVar[tuple[str, ...]] = ("TuiSessionCleared",)
59+
60+
session_id: Annotated[
61+
str | None,
62+
Field(default=None, description="The new post-clear session ID, when known"),
63+
]
64+
65+
66+
# Transient lifecycle events are deliberately absent here: this tuple is the
67+
# set persisted with the session, and those two are runtime-only.
3968
SESSION_EVENT_TYPES: tuple[type[Metadata], ...] = (
4069
SessionStarted,
4170
SessionTitleUpdated,

packages/nooa-cli/src/nooa_cli/tui/bootstrap.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from pathlib import Path
1111
from typing import TYPE_CHECKING
1212

13-
from nooa.sessions import SessionResumed
13+
from nooa_cli.sessions import SessionResumed
1414

1515
from .output import Output, TextOutput
1616

packages/nooa-cli/src/nooa_cli/tui/commands.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -342,7 +342,7 @@ async def _reset_agent_working_state(agent: "Agent") -> None:
342342
em = getattr(agent, "event_manager", None)
343343
if em is not None:
344344
try:
345-
from nooa.sessions import SessionCleared
345+
from nooa_cli.sessions import SessionCleared
346346

347347
em.register_event_type(SessionCleared)
348348
sid = None
@@ -1407,7 +1407,7 @@ async def execute(self, args: list[str]) -> "CommandResult":
14071407
async def _restore_and_emit() -> list[Output]:
14081408
restored = new_sm._storage.restore_latest_snapshot(self.agent)
14091409
try:
1410-
from nooa.sessions import SessionResumed
1410+
from nooa_cli.sessions import SessionResumed
14111411

14121412
self.agent.event_manager.register_event_type(SessionResumed)
14131413
self.agent.event_manager.add(

packages/nooa-cli/src/nooa_cli/tui/session_manager.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
from typing import Literal
1313

1414
from nooa.paths import get_project_dir
15-
from nooa.sessions import SessionHandle, SessionInfo, SessionStore
1615
from nooa.storage.sqlite import delete_sqlite_database
16+
from nooa_cli.sessions import SessionHandle, SessionInfo, SessionStore
1717

1818
SESSIONS_DIR = get_project_dir("sessions")
1919

@@ -60,7 +60,7 @@ class Turn:
6060

6161

6262
class SessionManager:
63-
"""TUI-facing view of one :class:`nooa.sessions.SessionHandle`.
63+
"""TUI-facing view of one :class:`nooa_cli.sessions.SessionHandle`.
6464
6565
The adapter deliberately contains no persistence implementation. Both the
6666
native TUI and protocol hosts read and write the same session event schema.
@@ -84,7 +84,7 @@ def create(
8484
model=model,
8585
agent=agent_cls,
8686
working_directory=working_dir,
87-
host="tui",
87+
origin="tui",
8888
session_id=session_id,
8989
check_same_thread=False,
9090
)

packages/nooa-cli/tests/test_tui_mcp_config.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,7 @@ async def test_mcp_add_rejects_unsafe_name_before_persisting(monkeypatch, tmp_pa
6565
path = _project_settings(monkeypatch, tmp_path)
6666
registry = MCPRegistry(approval_path=tmp_path / "approvals.json")
6767

68-
result = await _command(registry).execute(
69-
["add", "evil\x1b[2J", "https://docs.example/mcp"]
70-
)
68+
result = await _command(registry).execute(["add", "evil\x1b[2J", "https://docs.example/mcp"])
7169

7270
assert result.success is False
7371
assert not path.exists()

packages/nooa-cli/tests/tui/test_completer.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -582,12 +582,8 @@ def test_mcp_remove_offers_configured_servers():
582582
def test_mcp_approve_and_revoke_complete_configured_servers():
583583
reg = _mcp_registry(["maas-jira"])
584584
completer = Completer(registry=reg)
585-
assert [item.text for item in completer.complete("/mcp approve ")] == [
586-
"/mcp approve maas-jira"
587-
]
588-
assert [item.text for item in completer.complete("/mcp revoke ")] == [
589-
"/mcp revoke maas-jira"
590-
]
585+
assert [item.text for item in completer.complete("/mcp approve ")] == ["/mcp approve maas-jira"]
586+
assert [item.text for item in completer.complete("/mcp revoke ")] == ["/mcp revoke maas-jira"]
591587

592588

593589
def test_mcp_completion_marks_approved_server():

packages/nooa-cli/tests/tui/test_event_explorer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ def test_event_explorer_renders_shared_session_events() -> None:
7070
"2",
7171
_FakeEvent(
7272
"SessionStarted",
73-
host="tui",
73+
origin="tui",
7474
model="test-model",
7575
agent="CodingAgent",
7676
working_directory="/work/repo",

packages/nooa-cli/tests/tui/test_loud_handler_diagnostics.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -511,7 +511,9 @@ async def test_get_bang_shell_syncs_cwd(self):
511511
session.agent.shell.cwd = Path(tempfile.gettempdir())
512512

513513
shell = await session._get_bang_shell()
514-
assert str(shell.cwd) == tempfile.gettempdir()
514+
# Resolve both sides: on macOS the temp dir is reached through the
515+
# /var -> /private/var symlink, and the shell reports the real path.
516+
assert Path(shell.cwd).resolve() == Path(tempfile.gettempdir()).resolve()
515517

516518
# Simulate agent changing directory to a real path
517519
session.agent.shell.cwd = Path("/")

packages/nooa-cli/tests/tui/test_resume_emit_order.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@
99
"""
1010

1111
import pytest
12-
13-
from nooa.sessions import SessionResumed
12+
from nooa_cli.sessions import SessionResumed
1413

1514

1615
@pytest.mark.asyncio

0 commit comments

Comments
 (0)