Show runtime owner and verification attempts in TUI
- SHA
a488ab3663ef5ee911445da6bab5973edd8c8412- Parents
-
d193644 - Tree
f0b1555
a488ab3
a488ab3663ef5ee911445da6bab5973edd8c8412d193644
f0b1555| Status | File | + | - |
|---|---|---|---|
| M |
src/loader/runtime/runtime_api.py
|
2 | 1 |
| M |
src/loader/ui/adapter.py
|
17 | 0 |
| M |
src/loader/ui/app.py
|
28 | 0 |
| M |
src/loader/ui/status_helpers.py
|
16 | 1 |
| M |
src/loader/ui/widgets/status_line.py
|
22 | 0 |
| M |
tests/test_status_surfaces.py
|
4 | 2 |
src/loader/runtime/runtime_api.pymodified@@ -9,7 +9,7 @@ from typing import Literal, Protocol | ||
| 9 | 9 | from ..context.project import ProjectContext |
| 10 | 10 | from ..tools.base import ToolRegistry |
| 11 | 11 | from .capabilities import CapabilityProfile |
| 12 | -from .events import AgentEvent | |
| 12 | +from .events import AgentEvent, TurnSummary | |
| 13 | 13 | from .session import ConversationSession |
| 14 | 14 | |
| 15 | 15 | RuntimeOwnerKind = Literal["runtime", "public-compat"] |
@@ -23,6 +23,7 @@ class RuntimeShellOwner(Protocol): | ||
| 23 | 23 | capability_profile: CapabilityProfile |
| 24 | 24 | safeguards: object |
| 25 | 25 | session: ConversationSession |
| 26 | + last_turn_summary: TurnSummary | None | |
| 26 | 27 | project_context: ProjectContext | None |
| 27 | 28 | workflow_mode: str |
| 28 | 29 | active_permission_mode: str |
src/loader/ui/adapter.pymodified@@ -191,6 +191,7 @@ class DefinitionOfDoneUpdated(Message): | ||
| 191 | 191 | dod_status: str |
| 192 | 192 | pending_items_count: int = 0 |
| 193 | 193 | last_verification_result: str | None = None |
| 194 | + verification_attempt: str | None = None | |
| 194 | 195 | |
| 195 | 196 | |
| 196 | 197 | @dataclass |
@@ -460,6 +461,9 @@ class EventAdapter: | ||
| 460 | 461 | dod_status=event.dod_status or "", |
| 461 | 462 | pending_items_count=event.pending_items_count or 0, |
| 462 | 463 | last_verification_result=event.last_verification_result, |
| 464 | + verification_attempt=_definition_of_done_verification_attempt( | |
| 465 | + event.definition_of_done | |
| 466 | + ), | |
| 463 | 467 | ) |
| 464 | 468 | ) |
| 465 | 469 | |
@@ -487,3 +491,16 @@ class EventAdapter: | ||
| 487 | 491 | artifact_path=event.artifact_path or "", |
| 488 | 492 | ) |
| 489 | 493 | ) |
| 494 | + | |
| 495 | + | |
| 496 | +def _definition_of_done_verification_attempt(dod) -> str | None: | |
| 497 | + """Render one compact verification-attempt label from DoD state.""" | |
| 498 | + | |
| 499 | + if dod is None: | |
| 500 | + return None | |
| 501 | + active_number = getattr(dod, "active_verification_attempt_number", None) | |
| 502 | + if active_number is None: | |
| 503 | + return None | |
| 504 | + if getattr(dod, "last_verification_result", None) == "stale" and active_number > 1: | |
| 505 | + return f"attempt {active_number - 1} -> attempt {active_number}" | |
| 506 | + return f"attempt {active_number}" | |
src/loader/ui/app.pymodified@@ -123,9 +123,21 @@ class LoaderApp(App): | ||
| 123 | 123 | status.mode = self.mode |
| 124 | 124 | status.capability_profile = self.capability_profile |
| 125 | 125 | status.session_id = self.session_id |
| 126 | + status.runtime_owner = self.shell_owner.session.runtime_owner_path or "" | |
| 126 | 127 | status.workflow_mode = self.workflow_mode |
| 127 | 128 | status.turn_phase = self.turn_phase |
| 128 | 129 | status.permission_mode = self.permission_mode |
| 130 | + if ( | |
| 131 | + self.shell_owner.last_turn_summary is not None | |
| 132 | + and self.shell_owner.last_turn_summary.definition_of_done is not None | |
| 133 | + ): | |
| 134 | + dod = self.shell_owner.last_turn_summary.definition_of_done | |
| 135 | + status.update_definition_of_done( | |
| 136 | + dod.status, | |
| 137 | + len(dod.pending_items), | |
| 138 | + dod.last_verification_result, | |
| 139 | + _definition_of_done_verification_attempt(dod), | |
| 140 | + ) | |
| 129 | 141 | |
| 130 | 142 | # Focus input |
| 131 | 143 | self.query_one(InputArea).focus_input() |
@@ -692,6 +704,7 @@ class LoaderApp(App): | ||
| 692 | 704 | message.dod_status, |
| 693 | 705 | message.pending_items_count, |
| 694 | 706 | message.last_verification_result, |
| 707 | + message.verification_attempt, | |
| 695 | 708 | ) |
| 696 | 709 | |
| 697 | 710 | def on_workflow_mode_changed(self, message: WorkflowModeChanged) -> None: |
@@ -860,6 +873,10 @@ class LoaderApp(App): | ||
| 860 | 873 | msg_area.remove_children() |
| 861 | 874 | self.shell_owner.clear_history() |
| 862 | 875 | self.query_one(StatusLine).clear_definition_of_done() |
| 876 | + self.query_one(StatusLine).update_session_id(self.shell_owner.session.session_id) | |
| 877 | + self.query_one(StatusLine).update_runtime_owner( | |
| 878 | + self.shell_owner.session.runtime_owner_path or "" | |
| 879 | + ) | |
| 863 | 880 | self.query_one(StatusLine).update_workflow_mode("execute") |
| 864 | 881 | self._add_message("[dim]Conversation cleared.[/dim]") |
| 865 | 882 | |
@@ -870,3 +887,14 @@ class LoaderApp(App): | ||
| 870 | 887 | self.is_generating = False |
| 871 | 888 | self._stop_timer() |
| 872 | 889 | self.query_one(StatusLine).set_generating(False) |
| 890 | + | |
| 891 | + | |
| 892 | +def _definition_of_done_verification_attempt(dod) -> str | None: | |
| 893 | + """Render one compact verification-attempt label from DoD state.""" | |
| 894 | + | |
| 895 | + active_number = getattr(dod, "active_verification_attempt_number", None) | |
| 896 | + if active_number is None: | |
| 897 | + return None | |
| 898 | + if getattr(dod, "last_verification_result", None) == "stale" and active_number > 1: | |
| 899 | + return f"attempt {active_number - 1} -> attempt {active_number}" | |
| 900 | + return f"attempt {active_number}" | |
src/loader/ui/status_helpers.pymodified@@ -2,11 +2,14 @@ | ||
| 2 | 2 | |
| 3 | 3 | from __future__ import annotations |
| 4 | 4 | |
| 5 | +from ..runtime.owner_metadata import normalize_runtime_owner_path | |
| 6 | + | |
| 5 | 7 | |
| 6 | 8 | def format_definition_of_done_parts( |
| 7 | 9 | status: str, |
| 8 | 10 | pending_items_count: int, |
| 9 | 11 | last_verification_result: str, |
| 12 | + verification_attempt: str = "", | |
| 10 | 13 | ) -> list[str]: |
| 11 | 14 | """Format definition-of-done state for the status line.""" |
| 12 | 15 | if not status: |
@@ -27,8 +30,11 @@ def format_definition_of_done_parts( | ||
| 27 | 30 | verify_color = "green" if last_verification_result == "passed" else "red" |
| 28 | 31 | if last_verification_result == "skipped": |
| 29 | 32 | verify_color = "dim" |
| 33 | + verify_label = f"verify {last_verification_result}" | |
| 34 | + if verification_attempt: | |
| 35 | + verify_label = f"{verify_label} ({verification_attempt})" | |
| 30 | 36 | parts.append( |
| 31 | - f"[{verify_color}]verify {last_verification_result}[/{verify_color}]" | |
| 37 | + f"[{verify_color}]{verify_label}[/{verify_color}]" | |
| 32 | 38 | ) |
| 33 | 39 | |
| 34 | 40 | return parts |
@@ -65,6 +71,15 @@ def format_session_part(session_id: str) -> str | None: | ||
| 65 | 71 | return f"[dim]session {session_id[-8:]}[/dim]" |
| 66 | 72 | |
| 67 | 73 | |
| 74 | +def format_runtime_owner_part(owner_path: str) -> str | None: | |
| 75 | + """Format the active runtime-owner path for the status line.""" | |
| 76 | + | |
| 77 | + normalized_path = normalize_runtime_owner_path(owner_path) | |
| 78 | + if not normalized_path: | |
| 79 | + return None | |
| 80 | + return f"[dim]owner {normalized_path}[/dim]" | |
| 81 | + | |
| 82 | + | |
| 68 | 83 | def format_workflow_mode_part(mode: str) -> str | None: |
| 69 | 84 | """Format the active workflow mode for the status line.""" |
| 70 | 85 | |
src/loader/ui/widgets/status_line.pymodified@@ -7,6 +7,7 @@ from ..status_helpers import ( | ||
| 7 | 7 | format_capability_part, |
| 8 | 8 | format_definition_of_done_parts, |
| 9 | 9 | format_permission_mode_part, |
| 10 | + format_runtime_owner_part, | |
| 10 | 11 | format_session_part, |
| 11 | 12 | format_turn_phase_part, |
| 12 | 13 | format_workflow_mode_part, |
@@ -20,6 +21,7 @@ class StatusLine(Static): | ||
| 20 | 21 | mode: reactive[str] = reactive("Native") |
| 21 | 22 | capability_profile: reactive[str] = reactive("") |
| 22 | 23 | session_id: reactive[str] = reactive("") |
| 24 | + runtime_owner: reactive[str] = reactive("") | |
| 23 | 25 | workflow_mode: reactive[str] = reactive("") |
| 24 | 26 | turn_phase: reactive[str] = reactive("") |
| 25 | 27 | permission_mode: reactive[str] = reactive("") |
@@ -29,6 +31,7 @@ class StatusLine(Static): | ||
| 29 | 31 | dod_status: reactive[str] = reactive("") |
| 30 | 32 | pending_items_count: reactive[int] = reactive(0) |
| 31 | 33 | last_verification_result: reactive[str] = reactive("") |
| 34 | + verification_attempt: reactive[str] = reactive("") | |
| 32 | 35 | |
| 33 | 36 | def render(self) -> str: |
| 34 | 37 | """Render the status line.""" |
@@ -51,6 +54,7 @@ class StatusLine(Static): | ||
| 51 | 54 | self.dod_status, |
| 52 | 55 | self.pending_items_count, |
| 53 | 56 | self.last_verification_result, |
| 57 | + self.verification_attempt, | |
| 54 | 58 | ) |
| 55 | 59 | ) |
| 56 | 60 | |
@@ -76,6 +80,9 @@ class StatusLine(Static): | ||
| 76 | 80 | session_part = format_session_part(self.session_id) |
| 77 | 81 | if session_part: |
| 78 | 82 | parts.append(session_part) |
| 83 | + runtime_owner = format_runtime_owner_part(self.runtime_owner) | |
| 84 | + if runtime_owner: | |
| 85 | + parts.append(runtime_owner) | |
| 79 | 86 | |
| 80 | 87 | return " · ".join(parts) if parts else "[dim]Ready[/dim]" |
| 81 | 88 | |
@@ -103,6 +110,10 @@ class StatusLine(Static): | ||
| 103 | 110 | """React to session id changes.""" |
| 104 | 111 | self.refresh() |
| 105 | 112 | |
| 113 | + def watch_runtime_owner(self, runtime_owner: str) -> None: | |
| 114 | + """React to runtime owner changes.""" | |
| 115 | + self.refresh() | |
| 116 | + | |
| 106 | 117 | def watch_workflow_mode(self, workflow_mode: str) -> None: |
| 107 | 118 | """React to workflow mode changes.""" |
| 108 | 119 | self.refresh() |
@@ -123,6 +134,10 @@ class StatusLine(Static): | ||
| 123 | 134 | """React to verification result changes.""" |
| 124 | 135 | self.refresh() |
| 125 | 136 | |
| 137 | + def watch_verification_attempt(self, verification_attempt: str) -> None: | |
| 138 | + """React to verification attempt changes.""" | |
| 139 | + self.refresh() | |
| 140 | + | |
| 126 | 141 | def set_generating(self, is_generating: bool) -> None: |
| 127 | 142 | """Set generating state.""" |
| 128 | 143 | if is_generating: |
@@ -151,6 +166,10 @@ class StatusLine(Static): | ||
| 151 | 166 | """Update the active session id.""" |
| 152 | 167 | self.session_id = session_id |
| 153 | 168 | |
| 169 | + def update_runtime_owner(self, runtime_owner: str) -> None: | |
| 170 | + """Update the active runtime owner path.""" | |
| 171 | + self.runtime_owner = runtime_owner | |
| 172 | + | |
| 154 | 173 | def update_workflow_mode(self, workflow_mode: str) -> None: |
| 155 | 174 | """Update the active workflow mode.""" |
| 156 | 175 | self.workflow_mode = workflow_mode |
@@ -164,14 +183,17 @@ class StatusLine(Static): | ||
| 164 | 183 | status: str, |
| 165 | 184 | pending_items_count: int, |
| 166 | 185 | last_verification_result: str | None, |
| 186 | + verification_attempt: str | None = None, | |
| 167 | 187 | ) -> None: |
| 168 | 188 | """Update definition-of-done status.""" |
| 169 | 189 | self.dod_status = status |
| 170 | 190 | self.pending_items_count = pending_items_count |
| 171 | 191 | self.last_verification_result = last_verification_result or "" |
| 192 | + self.verification_attempt = verification_attempt or "" | |
| 172 | 193 | |
| 173 | 194 | def clear_definition_of_done(self) -> None: |
| 174 | 195 | """Clear definition-of-done status.""" |
| 175 | 196 | self.dod_status = "" |
| 176 | 197 | self.pending_items_count = 0 |
| 177 | 198 | self.last_verification_result = "" |
| 199 | + self.verification_attempt = "" | |
tests/test_status_surfaces.pymodified@@ -10,6 +10,7 @@ from loader.ui.status_helpers import ( | ||
| 10 | 10 | format_capability_part, |
| 11 | 11 | format_definition_of_done_parts, |
| 12 | 12 | format_permission_mode_part, |
| 13 | + format_runtime_owner_part, | |
| 13 | 14 | format_session_part, |
| 14 | 15 | format_turn_phase_part, |
| 15 | 16 | format_workflow_mode_part, |
@@ -17,12 +18,12 @@ from loader.ui.status_helpers import ( | ||
| 17 | 18 | |
| 18 | 19 | |
| 19 | 20 | def test_status_helper_formats_definition_of_done_parts() -> None: |
| 20 | - parts = format_definition_of_done_parts("verifying", 1, "failed") | |
| 21 | + parts = format_definition_of_done_parts("verifying", 1, "failed", "attempt 2") | |
| 21 | 22 | |
| 22 | 23 | assert parts == [ |
| 23 | 24 | "[yellow]DoD: verifying[/yellow]", |
| 24 | 25 | "[dim]1 pending[/dim]", |
| 25 | - "[red]verify failed[/red]", | |
| 26 | + "[red]verify failed (attempt 2)[/red]", | |
| 26 | 27 | ] |
| 27 | 28 | |
| 28 | 29 | |
@@ -63,3 +64,4 @@ def test_turn_phase_helpers_use_expected_colors() -> None: | ||
| 63 | 64 | def test_status_helpers_format_capability_and_session_parts() -> None: |
| 64 | 65 | assert format_capability_part("native/strict") == "[dim]cap native/strict[/dim]" |
| 65 | 66 | assert format_session_part("20260406T120000Z-abcdef01") == "[dim]session abcdef01[/dim]" |
| 67 | + assert format_runtime_owner_part("runtime-handle") == "[dim]owner runtime-handle[/dim]" | |