Mike0021 commited on
Commit
66fc3e7
·
verified ·
1 Parent(s): 44b69ec

Restore viewer app on Python 3.11

Browse files
Files changed (1) hide show
  1. app.py +1413 -6
app.py CHANGED
@@ -1,17 +1,1424 @@
1
  import os
2
 
 
 
 
3
  os.environ.setdefault("GRADIO_ANALYTICS_ENABLED", "False")
4
  os.environ.setdefault("GRADIO_SSR_MODE", "false")
 
 
 
 
 
5
 
6
- print("probe startup: before gradio import", flush=True)
 
 
 
 
 
 
 
 
 
7
 
8
  import gradio as gr
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
 
10
- print("probe startup: after gradio import", flush=True)
11
 
12
- with gr.Blocks(title="Codex Traces Probe") as demo:
13
- gr.Markdown("# Codex Traces Probe\n\nRuntime startup check.")
14
- out = gr.Textbox(value="ready", label="Status")
15
 
16
- print("probe startup: launching", flush=True)
17
  demo.launch(show_error=True)
 
1
  import os
2
 
3
+ os.environ.setdefault("HF_HOME", "/tmp/.cache/huggingface")
4
+ os.environ.setdefault("HF_MODULES_CACHE", "/tmp/hf_modules")
5
+ os.environ.setdefault("MPLCONFIGDIR", "/tmp/matplotlib")
6
  os.environ.setdefault("GRADIO_ANALYTICS_ENABLED", "False")
7
  os.environ.setdefault("GRADIO_SSR_MODE", "false")
8
+ if not os.environ.get("HF_TOKEN"):
9
+ for _token_key in ("HUGGING_FACE_HUB_TOKEN", "HUGGINGFACE_HUB_TOKEN"):
10
+ if os.environ.get(_token_key):
11
+ os.environ["HF_TOKEN"] = os.environ[_token_key]
12
+ break
13
 
14
+ print("codex-traces startup: env configured", flush=True)
15
+
16
+ import html
17
+ import json
18
+ import math
19
+ import re
20
+ from datetime import datetime, timezone
21
+ from functools import lru_cache
22
+ from pathlib import Path
23
+ from typing import Any
24
 
25
  import gradio as gr
