feat: add request table preferences
This commit is contained in:
+442
-32
@@ -29,6 +29,7 @@ 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_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"
|
||||
@@ -50,6 +51,33 @@ DEFAULT_PROFILE_ENDPOINTS = [
|
||||
"/v1/responses",
|
||||
"/v1/chat/completions",
|
||||
]
|
||||
REQUEST_TABLE_COLUMNS = (
|
||||
{"key": "seq", "label": "Seq", "width_key": "seq"},
|
||||
{"key": "req_id", "label": "Req ID", "width_key": "req_id"},
|
||||
{"key": "resp_id", "label": "Resp ID", "width_key": "resp_id"},
|
||||
{"key": "thread", "label": "Thread", "width_key": "thread"},
|
||||
{"key": "started", "label": "Started", "width_key": "started"},
|
||||
{"key": "status", "label": "Status", "width_key": "status"},
|
||||
{"key": "path", "label": "Path", "width_key": "path"},
|
||||
{"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": "req_bytes", "label": "Req Size", "width_key": "req"},
|
||||
{"key": "resp_bytes", "label": "Resp Size", "width_key": "resp"},
|
||||
{"key": "chunks", "label": "Chunks", "width_key": "chunks"},
|
||||
{"key": "first", "label": "First", "width_key": "first"},
|
||||
{"key": "duration", "label": "Duration", "width_key": "duration"},
|
||||
{"key": "updated", "label": "Updated", "width_key": "updated"},
|
||||
{"key": "note", "label": "Note", "width_key": "note"},
|
||||
{"key": "round", "label": "Round", "width_key": "round"},
|
||||
)
|
||||
REQUEST_TABLE_COLUMN_BY_KEY = {column["key"]: column for column in REQUEST_TABLE_COLUMNS}
|
||||
REQUEST_TABLE_COLUMN_KEYS = tuple(column["key"] for column in REQUEST_TABLE_COLUMNS)
|
||||
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"
|
||||
INSTALL_COMMAND = "uv tool install --force git+https://gitea.shujk.top/shujakuin/codex-retry-gateway-tui.git"
|
||||
INSTALL_COMMAND_ARGS = [
|
||||
"uv",
|
||||
@@ -148,6 +176,15 @@ def access_key_file_path() -> Path:
|
||||
return Path(os.environ.get("CODEX_RETRY_GATEWAY_TUI_ACCESS_KEY_FILE", DEFAULT_ACCESS_KEY_FILE)).expanduser()
|
||||
|
||||
|
||||
def request_table_preferences_file_path() -> Path:
|
||||
return Path(
|
||||
os.environ.get(
|
||||
"CODEX_RETRY_GATEWAY_TUI_REQUEST_TABLE_PREFERENCES_FILE",
|
||||
DEFAULT_REQUEST_TABLE_PREFERENCES_FILE,
|
||||
)
|
||||
).expanduser()
|
||||
|
||||
|
||||
def default_status_url() -> str:
|
||||
return configured_url(
|
||||
("CODEX_RETRY_GATEWAY_TUI_STATUS_URL",),
|
||||
@@ -164,6 +201,57 @@ 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 = []
|
||||
seen: set[str] = set()
|
||||
for column_key in REQUEST_TABLE_COLUMN_KEYS:
|
||||
if column_key not in requested or column_key in seen:
|
||||
continue
|
||||
visible.append(column_key)
|
||||
seen.add(column_key)
|
||||
return visible or list(DEFAULT_REQUEST_TABLE_VISIBLE_COLUMNS)
|
||||
|
||||
|
||||
def normalize_request_table_sort_column(value: Any) -> str:
|
||||
column_key = str(value or "").strip()
|
||||
if column_key in REQUEST_TABLE_COLUMN_KEY_SET:
|
||||
return column_key
|
||||
return DEFAULT_REQUEST_TABLE_SORT_COLUMN
|
||||
|
||||
|
||||
def normalize_request_table_preferences(data: Any) -> dict[str, Any]:
|
||||
source = data if isinstance(data, dict) else {}
|
||||
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),
|
||||
}
|
||||
|
||||
|
||||
def load_request_table_preferences() -> dict[str, Any]:
|
||||
path = request_table_preferences_file_path()
|
||||
return normalize_request_table_preferences(read_json_object(str(path)))
|
||||
|
||||
|
||||
def write_request_table_preferences(preferences: dict[str, Any]) -> Path:
|
||||
path = request_table_preferences_file_path()
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
try:
|
||||
path.parent.chmod(0o700)
|
||||
except OSError:
|
||||
pass
|
||||
normalized = normalize_request_table_preferences(preferences)
|
||||
path.write_text(json.dumps(normalized, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||
try:
|
||||
path.chmod(0o600)
|
||||
except OSError:
|
||||
pass
|
||||
return path
|
||||
|
||||
|
||||
def normalize_gateway_url(api_url: str) -> str:
|
||||
value = str(api_url or "").strip()
|
||||
if not value:
|
||||
@@ -476,6 +564,82 @@ def request_effort_text(row: dict[str, Any]) -> str:
|
||||
return str(row.get("reasoning_effort") or "").strip() or "-"
|
||||
|
||||
|
||||
def request_model_text(row: dict[str, Any]) -> str:
|
||||
return str(row.get("model") or row.get("requested_model") or row.get("forwarded_model") or "").strip()
|
||||
|
||||
|
||||
def normalize_request_retry_firsts(value: Any) -> list[dict[str, Any]]:
|
||||
if not isinstance(value, (list, tuple)):
|
||||
return []
|
||||
firsts = []
|
||||
for item in value:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
firsts.append(
|
||||
{
|
||||
"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"))),
|
||||
"outcome": str(item.get("outcome") or "").strip(),
|
||||
"status_code": item.get("status_code", item.get("upstream_status_code", item.get("status"))),
|
||||
"reasoning_effort": str(item.get("reasoning_effort") or "").strip(),
|
||||
"reasoning_tokens": item.get("reasoning_tokens"),
|
||||
"reasoning": str(item.get("reasoning") or item.get("reason") or item.get("error") or "").strip(),
|
||||
}
|
||||
)
|
||||
return firsts
|
||||
|
||||
|
||||
def request_retry_round_text(row: dict[str, Any]) -> str:
|
||||
round_number = parse_int_value(row.get("reasoning_retry_current_round"))
|
||||
width = parse_int_value(row.get("reasoning_retry_current_width"))
|
||||
if round_number is None and width is None:
|
||||
return "-"
|
||||
if round_number is None:
|
||||
return f"-({max(0, width or 0)})"
|
||||
if width is None:
|
||||
return str(round_number)
|
||||
return f"{round_number}({max(0, width)})"
|
||||
|
||||
|
||||
def request_retry_first_summary(first: dict[str, Any]) -> str:
|
||||
bits = []
|
||||
slot = parse_int_value(first.get("slot"))
|
||||
if slot is not None:
|
||||
bits.append(f"slot {slot}")
|
||||
first_delay = format_duration_ms_as_seconds(first.get("first_delay_ms"))
|
||||
if first_delay != "-":
|
||||
bits.append(f"first {first_delay}")
|
||||
outcome = str(first.get("outcome") or "").strip()
|
||||
status_code = first.get("status_code")
|
||||
status_bits = [bit for bit in (outcome, str(status_code).strip() if status_code not in (None, "") else "") if bit]
|
||||
if status_bits:
|
||||
bits.append(f"status {'/'.join(status_bits)}")
|
||||
reasoning_bits = []
|
||||
if first.get("reasoning_effort"):
|
||||
reasoning_bits.append(str(first["reasoning_effort"]))
|
||||
reasoning_tokens = parse_int_value(first.get("reasoning_tokens"))
|
||||
if reasoning_tokens is not None:
|
||||
reasoning_bits.append(format_count(reasoning_tokens))
|
||||
reasoning = str(first.get("reasoning") or "").strip()
|
||||
if reasoning_bits or reasoning:
|
||||
detail = "/".join(reasoning_bits)
|
||||
if reasoning:
|
||||
detail = f"{detail} {short_text(reasoning, 40)}".strip()
|
||||
bits.append(f"reasoning {detail}".strip())
|
||||
return " ".join(bits)
|
||||
|
||||
|
||||
def request_retry_wave_summary(row: dict[str, Any]) -> str:
|
||||
summaries = []
|
||||
for first in row.get("reasoning_retry_current_firsts") or []:
|
||||
if not isinstance(first, dict):
|
||||
continue
|
||||
summary = request_retry_first_summary(first)
|
||||
if summary:
|
||||
summaries.append(summary)
|
||||
return "; ".join(summaries)
|
||||
|
||||
|
||||
def request_retry_note(row: dict[str, Any]) -> str:
|
||||
attempts = as_int(row.get("upstream_attempt_count"))
|
||||
if attempts <= 1:
|
||||
@@ -499,7 +663,7 @@ def request_row_cells(row: dict[str, Any], text_limits: dict[str, int] | None =
|
||||
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)),
|
||||
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)),
|
||||
@@ -510,9 +674,96 @@ def request_row_cells(row: dict[str, Any], text_limits: dict[str, int] | None =
|
||||
format_duration_ms_as_seconds(row["duration_ms"]),
|
||||
request_updated_elapsed(row),
|
||||
short_text(retry_note, note_width),
|
||||
short_text(request_retry_round_text(row), limits.get("round", 10)),
|
||||
)
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
def request_sort_value(row: dict[str, Any], column_key: str) -> Any:
|
||||
if column_key == "seq":
|
||||
return parse_int_value(row.get("seq"))
|
||||
if column_key == "req_id":
|
||||
text = request_id_text(row)
|
||||
return None if text == "-" else text.lower()
|
||||
if column_key == "resp_id":
|
||||
text = response_id_text(row)
|
||||
return None if text == "-" else text.lower()
|
||||
if column_key == "thread":
|
||||
text = str(row.get("thread_id") or "").strip()
|
||||
return text.lower() if text else None
|
||||
if column_key == "started":
|
||||
started = parse_datetime(row.get("started_at"))
|
||||
return started.timestamp() if started else None
|
||||
if column_key == "status":
|
||||
return parse_int_value(row.get("status_code"))
|
||||
if column_key == "path":
|
||||
text = str(row.get("path") or "").strip()
|
||||
return text.lower() if text else None
|
||||
if column_key == "model":
|
||||
text = request_model_text(row)
|
||||
return text.lower() if text else None
|
||||
if column_key == "effort":
|
||||
text = str(row.get("reasoning_effort") or "").strip()
|
||||
return text.lower() if text else None
|
||||
if column_key == "reasoning":
|
||||
return parse_int_value(row.get("reasoning_tokens"))
|
||||
if column_key == "usage":
|
||||
total_tokens = parse_int_value(row.get("total_tokens"))
|
||||
if total_tokens is not None:
|
||||
return total_tokens
|
||||
input_tokens = parse_int_value(row.get("input_tokens")) or 0
|
||||
output_tokens = parse_int_value(row.get("output_tokens")) or 0
|
||||
cached_tokens = parse_int_value(row.get("cached_tokens")) or 0
|
||||
combined = input_tokens + output_tokens + cached_tokens
|
||||
return combined if combined else None
|
||||
if column_key == "req_bytes":
|
||||
return parse_int_value(row.get("request_body_bytes"))
|
||||
if column_key == "resp_bytes":
|
||||
return parse_int_value(row.get("response_bytes_received"))
|
||||
if column_key == "chunks":
|
||||
return parse_int_value(row.get("stream_chunk_count"))
|
||||
if column_key == "first":
|
||||
return parse_int_value(row.get("first_response_delay_ms"))
|
||||
if column_key == "duration":
|
||||
return parse_int_value(row.get("duration_ms"))
|
||||
if column_key == "updated":
|
||||
updated = parse_datetime(request_updated_at(row))
|
||||
return updated.timestamp() if updated else None
|
||||
if column_key == "note":
|
||||
text = request_retry_note(row)
|
||||
return text.lower() if text else None
|
||||
if column_key == "round":
|
||||
round_number = parse_int_value(row.get("reasoning_retry_current_round"))
|
||||
width = parse_int_value(row.get("reasoning_retry_current_width"))
|
||||
if round_number is None and width is None:
|
||||
return None
|
||||
return (round_number or 0, width or 0)
|
||||
return None
|
||||
|
||||
|
||||
def sort_request_rows(
|
||||
rows: list[dict[str, Any]],
|
||||
sort_column: str = DEFAULT_REQUEST_TABLE_SORT_COLUMN,
|
||||
*,
|
||||
reverse: bool = True,
|
||||
) -> list[dict[str, Any]]:
|
||||
column_key = normalize_request_table_sort_column(sort_column)
|
||||
known: list[tuple[Any, dict[str, Any]]] = []
|
||||
missing: list[dict[str, Any]] = []
|
||||
for row in rows:
|
||||
value = request_sort_value(row, column_key)
|
||||
if value is None or value == "":
|
||||
missing.append(row)
|
||||
continue
|
||||
known.append((value, row))
|
||||
known.sort(key=lambda item: item[0], reverse=reverse)
|
||||
return [row for _, row in known] + missing
|
||||
|
||||
|
||||
def status_kind(value: Any) -> str:
|
||||
text = str(value or "").strip().lower()
|
||||
if text in {"ok", "operational", "success"}:
|
||||
@@ -715,8 +966,12 @@ def request_match_score(row: dict[str, Any], needle: str) -> bool:
|
||||
"output_tokens",
|
||||
"total_tokens",
|
||||
"cached_tokens",
|
||||
"reasoning_retry_current_round",
|
||||
"reasoning_retry_current_width",
|
||||
)
|
||||
).lower()
|
||||
current_firsts = json.dumps(row.get("reasoning_retry_current_firsts") or [], ensure_ascii=False).lower()
|
||||
haystack = f"{haystack} {current_firsts}"
|
||||
return needle in haystack
|
||||
|
||||
|
||||
@@ -780,6 +1035,9 @@ def normalize_request_rows(payload: dict[str, Any], filter_text: str = "") -> li
|
||||
"response_bytes_received": entry.get("response_bytes_received"),
|
||||
"stream_chunk_count": entry.get("stream_chunk_count"),
|
||||
"first_response_delay_ms": entry.get("first_response_delay_ms"),
|
||||
"reasoning_retry_current_round": parse_int_value(entry.get("reasoning_retry_current_round")),
|
||||
"reasoning_retry_current_width": parse_int_value(entry.get("reasoning_retry_current_width")),
|
||||
"reasoning_retry_current_firsts": normalize_request_retry_firsts(entry.get("reasoning_retry_current_firsts")),
|
||||
"duration_ms": entry.get("duration_ms"),
|
||||
"started_at": entry.get("started_at"),
|
||||
"finished_at": entry.get("finished_at"),
|
||||
@@ -818,6 +1076,9 @@ def render_request_detail(row: dict[str, Any]) -> str:
|
||||
f"finished {short_time(row['finished_at'])}",
|
||||
f"updated {request_updated_elapsed(row)}",
|
||||
]
|
||||
retry_round = request_retry_round_text(row)
|
||||
if retry_round != "-":
|
||||
bits.append(f"retry round {retry_round}")
|
||||
if row.get("model"):
|
||||
bits.append(f"model {row['model']}")
|
||||
if row.get("requested_model") and row.get("requested_model") != row.get("model"):
|
||||
@@ -840,6 +1101,9 @@ def render_request_detail(row: dict[str, Any]) -> str:
|
||||
bits.append(f"upstream path {row['upstream_path']}")
|
||||
if row.get("upstream_auth_mode"):
|
||||
bits.append(f"auth {row['upstream_auth_mode']}/{row.get('upstream_auth_source') or '-'}")
|
||||
retry_wave = request_retry_wave_summary(row)
|
||||
if retry_wave:
|
||||
bits.append(f"current wave {retry_wave}")
|
||||
return " | ".join(bits)
|
||||
|
||||
|
||||
@@ -1112,6 +1376,8 @@ 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 = {
|
||||
@@ -1143,7 +1409,11 @@ def fetch_dashboard_snapshot(
|
||||
"payload": payload,
|
||||
"status_payload": payload,
|
||||
"status_error": " | ".join(errors),
|
||||
"requests": normalize_request_rows(requests_source, filter_text),
|
||||
"requests": sort_request_rows(
|
||||
normalize_request_rows(requests_source, filter_text),
|
||||
request_sort_column,
|
||||
reverse=request_sort_reverse,
|
||||
),
|
||||
"logs": normalize_log_rows(logs_source),
|
||||
"profiles": normalize_profile_rows(
|
||||
profiles_source,
|
||||
@@ -1199,6 +1469,11 @@ def run_textual(
|
||||
("3", "show_logs", "Logs"),
|
||||
("4", "show_profiles", "Profiles"),
|
||||
("m", "toggle_request_table_density", "Wide/Compact"),
|
||||
(",", "select_previous_request_column", "Prev Column"),
|
||||
(".", "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"),
|
||||
("shift+left", "scroll_table_left", "Scroll Left"),
|
||||
("shift+right", "scroll_table_right", "Scroll Right"),
|
||||
("[", "narrow_usage_column", "Narrow Usage"),
|
||||
@@ -1225,7 +1500,7 @@ def run_textual(
|
||||
self.log_by_key: dict[str, dict[str, Any]] = {}
|
||||
self.current_view = "overview"
|
||||
self.filter_mode = "requests"
|
||||
self.last_request_id = ""
|
||||
self.last_request_key = ""
|
||||
self.loading = False
|
||||
self.executor = ThreadPoolExecutor(max_workers=1)
|
||||
self.refresh_generation = 0
|
||||
@@ -1237,6 +1512,10 @@ def run_textual(
|
||||
"profiles": "profiles",
|
||||
}
|
||||
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": {
|
||||
@@ -1258,6 +1537,7 @@ def run_textual(
|
||||
"duration": 8,
|
||||
"updated": 8,
|
||||
"note": 22,
|
||||
"round": 10,
|
||||
},
|
||||
"wide": {
|
||||
"seq": 7,
|
||||
@@ -1278,6 +1558,7 @@ def run_textual(
|
||||
"duration": 8,
|
||||
"updated": 8,
|
||||
"note": 48,
|
||||
"round": 10,
|
||||
},
|
||||
}
|
||||
self.request_usage_width = self.request_table_width_profiles[self.request_table_density]["usage"]
|
||||
@@ -1300,27 +1581,7 @@ def run_textual(
|
||||
requests = self.query_one("#requests_table", DataTable)
|
||||
requests.cursor_type = "row"
|
||||
requests.zebra_stripes = True
|
||||
width_profile = self.request_table_width_profiles[self.request_table_density]
|
||||
self.request_table_column_keys = {
|
||||
"seq": requests.add_column("Seq", width=width_profile["seq"], key="seq"),
|
||||
"req_id": requests.add_column("Req ID", width=width_profile["req_id"], key="req_id"),
|
||||
"resp_id": requests.add_column("Resp ID", width=width_profile["resp_id"], key="resp_id"),
|
||||
"thread": requests.add_column("Thread", width=width_profile["thread"], key="thread"),
|
||||
"started": requests.add_column("Started", width=width_profile["started"], key="started"),
|
||||
"status": requests.add_column("Status", width=width_profile["status"], key="status"),
|
||||
"path": requests.add_column("Path", width=width_profile["path"], key="path"),
|
||||
"model": requests.add_column("Model", width=width_profile["model"], key="model"),
|
||||
"effort": requests.add_column("Effort", width=width_profile["effort"], key="effort"),
|
||||
"reasoning": requests.add_column("R.Tok", width=width_profile["reasoning"], key="reasoning"),
|
||||
"usage": requests.add_column("Usage", width=self.request_usage_width, key="usage"),
|
||||
"req_bytes": requests.add_column("Req Size", width=width_profile["req"], key="req_bytes"),
|
||||
"resp_bytes": requests.add_column("Resp Size", width=width_profile["resp"], key="resp_bytes"),
|
||||
"chunks": requests.add_column("Chunks", width=width_profile["chunks"], key="chunks"),
|
||||
"first": requests.add_column("First", width=width_profile["first"], key="first"),
|
||||
"duration": requests.add_column("Duration", width=width_profile["duration"], key="duration"),
|
||||
"updated": requests.add_column("Updated", width=width_profile["updated"], key="updated"),
|
||||
"note": requests.add_column("Note", width=width_profile["note"], key="note"),
|
||||
}
|
||||
self._rebuild_request_table_columns()
|
||||
|
||||
logs = self.query_one("#logs_table", DataTable)
|
||||
logs.cursor_type = "row"
|
||||
@@ -1346,7 +1607,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 | [ ] 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 | , . 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"
|
||||
)
|
||||
self._focus_table()
|
||||
self._update_table_headers()
|
||||
@@ -1361,12 +1622,82 @@ def run_textual(
|
||||
self.query_one("#paths", Static).update(state_summary(self.payload))
|
||||
|
||||
def _update_table_headers(self) -> None:
|
||||
counts = f"requests {len(self.requests)} | logs {len(self.logs)} | profiles {len(self.profiles)}"
|
||||
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"
|
||||
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}"
|
||||
)
|
||||
self.query_one("#tables", Static).update(counts)
|
||||
|
||||
def _current_filter(self) -> str:
|
||||
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"))
|
||||
|
||||
def _request_table_actions_enabled(self) -> bool:
|
||||
if self.visible_tables.get(self.current_view, "requests") == "requests":
|
||||
return True
|
||||
self._set_status("switch to requests or overview first")
|
||||
return False
|
||||
|
||||
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:
|
||||
self._set_status(f"save request table prefs error: {exc}")
|
||||
return False
|
||||
return True
|
||||
|
||||
def _request_row_key(self, row: dict[str, Any]) -> str:
|
||||
return f"{row['seq']}:{row['request_id']}"
|
||||
|
||||
def _selected_request_key(self) -> str:
|
||||
row = self._selected_request_row()
|
||||
if row:
|
||||
return self._request_row_key(row)
|
||||
return self.last_request_key
|
||||
|
||||
def _select_request_row(self, row_key: str) -> bool:
|
||||
if not row_key:
|
||||
return False
|
||||
table = self.query_one("#requests_table", DataTable)
|
||||
for index, row in enumerate(self.requests):
|
||||
if self._request_row_key(row) == row_key:
|
||||
return self._select_table_row(table, index)
|
||||
return False
|
||||
|
||||
def _rebuild_request_table_columns(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
|
||||
selected_row_key = self._selected_request_key()
|
||||
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"]]
|
||||
self.request_table_column_keys[column_key] = table.add_column(
|
||||
column["label"],
|
||||
width=width,
|
||||
key=column_key,
|
||||
)
|
||||
if self.requests:
|
||||
self.render_requests()
|
||||
if not self._select_request_row(selected_row_key):
|
||||
self._select_table_row(table, min(selected_row_index, len(self.requests) - 1))
|
||||
else:
|
||||
self.request_by_key = {}
|
||||
|
||||
def _current_table_widget(self) -> DataTable | None:
|
||||
table_id = self.visible_tables.get(self.current_view, "requests")
|
||||
if table_id == "logs":
|
||||
@@ -1437,6 +1768,8 @@ 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)
|
||||
@@ -1446,6 +1779,7 @@ def run_textual(
|
||||
def _apply_refresh_result(self, result: dict[str, Any]) -> None:
|
||||
if result.get("generation") != self.refresh_generation:
|
||||
return
|
||||
selected_request_key = self._selected_request_key()
|
||||
self.payload = result["payload"]
|
||||
self.status_payload = result["status_payload"]
|
||||
self.status_error = result["status_error"]
|
||||
@@ -1453,6 +1787,7 @@ def run_textual(
|
||||
self.logs = result["logs"]
|
||||
self.profiles = result["profiles"]
|
||||
self.active_profile_name = result.get("active_profile_name") or ""
|
||||
self.last_request_key = selected_request_key
|
||||
self.loading = False
|
||||
self.render_all()
|
||||
if self.current_view == "profiles":
|
||||
@@ -1495,15 +1830,19 @@ def run_textual(
|
||||
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
|
||||
selected_row_key = self._selected_request_key()
|
||||
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']}"
|
||||
key = self._request_row_key(row)
|
||||
self.request_by_key[key] = row
|
||||
table.add_row(*request_row_cells(row, text_limits), key=key)
|
||||
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()]
|
||||
table.add_row(*visible_cells, key=key)
|
||||
if self.requests:
|
||||
self._select_table_row(table, min(selected_row_index, len(self.requests) - 1))
|
||||
if not self._select_request_row(selected_row_key):
|
||||
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)
|
||||
@@ -1600,8 +1939,9 @@ def run_textual(
|
||||
"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"):
|
||||
for name in ("req_id", "resp_id", "thread", "path", "model", "effort", "usage", "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):
|
||||
@@ -1611,6 +1951,7 @@ def run_textual(
|
||||
|
||||
def _rerender_requests_after_layout_change(self) -> None:
|
||||
self.render_requests()
|
||||
self._update_table_headers()
|
||||
if self.current_view not in {"overview", "requests"}:
|
||||
return
|
||||
row = self._selected_request_row()
|
||||
@@ -1624,9 +1965,10 @@ def run_textual(
|
||||
width_profile = self.request_table_width_profiles[density]
|
||||
for name, key in self.request_table_column_keys.items():
|
||||
column = table.columns.get(key)
|
||||
if column is None or name not in width_profile:
|
||||
width_key = REQUEST_TABLE_COLUMN_BY_KEY.get(name, {}).get("width_key", name)
|
||||
if column is None or width_key not in width_profile:
|
||||
continue
|
||||
column.width = width_profile[name]
|
||||
column.width = width_profile[width_key]
|
||||
self.request_table_density = density
|
||||
self.request_usage_width = width_profile["usage"]
|
||||
self._rerender_requests_after_layout_change()
|
||||
@@ -1636,6 +1978,73 @@ 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
|
||||
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")
|
||||
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)
|
||||
|
||||
@@ -1742,6 +2151,7 @@ def run_textual(
|
||||
def on_data_table_row_highlighted(self, event: DataTable.RowHighlighted) -> None:
|
||||
if event.data_table.id == "requests_table":
|
||||
key = str(event.row_key.value)
|
||||
self.last_request_key = key
|
||||
row = self.request_by_key.get(key)
|
||||
if row:
|
||||
self.render_detail(row, kind="request")
|
||||
|
||||
Reference in New Issue
Block a user