feat: improve retry first column controls

This commit is contained in:
2026-07-07 20:28:13 +08:00
parent c5fa238598
commit 184b6b65b0
2 changed files with 132 additions and 61 deletions
+121 -56
View File
@@ -81,6 +81,51 @@ 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"
REQUEST_TABLE_WIDTH_PROFILES = {
"compact": {
"seq": 7,
"req_id": 18,
"resp_id": 18,
"thread": 18,
"started": 19,
"status": 7,
"path": 22,
"model": 16,
"effort": 8,
"reasoning": 8,
"token_col": 8,
"req": 10,
"resp": 10,
"chunks": 14,
"first": 12,
"duration": 8,
"updated": 8,
"note": 22,
"round": 10,
},
"wide": {
"seq": 7,
"req_id": 24,
"resp_id": 24,
"thread": 36,
"started": 19,
"status": 7,
"path": 40,
"model": 24,
"effort": 8,
"reasoning": 8,
"token_col": 8,
"req": 12,
"resp": 12,
"chunks": 16,
"first": 14,
"duration": 8,
"updated": 8,
"note": 48,
"round": 10,
},
}
REQUEST_TABLE_MIN_COLUMN_WIDTH = 4
REQUEST_TABLE_COLUMN_ALIASES = { REQUEST_TABLE_COLUMN_ALIASES = {
"seq": ("seq",), "seq": ("seq",),
"req_id": ("req id", "request id", "request_id"), "req_id": ("req id", "request id", "request_id"),
@@ -295,6 +340,29 @@ def request_table_hidden_columns(value: Any, columns: list[str]) -> list[str]:
return hidden return hidden
def request_table_column_width_value(value: Any) -> int | None:
try:
width = int(str(value or "").strip())
except Exception:
return None
if width <= 0:
return None
return max(REQUEST_TABLE_MIN_COLUMN_WIDTH, width)
def request_table_width_overrides(value: Any) -> dict[str, int]:
requested = value if isinstance(value, dict) else {}
overrides: dict[str, int] = {}
for raw_key, raw_width in requested.items():
width = request_table_column_width_value(raw_width)
if width is None:
continue
for column_key in request_table_column_keys(raw_key):
if column_key:
overrides[column_key] = width
return overrides
def normalize_request_table_sort_column(value: Any) -> str: def normalize_request_table_sort_column(value: Any) -> str:
column_key = str(value or "").strip() column_key = str(value or "").strip()
if column_key in REQUEST_TABLE_COLUMN_KEY_SET: if column_key in REQUEST_TABLE_COLUMN_KEY_SET:
@@ -320,9 +388,11 @@ def normalize_request_table_preferences(data: Any) -> dict[str, Any]:
hidden_columns = request_table_hidden_columns(source.get("hidden_columns"), columns) hidden_columns = request_table_hidden_columns(source.get("hidden_columns"), columns)
if len(hidden_columns) >= len(columns) and columns: if len(hidden_columns) >= len(columns) and columns:
hidden_columns = [column_key for column_key in hidden_columns if column_key != columns[0]] hidden_columns = [column_key for column_key in hidden_columns if column_key != columns[0]]
width_overrides = request_table_width_overrides(source.get("widths"))
return { return {
"columns": columns, "columns": columns,
"hidden_columns": hidden_columns, "hidden_columns": hidden_columns,
"widths": {column_key: width for column_key, width in width_overrides.items() if column_key in columns},
} }
@@ -338,6 +408,7 @@ def request_table_visible_columns(preferences: dict[str, Any]) -> list[str]:
def request_table_preferences_from_text(text: str) -> dict[str, Any]: def request_table_preferences_from_text(text: str) -> dict[str, Any]:
columns = [] columns = []
hidden_columns = [] hidden_columns = []
width_overrides: dict[str, int] = {}
seen: set[str] = set() seen: set[str] = set()
for line_number, raw_line in enumerate(text.splitlines(), start=1): for line_number, raw_line in enumerate(text.splitlines(), start=1):
stripped = raw_line.strip() stripped = raw_line.strip()
@@ -350,6 +421,11 @@ def request_table_preferences_from_text(text: str) -> dict[str, Any]:
token = token[1:].strip() token = token[1:].strip()
if not token: if not token:
continue continue
width = None
width_match = re.match(r"^(.*?)(?:\s+(\d+))?$", token)
if width_match:
token = (width_match.group(1) or "").strip()
width = request_table_column_width_value(width_match.group(2))
column_keys = request_table_column_keys(token) column_keys = request_table_column_keys(token)
if not column_keys: if not column_keys:
if hidden: if hidden:
@@ -362,6 +438,8 @@ def request_table_preferences_from_text(text: str) -> dict[str, Any]:
columns.append(column_key) columns.append(column_key)
if hidden: if hidden:
hidden_columns.append(column_key) hidden_columns.append(column_key)
if width is not None:
width_overrides[column_key] = width
if not columns: if not columns:
raise ValueError("no request columns found in editor document") 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] missing = [column_key for column_key in REQUEST_TABLE_COLUMN_KEYS if column_key not in seen]
@@ -369,6 +447,7 @@ def request_table_preferences_from_text(text: str) -> dict[str, Any]:
{ {
"columns": columns + missing, "columns": columns + missing,
"hidden_columns": hidden_columns + missing, "hidden_columns": hidden_columns + missing,
"widths": width_overrides,
} }
) )
@@ -376,17 +455,20 @@ def request_table_preferences_from_text(text: str) -> dict[str, Any]:
def request_table_preferences_document(preferences: dict[str, Any]) -> str: def request_table_preferences_document(preferences: dict[str, Any]) -> str:
normalized = normalize_request_table_preferences(preferences) normalized = normalize_request_table_preferences(preferences)
hidden = set(normalized["hidden_columns"]) hidden = set(normalized["hidden_columns"])
widths = normalized["widths"]
lines = [ lines = [
"# Request table columns.", "# Request table columns.",
"# One column key per line.", "# One column key per line.",
"# Prefix with # to hide a column.", "# Prefix with # to hide a column.",
"# Optional: append an integer width, e.g. `first 14` or `in 10`.",
"# Move lines up or down to change display order.", "# Move lines up or down to change display order.",
"# Common keys: in, out, cache, req_id, resp_id, req_bytes, resp_bytes, round.", "# Common keys: in, out, cache, req_id, resp_id, req_bytes, resp_bytes, round.",
"", "",
] ]
for column_key in normalized["columns"]: for column_key in normalized["columns"]:
prefix = "# " if column_key in hidden else "" prefix = "# " if column_key in hidden else ""
lines.append(f"{prefix}{column_key}") width_suffix = f" {widths[column_key]}" if column_key in widths else ""
lines.append(f"{prefix}{column_key}{width_suffix}")
return "\n".join(lines).rstrip() + "\n" return "\n".join(lines).rstrip() + "\n"
@@ -769,6 +851,7 @@ def normalize_request_retry_firsts(value: Any) -> list[dict[str, Any]]:
continue continue
firsts.append( firsts.append(
{ {
"round": parse_int_value(item.get("round")),
"slot": parse_int_value(item.get("slot")), "slot": parse_int_value(item.get("slot")),
"first_delay_ms": item.get("first_response_delay_ms", item.get("first_delay_ms", item.get("first_ms"))), "first_delay_ms": item.get("first_response_delay_ms", item.get("first_delay_ms", item.get("first_ms"))),
"outcome": str(item.get("outcome") or "").strip(), "outcome": str(item.get("outcome") or "").strip(),
@@ -781,6 +864,20 @@ def normalize_request_retry_firsts(value: Any) -> list[dict[str, Any]]:
return firsts return firsts
def request_retry_first_label(first: dict[str, Any], fallback_round: Any = None) -> str:
round_number = parse_int_value(first.get("round"))
if round_number is None:
round_number = parse_int_value(fallback_round)
slot = parse_int_value(first.get("slot"))
if round_number is not None:
if slot is not None and slot > 1:
return f"{round_number}-{slot}"
return str(round_number)
if slot is not None:
return str(slot)
return "?"
def request_retry_round_text(row: dict[str, Any]) -> str: def request_retry_round_text(row: dict[str, Any]) -> str:
round_number = parse_int_value(row.get("reasoning_retry_current_round")) round_number = parse_int_value(row.get("reasoning_retry_current_round"))
width = parse_int_value(row.get("reasoning_retry_current_width")) width = parse_int_value(row.get("reasoning_retry_current_width"))
@@ -793,8 +890,11 @@ def request_retry_round_text(row: dict[str, Any]) -> str:
return f"{round_number}({max(0, width)})" return f"{round_number}({max(0, width)})"
def request_retry_first_summary(first: dict[str, Any]) -> str: def request_retry_first_summary(first: dict[str, Any], fallback_round: Any = None) -> str:
bits = [] bits = []
round_label = request_retry_first_label(first, fallback_round)
if round_label != "?":
bits.append(f"round {round_label}")
slot = parse_int_value(first.get("slot")) slot = parse_int_value(first.get("slot"))
if slot is not None: if slot is not None:
bits.append(f"slot {slot}") bits.append(f"slot {slot}")
@@ -823,10 +923,11 @@ def request_retry_first_summary(first: dict[str, Any]) -> str:
def request_retry_wave_summary(row: dict[str, Any]) -> str: def request_retry_wave_summary(row: dict[str, Any]) -> str:
summaries = [] summaries = []
fallback_round = row.get("reasoning_retry_current_round")
for first in row.get("reasoning_retry_current_firsts") or []: for first in row.get("reasoning_retry_current_firsts") or []:
if not isinstance(first, dict): if not isinstance(first, dict):
continue continue
summary = request_retry_first_summary(first) summary = request_retry_first_summary(first, fallback_round)
if summary: if summary:
summaries.append(summary) summaries.append(summary)
return "; ".join(summaries) return "; ".join(summaries)
@@ -838,10 +939,7 @@ def request_retry_firsts_compact_text(row: dict[str, Any]) -> str:
if not isinstance(first, dict): if not isinstance(first, dict):
continue continue
delay = format_duration_ms_as_seconds(first.get("first_delay_ms")) delay = format_duration_ms_as_seconds(first.get("first_delay_ms"))
if delay == "-": parts.append(delay if delay != "-" else "?")
delay = str(first.get("outcome") or "-").strip() or "-"
slot = parse_int_value(first.get("slot"))
parts.append(f"{slot if slot is not None else '?'}:{delay}")
return " ".join(parts) return " ".join(parts)
@@ -1744,48 +1842,7 @@ def run_textual(
self.request_table_preferences = load_request_table_preferences() self.request_table_preferences = load_request_table_preferences()
self.request_table_density = "compact" self.request_table_density = "compact"
self.request_table_width_profiles = { self.request_table_width_profiles = {
"compact": { density: dict(widths) for density, widths in REQUEST_TABLE_WIDTH_PROFILES.items()
"seq": 7,
"req_id": 18,
"resp_id": 18,
"thread": 18,
"started": 19,
"status": 7,
"path": 22,
"model": 16,
"effort": 8,
"reasoning": 8,
"token_col": 8,
"req": 10,
"resp": 10,
"chunks": 14,
"first": 8,
"duration": 8,
"updated": 8,
"note": 22,
"round": 10,
},
"wide": {
"seq": 7,
"req_id": 24,
"resp_id": 24,
"thread": 36,
"started": 19,
"status": 7,
"path": 40,
"model": 24,
"effort": 8,
"reasoning": 8,
"token_col": 8,
"req": 12,
"resp": 12,
"chunks": 16,
"first": 8,
"duration": 8,
"updated": 8,
"note": 48,
"round": 10,
},
} }
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
@@ -1875,6 +1932,16 @@ def run_textual(
return False return False
return True return True
def _request_table_column_width(self, column_key: str, density: str | None = None) -> int:
normalized = normalize_request_table_preferences(self.request_table_preferences)
override = request_table_column_width_value(normalized.get("widths", {}).get(column_key))
if override is not None:
return override
active_density = density or self.request_table_density
width_profile = self.request_table_width_profiles[active_density]
column = REQUEST_TABLE_COLUMN_BY_KEY[column_key]
return width_profile[column["width_key"]]
def _request_row_key(self, row: dict[str, Any]) -> str: def _request_row_key(self, row: dict[str, Any]) -> str:
return f"{row['seq']}:{row['request_id']}" return f"{row['seq']}:{row['request_id']}"
@@ -1897,12 +1964,11 @@ def run_textual(
table = self.query_one("#requests_table", DataTable) 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 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()
width_profile = self.request_table_width_profiles[self.request_table_density]
table.clear(columns=True) table.clear(columns=True)
self.request_table_column_keys = {} self.request_table_column_keys = {}
for column_key in self._visible_request_column_keys(): for column_key in self._visible_request_column_keys():
column = REQUEST_TABLE_COLUMN_BY_KEY[column_key] column = REQUEST_TABLE_COLUMN_BY_KEY[column_key]
width = width_profile[column["width_key"]] width = self._request_table_column_width(column_key)
self.request_table_column_keys[column_key] = table.add_column( self.request_table_column_keys[column_key] = table.add_column(
column["label"], column["label"],
width=width, width=width,
@@ -2140,10 +2206,11 @@ def run_textual(
"path": width_profile["path"], "path": width_profile["path"],
"model": width_profile["model"], "model": width_profile["model"],
"effort": width_profile["effort"], "effort": width_profile["effort"],
"first": width_profile["first"],
"note": width_profile["note"], "note": width_profile["note"],
"round": width_profile["round"], "round": width_profile["round"],
} }
for name in ("req_id", "resp_id", "thread", "path", "model", "effort", "note", "round"): for name in ("req_id", "resp_id", "thread", "path", "model", "effort", "first", "note", "round"):
key = self.request_table_column_keys.get(name) key = self.request_table_column_keys.get(name)
column = table.columns.get(key) if key is not None else None column = table.columns.get(key) if key is not None else None
if column is None or not getattr(column, "width", 0): if column is None or not getattr(column, "width", 0):
@@ -2164,13 +2231,11 @@ def run_textual(
def _apply_request_table_width_profile(self, density: str) -> None: def _apply_request_table_width_profile(self, density: str) -> None:
table = self.query_one("#requests_table", DataTable) table = self.query_one("#requests_table", DataTable)
width_profile = self.request_table_width_profiles[density]
for name, key in self.request_table_column_keys.items(): for name, key in self.request_table_column_keys.items():
column = table.columns.get(key) column = table.columns.get(key)
width_key = REQUEST_TABLE_COLUMN_BY_KEY.get(name, {}).get("width_key", name) if column is None:
if column is None or width_key not in width_profile:
continue continue
column.width = width_profile[width_key] column.width = self._request_table_column_width(name, density)
self.request_table_density = density self.request_table_density = density
self._rerender_requests_after_layout_change() self._rerender_requests_after_layout_change()
self._set_status(f"request table mode {density}") self._set_status(f"request table mode {density}")
+11 -5
View File
@@ -230,6 +230,7 @@ class CodexRetryGatewayTUITests(unittest.TestCase):
"reasoning_retry_current_width": 2, "reasoning_retry_current_width": 2,
"reasoning_retry_current_firsts": [ "reasoning_retry_current_firsts": [
{ {
"round": 3,
"slot": 1, "slot": 1,
"first_response_delay_ms": 1200, "first_response_delay_ms": 1200,
"outcome": "ok", "outcome": "ok",
@@ -238,6 +239,7 @@ class CodexRetryGatewayTUITests(unittest.TestCase):
"reasoning_tokens": 516, "reasoning_tokens": 516,
}, },
{ {
"round": 3,
"slot": 2, "slot": 2,
"first_delay_ms": 1800, "first_delay_ms": 1800,
"outcome": "retry", "outcome": "retry",
@@ -254,10 +256,11 @@ class CodexRetryGatewayTUITests(unittest.TestCase):
self.assertEqual(row["reasoning_retry_current_width"], 2) self.assertEqual(row["reasoning_retry_current_width"], 2)
self.assertEqual(len(row["reasoning_retry_current_firsts"]), 2) self.assertEqual(len(row["reasoning_retry_current_firsts"]), 2)
self.assertEqual(mod.request_retry_round_text(row), "3(2)") self.assertEqual(mod.request_retry_round_text(row), "3(2)")
self.assertIn("1:1.2s", mod.request_first_text(row)) self.assertEqual(mod.request_first_text(row), "1.2s 1.8s")
self.assertIn("slot 1", mod.request_retry_wave_summary(row)) self.assertIn("round 3 slot 1", mod.request_retry_wave_summary(row))
self.assertIn("retry round 3(2)", mod.render_request_detail(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.assertIn("first 1.2s 1.8s", 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_table_preferences_round_trip(self) -> None: def test_request_table_preferences_round_trip(self) -> None:
@@ -273,12 +276,14 @@ class CodexRetryGatewayTUITests(unittest.TestCase):
{ {
"columns": ["round", "seq", "status", "unknown"], "columns": ["round", "seq", "status", "unknown"],
"hidden_columns": ["status", "unknown"], "hidden_columns": ["status", "unknown"],
"widths": {"round": 14, "status": 9, "unknown": 99},
} }
) )
self.assertEqual(saved_path, prefs_path) self.assertEqual(saved_path, prefs_path)
loaded = mod.load_request_table_preferences() loaded = mod.load_request_table_preferences()
self.assertEqual(loaded["columns"][:3], ["round", "seq", "status"]) self.assertEqual(loaded["columns"][:3], ["round", "seq", "status"])
self.assertEqual(loaded["hidden_columns"], ["status"]) self.assertEqual(loaded["hidden_columns"], ["status"])
self.assertEqual(loaded["widths"], {"round": 14, "status": 9})
def test_request_table_preferences_from_text_hides_and_reorders_columns(self) -> None: def test_request_table_preferences_from_text_hides_and_reorders_columns(self) -> None:
mod = load_module() mod = load_module()
@@ -286,9 +291,9 @@ class CodexRetryGatewayTUITests(unittest.TestCase):
"\n".join( "\n".join(
[ [
"# comment", "# comment",
"thread", "thread 24",
"seq", "seq",
"# cache", "# cache 9",
"in", "in",
"out", "out",
"", "",
@@ -297,6 +302,7 @@ class CodexRetryGatewayTUITests(unittest.TestCase):
) )
self.assertEqual(prefs["columns"][:6], ["thread", "seq", "cache", "in", "out", "req_id"]) self.assertEqual(prefs["columns"][:6], ["thread", "seq", "cache", "in", "out", "req_id"])
self.assertIn("cache", prefs["hidden_columns"]) self.assertIn("cache", prefs["hidden_columns"])
self.assertEqual(prefs["widths"], {"thread": 24, "cache": 9})
def test_request_table_preferences_expand_legacy_usage_column(self) -> None: def test_request_table_preferences_expand_legacy_usage_column(self) -> None:
mod = load_module() mod = load_module()