From f12f6de52351d872ec72a1b9692483fd9158adfa Mon Sep 17 00:00:00 2001 From: yunyaozhou Date: Tue, 7 Jul 2026 11:43:31 +0800 Subject: [PATCH] feat: simplify request column editor --- codex_retry_gateway_tui.py | 398 ++++++++++++++++++++++++------------- tests/test_payload.py | 55 +++-- 2 files changed, 296 insertions(+), 157 deletions(-) diff --git a/codex_retry_gateway_tui.py b/codex_retry_gateway_tui.py index be0b4b6..79150b8 100644 --- a/codex_retry_gateway_tui.py +++ b/codex_retry_gateway_tui.py @@ -29,7 +29,8 @@ DEFAULT_API_URL = DEFAULT_GATEWAY_URL DEFAULT_CONFIG_FILE = "~/.config/codex-retry-gateway-tui/api-url" DEFAULT_STATUS_CONFIG_FILE = "~/.config/codex-retry-gateway-tui/status-url" DEFAULT_ACCESS_KEY_FILE = "~/.config/codex-retry-gateway-tui/access-key" -DEFAULT_REQUEST_TABLE_PREFERENCES_FILE = "~/.config/codex-retry-gateway-tui/request-table.json" +DEFAULT_REQUEST_TABLE_PREFERENCES_FILE = "~/.config/codex-retry-gateway-tui/request-table-columns.txt" +LEGACY_REQUEST_TABLE_PREFERENCES_FILE = "~/.config/codex-retry-gateway-tui/request-table.json" DEFAULT_GATEWAY_STATE_FILE = "~/.codex-retry-gateway/state.json" DEFAULT_GATEWAY_JSON_CONFIG_FILE = "~/.codex-retry-gateway/config/config.json" DEFAULT_VERSION_CHECK_URL = "https://gitea.shujk.top/shujakuin/codex-retry-gateway-tui/raw/branch/main/pyproject.toml" @@ -62,7 +63,9 @@ REQUEST_TABLE_COLUMNS = ( {"key": "model", "label": "Model", "width_key": "model"}, {"key": "effort", "label": "Effort", "width_key": "effort"}, {"key": "reasoning", "label": "R.Tok", "width_key": "reasoning"}, - {"key": "usage", "label": "Usage", "width_key": "usage"}, + {"key": "in", "label": "In", "width_key": "token_col"}, + {"key": "out", "label": "Out", "width_key": "token_col"}, + {"key": "cache", "label": "Cache", "width_key": "token_col"}, {"key": "req_bytes", "label": "Req Size", "width_key": "req"}, {"key": "resp_bytes", "label": "Resp Size", "width_key": "resp"}, {"key": "chunks", "label": "Chunks", "width_key": "chunks"}, @@ -78,6 +81,29 @@ 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" +REQUEST_TABLE_COLUMN_ALIASES = { + "seq": ("seq",), + "req_id": ("req id", "request id", "request_id"), + "resp_id": ("resp id", "response id", "response_id"), + "thread": ("thread", "thread id", "thread_id"), + "started": ("started", "start", "started at"), + "status": ("status", "code", "status code"), + "path": ("path", "endpoint"), + "model": ("model",), + "effort": ("effort", "reasoning effort"), + "reasoning": ("reasoning", "r tok", "rtok", "reasoning tokens", "reasoning_tokens"), + "in": ("in", "input", "input tokens", "input_tokens"), + "out": ("out", "output", "output tokens", "output_tokens"), + "cache": ("cache", "cached", "cached tokens", "cached_tokens"), + "req_bytes": ("req", "req size", "request size", "request bytes", "request_bytes"), + "resp_bytes": ("resp", "resp size", "response size", "response bytes", "response_bytes"), + "chunks": ("chunks", "stream chunks", "chunk count"), + "first": ("first", "first response", "first delay"), + "duration": ("duration", "elapsed"), + "updated": ("updated", "last update"), + "note": ("note", "retry note"), + "round": ("round", "retry round"), +} INSTALL_COMMAND = "uv tool install --force git+https://gitea.shujk.top/shujakuin/codex-retry-gateway-tui.git" INSTALL_COMMAND_ARGS = [ "uv", @@ -185,6 +211,10 @@ def request_table_preferences_file_path() -> Path: ).expanduser() +def legacy_request_table_preferences_file_path() -> Path: + return Path(LEGACY_REQUEST_TABLE_PREFERENCES_FILE).expanduser() + + def default_status_url() -> str: return configured_url( ("CODEX_RETRY_GATEWAY_TUI_STATUS_URL",), @@ -201,18 +231,60 @@ def default_access_key() -> str: ) -def request_table_visible_columns(value: Any) -> list[str]: - requested = [] - if isinstance(value, (list, tuple, set)): - requested = [str(item or "").strip() for item in value] - visible = [] +def request_table_column_token(value: Any) -> str: + return re.sub(r"[^a-z0-9]+", "", str(value or "").strip().lower()) + + +def build_request_table_column_token_map() -> dict[str, str]: + token_map: dict[str, str] = {} + for column in REQUEST_TABLE_COLUMNS: + key = column["key"] + aliases = set(REQUEST_TABLE_COLUMN_ALIASES.get(key, ())) + aliases.add(key) + aliases.add(column["label"]) + for alias in aliases: + token = request_table_column_token(alias) + if token: + token_map[token] = key + return token_map + + +REQUEST_TABLE_COLUMN_TOKEN_MAP = build_request_table_column_token_map() + + +def request_table_column_key(value: Any) -> str: + token = request_table_column_token(value) + return REQUEST_TABLE_COLUMN_TOKEN_MAP.get(token, "") + + +def request_table_column_order(value: Any) -> list[str]: + requested = value if isinstance(value, (list, tuple, set)) else [] + columns = [] seen: set[str] = set() - for column_key in REQUEST_TABLE_COLUMN_KEYS: - if column_key not in requested or column_key in seen: + for item in requested: + column_key = request_table_column_key(item) + if not column_key or column_key in seen: continue - visible.append(column_key) + columns.append(column_key) seen.add(column_key) - return visible or list(DEFAULT_REQUEST_TABLE_VISIBLE_COLUMNS) + for column_key in REQUEST_TABLE_COLUMN_KEYS: + if column_key not in seen: + columns.append(column_key) + return columns or list(DEFAULT_REQUEST_TABLE_VISIBLE_COLUMNS) + + +def request_table_hidden_columns(value: Any, columns: list[str]) -> list[str]: + requested = value if isinstance(value, (list, tuple, set)) else [] + allowed = set(columns) + hidden = [] + seen: set[str] = set() + for item in requested: + column_key = request_table_column_key(item) + if not column_key or column_key not in allowed or column_key in seen: + continue + hidden.append(column_key) + seen.add(column_key) + return hidden def normalize_request_table_sort_column(value: Any) -> str: @@ -224,16 +296,112 @@ def normalize_request_table_sort_column(value: Any) -> str: def normalize_request_table_preferences(data: Any) -> dict[str, Any]: source = data if isinstance(data, dict) else {} + if isinstance(source.get("visible_columns"), (list, tuple, set)): + legacy_visible = [] + seen: set[str] = set() + for item in source.get("visible_columns") or []: + column_key = request_table_column_key(item) + if not column_key or column_key in seen: + continue + legacy_visible.append(column_key) + seen.add(column_key) + columns = request_table_column_order(legacy_visible) + hidden_columns = [column_key for column_key in columns if column_key not in seen] + else: + columns = request_table_column_order(source.get("columns")) + hidden_columns = request_table_hidden_columns(source.get("hidden_columns"), columns) + if len(hidden_columns) >= len(columns) and columns: + hidden_columns = [column_key for column_key in hidden_columns if column_key != columns[0]] return { - "visible_columns": request_table_visible_columns(source.get("visible_columns")), - "sort_column": normalize_request_table_sort_column(source.get("sort_column")), - "sort_reverse": parse_bool_value(source.get("sort_reverse"), True), + "columns": columns, + "hidden_columns": hidden_columns, } +def request_table_visible_columns(preferences: dict[str, Any]) -> list[str]: + normalized = normalize_request_table_preferences(preferences) + hidden = set(normalized["hidden_columns"]) + visible = [column_key for column_key in normalized["columns"] if column_key not in hidden] + if visible: + return visible + return [normalized["columns"][0]] + + +def request_table_preferences_from_text(text: str) -> dict[str, Any]: + columns = [] + hidden_columns = [] + seen: set[str] = set() + for line_number, raw_line in enumerate(text.splitlines(), start=1): + stripped = raw_line.strip() + if not stripped: + continue + hidden = False + token = stripped + if token.startswith("#"): + hidden = True + token = token[1:].strip() + if not token: + continue + column_key = request_table_column_key(token) + if not column_key: + if hidden: + continue + raise ValueError(f"line {line_number}: unknown request column {token!r}") + if column_key in seen: + continue + seen.add(column_key) + columns.append(column_key) + if hidden: + hidden_columns.append(column_key) + if not columns: + raise ValueError("no request columns found in editor document") + missing = [column_key for column_key in REQUEST_TABLE_COLUMN_KEYS if column_key not in seen] + return normalize_request_table_preferences( + { + "columns": columns + missing, + "hidden_columns": hidden_columns + missing, + } + ) + + +def request_table_preferences_document(preferences: dict[str, Any]) -> str: + normalized = normalize_request_table_preferences(preferences) + hidden = set(normalized["hidden_columns"]) + lines = [ + "# Request table columns.", + "# One column key per line.", + "# Prefix with # to hide a column.", + "# Move lines up or down to change display order.", + "# Common keys: in, out, cache, req_id, resp_id, req_bytes, resp_bytes, round.", + "", + ] + for column_key in normalized["columns"]: + prefix = "# " if column_key in hidden else "" + lines.append(f"{prefix}{column_key}") + return "\n".join(lines).rstrip() + "\n" + + def load_request_table_preferences() -> dict[str, Any]: - path = request_table_preferences_file_path() - return normalize_request_table_preferences(read_json_object(str(path))) + configured_path = request_table_preferences_file_path() + configured_env = os.environ.get("CODEX_RETRY_GATEWAY_TUI_REQUEST_TABLE_PREFERENCES_FILE", "").strip() + candidate_paths = [configured_path] + if not configured_env: + candidate_paths.append(legacy_request_table_preferences_file_path()) + for path in candidate_paths: + try: + text = path.read_text(encoding="utf-8") + except OSError: + continue + stripped = text.strip() + if not stripped: + continue + try: + if stripped.startswith("{"): + return normalize_request_table_preferences(json.loads(text)) + return request_table_preferences_from_text(text) + except Exception: + continue + return normalize_request_table_preferences({}) def write_request_table_preferences(preferences: dict[str, Any]) -> Path: @@ -244,7 +412,7 @@ def write_request_table_preferences(preferences: dict[str, Any]) -> Path: except OSError: pass normalized = normalize_request_table_preferences(preferences) - path.write_text(json.dumps(normalized, indent=2, sort_keys=True) + "\n", encoding="utf-8") + path.write_text(request_table_preferences_document(normalized), encoding="utf-8") try: path.chmod(0o600) except OSError: @@ -495,6 +663,21 @@ def request_usage_summary(row: dict[str, Any]) -> str: return f"in {in_text} | out {out_text} | cached {cached_text}" +def request_input_tokens_text(row: dict[str, Any]) -> str: + effective_in = effective_input_tokens(row.get("input_tokens"), row.get("cached_tokens")) + return format_count(effective_in) if effective_in is not None else "-" + + +def request_output_tokens_text(row: dict[str, Any]) -> str: + output_tokens = row.get("output_tokens") + return format_count(output_tokens) if output_tokens is not None else "-" + + +def request_cached_tokens_text(row: dict[str, Any]) -> str: + cached_tokens = row.get("cached_tokens") + return format_count(cached_tokens) if cached_tokens is not None else "-" + + def format_count(value: Any) -> str: number = as_int(value) if abs(number) >= 1_000_000: @@ -686,7 +869,9 @@ def request_row_cells(row: dict[str, Any], text_limits: dict[str, int] | None = short_text(request_model_text(row), limits.get("model", 16)), short_text(request_effort_text(row), limits.get("effort", 10)), request_reasoning_tokens_text(row), - short_text(request_usage_summary(row), limits.get("usage", 24)), + request_input_tokens_text(row), + request_output_tokens_text(row), + request_cached_tokens_text(row), format_bytes(row.get("request_body_bytes")), format_bytes(row.get("response_bytes_received")), request_chunk_progress(row), @@ -1350,6 +1535,33 @@ def edit_profile_payload_with_editor(row: dict[str, Any]) -> dict[str, Any] | No pass +def edit_request_table_preferences_with_editor(preferences: dict[str, Any]) -> dict[str, Any] | None: + original_text = request_table_preferences_document(preferences) + editor_command = resolve_editor_command() + fd, raw_path = tempfile.mkstemp(prefix="codex-retry-columns-", suffix=".txt") + os.close(fd) + temp_path = Path(raw_path) + temp_path.write_text(original_text, encoding="utf-8") + try: + result = subprocess.run(editor_command + [str(temp_path)], check=False) + edited_text = temp_path.read_text(encoding="utf-8") + if edited_text == original_text: + return None + preferences = request_table_preferences_from_text(edited_text) + if result.returncode != 0: + raise RuntimeError(f"editor exited with status {result.returncode}") + return preferences + except Exception as exc: + raise RuntimeError(f"{exc}; kept draft at {temp_path}") from exc + finally: + if temp_path.exists(): + try: + if temp_path.read_text(encoding="utf-8") == original_text: + temp_path.unlink() + except Exception: + pass + + def action_url(api_url: str, suffix: str) -> str: base = api_url.rstrip("/") return f"{base}{suffix}" @@ -1396,8 +1608,6 @@ def fetch_dashboard_snapshot( access_key: str = "", filter_text: str = "", current_view: str = "overview", - request_sort_column: str = DEFAULT_REQUEST_TABLE_SORT_COLUMN, - request_sort_reverse: bool = True, ) -> dict[str, Any]: gateway_root = gateway_admin_url(gateway_url) endpoints = { @@ -1429,11 +1639,7 @@ def fetch_dashboard_snapshot( "payload": payload, "status_payload": payload, "status_error": " | ".join(errors), - "requests": sort_request_rows( - normalize_request_rows(requests_source, filter_text), - request_sort_column, - reverse=request_sort_reverse, - ), + "requests": normalize_request_rows(requests_source, filter_text), "logs": normalize_log_rows(logs_source), "profiles": normalize_profile_rows( profiles_source, @@ -1489,15 +1695,9 @@ def run_textual( ("3", "show_logs", "Logs"), ("4", "show_profiles", "Profiles"), ("m", "toggle_request_table_density", "Wide/Compact"), - ("comma", "select_previous_request_column", "Prev Column"), - ("full_stop", "select_next_request_column", "Next Column"), - ("v", "toggle_request_column_visibility", "Show/Hide Column"), - ("c", "sort_requests_by_selected_column", "Sort Column"), - ("shift+c", "toggle_request_sort_direction", "Sort Asc/Desc"), + ("c", "edit_request_columns", "Columns"), ("shift+left", "scroll_table_left", "Scroll Left"), ("shift+right", "scroll_table_right", "Scroll Right"), - ("[", "narrow_usage_column", "Narrow Usage"), - ("]", "widen_usage_column", "Widen Usage"), ("e", "edit_profile", "Edit Profile"), ("p", "probe_profile", "Probe Profile"), ("s", "switch_profile", "Switch Profile"), @@ -1533,9 +1733,6 @@ def run_textual( } self.request_table_column_keys: dict[str, Any] = {} self.request_table_preferences = load_request_table_preferences() - self.request_table_sort_column = self.request_table_preferences["sort_column"] - self.request_table_sort_reverse = bool(self.request_table_preferences["sort_reverse"]) - self.request_table_selected_column = self.request_table_sort_column self.request_table_density = "compact" self.request_table_width_profiles = { "compact": { @@ -1549,7 +1746,7 @@ def run_textual( "model": 16, "effort": 8, "reasoning": 8, - "usage": 36, + "token_col": 8, "req": 10, "resp": 10, "chunks": 14, @@ -1570,7 +1767,7 @@ def run_textual( "model": 24, "effort": 8, "reasoning": 8, - "usage": 72, + "token_col": 8, "req": 12, "resp": 12, "chunks": 16, @@ -1581,7 +1778,6 @@ def run_textual( "round": 10, }, } - self.request_usage_width = self.request_table_width_profiles[self.request_table_density]["usage"] def compose(self) -> ComposeResult: yield Header(show_clock=True) @@ -1627,7 +1823,7 @@ def run_textual( self.query_one("#logs_table").display = active_table == "logs" self.query_one("#profiles_table").display = active_table == "profiles" self.query_one("#controls", Static).update( - "views: 1 overview | 2 requests | 3 logs | 4 profiles | / filter | r refresh | m compact/wide | , . column | v hide/show | c sort | C asc/desc | [ ] usage width | Shift-Left/Right scroll | e edit | p probe | s switch | w save | d delete | u open upstream" + "views: 1 overview | 2 requests | 3 logs | 4 profiles | / filter | r refresh | m compact/wide | c columns | Shift-Left/Right scroll | e edit | p probe | s switch | w save | d delete | u open upstream" ) self._focus_table() self._update_table_headers() @@ -1642,14 +1838,10 @@ def run_textual( self.query_one("#paths", Static).update(state_summary(self.payload)) def _update_table_headers(self) -> None: - selected_label = request_table_column_label(self.request_table_selected_column) - selected_visible = self.request_table_selected_column in self.request_table_preferences["visible_columns"] - sort_label = request_table_column_label(self.request_table_sort_column) - sort_direction = "desc" if self.request_table_sort_reverse else "asc" + visible_columns = request_table_visible_columns(self.request_table_preferences) counts = ( f"requests {len(self.requests)} | logs {len(self.logs)} | profiles {len(self.profiles)} | " - f"req cols {len(self.request_table_preferences['visible_columns'])}/{len(REQUEST_TABLE_COLUMNS)} | " - f"selected {selected_label} ({'shown' if selected_visible else 'hidden'}) | sort {sort_label} {sort_direction}" + f"req cols {len(visible_columns)}/{len(REQUEST_TABLE_COLUMNS)} | c edit columns" ) self.query_one("#tables", Static).update(counts) @@ -1657,7 +1849,7 @@ def run_textual( return self.query_one("#filter", Input).value.strip() def _visible_request_column_keys(self) -> list[str]: - return request_table_visible_columns(self.request_table_preferences.get("visible_columns")) + return request_table_visible_columns(self.request_table_preferences) def _request_table_actions_enabled(self) -> bool: if self.visible_tables.get(self.current_view, "requests") == "requests": @@ -1667,8 +1859,6 @@ def run_textual( def _persist_request_table_preferences(self) -> bool: self.request_table_preferences = normalize_request_table_preferences(self.request_table_preferences) - self.request_table_sort_column = self.request_table_preferences["sort_column"] - self.request_table_sort_reverse = bool(self.request_table_preferences["sort_reverse"]) try: write_request_table_preferences(self.request_table_preferences) except Exception as exc: @@ -1701,11 +1891,9 @@ def run_textual( width_profile = self.request_table_width_profiles[self.request_table_density] table.clear(columns=True) self.request_table_column_keys = {} - for column in REQUEST_TABLE_COLUMNS: - column_key = column["key"] - if column_key not in self._visible_request_column_keys(): - continue - width = self.request_usage_width if column_key == "usage" else width_profile[column["width_key"]] + for column_key in self._visible_request_column_keys(): + column = REQUEST_TABLE_COLUMN_BY_KEY[column_key] + width = width_profile[column["width_key"]] self.request_table_column_keys[column_key] = table.add_column( column["label"], width=width, @@ -1788,8 +1976,6 @@ def run_textual( access_key=access_key, filter_text=current_filter, current_view=self.current_view, - request_sort_column=self.request_table_sort_column, - request_sort_reverse=self.request_table_sort_reverse, ) result["generation"] = generation self.call_from_thread(self._apply_refresh_result, result) @@ -1936,18 +2122,6 @@ def run_textual( def action_focus_profiles(self) -> None: self._set_view("profiles") - def _set_request_usage_width(self, width: int) -> None: - table = self.query_one("#requests_table", DataTable) - key = self.request_table_column_keys.get("usage") - column = table.columns.get(key) if key is not None else None - if column is None: - self._set_status("usage column not ready") - return - self.request_usage_width = max(18, min(80, width)) - column.width = self.request_usage_width - 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 = { @@ -1957,11 +2131,10 @@ def run_textual( "path": width_profile["path"], "model": width_profile["model"], "effort": width_profile["effort"], - "usage": self.request_usage_width, "note": width_profile["note"], "round": width_profile["round"], } - for name in ("req_id", "resp_id", "thread", "path", "model", "effort", "usage", "note", "round"): + for name in ("req_id", "resp_id", "thread", "path", "model", "effort", "note", "round"): 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): @@ -1990,7 +2163,6 @@ def run_textual( continue column.width = width_profile[width_key] self.request_table_density = density - self.request_usage_width = width_profile["usage"] self._rerender_requests_after_layout_change() self._set_status(f"request table mode {density}") @@ -1998,78 +2170,24 @@ def run_textual( next_density = "wide" if self.request_table_density == "compact" else "compact" self._apply_request_table_width_profile(next_density) - def _step_request_column(self, offset: int) -> None: - if not self._request_table_actions_enabled(): - return + def action_edit_request_columns(self) -> None: try: - index = REQUEST_TABLE_COLUMN_KEYS.index(self.request_table_selected_column) - except ValueError: - index = 0 - self.request_table_selected_column = REQUEST_TABLE_COLUMN_KEYS[(index + offset) % len(REQUEST_TABLE_COLUMN_KEYS)] - visibility = "shown" if self.request_table_selected_column in self.request_table_preferences["visible_columns"] else "hidden" - self._update_table_headers() - self._set_status(f"selected request column {request_table_column_label(self.request_table_selected_column)} ({visibility})") - - def action_select_previous_request_column(self) -> None: - self._step_request_column(-1) - - def action_select_next_request_column(self) -> None: - self._step_request_column(1) - - def action_toggle_request_column_visibility(self) -> None: - if not self._request_table_actions_enabled(): - return - visible_columns = self._visible_request_column_keys() - column_key = self.request_table_selected_column - if column_key in visible_columns: - if len(visible_columns) <= 1: - self._set_status("keep at least one request column visible") + with self.suspend(): + updated = edit_request_table_preferences_with_editor(self.request_table_preferences) + if updated is None: + self._set_status("request columns unchanged") return - visible_columns = [key for key in visible_columns if key != column_key] - action = "hidden" - else: - visible_columns.append(column_key) - action = "shown" - self.request_table_preferences["visible_columns"] = request_table_visible_columns(visible_columns) - if not self._persist_request_table_preferences(): - return - self._rebuild_request_table_columns() - self._rerender_requests_after_layout_change() - self._set_status(f"request column {request_table_column_label(column_key)} {action}") - - def _apply_request_sort(self, column_key: str, reverse: bool) -> None: - if not self._request_table_actions_enabled(): - return - selected_request_key = self._selected_request_key() - self.request_table_sort_column = normalize_request_table_sort_column(column_key) - self.request_table_sort_reverse = bool(reverse) - self.request_table_preferences["sort_column"] = self.request_table_sort_column - self.request_table_preferences["sort_reverse"] = self.request_table_sort_reverse - if not self._persist_request_table_preferences(): - return - self.requests = sort_request_rows( - self.requests, - self.request_table_sort_column, - reverse=self.request_table_sort_reverse, - ) - self.last_request_key = selected_request_key - self._rerender_requests_after_layout_change() - self._set_status( - f"request sort {request_table_column_label(self.request_table_sort_column)} " - f"{'desc' if self.request_table_sort_reverse else 'asc'}" - ) - - def action_sort_requests_by_selected_column(self) -> None: - self._apply_request_sort(self.request_table_selected_column, self.request_table_sort_reverse) - - def action_toggle_request_sort_direction(self) -> None: - self._apply_request_sort(self.request_table_sort_column, not self.request_table_sort_reverse) - - def action_widen_usage_column(self) -> None: - self._set_request_usage_width(self.request_usage_width + 4) - - def action_narrow_usage_column(self) -> None: - self._set_request_usage_width(self.request_usage_width - 4) + self.request_table_preferences = updated + if not self._persist_request_table_preferences(): + return + self._rebuild_request_table_columns() + self._rerender_requests_after_layout_change() + self._set_status( + f"request columns saved -> {request_table_preferences_file_path()} | " + f"{len(self._visible_request_column_keys())}/{len(REQUEST_TABLE_COLUMNS)} shown" + ) + except Exception as exc: + self._set_status(f"request columns error: {exc}") def action_scroll_table_left(self) -> None: table = self._current_table_widget() diff --git a/tests/test_payload.py b/tests/test_payload.py index f37dd2d..97cc40e 100644 --- a/tests/test_payload.py +++ b/tests/test_payload.py @@ -144,18 +144,20 @@ class CodexRetryGatewayTUITests(unittest.TestCase): "error": "Selected model is at capacity. Please try a different model.", "finished_at": "2026-06-30T12:25:00Z", } - compact = mod.request_row_cells(row, {"req_id": 18, "resp_id": 18, "thread": 18, "path": 22, "model": 16, "usage": 24, "note": 22}) - wide = mod.request_row_cells(row, {"req_id": 36, "resp_id": 36, "thread": 36, "path": 40, "model": 24, "usage": 72, "note": 48}) - self.assertEqual(compact[11], "0B") - self.assertEqual(wide[11], "0B") + compact = mod.request_row_cells(row, {"req_id": 18, "resp_id": 18, "thread": 18, "path": 22, "model": 16, "note": 22}) + wide = mod.request_row_cells(row, {"req_id": 36, "resp_id": 36, "thread": 36, "path": 40, "model": 24, "note": 48}) + self.assertEqual(compact[13], "0B") + self.assertEqual(wide[13], "0B") self.assertEqual(compact[8], "-") self.assertEqual(compact[9], "516") + self.assertEqual(compact[10], "1.1K") + self.assertEqual(compact[11], "340") + self.assertEqual(compact[12], "128") self.assertLess(len(compact[1]), len(wide[1])) self.assertLess(len(compact[2]), len(wide[2])) self.assertLess(len(compact[3]), len(wide[3])) self.assertLess(len(compact[6]), len(wide[6])) - self.assertLess(len(compact[10]), len(wide[10])) - self.assertLess(len(compact[17]), len(wide[17])) + self.assertLess(len(compact[19]), len(wide[19])) def test_request_row_cells_show_request_bytes(self) -> None: mod = load_module() @@ -190,9 +192,12 @@ class CodexRetryGatewayTUITests(unittest.TestCase): self.assertEqual(cells[2], "resp_1") self.assertEqual(cells[8], "xhigh") self.assertEqual(cells[9], "7") - self.assertEqual(cells[11], "1.5KB") - self.assertEqual(cells[12], "4.1KB") - self.assertEqual(cells[17], "-") + self.assertEqual(cells[10], "15") + self.assertEqual(cells[11], "10") + self.assertEqual(cells[12], "5") + self.assertEqual(cells[13], "1.5KB") + self.assertEqual(cells[14], "4.1KB") + self.assertEqual(cells[19], "-") def test_request_retry_note_only_shows_retry_reason(self) -> None: mod = load_module() @@ -253,12 +258,12 @@ class CodexRetryGatewayTUITests(unittest.TestCase): self.assertIn("slot 1", mod.request_retry_wave_summary(row)) self.assertIn("retry round 3(2)", mod.render_request_detail(row)) self.assertIn("current wave slot 1 first 1.2s", mod.render_request_detail(row)) - self.assertEqual(mod.request_row_cells(row)[18], "3(2)") + self.assertEqual(mod.request_row_cells(row)[20], "3(2)") def test_request_table_preferences_round_trip(self) -> None: mod = load_module() with tempfile.TemporaryDirectory() as tmpdir: - prefs_path = Path(tmpdir) / "request-table.json" + prefs_path = Path(tmpdir) / "request-table-columns.txt" with mock.patch.dict( os.environ, {"CODEX_RETRY_GATEWAY_TUI_REQUEST_TABLE_PREFERENCES_FILE": str(prefs_path)}, @@ -266,16 +271,32 @@ class CodexRetryGatewayTUITests(unittest.TestCase): ): saved_path = mod.write_request_table_preferences( { - "visible_columns": ["round", "seq", "status", "unknown"], - "sort_column": "round", - "sort_reverse": False, + "columns": ["round", "seq", "status", "unknown"], + "hidden_columns": ["status", "unknown"], } ) self.assertEqual(saved_path, prefs_path) loaded = mod.load_request_table_preferences() - self.assertEqual(loaded["visible_columns"], ["seq", "status", "round"]) - self.assertEqual(loaded["sort_column"], "round") - self.assertFalse(loaded["sort_reverse"]) + self.assertEqual(loaded["columns"][:3], ["round", "seq", "status"]) + self.assertEqual(loaded["hidden_columns"], ["status"]) + + def test_request_table_preferences_from_text_hides_and_reorders_columns(self) -> None: + mod = load_module() + prefs = mod.request_table_preferences_from_text( + "\n".join( + [ + "# comment", + "thread", + "seq", + "# cache", + "in", + "out", + "", + ] + ) + ) + self.assertEqual(prefs["columns"][:6], ["thread", "seq", "cache", "in", "out", "req_id"]) + self.assertIn("cache", prefs["hidden_columns"]) def test_sort_request_rows_supports_custom_column_and_missing_values(self) -> None: mod = load_module()