Compare commits

..
2 Commits
Author SHA1 Message Date
shujakuin 58638d9fe3 feat: color requests by thread id 2026-07-09 08:43:32 +08:00
shujakuin a97ac87367 docs: add infra app metadata 2026-07-08 16:03:23 +08:00
3 changed files with 107 additions and 1 deletions
+15
View File
@@ -1,5 +1,20 @@
# codex-retry-gateway-tui # codex-retry-gateway-tui
- 文档层级:客户端工具
- 应用性质:客户端 TUI
- 源码来源:git submodule
- 运行关系:客户端工具
- 部署模型:外部安装器
- 对应服务:`services/pc/user-app/codex-retry-gateway-tui/current/README.md`、`services/a100/user-app/codex-retry-gateway-tui/current/README.md`
- 对应 stack:无
- Secret 边界:本地用户配置
- 分发方式:`uvx` / `uv tool install`。
- 包名:`codex-retry-gateway-tui`
- 命令名:`codex-retry-gateway-tui`
- 本地状态路径:`~/.config/codex-retry-gateway-tui/`
- 目标 API:本机或内网 codex-retry-gateway。
- 备注:客户端工具本身不是长期服务;user-app README 只记录具体宿主安装状态。
Terminal UI for `codex-retry-gateway`. Terminal UI for `codex-retry-gateway`.
## Quick Start ## Quick Start
+47 -1
View File
@@ -5,6 +5,7 @@ from __future__ import annotations
import argparse import argparse
import datetime as dt import datetime as dt
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
import hashlib
import importlib.metadata import importlib.metadata
import json import json
import os 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)} 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_VISIBLE_COLUMNS = REQUEST_TABLE_COLUMN_KEYS
DEFAULT_REQUEST_TABLE_SORT_COLUMN = "seq" 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 = { REQUEST_TABLE_WIDTH_PROFILES = {
"compact": { "compact": {
"seq": 7, "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: def request_table_column_label(column_key: str) -> str:
column = REQUEST_TABLE_COLUMN_BY_KEY.get(column_key) column = REQUEST_TABLE_COLUMN_BY_KEY.get(column_key)
return str(column["label"]) if column else 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_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() selected_row_key = self._selected_request_key()
text_limits = self._request_text_limits(table) text_limits = self._request_text_limits(table)
visible_column_keys = self._visible_request_column_keys()
table.clear() table.clear()
self.request_by_key = {} self.request_by_key = {}
for row in self.requests: for row in self.requests:
key = self._request_row_key(row) key = self._request_row_key(row)
self.request_by_key[key] = row self.request_by_key[key] = row
cells = request_row_cells(row, text_limits) 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) table.add_row(*visible_cells, key=key)
if self.requests: if self.requests:
if not self._select_request_row(selected_row_key): if not self._select_request_row(selected_row_key):
+45
View File
@@ -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.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)") 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: def test_request_table_preferences_round_trip(self) -> None:
mod = load_module() mod = load_module()
with tempfile.TemporaryDirectory() as tmpdir: with tempfile.TemporaryDirectory() as tmpdir: