From c85c7d896d9a16891ad50b7a414dc6815c9f7ceb Mon Sep 17 00:00:00 2001 From: yunyaozhou Date: Tue, 30 Jun 2026 17:40:34 +0800 Subject: [PATCH] fix: rerender wide mode request text --- codex_retry_gateway_tui.py | 105 ++++++++++++++++++++++++++----------- tests/test_payload.py | 34 ++++++++++++ 2 files changed, 107 insertions(+), 32 deletions(-) diff --git a/codex_retry_gateway_tui.py b/codex_retry_gateway_tui.py index 289da58..7dceebf 100644 --- a/codex_retry_gateway_tui.py +++ b/codex_retry_gateway_tui.py @@ -436,6 +436,29 @@ def request_chunk_progress(row: dict[str, Any]) -> str: return f"{row.get('stream_chunk_count') or 0} / {format_bytes(row.get('response_bytes_received'))}" +def request_row_cells(row: dict[str, Any], text_limits: dict[str, int] | None = None) -> tuple[Any, ...]: + limits = text_limits or {} + note_prefix = f"{row['upstream_attempt_count'] or 0}x " + note_width = max(4, limits.get("note", 22) - len(note_prefix)) + return ( + str(row["seq"]), + short_text(primary_request_id(row), limits.get("id", 18)) or "-", + short_text(row["thread_id"], limits.get("thread", 18)) or "-", + short_time(row["started_at"]), + f"{status_symbol(row)} {row.get('status_code') or '-'}", + short_text(row["path"], limits.get("path", 22)), + short_text(row["model"] or row["requested_model"] or row["forwarded_model"], limits.get("model", 16)), + row["reasoning_tokens"] if row["reasoning_tokens"] is not None else "-", + short_text(request_usage_summary(row), limits.get("usage", 24)), + format_bytes(row["response_bytes_received"]), + request_chunk_progress(row), + format_duration_ms_as_seconds(row["first_response_delay_ms"]), + format_duration_ms_as_seconds(row["duration_ms"]), + request_updated_elapsed(row), + f"{note_prefix}{short_text(row['error'], note_width)}", + ) + + def status_kind(value: Any) -> str: text = str(value or "").strip().lower() if text in {"ok", "operational", "success"}: @@ -1160,20 +1183,20 @@ def run_textual(api_url: str, status_url: str, refresh_seconds: int, timeout: in }, "wide": { "seq": 7, - "id": 20, - "thread": 30, + "id": 36, + "thread": 36, "started": 19, "status": 7, - "path": 34, - "model": 18, + "path": 40, + "model": 24, "reasoning": 10, - "usage": 52, + "usage": 72, "resp": 12, "chunks": 16, "first": 8, "duration": 8, "updated": 8, - "note": 34, + "note": 48, }, } self.request_usage_width = self.request_table_width_profiles[self.request_table_density]["usage"] @@ -1366,46 +1389,36 @@ def run_textual(api_url: str, status_url: str, refresh_seconds: int, timeout: in self.render_logs() self.render_profiles() if self.current_view == "requests": - if self.requests: - self.render_detail(self.requests[0], kind="request") + row = self._selected_request_row() + if row: + self.render_detail(row, kind="request") else: self.query_one("#detail", Static).update("no requests") elif self.current_view == "logs": - if self.logs: - self.render_detail(self.logs[0], kind="log") + row = self._selected_log_row() + if row: + self.render_detail(row, kind="log") else: self.query_one("#detail", Static).update("no logs") elif self.current_view == "profiles": - if self.profiles: - self.render_detail(self.profiles[0], kind="profile") + row = self._selected_profile_row() + if row: + self.render_detail(row, kind="profile") else: self.query_one("#detail", Static).update("no profiles") def render_requests(self) -> None: table = self.query_one("#requests_table", DataTable) + selected_row_index = table.cursor_row if table.cursor_row is not None and table.cursor_row >= 0 else 0 + text_limits = self._request_text_limits(table) table.clear() self.request_by_key = {} for row in self.requests: key = f"{row['seq']}:{row['request_id']}" self.request_by_key[key] = row - table.add_row( - str(row["seq"]), - short_text(primary_request_id(row), 18) or "-", - short_text(row["thread_id"], 18) or "-", - short_time(row["started_at"]), - f"{status_symbol(row)} {row.get('status_code') or '-'}", - short_text(row["path"], 22), - short_text(row["model"] or row["requested_model"] or row["forwarded_model"], 16), - row["reasoning_tokens"] if row["reasoning_tokens"] is not None else "-", - short_text(request_usage_summary(row), 24), - format_bytes(row["response_bytes_received"]), - request_chunk_progress(row), - format_duration_ms_as_seconds(row["first_response_delay_ms"]), - format_duration_ms_as_seconds(row["duration_ms"]), - request_updated_elapsed(row), - f"{row['upstream_attempt_count'] or 0}x {short_text(row['error'], 18)}", - key=key, - ) + table.add_row(*request_row_cells(row, text_limits), key=key) + if self.requests: + self._select_table_row(table, min(selected_row_index, len(self.requests) - 1)) def render_logs(self) -> None: table = self.query_one("#logs_table", DataTable) @@ -1488,9 +1501,37 @@ def run_textual(api_url: str, status_url: str, refresh_seconds: int, timeout: in return self.request_usage_width = max(18, min(80, width)) column.width = self.request_usage_width - table.refresh(layout=True) + self._rerender_requests_after_layout_change() self._set_status(f"usage column width {self.request_usage_width}") + def _request_text_limits(self, table: DataTable) -> dict[str, int]: + width_profile = self.request_table_width_profiles[self.request_table_density] + limits = { + "id": width_profile["id"], + "thread": width_profile["thread"], + "path": width_profile["path"], + "model": width_profile["model"], + "usage": self.request_usage_width, + "note": width_profile["note"], + } + for name in ("id", "thread", "path", "model", "usage", "note"): + key = self.request_table_column_keys.get(name) + column = table.columns.get(key) if key is not None else None + if column is None or not getattr(column, "width", 0): + continue + limits[name] = int(column.width) + return limits + + def _rerender_requests_after_layout_change(self) -> None: + self.render_requests() + if self.current_view not in {"overview", "requests"}: + return + row = self._selected_request_row() + if row: + self.render_detail(row, kind="request") + else: + self.query_one("#detail", Static).update("no requests") + def _apply_request_table_width_profile(self, density: str) -> None: table = self.query_one("#requests_table", DataTable) width_profile = self.request_table_width_profiles[density] @@ -1501,7 +1542,7 @@ def run_textual(api_url: str, status_url: str, refresh_seconds: int, timeout: in column.width = width_profile[name] self.request_table_density = density self.request_usage_width = width_profile["usage"] - table.refresh(layout=True) + self._rerender_requests_after_layout_change() self._set_status(f"request table mode {density}") def action_toggle_request_table_density(self) -> None: diff --git a/tests/test_payload.py b/tests/test_payload.py index 8fe73c5..463a697 100644 --- a/tests/test_payload.py +++ b/tests/test_payload.py @@ -109,6 +109,40 @@ class CodexRetryGatewayTUITests(unittest.TestCase): } self.assertEqual(mod.request_usage_summary(row), "in 75 | out 40 | cached 25 (25%)") + def test_request_row_cells_expand_with_wider_limits(self) -> None: + mod = load_module() + row = { + "seq": 1, + "request_id": "req_abcdefghijklmnopqrstuvwxyz0123456789", + "response_id": "resp_abcdefghijklmnopqrstuvwxyz0123456789", + "thread_id": "thread_abcdefghijklmnopqrstuvwxyz0123456789", + "started_at": "2026-06-30T12:00:00Z", + "status_code": 200, + "path": "/v1/responses/this/is/a/very/long/path/for/testing/truncation", + "model": "gpt-very-long-model-name-for-testing", + "requested_model": "", + "forwarded_model": "", + "reasoning_tokens": 516, + "input_tokens": 1200, + "output_tokens": 340, + "cached_tokens": 128, + "response_bytes_received": 363800, + "response_stream": True, + "stream_chunk_count": 587, + "first_response_delay_ms": 1500, + "duration_ms": 1542200, + "upstream_attempt_count": 2, + "error": "Selected model is at capacity. Please try a different model.", + "finished_at": "2026-06-30T12:25:00Z", + } + compact = mod.request_row_cells(row, {"id": 18, "thread": 18, "path": 22, "model": 16, "usage": 24, "note": 22}) + wide = mod.request_row_cells(row, {"id": 36, "thread": 36, "path": 40, "model": 24, "usage": 72, "note": 48}) + self.assertLess(len(compact[1]), len(wide[1])) + self.assertLess(len(compact[2]), len(wide[2])) + self.assertLess(len(compact[5]), len(wide[5])) + self.assertLess(len(compact[8]), len(wide[8])) + self.assertLess(len(compact[14]), len(wide[14])) + def test_request_rows_read_usage_from_nested_usage_object(self) -> None: mod = load_module() payload = {