26
+ from huggingface_hub import hf_hub_download, list_repo_files
27
+
28
+ print("codex-traces startup: imports complete", flush=True)
29
+
30
+
31
+ APP_TITLE = "Codex Traces"
32
+ APP_REVISION = "parser-v2"
33
+ DATASET_REPO = "Mike0021/codex-sessions"
34
+ REPO_TYPE = "dataset"
35
+ FILE_SUFFIX = "_rollout.jsonl"
36
+ SESSION_PATH_PREFIX = "sessions/rollout-"
37
+ SESSION_PATH_SUFFIX = ".jsonl"
38
+ PAGE_SIZE = 120
39
+ MAX_OUTPUT_CHARS = 22000
40
+ MAX_MESSAGE_CHARS = 30000
41
+ KNOWN_SESSIONS = (
42
+ "agents-a1-demo",
43
+ "ai-agent-soccer",
44
+ "anime-soccer-generator",
45
+ "asasr-space",
46
+ "codex-traces-viewer",
47
+ "craft-agents-oss",
48
+ "deepspec-space",
49
+ "edit-anything",
50
+ "fractal-pi-extension",
51
+ "gaussian-splat-demo",
52
+ "hf-history-article",
53
+ "hf-motion-video",
54
+ "horus-hiero-space",
55
+ "locate-anything-space",
56
+ "ltx-3dreal-space",
57
+ "microworld-space",
58
+ "olmoearth-demo",
59
+ "paris-13-landing",
60
+ "pulpie-demo",
61
+ "pulpie-gguf",
62
+ "pulpie-mlx",
63
+ "pulpie-onnx",
64
+ "pulpie-web-demo",
65
+ "qwen3-asr-space",
66
+ "rampart-demo",
67
+ "sync-lora-space",
68
+ "tabfm-arena",
69
+ )
70
+
71
+ ANSI_RE = re.compile(r"\x1b\[[0-9;?]*[ -/]*[@-~]")
72
+ TIMESTAMP_KEYS = (
73
+ "timestamp",
74
+ "time",
75
+ "created_at",
76
+ "updated_at",
77
+ "started_at",
78
+ "completed_at",
79
+ )
80
+
81
+
82
+ def _hub_token() -> str | None:
83
+ return (
84
+ os.environ.get("HF_TOKEN")
85
+ or os.environ.get("HUGGING_FACE_HUB_TOKEN")
86
+ or os.environ.get("HUGGINGFACE_HUB_TOKEN")
87
+ or None
88
+ )
89
+
90
+
91
+ def _display_name(file_name: str) -> str:
92
+ if file_name.startswith(SESSION_PATH_PREFIX) and file_name.endswith(SESSION_PATH_SUFFIX):
93
+ return file_name[len(SESSION_PATH_PREFIX) : -len(SESSION_PATH_SUFFIX)]
94
+ return file_name[: -len(FILE_SUFFIX)] if file_name.endswith(FILE_SUFFIX) else file_name
95
+
96
+
97
+ def _is_rollout_file(file_name: str) -> bool:
98
+ return (
99
+ file_name.startswith(SESSION_PATH_PREFIX)
100
+ and file_name.endswith(SESSION_PATH_SUFFIX)
101
+ ) or file_name.endswith(FILE_SUFFIX)
102
+
103
+
104
+ def _file_candidates(session_name: str | None) -> list[str]:
105
+ if not session_name:
106
+ return []
107
+ name = str(session_name).strip()
108
+ if name.endswith(".jsonl"):
109
+ return [name]
110
+
111
+ candidates = [
112
+ f"{SESSION_PATH_PREFIX}{name}{SESSION_PATH_SUFFIX}",
113
+ f"{name}{FILE_SUFFIX}",
114
+ ]
115
+ return list(dict.fromkeys(candidates))
116
+
117
+
118
+ def _fmt_number(value: int | None) -> str:
119
+ return f"{int(value or 0):,}"
120
+
121
+
122
+ def _fmt_bytes(value: int | None) -> str:
123
+ value = int(value or 0)
124
+ units = ("B", "KB", "MB", "GB")
125
+ size = float(value)
126
+ for unit in units:
127
+ if size < 1024 or unit == units[-1]:
128
+ return f"{size:.1f} {unit}" if unit != "B" else f"{value} B"
129
+ size /= 1024
130
+ return f"{value} B"
131
+
132
+
133
+ def _strip_ansi(text: Any) -> str:
134
+ return ANSI_RE.sub("", str(text or ""))
135
+
136
+
137
+ def _escape(text: Any) -> str:
138
+ return html.escape(str(text or ""), quote=True)
139
+
140
+
141
+ def _limit_text(text: str, limit: int, label: str) -> tuple[str, str]:
142
+ if len(text) <= limit:
143
+ return text, ""
144
+ hidden = len(text) - limit
145
+ note = f"\n\n[{label} truncated by {hidden:,} characters for browser responsiveness.]"
146
+ return text[:limit] + note, f" truncated {hidden:,} chars"
147
+
148
+
149
+ def _parse_time(value: Any) -> datetime | None:
150
+ if value is None:
151
+ return None
152
+ if isinstance(value, (int, float)):
153
+ seconds = float(value) / 1000 if value > 10_000_000_000 else float(value)
154
+ try:
155
+ return datetime.fromtimestamp(seconds, tz=timezone.utc)
156
+ except (OSError, OverflowError, ValueError):
157
+ return None
158
+ if isinstance(value, str):
159
+ raw = value.strip()
160
+ if not raw:
161
+ return None
162
+ try:
163
+ if raw.endswith("Z"):
164
+ raw = raw[:-1] + "+00:00"
165
+ parsed = datetime.fromisoformat(raw)
166
+ return parsed if parsed.tzinfo else parsed.replace(tzinfo=timezone.utc)
167
+ except ValueError:
168
+ return None
169
+ return None
170
+
171
+
172
+ def _duration_label(timestamps: list[datetime]) -> str:
173
+ if len(timestamps) < 2:
174
+ return "not recorded"
175
+ delta = max(timestamps) - min(timestamps)
176
+ seconds = max(0, int(delta.total_seconds()))
177
+ if seconds < 60:
178
+ return f"{seconds}s"
179
+ minutes, sec = divmod(seconds, 60)
180
+ if minutes < 60:
181
+ return f"{minutes}m {sec}s"
182
+ hours, minutes = divmod(minutes, 60)
183
+ return f"{hours}h {minutes}m"
184
+
185
+
186
+ @lru_cache(maxsize=1)
187
+ def _list_sessions_cached() -> tuple[tuple[str, ...], str]:
188
+ try:
189
+ files = list_repo_files(DATASET_REPO, repo_type=REPO_TYPE, token=_hub_token())
190
+ except Exception as exc:
191
+ return (), f"{type(exc).__name__}: {exc}"
192
+ sessions = tuple(sorted({_display_name(f) for f in files if _is_rollout_file(f)}))
193
+ if not sessions:
194
+ return (), "No *_rollout.jsonl files were found in the dataset."
195
+ return sessions, ""
196
+
197
+
198
+ def _session_choices(live: bool = True) -> list[str]:
199
+ if not live:
200
+ return list(KNOWN_SESSIONS)
201
+ sessions, _ = _list_sessions_cached()
202
+ return list(sessions or KNOWN_SESSIONS)
203
+
204
+
205
+ def _default_session() -> str | None:
206
+ choices = _session_choices(live=False)
207
+ return choices[0] if choices else None
208
+
209
+
210
+ def _download_session(file_name: str) -> Path:
211
+ return Path(
212
+ hf_hub_download(
213
+ repo_id=DATASET_REPO,
214
+ filename=file_name,
215
+ repo_type=REPO_TYPE,
216
+ token=_hub_token(),
217
+ )
218
+ )
219
+
220
+
221
+ def _command_status(item: dict[str, Any]) -> tuple[str, str]:
222
+ status = str(item.get("status") or "").lower()
223
+ exit_code = item.get("exit_code")
224
+ if status in {"failed", "error"} or (exit_code not in (None, 0)):
225
+ return "failed", "fail"
226
+ if status in {"completed", "succeeded", "success"}:
227
+ return "success", "success"
228
+ if status in {"in_progress", "running"}:
229
+ return "running", "running"
230
+ return status or "unknown", "neutral"
231
+
232
+
233
+ def _usage_total(usage: dict[str, Any]) -> int:
234
+ return int(usage.get("input_tokens") or 0) + int(usage.get("output_tokens") or 0)
235
+
236
+
237
+ def _collect_timestamps(obj: dict[str, Any], timestamps: list[datetime]) -> None:
238
+ for key in TIMESTAMP_KEYS:
239
+ parsed = _parse_time(obj.get(key))
240
+ if parsed:
241
+ timestamps.append(parsed)
242
+ item = obj.get("item")
243
+ if isinstance(item, dict):
244
+ for key in TIMESTAMP_KEYS:
245
+ parsed = _parse_time(item.get(key))
246
+ if parsed:
247
+ timestamps.append(parsed)
248
+
249
+
250
+ def _payload_command(payload: dict[str, Any]) -> str:
251
+ name = str(payload.get("name") or "function_call")
252
+ arguments = payload.get("arguments")
253
+ if isinstance(arguments, dict):
254
+ command = arguments.get("command")
255
+ if command:
256
+ return str(command)
257
+ return f"{name}({json.dumps(arguments, ensure_ascii=True)})"
258
+ if arguments:
259
+ return f"{name}({arguments})"
260
+ return name
261
+
262
+
263
+ @lru_cache(maxsize=6)
264
+ def _load_session(file_name: str) -> dict[str, Any]:
265
+ path = _download_session(file_name)
266
+ events: list[dict[str, Any]] = []
267
+ timestamps: list[datetime] = []
268
+ pending_commands: list[str] = []
269
+ command_ids: set[str] = set()
270
+ failed_commands = 0
271
+ turn_completed = 0
272
+ turn_started = 0
273
+ parse_errors = 0
274
+ usage_summary = {
275
+ "input_tokens": 0,
276
+ "cached_input_tokens": 0,
277
+ "output_tokens": 0,
278
+ "reasoning_output_tokens": 0,
279
+ }
280
+
281
+ with path.open("r", encoding="utf-8", errors="replace") as handle:
282
+ for line_no, raw_line in enumerate(handle, start=1):
283
+ line = raw_line.strip()
284
+ if not line.startswith("{"):
285
+ parse_errors += 1
286
+ continue
287
+ try:
288
+ obj = json.loads(line)
289
+ except json.JSONDecodeError:
290
+ parse_errors += 1
291
+ continue
292
+
293
+ obj_type = obj.get("type")
294
+ payload = obj.get("payload") if isinstance(obj.get("payload"), dict) else {}
295
+ payload_type = payload.get("type")
296
+ item = obj.get("item") if isinstance(obj.get("item"), dict) else {}
297
+ item_type = item.get("type")
298
+ _collect_timestamps(obj, timestamps)
299
+
300
+ if obj_type == "turn_context":
301
+ turn_started += 1
302
+ continue
303
+
304
+ if obj_type == "event_msg":
305
+ if payload_type in {"agent_message", "user_message"}:
306
+ text = str(payload.get("message") or "")
307
+ is_user = payload_type == "user_message"
308
+ events.append(
309
+ {
310
+ "kind": "user" if is_user else "agent",
311
+ "role": "USER" if is_user else "AGENT",
312
+ "line": line_no,
313
+ "id": f"line-{line_no}",
314
+ "text": text,
315
+ "search": text,
316
+ }
317
+ )
318
+ continue
319
+
320
+ if payload_type == "token_count":
321
+ turn_completed += 1
322
+ usage = {key: int(payload.get(key) or 0) for key in usage_summary}
323
+ for key, value in usage.items():
324
+ usage_summary[key] += value
325
+ events.append(
326
+ {
327
+ "kind": "usage",
328
+ "line": line_no,
329
+ "usage": usage,
330
+ "search": " ".join(
331
+ [
332
+ "turn completed usage tokens",
333
+ " ".join(f"{k} {v}" for k, v in usage.items()),
334
+ ]
335
+ ),
336
+ }
337
+ )
338
+ continue
339
+
340
+ if payload_type == "item_completed" and payload.get("item_type") == "todo_list":
341
+ todos = payload.get("todos") or payload.get("items") or []
342
+ if isinstance(todos, list):
343
+ text = " ".join(str(todo.get("text") or "") for todo in todos if isinstance(todo, dict))
344
+ events.append(
345
+ {
346
+ "kind": "todo",
347
+ "line": line_no,
348
+ "id": f"line-{line_no}",
349
+ "items": todos,
350
+ "search": text,
351
+ }
352
+ )
353
+ continue
354
+
355
+ if payload_type == "patch_apply_end":
356
+ path_value = str(payload.get("path") or "patch")
357
+ events.append(
358
+ {
359
+ "kind": "files",
360
+ "line": line_no,
361
+ "id": f"line-{line_no}",
362
+ "changes": [{"kind": "patch", "path": path_value}],
363
+ "status": payload.get("status") or "",
364
+ "search": f"patch {path_value} {payload.get('status') or ''}",
365
+ }
366
+ )
367
+ continue
368
+
369
+ if obj_type == "response_item":
370
+ if payload_type == "function_call":
371
+ pending_commands.append(_payload_command(payload))
372
+ continue
373
+
374
+ if payload_type == "function_call_output":
375
+ command = pending_commands.pop(0) if pending_commands else "function_call_output"
376
+ output = _strip_ansi(payload.get("output") or "")
377
+ exit_code = payload.get("exit_code")
378
+ status = "failed" if exit_code not in (None, 0) else "completed"
379
+ label, status_class = _command_status({"status": status, "exit_code": exit_code})
380
+ if status_class == "fail":
381
+ failed_commands += 1
382
+ item_id = f"line-{line_no}"
383
+ command_ids.add(item_id)
384
+ events.append(
385
+ {
386
+ "kind": "command",
387
+ "line": line_no,
388
+ "id": item_id,
389
+ "command": command,
390
+ "output": output,
391
+ "exit_code": exit_code,
392
+ "status": label,
393
+ "status_class": status_class,
394
+ "search": " ".join([command, output, str(exit_code), label]),
395
+ }
396
+ )
397
+ continue
398
+
399
+ if obj_type == "turn.started":
400
+ turn_started += 1
401
+ continue
402
+
403
+ if obj_type == "turn.completed":
404
+ turn_completed += 1
405
+ usage = obj.get("usage") or {}
406
+ if isinstance(usage, dict):
407
+ for key in usage_summary:
408
+ usage_summary[key] += int(usage.get(key) or 0)
409
+ events.append(
410
+ {
411
+ "kind": "usage",
412
+ "line": line_no,
413
+ "usage": usage,
414
+ "search": " ".join(
415
+ [
416
+ "turn completed usage tokens",
417
+ " ".join(f"{k} {v}" for k, v in usage.items()),
418
+ ]
419
+ ),
420
+ }
421
+ )
422
+ continue
423
+
424
+ if obj_type not in {"item.completed", "item.updated"}:
425
+ continue
426
+
427
+ if item_type == "agent_message":
428
+ text = str(item.get("text") or "")
429
+ events.append(
430
+ {
431
+ "kind": "agent",
432
+ "line": line_no,
433
+ "id": item.get("id") or f"line-{line_no}",
434
+ "text": text,
435
+ "search": text,
436
+ }
437
+ )
438
+ continue
439
+
440
+ if item_type == "command_execution":
441
+ item_id = str(item.get("id") or f"line-{line_no}")
442
+ if item_id not in command_ids:
443
+ command_ids.add(item_id)
444
+ label, status_class = _command_status(item)
445
+ if status_class == "fail":
446
+ failed_commands += 1
447
+ command = str(item.get("command") or "")
448
+ output = _strip_ansi(item.get("aggregated_output") or "")
449
+ exit_code = item.get("exit_code")
450
+ events.append(
451
+ {
452
+ "kind": "command",
453
+ "line": line_no,
454
+ "id": item_id,
455
+ "command": command,
456
+ "output": output,
457
+ "exit_code": exit_code,
458
+ "status": label,
459
+ "status_class": status_class,
460
+ "search": " ".join([command, output, str(exit_code), label]),
461
+ }
462
+ )
463
+ continue
464
+
465
+ if item_type == "todo_list":
466
+ todos = item.get("items") or []
467
+ if isinstance(todos, list):
468
+ text = " ".join(str(todo.get("text") or "") for todo in todos if isinstance(todo, dict))
469
+ events.append(
470
+ {
471
+ "kind": "todo",
472
+ "line": line_no,
473
+ "id": item.get("id") or f"line-{line_no}",
474
+ "items": todos,
475
+ "search": text,
476
+ }
477
+ )
478
+ continue
479
+
480
+ if item_type == "file_change":
481
+ changes = item.get("changes") or []
482
+ if isinstance(changes, list):
483
+ text = " ".join(
484
+ f"{change.get('kind', '')} {change.get('path', '')}"
485
+ for change in changes
486
+ if isinstance(change, dict)
487
+ )
488
+ events.append(
489
+ {
490
+ "kind": "files",
491
+ "line": line_no,
492
+ "id": item.get("id") or f"line-{line_no}",
493
+ "changes": changes,
494
+ "status": item.get("status") or "",
495
+ "search": text,
496
+ }
497
+ )
498
+
499
+ total_turns = turn_completed or turn_started
500
+ return {
501
+ "file_name": file_name,
502
+ "path": str(path),
503
+ "bytes": path.stat().st_size,
504
+ "events": events,
505
+ "summary": {
506
+ "turns": total_turns,
507
+ "commands": len(command_ids),
508
+ "failed_commands": failed_commands,
509
+ "duration": _duration_label(timestamps),
510
+ "parse_errors": parse_errors,
511
+ **usage_summary,
512
+ "total_tokens": usage_summary["input_tokens"] + usage_summary["output_tokens"],
513
+ },
514
+ }
515
+
516
+
517
+ def _filter_events(events: list[dict[str, Any]], query: str | None) -> list[dict[str, Any]]:
518
+ needle = (query or "").strip().lower()
519
+ if not needle:
520
+ return events
521
+ terms = [part for part in needle.split() if part]
522
+ return [
523
+ event
524
+ for event in events
525
+ if all(term in str(event.get("search", "")).lower() for term in terms)
526
+ ]
527
+
528
+
529
+ def _metric(label: str, value: str, tone: str = "") -> str:
530
+ return (
531
+ f'<div class="metric {tone}">'
532
+ f'<span class="metric-label">{_escape(label)}</span>'
533
+ f'<span class="metric-value">{_escape(value)}</span>'
534
+ "</div>"
535
+ )
536
+
537
+
538
+ def _render_summary(data: dict[str, Any], matches: int, query: str) -> str:
539
+ summary = data["summary"]
540
+ file_label = _display_name(data["file_name"])
541
+ query_label = f"Filtered: {matches:,} matches" if query.strip() else f"{matches:,} visible events"
542
+ failed = int(summary["failed_commands"])
543
+ return (
544
+ '<section class="summary-panel">'
545
+ '<div class="summary-topline">'
546
+ f'<div><h1>{_escape(file_label)}</h1>'
547
+ f'<p>{_escape(data["file_name"])} · {_fmt_bytes(data["bytes"])} · {query_label}</p></div>'
548
+ f'<div class="repo-pill">private dataset · {_escape(APP_REVISION)}</div>'
549
+ "</div>"
550
+ '<div class="metrics-grid">'
551
+ f'{_metric("Turns", _fmt_number(summary["turns"]))}'
552
+ f'{_metric("Total tokens", _fmt_number(summary["total_tokens"]))}'
553
+ f'{_metric("Input", _fmt_number(summary["input_tokens"]))}'
554
+ f'{_metric("Output", _fmt_number(summary["output_tokens"]))}'
555
+ f'{_metric("Reasoning", _fmt_number(summary["reasoning_output_tokens"]))}'
556
+ f'{_metric("Duration", str(summary["duration"]))}'
557
+ f'{_metric("Commands", _fmt_number(summary["commands"]))}'
558
+ f'{_metric("Failed", _fmt_number(failed), "danger" if failed else "ok")}'
559
+ "</div>"
560
+ "</section>"
561
+ )
562
+
563
+
564
+ def _render_agent(event: dict[str, Any]) -> str:
565
+ text, note = _limit_text(str(event.get("text") or ""), MAX_MESSAGE_CHARS, "message")
566
+ role = str(event.get("role") or "AGENT").upper()
567
+ role_class = "user" if role == "USER" else "agent"
568
+ return (
569
+ f'<article class="event {role_class}-event">'
570
+ f'<div class="event-rail {role_class}-rail">{_escape(role)}</div>'
571
+ f'<div class="bubble {role_class}-bubble">'
572
+ f'<div class="event-meta">line {_fmt_number(event["line"])}{_escape(note)}</div>'
573
+ f'<div class="message-text">{_escape(text)}</div>'
574
+ "</div>"
575
+ "</article>"
576
+ )
577
+
578
+
579
+ def _render_command(event: dict[str, Any]) -> str:
580
+ output = str(event.get("output") or "")
581
+ output_limited, note = _limit_text(output, MAX_OUTPUT_CHARS, "output")
582
+ exit_code = event.get("exit_code")
583
+ exit_label = "pending" if exit_code is None else str(exit_code)
584
+ status = str(event.get("status") or "unknown")
585
+ status_class = str(event.get("status_class") or "neutral")
586
+ output_count = f"{len(output):,} chars"
587
+ return (
588
+ '<article class="event command-event">'
589
+ '<div class="event-rail command-rail">CMD</div>'
590
+ '<div class="command-card">'
591
+ '<div class="command-head">'
592
+ f'<span class="status-badge {status_class}">{_escape(status)}</span>'
593
+ f'<span class="exit-badge">exit {_escape(exit_label)}</span>'
594
+ f'<span class="event-meta">line {_fmt_number(event["line"])}</span>'
595
+ "</div>"
596
+ f'<pre class="code command-code"><code>{_escape(event.get("command") or "")}</code></pre>'
597
+ f'<details class="output-drawer"><summary>Command output · {output_count}{_escape(note)}</summary>'
598
+ f'<pre class="code output-code"><code>{_escape(output_limited) if output_limited else "No output."}</code></pre>'
599
+ "</details>"
600
+ "</div>"
601
+ "</article>"
602
+ )
603
+
604
+
605
+ def _render_usage(event: dict[str, Any]) -> str:
606
+ usage = event.get("usage") or {}
607
+ input_tokens = int(usage.get("input_tokens") or 0)
608
+ cached = int(usage.get("cached_input_tokens") or 0)
609
+ output = int(usage.get("output_tokens") or 0)
610
+ reasoning = int(usage.get("reasoning_output_tokens") or 0)
611
+ total = input_tokens + output
612
+ return (
613
+ '<article class="event usage-event">'
614
+ '<div class="event-rail usage-rail">TURN</div>'
615
+ '<div class="usage-card">'
616
+ f'<span>Total {_fmt_number(total)}</span>'
617
+ f'<span>Input {_fmt_number(input_tokens)}</span>'
618
+ f'<span>Cached {_fmt_number(cached)}</span>'
619
+ f'<span>Output {_fmt_number(output)}</span>'
620
+ f'<span>Reasoning {_fmt_number(reasoning)}</span>'
621
+ f'<span class="event-meta">line {_fmt_number(event["line"])}</span>'
622
+ "</div>"
623
+ "</article>"
624
+ )
625
+
626
+
627
+ def _render_todo(event: dict[str, Any]) -> str:
628
+ items = event.get("items") or []
629
+ rows = []
630
+ for todo in items:
631
+ if not isinstance(todo, dict):
632
+ continue
633
+ done = bool(todo.get("completed"))
634
+ rows.append(
635
+ '<li class="todo-row">'
636
+ f'<span class="todo-check {"done" if done else "open"}">{"&#10003;" if done else "&#9711;"}</span>'
637
+ f'<span>{_escape(todo.get("text") or "")}</span>'
638
+ "</li>"
639
+ )
640
+ return (
641
+ '<article class="event todo-event">'
642
+ '<div class="event-rail todo-rail">TODO</div>'
643
+ '<div class="todo-card">'
644
+ f'<div class="event-meta">line {_fmt_number(event["line"])}</div>'
645
+ f'<ul>{"".join(rows) if rows else "<li>No todo items.</li>"}</ul>'
646
+ "</div>"
647
+ "</article>"
648
+ )
649
+
650
+
651
+ def _render_files(event: dict[str, Any]) -> str:
652
+ changes = event.get("changes") or []
653
+ rows = []
654
+ for change in changes:
655
+ if not isinstance(change, dict):
656
+ continue
657
+ rows.append(
658
+ '<li>'
659
+ f'<span class="file-kind">{_escape(change.get("kind") or "change")}</span>'
660
+ f'<code>{_escape(change.get("path") or "")}</code>'
661
+ "</li>"
662
+ )
663
+ return (
664
+ '<article class="event file-event">'
665
+ '<div class="event-rail file-rail">FILE</div>'
666
+ '<div class="file-card">'
667
+ f'<div class="event-meta">line {_fmt_number(event["line"])} · {_escape(event.get("status") or "")}</div>'
668
+ f'<ul>{"".join(rows) if rows else "<li>No file paths recorded.</li>"}</ul>'
669
+ "</div>"
670
+ "</article>"
671
+ )
672
+
673
+
674
+ def _render_event(event: dict[str, Any]) -> str:
675
+ kind = event.get("kind")
676
+ if kind in {"agent", "user"}:
677
+ return _render_agent(event)
678
+ if kind == "command":
679
+ return _render_command(event)
680
+ if kind == "usage":
681
+ return _render_usage(event)
682
+ if kind == "todo":
683
+ return _render_todo(event)
684
+ if kind == "files":
685
+ return _render_files(event)
686
+ return ""
687
+
688
+
689
+ def _render_transcript(events: list[dict[str, Any]], page: int, total_pages: int) -> str:
690
+ if not events:
691
+ return (
692
+ '<section class="empty-state">'
693
+ "<h2>No matching transcript events</h2>"
694
+ "<p>Try a broader search term or choose another session.</p>"
695
+ "</section>"
696
+ )
697
+ start = (page - 1) * PAGE_SIZE
698
+ visible = events[start : start + PAGE_SIZE]
699
+ rendered = "".join(_render_event(event) for event in visible)
700
+ return (
701
+ '<section class="transcript">'
702
+ f'<div class="page-marker">Page {page:,} of {total_pages:,}</div>'
703
+ f"{rendered}"
704
+ "</section>"
705
+ )
706
+
707
+
708
+ def _render_error(title: str, details: str) -> tuple[str, str, str, int]:
709
+ return (
710
+ '<section class="summary-panel error-panel">'
711
+ f"<h1>{_escape(title)}</h1>"
712
+ f"<p>{_escape(details)}</p>"
713
+ "</section>",
714
+ '<section class="empty-state"><h2>Session unavailable</h2></section>',
715
+ "No session loaded.",
716
+ 1,
717
+ )
718
+
719
+
720
+ def _initial_view(choices: list[str]) -> tuple[str, str, str, int]:
721
+ choices_label = f"{len(choices):,} session files available" if choices else "No session files available"
722
+ summary = (
723
+ '<section class="summary-panel">'
724
+ '<div class="summary-topline">'
725
+ "<div><h1>Select a session</h1>"
726
+ f"<p>{_escape(choices_label)} from {DATASET_REPO}</p></div>"
727
+ f'<div class="repo-pill">private dataset · {_escape(APP_REVISION)}</div>'
728
+ "</div>"
729
+ '<div class="metrics-grid">'
730
+ f'{_metric("Startup mode", "metadata only")}'
731
+ f'{_metric("Files listed", _fmt_number(len(choices)))}'
732
+ f'{_metric("Downloads", "on demand")}'
733
+ f'{_metric("Page size", _fmt_number(PAGE_SIZE))}'
734
+ "</div>"
735
+ "</section>"
736
+ )
737
+ transcript = (
738
+ '<section class="empty-state">'
739
+ "<h2>No session loaded yet</h2>"
740
+ "<p>Choose a session in the sidebar and select Load session. Files are downloaded lazily.</p>"
741
+ "</section>"
742
+ )
743
+ return summary, transcript, "No session file downloaded yet.", 1
744
+
745
+
746
+ def render_session(session_name: str | None, query: str | None = "", page: int | float | None = 1) -> tuple[str, str, str, int]:
747
+ choices = KNOWN_SESSIONS
748
+ list_error = ""
749
+ if not choices:
750
+ return _render_error("Dataset file list unavailable", list_error or "HF_TOKEN is not configured.")
751
+
752
+ session_name = session_name or choices[0]
753
+ if session_name not in choices and _display_name(str(session_name)) in choices:
754
+ session_name = _display_name(str(session_name))
755
+ candidates = _file_candidates(session_name)
756
+ if not candidates:
757
+ return _render_error("No session selected", "Choose a session from the dropdown.")
758
+
759
+ last_error: Exception | None = None
760
+ data = None
761
+ for file_name in candidates:
762
+ try:
763
+ data = _load_session(file_name)
764
+ break
765
+ except Exception as exc:
766
+ last_error = exc
767
+ if data is None:
768
+ details = f"{type(last_error).__name__}: {last_error}" if last_error else "No file candidates were available."
769
+ return _render_error("Could not load session", details)
770
+
771
+ query = query or ""
772
+ filtered = _filter_events(data["events"], query)
773
+ total_pages = max(1, math.ceil(len(filtered) / PAGE_SIZE))
774
+ try:
775
+ page_int = int(page or 1)
776
+ except (TypeError, ValueError):
777
+ page_int = 1
778
+ page_int = min(max(1, page_int), total_pages)
779
+ start = (page_int - 1) * PAGE_SIZE + 1 if filtered else 0
780
+ end = min(page_int * PAGE_SIZE, len(filtered))
781
+ page_info = (
782
+ f"Showing {start:,}-{end:,} of {len(filtered):,} matching events "
783
+ f"({len(data['events']):,} total parsed)."
784
+ )
785
+ if data["summary"]["parse_errors"]:
786
+ page_info += f" Skipped {data['summary']['parse_errors']:,} non-JSON lines."
787
+ summary = _render_summary(data, len(filtered), query)
788
+ transcript = _render_transcript(filtered, page_int, total_pages)
789
+ return summary, transcript, page_info, page_int
790
+
791
+
792
+ def load_session(session_name: str | None) -> tuple[str, str, str, int, str]:
793
+ summary, transcript, page_info, page = render_session(session_name, "", 1)
794
+ return summary, transcript, page_info, page, ""
795
+
796
+
797
+ def filter_session(session_name: str | None, query: str | None) -> tuple[str, str, str, int]:
798
+ return render_session(session_name, query, 1)
799
+
800
+
801
+ def next_page(session_name: str | None, query: str | None, page: int | float | None) -> tuple[str, str, str, int]:
802
+ return render_session(session_name, query, int(page or 1) + 1)
803
+
804
+
805
+ def previous_page(session_name: str | None, query: str | None, page: int | float | None) -> tuple[str, str, str, int]:
806
+ return render_session(session_name, query, int(page or 1) - 1)
807
+
808
+
809
+ def refresh_sessions() -> tuple[Any, str, str, str, int, str]:
810
+ _list_sessions_cached.cache_clear()
811
+ choices = _session_choices()
812
+ value = None
813
+ summary, transcript, page_info, page = _initial_view(choices)
814
+ return gr.update(choices=choices, value=value), summary, transcript, page_info, page, ""
815
+
816
+
817
+ CUSTOM_CSS = """
818
+ :root {
819
+ --ct-bg: #0b0d10;
820
+ --ct-panel: #11151a;
821
+ --ct-panel-2: #151a21;
822
+ --ct-line: #30363d;
823
+ --ct-text: #e6edf3;
824
+ --ct-muted: #8b949e;
825
+ --ct-green: #3fb950;
826
+ --ct-red: #f85149;
827
+ --ct-yellow: #d29922;
828
+ --ct-blue: #58a6ff;
829
+ --ct-cyan: #39c5cf;
830
+ --ct-mono: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
831
+ --ct-sans: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
832
+ }
833
+
834
+ html, body, .gradio-container {
835
+ min-height: 100%;
836
+ background: var(--ct-bg) !important;
837
+ color: var(--ct-text) !important;
838
+ font-family: var(--ct-sans) !important;
839
+ }
840
+
841
+ .gradio-container,
842
+ .gradio-container .contain,
843
+ .gradio-container main {
844
+ max-width: none !important;
845
+ width: 100% !important;
846
+ padding: 0 !important;
847
+ }
848
+
849
+ #ct-app {
850
+ min-height: 100vh;
851
+ background:
852
+ linear-gradient(180deg, rgba(88, 166, 255, 0.07), transparent 210px),
853
+ var(--ct-bg);
854
+ }
855
+
856
+ .ct-shell {
857
+ display: grid;
858
+ grid-template-columns: minmax(260px, 330px) minmax(0, 1fr);
859
+ gap: 0;
860
+ min-height: 100vh;
861
+ }
862
+
863
+ .ct-sidebar {
864
+ background: #0f1318;
865
+ border-right: 1px solid var(--ct-line);
866
+ padding: 22px 18px;
867
+ position: sticky;
868
+ top: 0;
869
+ height: 100vh;
870
+ overflow: auto;
871
+ }
872
+
873
+ .ct-brand {
874
+ margin-bottom: 22px;
875
+ }
876
+
877
+ .ct-brand h1 {
878
+ font-family: var(--ct-mono);
879
+ font-size: 21px;
880
+ line-height: 1.2;
881
+ margin: 0 0 6px;
882
+ color: var(--ct-text);
883
+ letter-spacing: 0;
884
+ }
885
+
886
+ .ct-brand p {
887
+ margin: 0;
888
+ color: var(--ct-muted);
889
+ font-size: 13px;
890
+ line-height: 1.45;
891
+ }
892
+
893
+ .ct-sidebar .gradio-dropdown,
894
+ .ct-sidebar .gradio-textbox,
895
+ .ct-sidebar .gradio-button {
896
+ font-family: var(--ct-mono) !important;
897
+ }
898
+
899
+ .ct-sidebar .block,
900
+ .ct-sidebar .form,
901
+ .ct-sidebar .wrap,
902
+ .ct-sidebar .container,
903
+ .ct-sidebar .secondary-wrap,
904
+ .ct-sidebar .input-container,
905
+ .ct-sidebar .block > div {
906
+ background: transparent !important;
907
+ border-color: var(--ct-line) !important;
908
+ box-shadow: none !important;
909
+ }
910
+
911
+ .ct-sidebar .block {
912
+ background: var(--ct-panel) !important;
913
+ border: 1px solid var(--ct-line) !important;
914
+ border-radius: 8px !important;
915
+ }
916
+
917
+ .ct-sidebar label,
918
+ .ct-sidebar .block span {
919
+ color: var(--ct-muted) !important;
920
+ }
921
+
922
+ .ct-sidebar input,
923
+ .ct-sidebar textarea,
924
+ .ct-sidebar .wrap,
925
+ .ct-sidebar .container {
926
+ background: var(--ct-panel) !important;
927
+ color: var(--ct-text) !important;
928
+ border-color: var(--ct-line) !important;
929
+ }
930
+
931
+ .ct-sidebar button {
932
+ background: var(--ct-panel-2) !important;
933
+ border: 1px solid var(--ct-line) !important;
934
+ border-radius: 8px !important;
935
+ color: var(--ct-text) !important;
936
+ font-family: var(--ct-mono) !important;
937
+ min-height: 42px;
938
+ }
939
+
940
+ .ct-sidebar button:hover {
941
+ border-color: var(--ct-blue) !important;
942
+ color: #ffffff !important;
943
+ }
944
+
945
+ .ct-sidebar button.primary,
946
+ .ct-sidebar .primary button {
947
+ background: #1f6feb !important;
948
+ border-color: #388bfd !important;
949
+ color: #ffffff !important;
950
+ }
951
+
952
+ .ct-main {
953
+ padding: 22px;
954
+ min-width: 0;
955
+ }
956
+
957
+ .summary-panel {
958
+ background: var(--ct-panel);
959
+ border: 1px solid var(--ct-line);
960
+ border-radius: 8px;
961
+ margin-bottom: 18px;
962
+ padding: 18px;
963
+ }
964
+
965
+ .summary-topline {
966
+ display: flex;
967
+ align-items: flex-start;
968
+ justify-content: space-between;
969
+ gap: 16px;
970
+ margin-bottom: 16px;
971
+ }
972
+
973
+ .summary-topline h1 {
974
+ font-family: var(--ct-mono);
975
+ font-size: 22px;
976
+ margin: 0 0 6px;
977
+ color: var(--ct-text);
978
+ letter-spacing: 0;
979
+ }
980
+
981
+ .summary-topline p {
982
+ margin: 0;
983
+ color: var(--ct-muted);
984
+ font-family: var(--ct-mono);
985
+ font-size: 13px;
986
+ }
987
+
988
+ .repo-pill {
989
+ border: 1px solid rgba(57, 197, 207, 0.45);
990
+ color: var(--ct-cyan);
991
+ border-radius: 999px;
992
+ padding: 5px 10px;
993
+ font-family: var(--ct-mono);
994
+ font-size: 12px;
995
+ white-space: nowrap;
996
+ }
997
+
998
+ .metrics-grid {
999
+ display: grid;
1000
+ grid-template-columns: repeat(auto-fit, minmax(135px, 1fr));
1001
+ gap: 10px;
1002
+ }
1003
+
1004
+ .metric {
1005
+ background: var(--ct-panel-2);
1006
+ border: 1px solid var(--ct-line);
1007
+ border-radius: 8px;
1008
+ padding: 10px 12px;
1009
+ min-height: 62px;
1010
+ }
1011
+
1012
+ .metric-label {
1013
+ display: block;
1014
+ color: var(--ct-muted);
1015
+ font-size: 12px;
1016
+ margin-bottom: 6px;
1017
+ }
1018
+
1019
+ .metric-value {
1020
+ display: block;
1021
+ color: var(--ct-text);
1022
+ font-family: var(--ct-mono);
1023
+ font-size: 17px;
1024
+ overflow-wrap: anywhere;
1025
+ }
1026
+
1027
+ .metric.ok .metric-value {
1028
+ color: var(--ct-green);
1029
+ }
1030
+
1031
+ .metric.danger .metric-value {
1032
+ color: var(--ct-red);
1033
+ }
1034
+
1035
+ .transcript {
1036
+ display: flex;
1037
+ flex-direction: column;
1038
+ gap: 14px;
1039
+ }
1040
+
1041
+ .page-marker {
1042
+ color: var(--ct-muted);
1043
+ font-family: var(--ct-mono);
1044
+ font-size: 12px;
1045
+ padding: 0 2px;
1046
+ }
1047
+
1048
+ .event {
1049
+ display: grid;
1050
+ grid-template-columns: 58px minmax(0, 1fr);
1051
+ gap: 12px;
1052
+ align-items: start;
1053
+ }
1054
+
1055
+ .event-rail {
1056
+ position: sticky;
1057
+ top: 18px;
1058
+ border: 1px solid var(--ct-line);
1059
+ border-radius: 8px;
1060
+ padding: 8px 0;
1061
+ text-align: center;
1062
+ font-family: var(--ct-mono);
1063
+ font-size: 11px;
1064
+ color: var(--ct-muted);
1065
+ background: #0f1318;
1066
+ }
1067
+
1068
+ .agent-rail { color: var(--ct-blue); }
1069
+ .user-rail { color: var(--ct-cyan); }
1070
+ .command-rail { color: var(--ct-yellow); }
1071
+ .usage-rail { color: var(--ct-green); }
1072
+ .todo-rail { color: var(--ct-cyan); }
1073
+ .file-rail { color: #c9d1d9; }
1074
+
1075
+ .bubble,
1076
+ .command-card,
1077
+ .usage-card,
1078
+ .todo-card,
1079
+ .file-card,
1080
+ .empty-state {
1081
+ border: 1px solid var(--ct-line);
1082
+ border-radius: 8px;
1083
+ background: var(--ct-panel);
1084
+ min-width: 0;
1085
+ }
1086
+
1087
+ .agent-bubble {
1088
+ padding: 14px 15px;
1089
+ border-left: 3px solid var(--ct-blue);
1090
+ }
1091
+
1092
+ .user-bubble {
1093
+ padding: 14px 15px;
1094
+ border-left: 3px solid var(--ct-cyan);
1095
+ background: #101820;
1096
+ }
1097
+
1098
+ .event-meta {
1099
+ color: var(--ct-muted);
1100
+ font-family: var(--ct-mono);
1101
+ font-size: 12px;
1102
+ margin-bottom: 8px;
1103
+ }
1104
+
1105
+ .message-text {
1106
+ white-space: pre-wrap;
1107
+ overflow-wrap: anywhere;
1108
+ color: var(--ct-text);
1109
+ line-height: 1.55;
1110
+ font-size: 14px;
1111
+ }
1112
+
1113
+ .command-card {
1114
+ overflow: hidden;
1115
+ border-left: 3px solid var(--ct-yellow);
1116
+ }
1117
+
1118
+ .command-head {
1119
+ display: flex;
1120
+ align-items: center;
1121
+ flex-wrap: wrap;
1122
+ gap: 8px;
1123
+ padding: 12px 12px 0;
1124
+ }
1125
+
1126
+ .status-badge,
1127
+ .exit-badge {
1128
+ font-family: var(--ct-mono);
1129
+ font-size: 12px;
1130
+ border-radius: 999px;
1131
+ border: 1px solid var(--ct-line);
1132
+ padding: 3px 8px;
1133
+ }
1134
+
1135
+ .status-badge.success { color: var(--ct-green); border-color: rgba(63, 185, 80, .5); }
1136
+ .status-badge.fail { color: var(--ct-red); border-color: rgba(248, 81, 73, .55); }
1137
+ .status-badge.running { color: var(--ct-yellow); border-color: rgba(210, 153, 34, .55); }
1138
+ .status-badge.neutral { color: var(--ct-muted); }
1139
+
1140
+ .exit-badge {
1141
+ color: var(--ct-muted);
1142
+ }
1143
+
1144
+ .code {
1145
+ margin: 0;
1146
+ overflow: auto;
1147
+ font-family: var(--ct-mono);
1148
+ font-size: 12.5px;
1149
+ line-height: 1.55;
1150
+ color: #dbeafe;
1151
+ background: #070a0f;
1152
+ }
1153
+
1154
+ .command-code {
1155
+ margin: 12px;
1156
+ padding: 12px;
1157
+ border: 1px solid #1f2937;
1158
+ border-radius: 8px;
1159
+ }
1160
+
1161
+ .output-drawer {
1162
+ border-top: 1px solid var(--ct-line);
1163
+ }
1164
+
1165
+ .output-drawer summary {
1166
+ cursor: pointer;
1167
+ color: var(--ct-muted);
1168
+ font-family: var(--ct-mono);
1169
+ font-size: 12px;
1170
+ padding: 10px 12px;
1171
+ }
1172
+
1173
+ .output-code {
1174
+ max-height: 520px;
1175
+ padding: 12px;
1176
+ border-top: 1px solid #1f2937;
1177
+ }
1178
+
1179
+ .usage-card {
1180
+ display: flex;
1181
+ flex-wrap: wrap;
1182
+ gap: 8px;
1183
+ padding: 12px;
1184
+ border-left: 3px solid var(--ct-green);
1185
+ }
1186
+
1187
+ .usage-card span {
1188
+ font-family: var(--ct-mono);
1189
+ font-size: 12px;
1190
+ color: var(--ct-text);
1191
+ background: #0b1117;
1192
+ border: 1px solid var(--ct-line);
1193
+ border-radius: 999px;
1194
+ padding: 4px 9px;
1195
+ }
1196
+
1197
+ .todo-card,
1198
+ .file-card {
1199
+ padding: 12px;
1200
+ }
1201
+
1202
+ .todo-card {
1203
+ border-left: 3px solid var(--ct-cyan);
1204
+ }
1205
+
1206
+ .file-card {
1207
+ border-left: 3px solid #c9d1d9;
1208
+ }
1209
+
1210
+ .todo-card ul,
1211
+ .file-card ul {
1212
+ list-style: none;
1213
+ padding: 0;
1214
+ margin: 0;
1215
+ display: grid;
1216
+ gap: 8px;
1217
+ }
1218
+
1219
+ .todo-row {
1220
+ display: grid;
1221
+ grid-template-columns: 24px minmax(0, 1fr);
1222
+ gap: 8px;
1223
+ color: var(--ct-text);
1224
+ line-height: 1.45;
1225
+ }
1226
+
1227
+ .todo-check {
1228
+ font-family: var(--ct-mono);
1229
+ color: var(--ct-muted);
1230
+ }
1231
+
1232
+ .todo-check.done {
1233
+ color: var(--ct-green);
1234
+ }
1235
+
1236
+ .file-card li {
1237
+ display: flex;
1238
+ gap: 10px;
1239
+ align-items: baseline;
1240
+ min-width: 0;
1241
+ }
1242
+
1243
+ .file-kind {
1244
+ color: var(--ct-yellow);
1245
+ font-family: var(--ct-mono);
1246
+ font-size: 12px;
1247
+ min-width: 46px;
1248
+ }
1249
+
1250
+ .file-card code {
1251
+ color: var(--ct-text);
1252
+ font-family: var(--ct-mono);
1253
+ overflow-wrap: anywhere;
1254
+ }
1255
+
1256
+ .empty-state {
1257
+ padding: 32px;
1258
+ color: var(--ct-muted);
1259
+ }
1260
+
1261
+ .empty-state h2 {
1262
+ color: var(--ct-text);
1263
+ margin: 0 0 8px;
1264
+ font-family: var(--ct-mono);
1265
+ font-size: 18px;
1266
+ letter-spacing: 0;
1267
+ }
1268
+
1269
+ .error-panel {
1270
+ border-color: rgba(248, 81, 73, .6);
1271
+ }
1272
+
1273
+ .ct-page-info {
1274
+ color: var(--ct-muted);
1275
+ font-family: var(--ct-mono);
1276
+ font-size: 12px;
1277
+ margin: 8px 0 12px;
1278
+ }
1279
+
1280
+ .ct-page-info,
1281
+ .ct-page-info * {
1282
+ color: var(--ct-muted) !important;
1283
+ }
1284
+
1285
+ .ct-pager {
1286
+ display: grid;
1287
+ grid-template-columns: 1fr 1fr;
1288
+ gap: 8px;
1289
+ }
1290
+
1291
+ @media (max-width: 860px) {
1292
+ .ct-shell {
1293
+ grid-template-columns: 1fr;
1294
+ }
1295
+ .ct-sidebar {
1296
+ position: relative;
1297
+ height: auto;
1298
+ border-right: 0;
1299
+ border-bottom: 1px solid var(--ct-line);
1300
+ }
1301
+ .ct-main {
1302
+ padding: 14px;
1303
+ }
1304
+ .event {
1305
+ grid-template-columns: 1fr;
1306
+ }
1307
+ .event-rail {
1308
+ position: relative;
1309
+ top: 0;
1310
+ width: 58px;
1311
+ }
1312
+ .summary-topline {
1313
+ flex-direction: column;
1314
+ }
1315
+ }
1316
+ """
1317
+
1318
+
1319
+ def build_app() -> gr.Blocks:
1320
+ print("codex-traces startup: building app", flush=True)
1321
+ choices = _session_choices(live=False)
1322
+ default = None
1323
+ initial_summary, initial_transcript, initial_page_info, initial_page = _initial_view(choices)
1324
+ theme = gr.themes.Base(
1325
+ primary_hue="blue",
1326
+ secondary_hue="cyan",
1327
+ neutral_hue="slate",
1328
+ radius_size="sm",
1329
+ )
1330
+
1331
+ with gr.Blocks(title=APP_TITLE, theme=theme, css=CUSTOM_CSS) as demo:
1332
+ with gr.Row(elem_id="ct-app", elem_classes=["ct-shell"]):
1333
+ with gr.Column(elem_classes=["ct-sidebar"], scale=0):
1334
+ gr.HTML(
1335
+ '<div class="ct-brand">'
1336
+ "<h1>Codex Traces</h1>"
1337
+ "<p>Private rollout.jsonl session viewer</p>"
1338
+ "</div>"
1339
+ )
1340
+ session = gr.Dropdown(
1341
+ choices=choices,
1342
+ value=default,
1343
+ label="Session file",
1344
+ interactive=True,
1345
+ allow_custom_value=False,
1346
+ )
1347
+ search = gr.Textbox(
1348
+ label="Search transcript",
1349
+ placeholder="Filter by keyword, command, output, token...",
1350
+ lines=1,
1351
+ max_lines=1,
1352
+ )
1353
+ filter_button = gr.Button("Filter", variant="primary")
1354
+ load_button = gr.Button("Load session")
1355
+ refresh_button = gr.Button("Refresh file list")
1356
+ page_state = gr.State(initial_page)
1357
+ page_info = gr.Markdown(initial_page_info, elem_classes=["ct-page-info"])
1358
+ with gr.Row(elem_classes=["ct-pager"]):
1359
+ prev_button = gr.Button("Previous")
1360
+ next_button = gr.Button("Next")
1361
+
1362
+ with gr.Column(elem_classes=["ct-main"], scale=1):
1363
+ summary = gr.HTML(initial_summary)
1364
+ transcript = gr.HTML(initial_transcript)
1365
+
1366
+ session.change(
1367
+ fn=load_session,
1368
+ inputs=session,
1369
+ outputs=[summary, transcript, page_info, page_state, search],
1370
+ api_name="load_session",
1371
+ show_progress="minimal",
1372
+ )
1373
+ load_button.click(
1374
+ fn=load_session,
1375
+ inputs=session,
1376
+ outputs=[summary, transcript, page_info, page_state, search],
1377
+ api_name=False,
1378
+ show_progress="minimal",
1379
+ )
1380
+ filter_button.click(
1381
+ fn=filter_session,
1382
+ inputs=[session, search],
1383
+ outputs=[summary, transcript, page_info, page_state],
1384
+ api_name="filter_session",
1385
+ show_progress="minimal",
1386
+ )
1387
+ search.submit(
1388
+ fn=filter_session,
1389
+ inputs=[session, search],
1390
+ outputs=[summary, transcript, page_info, page_state],
1391
+ api_name=False,
1392
+ show_progress="minimal",
1393
+ )
1394
+ prev_button.click(
1395
+ fn=previous_page,
1396
+ inputs=[session, search, page_state],
1397
+ outputs=[summary, transcript, page_info, page_state],
1398
+ api_name="previous_page",
1399
+ show_progress="minimal",
1400
+ )
1401
+ next_button.click(
1402
+ fn=next_page,
1403
+ inputs=[session, search, page_state],
1404
+ outputs=[summary, transcript, page_info, page_state],
1405
+ api_name="next_page",
1406
+ show_progress="minimal",
1407
+ )
1408
+ refresh_button.click(
1409
+ fn=refresh_sessions,
1410
+ inputs=None,
1411
+ outputs=[session, summary, transcript, page_info, page_state, search],
1412
+ api_name="refresh_sessions",
1413
+ show_progress="minimal",
1414
+ )
1415
+
1416
+ print("codex-traces startup: app built", flush=True)
1417
+ return demo
1418
 
 
1419
 
1420
+ demo = build_app()
1421
+ print("codex-traces startup: demo ready", flush=True)
 
1422
 
1423
+ print("codex-traces startup: launching", flush=True)
1424
  demo.launch(show_error=True)