diff --git a/codex_retry_gateway_tui.py b/codex_retry_gateway_tui.py index f27a6c2..e39f339 100644 --- a/codex_retry_gateway_tui.py +++ b/codex_retry_gateway_tui.py @@ -5,6 +5,7 @@ from __future__ import annotations import argparse import datetime as dt from concurrent.futures import ThreadPoolExecutor +import hashlib import importlib.metadata import json import os @@ -81,6 +82,19 @@ REQUEST_TABLE_COLUMN_KEY_SET = set(REQUEST_TABLE_COLUMN_KEYS) REQUEST_TABLE_CELL_INDEX = {key: index for index, key in enumerate(REQUEST_TABLE_COLUMN_KEYS)} DEFAULT_REQUEST_TABLE_VISIBLE_COLUMNS = REQUEST_TABLE_COLUMN_KEYS DEFAULT_REQUEST_TABLE_SORT_COLUMN = "seq" +THREAD_COLOR_PALETTE = ( + "#5fd7ff", + "#87d75f", + "#ffaf5f", + "#d787ff", + "#5fafff", + "#ff87d7", + "#d7af5f", + "#5fd7af", + "#af87ff", + "#87afd7", +) +THREAD_COLOR_COLUMN_KEYS = frozenset({"req_id", "resp_id", "thread"}) REQUEST_TABLE_WIDTH_PROFILES = { "compact": { "seq": 7, @@ -990,6 +1004,37 @@ def request_row_cells(row: dict[str, Any], text_limits: dict[str, int] | None = ) +def request_thread_color(thread_id: Any) -> str | None: + text = str(thread_id or "").strip() + if not text: + return None + digest = hashlib.blake2b(text.encode("utf-8"), digest_size=4).digest() + color_index = int.from_bytes(digest[:2], "big") % len(THREAD_COLOR_PALETTE) + return THREAD_COLOR_PALETTE[color_index] + + +def request_visible_cells( + row: dict[str, Any], + cells: tuple[Any, ...], + visible_column_keys: list[str], +) -> list[Any]: + visible_cells = [cells[REQUEST_TABLE_CELL_INDEX[column_key]] for column_key in visible_column_keys] + thread_color = request_thread_color(row.get("thread_id")) + if not thread_color: + return visible_cells + try: + from rich.text import Text + except ImportError: + return visible_cells + styled_cells: list[Any] = [] + for column_key, cell in zip(visible_column_keys, visible_cells): + if column_key in THREAD_COLOR_COLUMN_KEYS: + styled_cells.append(Text(str(cell), style=f"bold {thread_color}")) + else: + styled_cells.append(cell) + return styled_cells + + def request_table_column_label(column_key: str) -> str: column = REQUEST_TABLE_COLUMN_BY_KEY.get(column_key) return str(column["label"]) if column else column_key @@ -2113,13 +2158,14 @@ def run_textual( selected_row_index = table.cursor_row if table.cursor_row is not None and table.cursor_row >= 0 else 0 selected_row_key = self._selected_request_key() text_limits = self._request_text_limits(table) + visible_column_keys = self._visible_request_column_keys() table.clear() self.request_by_key = {} for row in self.requests: key = self._request_row_key(row) self.request_by_key[key] = row cells = request_row_cells(row, text_limits) - visible_cells = [cells[REQUEST_TABLE_CELL_INDEX[column_key]] for column_key in self._visible_request_column_keys()] + visible_cells = request_visible_cells(row, cells, visible_column_keys) table.add_row(*visible_cells, key=key) if self.requests: if not self._select_request_row(selected_row_key): diff --git a/tests/test_payload.py b/tests/test_payload.py index 2c32543..930dfcf 100644 --- a/tests/test_payload.py +++ b/tests/test_payload.py @@ -263,6 +263,51 @@ class CodexRetryGatewayTUITests(unittest.TestCase): self.assertIn("current wave round 3 slot 1 first 1.2s", mod.render_request_detail(row)) self.assertEqual(mod.request_row_cells(row)[20], "3(2)") + def test_request_visible_cells_colorize_thread_identity_columns(self) -> None: + mod = load_module() + try: + from rich.text import Text + except ImportError: + self.skipTest("rich is not installed") + row = { + "seq": 1, + "request_id": "req_1", + "response_id": "resp_1", + "thread_id": "thread_same", + "started_at": "2026-06-30T12:00:00Z", + "status_code": 200, + "path": "/responses", + "model": "gpt-5", + "requested_model": "", + "forwarded_model": "", + "reasoning_tokens": 7, + "reasoning_effort": "xhigh", + "input_tokens": 20, + "output_tokens": 10, + "cached_tokens": 5, + "request_body_bytes": 1536, + "response_bytes_received": 4096, + "response_stream": True, + "stream_chunk_count": 4, + "first_response_delay_ms": 1200, + "duration_ms": 3400, + "upstream_attempt_count": 1, + "error": "", + "finished_at": "2026-06-30T12:00:03Z", + } + cells = mod.request_row_cells(row) + visible = mod.request_visible_cells(row, cells, ["seq", "req_id", "resp_id", "thread", "status"]) + self.assertEqual(visible[0], "1") + self.assertIsInstance(visible[1], Text) + self.assertIsInstance(visible[2], Text) + self.assertIsInstance(visible[3], Text) + self.assertEqual(visible[1].plain, "req_1") + self.assertEqual(visible[2].plain, "resp_1") + self.assertEqual(visible[3].plain, "thread_same") + self.assertEqual(visible[4], "ยท 200") + self.assertEqual(mod.request_thread_color("thread_same"), mod.request_thread_color("thread_same")) + self.assertIsNone(mod.request_thread_color("")) + def test_request_table_preferences_round_trip(self) -> None: mod = load_module() with tempfile.TemporaryDirectory() as tmpdir: