diff --git a/codex_retry_gateway_tui.py b/codex_retry_gateway_tui.py index 70b45bf..10a5df4 100644 --- a/codex_retry_gateway_tui.py +++ b/codex_retry_gateway_tui.py @@ -359,6 +359,24 @@ def age_since(value: Any) -> str: return format_elapsed_seconds(seconds) +def request_updated_at(row: dict[str, Any]) -> Any: + for key in ("last_activity_at", "usage_last_updated_at", "finished_at"): + value = row.get(key) + if parse_datetime(value): + return value + return None + + +def request_updated_elapsed(row: dict[str, Any]) -> str: + return elapsed_between(row.get("started_at"), request_updated_at(row)) + + +def request_chunk_progress(row: dict[str, Any]) -> str: + if not row.get("response_stream"): + return "-" + return f"{row.get('stream_chunk_count') or 0} / {format_bytes(row.get('response_bytes_received'))}" + + def status_kind(value: Any) -> str: text = str(value or "").strip().lower() if text in {"ok", "operational", "success"}: @@ -558,7 +576,7 @@ def render_request_detail(row: dict[str, Any]) -> str: f"life {request_lifecycle_label(row)}", f"started {short_time(row['started_at'])}", f"finished {short_time(row['finished_at'])}", - f"updated {age_since(row['usage_last_updated_at'])}", + f"updated {request_updated_elapsed(row)}", ] if row.get("model"): bits.append(f"model {row['model']}") @@ -753,11 +771,11 @@ def run_textual(api_url: str, status_url: str, refresh_seconds: int, timeout: in Screen { layout: vertical; } #summary { height: 1; padding: 0 1; color: $accent; } #paths { height: 1; padding: 0 1; color: $text-muted; } - #controls { height: 2; } - #tables { height: 1fr; } + #controls { height: 1; padding: 0 1; color: $text-muted; } + #tables { height: 1; padding: 0 1; color: $text-muted; } #requests_table, #logs_table, #profiles_table { height: 1fr; } - #detail { height: 5; padding: 0 1; border-top: solid $panel; } - #status { height: 2; padding: 0 1; color: $text-muted; } + #detail { height: 3; padding: 0 1; border-top: solid $panel; } + #status { height: 1; padding: 0 1; color: $text-muted; } """ BINDINGS = [ @@ -813,7 +831,7 @@ def run_textual(api_url: str, status_url: str, refresh_seconds: int, timeout: in requests = self.query_one("#requests_table", DataTable) requests.cursor_type = "row" requests.zebra_stripes = True - requests.add_columns("Seq", "ID", "Status", "Path", "Model", "Reasoning", "Req", "Resp", "First", "Duration", "Updated", "Note") + requests.add_columns("Seq", "ID", "Status", "Path", "Model", "Reasoning", "Req", "Resp", "Chunks", "First", "Duration", "Updated", "Note") logs = self.query_one("#logs_table", DataTable) logs.cursor_type = "row" @@ -994,9 +1012,10 @@ def run_textual(api_url: str, status_url: str, refresh_seconds: int, timeout: in row["reasoning_tokens"] if row["reasoning_tokens"] is not None else "-", format_bytes(row["request_body_bytes"]), format_bytes(row["response_bytes_received"]), + request_chunk_progress(row), format_ms(row["first_response_delay_ms"]), format_ms(row["duration_ms"]), - age_since(row.get("usage_last_updated_at")), + request_updated_elapsed(row), f"{row['upstream_attempt_count'] or 0}x {short_text(row['error'], 18)}", key=key, ) @@ -1236,7 +1255,7 @@ def print_once(snapshot: dict[str, Any], filter_text: str = "") -> None: print( f"{row['seq']:>6} {short_text(row['request_id'], 16):<16} {status_symbol(row)} {row.get('status_code') or '-':<4} " f"{short_text(row['path'], 20):<20} {short_text(row['model'] or row['requested_model'] or row['forwarded_model'], 16):<16} " - f"{format_ms(row['duration_ms']):<8} {age_since(row.get('usage_last_updated_at')):<8} " + f"{request_chunk_progress(row):<14} {format_ms(row['duration_ms']):<8} {request_updated_elapsed(row):<8} " f"{format_count(row['upstream_attempt_count'] or 0):<4} {short_text(row['error'], 30)}" ) diff --git a/pyproject.toml b/pyproject.toml index 7827d2f..e9a2795 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "codex-retry-gateway-tui" -version = "0.1.1" +version = "0.1.2" description = "Terminal UI for codex-retry-gateway monitoring and control" readme = "README.md" requires-python = ">=3.11" diff --git a/tests/test_payload.py b/tests/test_payload.py index 8488d23..860b11f 100644 --- a/tests/test_payload.py +++ b/tests/test_payload.py @@ -72,7 +72,27 @@ class CodexRetryGatewayTUITests(unittest.TestCase): mod.gateway_status_url("http://127.0.0.1:4610/__codex_retry_gateway"), "http://127.0.0.1:4610/__codex_retry_gateway/api/status", ) - self.assertIn("updated", mod.render_request_detail({"seq": 1, "request_id": "r", "method": "POST", "path": "/responses", "status_code": 200, "upstream_status_code": 200, "upstream_attempt_count": 1, "first_response_delay_ms": 10, "duration_ms": 20, "request_body_bytes": 3, "response_bytes_received": 4, "stream_chunk_count": 1, "started_at": "2026-06-30T00:00:00Z", "finished_at": "2026-06-30T00:00:01Z", "usage_last_updated_at": "2026-06-30T00:00:01Z"})) + row = { + "seq": 1, + "request_id": "r", + "method": "POST", + "path": "/responses", + "status_code": 200, + "upstream_status_code": 200, + "upstream_attempt_count": 1, + "first_response_delay_ms": 10, + "duration_ms": 20, + "request_body_bytes": 3, + "response_bytes_received": 4096, + "stream_chunk_count": 7, + "response_stream": True, + "started_at": "2026-06-30T00:00:00Z", + "finished_at": "2026-06-30T00:00:01Z", + "usage_last_updated_at": "2026-06-30T00:00:01Z", + } + self.assertIn("updated 1.0s", mod.render_request_detail(row)) + self.assertEqual(mod.request_updated_elapsed(row), "1.0s") + self.assertEqual(mod.request_chunk_progress(row), "7 / 4.1KB") def test_default_api_url_discovers_gateway_state(self) -> None: mod = load_module()