Mike0021 commited on
Commit
21e769e
·
verified ·
1 Parent(s): 6b8bb70

Initial Codex traces viewer

Browse files
Files changed (3) hide show
  1. README.md +9 -6
  2. app.py +1157 -0
  3. requirements.txt +2 -0
README.md CHANGED
@@ -1,13 +1,16 @@
1
  ---
2
  title: Codex Traces
3
- emoji: 📈
4
- colorFrom: green
5
  colorTo: indigo
6
  sdk: gradio
7
- sdk_version: 6.20.0
8
- python_version: '3.13'
9
  app_file: app.py
10
- pinned: false
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
1
  ---
2
  title: Codex Traces
3
+ emoji: 🚀
4
+ colorFrom: gray
5
  colorTo: indigo
6
  sdk: gradio
7
+ sdk_version: 6.19.0
 
8
  app_file: app.py
9
+ short_description: Visualize private Codex rollout logs
10
+ startup_duration_timeout: 1h
11
  ---
12
 
13
+ # Codex Traces
14
+
15
+ Gradio viewer for private Codex `rollout.jsonl` session logs stored in
16
+ `Mike0021/codex-sessions`.
app.py ADDED
@@ -0,0 +1,1157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+
9
+ import html
10
+ import json
11
+ import math
12
+ import re
13
+ from datetime import datetime, timezone
14
+ from functools import lru_cache
15
+ from pathlib import Path
16
+ from typing import Any
17
+
18
+ import gradio as gr
19
+ from huggingface_hub import hf_hub_download, list_repo_files
20
+
21
+
22
+ APP_TITLE = "Codex Traces"
23
+ DATASET_REPO = "Mike0021/codex-sessions"
24
+ REPO_TYPE = "dataset"
25
+ FILE_SUFFIX = "_rollout.jsonl"
26
+ PAGE_SIZE = 120
27
+ MAX_OUTPUT_CHARS = 22000
28
+ MAX_MESSAGE_CHARS = 30000
29
+
30
+ ANSI_RE = re.compile(r"\x1b\[[0-9;?]*[ -/]*[@-~]")
31
+ TIMESTAMP_KEYS = (
32
+ "timestamp",
33
+ "time",
34
+ "created_at",
35
+ "updated_at",
36
+ "started_at",
37
+ "completed_at",
38
+ )
39
+
40
+
41
+ def _hub_token() -> str | bool:
42
+ return (
43
+ os.environ.get("HF_TOKEN")
44
+ or os.environ.get("HUGGING_FACE_HUB_TOKEN")
45
+ or os.environ.get("HUGGINGFACE_HUB_TOKEN")
46
+ or True
47
+ )
48
+
49
+
50
+ def _display_name(file_name: str) -> str:
51
+ return file_name[: -len(FILE_SUFFIX)] if file_name.endswith(FILE_SUFFIX) else file_name
52
+
53
+
54
+ def _file_name(session_name: str | None) -> str | None:
55
+ if not session_name:
56
+ return None
57
+ name = str(session_name).strip()
58
+ if name.endswith(FILE_SUFFIX):
59
+ return name
60
+ return f"{name}{FILE_SUFFIX}"
61
+
62
+
63
+ def _fmt_number(value: int | None) -> str:
64
+ return f"{int(value or 0):,}"
65
+
66
+
67
+ def _fmt_bytes(value: int | None) -> str:
68
+ value = int(value or 0)
69
+ units = ("B", "KB", "MB", "GB")
70
+ size = float(value)
71
+ for unit in units:
72
+ if size < 1024 or unit == units[-1]:
73
+ return f"{size:.1f} {unit}" if unit != "B" else f"{value} B"
74
+ size /= 1024
75
+ return f"{value} B"
76
+
77
+
78
+ def _strip_ansi(text: Any) -> str:
79
+ return ANSI_RE.sub("", str(text or ""))
80
+
81
+
82
+ def _escape(text: Any) -> str:
83
+ return html.escape(str(text or ""), quote=True)
84
+
85
+
86
+ def _limit_text(text: str, limit: int, label: str) -> tuple[str, str]:
87
+ if len(text) <= limit:
88
+ return text, ""
89
+ hidden = len(text) - limit
90
+ note = f"\n\n[{label} truncated by {hidden:,} characters for browser responsiveness.]"
91
+ return text[:limit] + note, f" truncated {hidden:,} chars"
92
+
93
+
94
+ def _parse_time(value: Any) -> datetime | None:
95
+ if value is None:
96
+ return None
97
+ if isinstance(value, (int, float)):
98
+ seconds = float(value) / 1000 if value > 10_000_000_000 else float(value)
99
+ try:
100
+ return datetime.fromtimestamp(seconds, tz=timezone.utc)
101
+ except (OSError, OverflowError, ValueError):
102
+ return None
103
+ if isinstance(value, str):
104
+ raw = value.strip()
105
+ if not raw:
106
+ return None
107
+ try:
108
+ if raw.endswith("Z"):
109
+ raw = raw[:-1] + "+00:00"
110
+ parsed = datetime.fromisoformat(raw)
111
+ return parsed if parsed.tzinfo else parsed.replace(tzinfo=timezone.utc)
112
+ except ValueError:
113
+ return None
114
+ return None
115
+
116
+
117
+ def _duration_label(timestamps: list[datetime]) -> str:
118
+ if len(timestamps) < 2:
119
+ return "not recorded"
120
+ delta = max(timestamps) - min(timestamps)
121
+ seconds = max(0, int(delta.total_seconds()))
122
+ if seconds < 60:
123
+ return f"{seconds}s"
124
+ minutes, sec = divmod(seconds, 60)
125
+ if minutes < 60:
126
+ return f"{minutes}m {sec}s"
127
+ hours, minutes = divmod(minutes, 60)
128
+ return f"{hours}h {minutes}m"
129
+
130
+
131
+ @lru_cache(maxsize=1)
132
+ def _list_sessions_cached() -> tuple[tuple[str, ...], str]:
133
+ try:
134
+ files = list_repo_files(DATASET_REPO, repo_type=REPO_TYPE, token=_hub_token())
135
+ except Exception as exc:
136
+ return (), f"{type(exc).__name__}: {exc}"
137
+ sessions = tuple(sorted(_display_name(f) for f in files if f.endswith(FILE_SUFFIX)))
138
+ if not sessions:
139
+ return (), "No *_rollout.jsonl files were found in the dataset."
140
+ return sessions, ""
141
+
142
+
143
+ def _session_choices() -> list[str]:
144
+ sessions, _ = _list_sessions_cached()
145
+ return list(sessions)
146
+
147
+
148
+ def _default_session() -> str | None:
149
+ choices = _session_choices()
150
+ return choices[0] if choices else None
151
+
152
+
153
+ def _download_session(file_name: str) -> Path:
154
+ return Path(
155
+ hf_hub_download(
156
+ repo_id=DATASET_REPO,
157
+ filename=file_name,
158
+ repo_type=REPO_TYPE,
159
+ token=_hub_token(),
160
+ )
161
+ )
162
+
163
+
164
+ def _command_status(item: dict[str, Any]) -> tuple[str, str]:
165
+ status = str(item.get("status") or "").lower()
166
+ exit_code = item.get("exit_code")
167
+ if status in {"failed", "error"} or (exit_code not in (None, 0)):
168
+ return "failed", "fail"
169
+ if status in {"completed", "succeeded", "success"}:
170
+ return "success", "success"
171
+ if status in {"in_progress", "running"}:
172
+ return "running", "running"
173
+ return status or "unknown", "neutral"
174
+
175
+
176
+ def _usage_total(usage: dict[str, Any]) -> int:
177
+ return int(usage.get("input_tokens") or 0) + int(usage.get("output_tokens") or 0)
178
+
179
+
180
+ def _collect_timestamps(obj: dict[str, Any], timestamps: list[datetime]) -> None:
181
+ for key in TIMESTAMP_KEYS:
182
+ parsed = _parse_time(obj.get(key))
183
+ if parsed:
184
+ timestamps.append(parsed)
185
+ item = obj.get("item")
186
+ if isinstance(item, dict):
187
+ for key in TIMESTAMP_KEYS:
188
+ parsed = _parse_time(item.get(key))
189
+ if parsed:
190
+ timestamps.append(parsed)
191
+
192
+
193
+ @lru_cache(maxsize=6)
194
+ def _load_session(file_name: str) -> dict[str, Any]:
195
+ path = _download_session(file_name)
196
+ events: list[dict[str, Any]] = []
197
+ timestamps: list[datetime] = []
198
+ command_ids: set[str] = set()
199
+ failed_commands = 0
200
+ turn_completed = 0
201
+ turn_started = 0
202
+ parse_errors = 0
203
+ usage_summary = {
204
+ "input_tokens": 0,
205
+ "cached_input_tokens": 0,
206
+ "output_tokens": 0,
207
+ "reasoning_output_tokens": 0,
208
+ }
209
+
210
+ with path.open("r", encoding="utf-8", errors="replace") as handle:
211
+ for line_no, raw_line in enumerate(handle, start=1):
212
+ line = raw_line.strip()
213
+ if not line.startswith("{"):
214
+ parse_errors += 1
215
+ continue
216
+ try:
217
+ obj = json.loads(line)
218
+ except json.JSONDecodeError:
219
+ parse_errors += 1
220
+ continue
221
+
222
+ obj_type = obj.get("type")
223
+ item = obj.get("item") if isinstance(obj.get("item"), dict) else {}
224
+ item_type = item.get("type")
225
+ _collect_timestamps(obj, timestamps)
226
+
227
+ if obj_type == "turn.started":
228
+ turn_started += 1
229
+ continue
230
+
231
+ if obj_type == "turn.completed":
232
+ turn_completed += 1
233
+ usage = obj.get("usage") or {}
234
+ if isinstance(usage, dict):
235
+ for key in usage_summary:
236
+ usage_summary[key] += int(usage.get(key) or 0)
237
+ events.append(
238
+ {
239
+ "kind": "usage",
240
+ "line": line_no,
241
+ "usage": usage,
242
+ "search": " ".join(
243
+ [
244
+ "turn completed usage tokens",
245
+ " ".join(f"{k} {v}" for k, v in usage.items()),
246
+ ]
247
+ ),
248
+ }
249
+ )
250
+ continue
251
+
252
+ if obj_type not in {"item.completed", "item.updated"}:
253
+ continue
254
+
255
+ if item_type == "agent_message":
256
+ text = str(item.get("text") or "")
257
+ events.append(
258
+ {
259
+ "kind": "agent",
260
+ "line": line_no,
261
+ "id": item.get("id") or f"line-{line_no}",
262
+ "text": text,
263
+ "search": text,
264
+ }
265
+ )
266
+ continue
267
+
268
+ if item_type == "command_execution":
269
+ item_id = str(item.get("id") or f"line-{line_no}")
270
+ if item_id not in command_ids:
271
+ command_ids.add(item_id)
272
+ label, status_class = _command_status(item)
273
+ if status_class == "fail":
274
+ failed_commands += 1
275
+ command = str(item.get("command") or "")
276
+ output = _strip_ansi(item.get("aggregated_output") or "")
277
+ exit_code = item.get("exit_code")
278
+ events.append(
279
+ {
280
+ "kind": "command",
281
+ "line": line_no,
282
+ "id": item_id,
283
+ "command": command,
284
+ "output": output,
285
+ "exit_code": exit_code,
286
+ "status": label,
287
+ "status_class": status_class,
288
+ "search": " ".join([command, output, str(exit_code), label]),
289
+ }
290
+ )
291
+ continue
292
+
293
+ if item_type == "todo_list":
294
+ todos = item.get("items") or []
295
+ if isinstance(todos, list):
296
+ text = " ".join(str(todo.get("text") or "") for todo in todos if isinstance(todo, dict))
297
+ events.append(
298
+ {
299
+ "kind": "todo",
300
+ "line": line_no,
301
+ "id": item.get("id") or f"line-{line_no}",
302
+ "items": todos,
303
+ "search": text,
304
+ }
305
+ )
306
+ continue
307
+
308
+ if item_type == "file_change":
309
+ changes = item.get("changes") or []
310
+ if isinstance(changes, list):
311
+ text = " ".join(
312
+ f"{change.get('kind', '')} {change.get('path', '')}"
313
+ for change in changes
314
+ if isinstance(change, dict)
315
+ )
316
+ events.append(
317
+ {
318
+ "kind": "files",
319
+ "line": line_no,
320
+ "id": item.get("id") or f"line-{line_no}",
321
+ "changes": changes,
322
+ "status": item.get("status") or "",
323
+ "search": text,
324
+ }
325
+ )
326
+
327
+ total_turns = turn_completed or turn_started
328
+ return {
329
+ "file_name": file_name,
330
+ "path": str(path),
331
+ "bytes": path.stat().st_size,
332
+ "events": events,
333
+ "summary": {
334
+ "turns": total_turns,
335
+ "commands": len(command_ids),
336
+ "failed_commands": failed_commands,
337
+ "duration": _duration_label(timestamps),
338
+ "parse_errors": parse_errors,
339
+ **usage_summary,
340
+ "total_tokens": usage_summary["input_tokens"] + usage_summary["output_tokens"],
341
+ },
342
+ }
343
+
344
+
345
+ def _filter_events(events: list[dict[str, Any]], query: str | None) -> list[dict[str, Any]]:
346
+ needle = (query or "").strip().lower()
347
+ if not needle:
348
+ return events
349
+ terms = [part for part in needle.split() if part]
350
+ return [
351
+ event
352
+ for event in events
353
+ if all(term in str(event.get("search", "")).lower() for term in terms)
354
+ ]
355
+
356
+
357
+ def _metric(label: str, value: str, tone: str = "") -> str:
358
+ return (
359
+ f'<div class="metric {tone}">'
360
+ f'<span class="metric-label">{_escape(label)}</span>'
361
+ f'<span class="metric-value">{_escape(value)}</span>'
362
+ "</div>"
363
+ )
364
+
365
+
366
+ def _render_summary(data: dict[str, Any], matches: int, query: str) -> str:
367
+ summary = data["summary"]
368
+ file_label = _display_name(data["file_name"])
369
+ query_label = f"Filtered: {matches:,} matches" if query.strip() else f"{matches:,} visible events"
370
+ failed = int(summary["failed_commands"])
371
+ return (
372
+ '<section class="summary-panel">'
373
+ '<div class="summary-topline">'
374
+ f'<div><h1>{_escape(file_label)}</h1>'
375
+ f'<p>{_escape(data["file_name"])} · {_fmt_bytes(data["bytes"])} · {query_label}</p></div>'
376
+ '<div class="repo-pill">private dataset</div>'
377
+ "</div>"
378
+ '<div class="metrics-grid">'
379
+ f'{_metric("Turns", _fmt_number(summary["turns"]))}'
380
+ f'{_metric("Total tokens", _fmt_number(summary["total_tokens"]))}'
381
+ f'{_metric("Input", _fmt_number(summary["input_tokens"]))}'
382
+ f'{_metric("Output", _fmt_number(summary["output_tokens"]))}'
383
+ f'{_metric("Reasoning", _fmt_number(summary["reasoning_output_tokens"]))}'
384
+ f'{_metric("Duration", str(summary["duration"]))}'
385
+ f'{_metric("Commands", _fmt_number(summary["commands"]))}'
386
+ f'{_metric("Failed", _fmt_number(failed), "danger" if failed else "ok")}'
387
+ "</div>"
388
+ "</section>"
389
+ )
390
+
391
+
392
+ def _render_agent(event: dict[str, Any]) -> str:
393
+ text, note = _limit_text(str(event.get("text") or ""), MAX_MESSAGE_CHARS, "message")
394
+ return (
395
+ '<article class="event agent-event">'
396
+ '<div class="event-rail agent-rail">AGENT</div>'
397
+ '<div class="bubble agent-bubble">'
398
+ f'<div class="event-meta">line {_fmt_number(event["line"])}{_escape(note)}</div>'
399
+ f'<div class="message-text">{_escape(text)}</div>'
400
+ "</div>"
401
+ "</article>"
402
+ )
403
+
404
+
405
+ def _render_command(event: dict[str, Any]) -> str:
406
+ output = str(event.get("output") or "")
407
+ output_limited, note = _limit_text(output, MAX_OUTPUT_CHARS, "output")
408
+ exit_code = event.get("exit_code")
409
+ exit_label = "pending" if exit_code is None else str(exit_code)
410
+ status = str(event.get("status") or "unknown")
411
+ status_class = str(event.get("status_class") or "neutral")
412
+ output_count = f"{len(output):,} chars"
413
+ return (
414
+ '<article class="event command-event">'
415
+ '<div class="event-rail command-rail">CMD</div>'
416
+ '<div class="command-card">'
417
+ '<div class="command-head">'
418
+ f'<span class="status-badge {status_class}">{_escape(status)}</span>'
419
+ f'<span class="exit-badge">exit {_escape(exit_label)}</span>'
420
+ f'<span class="event-meta">line {_fmt_number(event["line"])}</span>'
421
+ "</div>"
422
+ f'<pre class="code command-code"><code>{_escape(event.get("command") or "")}</code></pre>'
423
+ f'<details class="output-drawer"><summary>Command output · {output_count}{_escape(note)}</summary>'
424
+ f'<pre class="code output-code"><code>{_escape(output_limited) if output_limited else "No output."}</code></pre>'
425
+ "</details>"
426
+ "</div>"
427
+ "</article>"
428
+ )
429
+
430
+
431
+ def _render_usage(event: dict[str, Any]) -> str:
432
+ usage = event.get("usage") or {}
433
+ input_tokens = int(usage.get("input_tokens") or 0)
434
+ cached = int(usage.get("cached_input_tokens") or 0)
435
+ output = int(usage.get("output_tokens") or 0)
436
+ reasoning = int(usage.get("reasoning_output_tokens") or 0)
437
+ total = input_tokens + output
438
+ return (
439
+ '<article class="event usage-event">'
440
+ '<div class="event-rail usage-rail">TURN</div>'
441
+ '<div class="usage-card">'
442
+ f'<span>Total {_fmt_number(total)}</span>'
443
+ f'<span>Input {_fmt_number(input_tokens)}</span>'
444
+ f'<span>Cached {_fmt_number(cached)}</span>'
445
+ f'<span>Output {_fmt_number(output)}</span>'
446
+ f'<span>Reasoning {_fmt_number(reasoning)}</span>'
447
+ f'<span class="event-meta">line {_fmt_number(event["line"])}</span>'
448
+ "</div>"
449
+ "</article>"
450
+ )
451
+
452
+
453
+ def _render_todo(event: dict[str, Any]) -> str:
454
+ items = event.get("items") or []
455
+ rows = []
456
+ for todo in items:
457
+ if not isinstance(todo, dict):
458
+ continue
459
+ done = bool(todo.get("completed"))
460
+ rows.append(
461
+ '<li class="todo-row">'
462
+ f'<span class="todo-check {"done" if done else "open"}">{"&#10003;" if done else "&#9711;"}</span>'
463
+ f'<span>{_escape(todo.get("text") or "")}</span>'
464
+ "</li>"
465
+ )
466
+ return (
467
+ '<article class="event todo-event">'
468
+ '<div class="event-rail todo-rail">TODO</div>'
469
+ '<div class="todo-card">'
470
+ f'<div class="event-meta">line {_fmt_number(event["line"])}</div>'
471
+ f'<ul>{"".join(rows) if rows else "<li>No todo items.</li>"}</ul>'
472
+ "</div>"
473
+ "</article>"
474
+ )
475
+
476
+
477
+ def _render_files(event: dict[str, Any]) -> str:
478
+ changes = event.get("changes") or []
479
+ rows = []
480
+ for change in changes:
481
+ if not isinstance(change, dict):
482
+ continue
483
+ rows.append(
484
+ '<li>'
485
+ f'<span class="file-kind">{_escape(change.get("kind") or "change")}</span>'
486
+ f'<code>{_escape(change.get("path") or "")}</code>'
487
+ "</li>"
488
+ )
489
+ return (
490
+ '<article class="event file-event">'
491
+ '<div class="event-rail file-rail">FILE</div>'
492
+ '<div class="file-card">'
493
+ f'<div class="event-meta">line {_fmt_number(event["line"])} · {_escape(event.get("status") or "")}</div>'
494
+ f'<ul>{"".join(rows) if rows else "<li>No file paths recorded.</li>"}</ul>'
495
+ "</div>"
496
+ "</article>"
497
+ )
498
+
499
+
500
+ def _render_event(event: dict[str, Any]) -> str:
501
+ kind = event.get("kind")
502
+ if kind == "agent":
503
+ return _render_agent(event)
504
+ if kind == "command":
505
+ return _render_command(event)
506
+ if kind == "usage":
507
+ return _render_usage(event)
508
+ if kind == "todo":
509
+ return _render_todo(event)
510
+ if kind == "files":
511
+ return _render_files(event)
512
+ return ""
513
+
514
+
515
+ def _render_transcript(events: list[dict[str, Any]], page: int, total_pages: int) -> str:
516
+ if not events:
517
+ return (
518
+ '<section class="empty-state">'
519
+ "<h2>No matching transcript events</h2>"
520
+ "<p>Try a broader search term or choose another session.</p>"
521
+ "</section>"
522
+ )
523
+ start = (page - 1) * PAGE_SIZE
524
+ visible = events[start : start + PAGE_SIZE]
525
+ rendered = "".join(_render_event(event) for event in visible)
526
+ return (
527
+ '<section class="transcript">'
528
+ f'<div class="page-marker">Page {page:,} of {total_pages:,}</div>'
529
+ f"{rendered}"
530
+ "</section>"
531
+ )
532
+
533
+
534
+ def _render_error(title: str, details: str) -> tuple[str, str, str, int]:
535
+ return (
536
+ '<section class="summary-panel error-panel">'
537
+ f"<h1>{_escape(title)}</h1>"
538
+ f"<p>{_escape(details)}</p>"
539
+ "</section>",
540
+ '<section class="empty-state"><h2>Session unavailable</h2></section>',
541
+ "No session loaded.",
542
+ 1,
543
+ )
544
+
545
+
546
+ def render_session(session_name: str | None, query: str | None = "", page: int | float | None = 1) -> tuple[str, str, str, int]:
547
+ choices, list_error = _list_sessions_cached()
548
+ if not choices:
549
+ return _render_error("Dataset file list unavailable", list_error or "HF_TOKEN is not configured.")
550
+
551
+ session_name = session_name or choices[0]
552
+ if session_name not in choices and _display_name(str(session_name)) in choices:
553
+ session_name = _display_name(str(session_name))
554
+ file_name = _file_name(session_name)
555
+ if file_name is None:
556
+ return _render_error("No session selected", "Choose a session from the dropdown.")
557
+
558
+ try:
559
+ data = _load_session(file_name)
560
+ except Exception as exc:
561
+ return _render_error("Could not load session", f"{type(exc).__name__}: {exc}")
562
+
563
+ query = query or ""
564
+ filtered = _filter_events(data["events"], query)
565
+ total_pages = max(1, math.ceil(len(filtered) / PAGE_SIZE))
566
+ try:
567
+ page_int = int(page or 1)
568
+ except (TypeError, ValueError):
569
+ page_int = 1
570
+ page_int = min(max(1, page_int), total_pages)
571
+ start = (page_int - 1) * PAGE_SIZE + 1 if filtered else 0
572
+ end = min(page_int * PAGE_SIZE, len(filtered))
573
+ page_info = (
574
+ f"Showing {start:,}-{end:,} of {len(filtered):,} matching events "
575
+ f"({len(data['events']):,} total parsed)."
576
+ )
577
+ if data["summary"]["parse_errors"]:
578
+ page_info += f" Skipped {data['summary']['parse_errors']:,} non-JSON lines."
579
+ summary = _render_summary(data, len(filtered), query)
580
+ transcript = _render_transcript(filtered, page_int, total_pages)
581
+ return summary, transcript, page_info, page_int
582
+
583
+
584
+ def load_session(session_name: str | None) -> tuple[str, str, str, int, str]:
585
+ summary, transcript, page_info, page = render_session(session_name, "", 1)
586
+ return summary, transcript, page_info, page, ""
587
+
588
+
589
+ def filter_session(session_name: str | None, query: str | None) -> tuple[str, str, str, int]:
590
+ return render_session(session_name, query, 1)
591
+
592
+
593
+ def next_page(session_name: str | None, query: str | None, page: int | float | None) -> tuple[str, str, str, int]:
594
+ return render_session(session_name, query, int(page or 1) + 1)
595
+
596
+
597
+ def previous_page(session_name: str | None, query: str | None, page: int | float | None) -> tuple[str, str, str, int]:
598
+ return render_session(session_name, query, int(page or 1) - 1)
599
+
600
+
601
+ def refresh_sessions() -> tuple[Any, str, str, str, int, str]:
602
+ _list_sessions_cached.cache_clear()
603
+ choices = _session_choices()
604
+ value = choices[0] if choices else None
605
+ summary, transcript, page_info, page = render_session(value, "", 1)
606
+ return gr.update(choices=choices, value=value), summary, transcript, page_info, page, ""
607
+
608
+
609
+ CUSTOM_CSS = """
610
+ :root {
611
+ --ct-bg: #0b0d10;
612
+ --ct-panel: #11151a;
613
+ --ct-panel-2: #151a21;
614
+ --ct-line: #30363d;
615
+ --ct-text: #e6edf3;
616
+ --ct-muted: #8b949e;
617
+ --ct-green: #3fb950;
618
+ --ct-red: #f85149;
619
+ --ct-yellow: #d29922;
620
+ --ct-blue: #58a6ff;
621
+ --ct-cyan: #39c5cf;
622
+ --ct-mono: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
623
+ --ct-sans: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
624
+ }
625
+
626
+ html, body, .gradio-container {
627
+ min-height: 100%;
628
+ background: var(--ct-bg) !important;
629
+ color: var(--ct-text) !important;
630
+ font-family: var(--ct-sans) !important;
631
+ }
632
+
633
+ .gradio-container,
634
+ .gradio-container .contain,
635
+ .gradio-container main {
636
+ max-width: none !important;
637
+ width: 100% !important;
638
+ padding: 0 !important;
639
+ }
640
+
641
+ #ct-app {
642
+ min-height: 100vh;
643
+ background:
644
+ linear-gradient(180deg, rgba(88, 166, 255, 0.07), transparent 210px),
645
+ var(--ct-bg);
646
+ }
647
+
648
+ .ct-shell {
649
+ display: grid;
650
+ grid-template-columns: minmax(260px, 330px) minmax(0, 1fr);
651
+ gap: 0;
652
+ min-height: 100vh;
653
+ }
654
+
655
+ .ct-sidebar {
656
+ background: #0f1318;
657
+ border-right: 1px solid var(--ct-line);
658
+ padding: 22px 18px;
659
+ position: sticky;
660
+ top: 0;
661
+ height: 100vh;
662
+ overflow: auto;
663
+ }
664
+
665
+ .ct-brand {
666
+ margin-bottom: 22px;
667
+ }
668
+
669
+ .ct-brand h1 {
670
+ font-family: var(--ct-mono);
671
+ font-size: 21px;
672
+ line-height: 1.2;
673
+ margin: 0 0 6px;
674
+ color: var(--ct-text);
675
+ letter-spacing: 0;
676
+ }
677
+
678
+ .ct-brand p {
679
+ margin: 0;
680
+ color: var(--ct-muted);
681
+ font-size: 13px;
682
+ line-height: 1.45;
683
+ }
684
+
685
+ .ct-sidebar .gradio-dropdown,
686
+ .ct-sidebar .gradio-textbox,
687
+ .ct-sidebar .gradio-button {
688
+ font-family: var(--ct-mono) !important;
689
+ }
690
+
691
+ .ct-sidebar label,
692
+ .ct-sidebar .block span {
693
+ color: var(--ct-muted) !important;
694
+ }
695
+
696
+ .ct-sidebar input,
697
+ .ct-sidebar textarea,
698
+ .ct-sidebar .wrap,
699
+ .ct-sidebar .container {
700
+ background: var(--ct-panel) !important;
701
+ color: var(--ct-text) !important;
702
+ border-color: var(--ct-line) !important;
703
+ }
704
+
705
+ .ct-main {
706
+ padding: 22px;
707
+ min-width: 0;
708
+ }
709
+
710
+ .summary-panel {
711
+ background: var(--ct-panel);
712
+ border: 1px solid var(--ct-line);
713
+ border-radius: 8px;
714
+ margin-bottom: 18px;
715
+ padding: 18px;
716
+ }
717
+
718
+ .summary-topline {
719
+ display: flex;
720
+ align-items: flex-start;
721
+ justify-content: space-between;
722
+ gap: 16px;
723
+ margin-bottom: 16px;
724
+ }
725
+
726
+ .summary-topline h1 {
727
+ font-family: var(--ct-mono);
728
+ font-size: 22px;
729
+ margin: 0 0 6px;
730
+ color: var(--ct-text);
731
+ letter-spacing: 0;
732
+ }
733
+
734
+ .summary-topline p {
735
+ margin: 0;
736
+ color: var(--ct-muted);
737
+ font-family: var(--ct-mono);
738
+ font-size: 13px;
739
+ }
740
+
741
+ .repo-pill {
742
+ border: 1px solid rgba(57, 197, 207, 0.45);
743
+ color: var(--ct-cyan);
744
+ border-radius: 999px;
745
+ padding: 5px 10px;
746
+ font-family: var(--ct-mono);
747
+ font-size: 12px;
748
+ white-space: nowrap;
749
+ }
750
+
751
+ .metrics-grid {
752
+ display: grid;
753
+ grid-template-columns: repeat(auto-fit, minmax(135px, 1fr));
754
+ gap: 10px;
755
+ }
756
+
757
+ .metric {
758
+ background: var(--ct-panel-2);
759
+ border: 1px solid var(--ct-line);
760
+ border-radius: 8px;
761
+ padding: 10px 12px;
762
+ min-height: 62px;
763
+ }
764
+
765
+ .metric-label {
766
+ display: block;
767
+ color: var(--ct-muted);
768
+ font-size: 12px;
769
+ margin-bottom: 6px;
770
+ }
771
+
772
+ .metric-value {
773
+ display: block;
774
+ color: var(--ct-text);
775
+ font-family: var(--ct-mono);
776
+ font-size: 17px;
777
+ overflow-wrap: anywhere;
778
+ }
779
+
780
+ .metric.ok .metric-value {
781
+ color: var(--ct-green);
782
+ }
783
+
784
+ .metric.danger .metric-value {
785
+ color: var(--ct-red);
786
+ }
787
+
788
+ .transcript {
789
+ display: flex;
790
+ flex-direction: column;
791
+ gap: 14px;
792
+ }
793
+
794
+ .page-marker {
795
+ color: var(--ct-muted);
796
+ font-family: var(--ct-mono);
797
+ font-size: 12px;
798
+ padding: 0 2px;
799
+ }
800
+
801
+ .event {
802
+ display: grid;
803
+ grid-template-columns: 58px minmax(0, 1fr);
804
+ gap: 12px;
805
+ align-items: start;
806
+ }
807
+
808
+ .event-rail {
809
+ position: sticky;
810
+ top: 18px;
811
+ border: 1px solid var(--ct-line);
812
+ border-radius: 8px;
813
+ padding: 8px 0;
814
+ text-align: center;
815
+ font-family: var(--ct-mono);
816
+ font-size: 11px;
817
+ color: var(--ct-muted);
818
+ background: #0f1318;
819
+ }
820
+
821
+ .agent-rail { color: var(--ct-blue); }
822
+ .command-rail { color: var(--ct-yellow); }
823
+ .usage-rail { color: var(--ct-green); }
824
+ .todo-rail { color: var(--ct-cyan); }
825
+ .file-rail { color: #c9d1d9; }
826
+
827
+ .bubble,
828
+ .command-card,
829
+ .usage-card,
830
+ .todo-card,
831
+ .file-card,
832
+ .empty-state {
833
+ border: 1px solid var(--ct-line);
834
+ border-radius: 8px;
835
+ background: var(--ct-panel);
836
+ min-width: 0;
837
+ }
838
+
839
+ .agent-bubble {
840
+ padding: 14px 15px;
841
+ border-left: 3px solid var(--ct-blue);
842
+ }
843
+
844
+ .event-meta {
845
+ color: var(--ct-muted);
846
+ font-family: var(--ct-mono);
847
+ font-size: 12px;
848
+ margin-bottom: 8px;
849
+ }
850
+
851
+ .message-text {
852
+ white-space: pre-wrap;
853
+ overflow-wrap: anywhere;
854
+ color: var(--ct-text);
855
+ line-height: 1.55;
856
+ font-size: 14px;
857
+ }
858
+
859
+ .command-card {
860
+ overflow: hidden;
861
+ border-left: 3px solid var(--ct-yellow);
862
+ }
863
+
864
+ .command-head {
865
+ display: flex;
866
+ align-items: center;
867
+ flex-wrap: wrap;
868
+ gap: 8px;
869
+ padding: 12px 12px 0;
870
+ }
871
+
872
+ .status-badge,
873
+ .exit-badge {
874
+ font-family: var(--ct-mono);
875
+ font-size: 12px;
876
+ border-radius: 999px;
877
+ border: 1px solid var(--ct-line);
878
+ padding: 3px 8px;
879
+ }
880
+
881
+ .status-badge.success { color: var(--ct-green); border-color: rgba(63, 185, 80, .5); }
882
+ .status-badge.fail { color: var(--ct-red); border-color: rgba(248, 81, 73, .55); }
883
+ .status-badge.running { color: var(--ct-yellow); border-color: rgba(210, 153, 34, .55); }
884
+ .status-badge.neutral { color: var(--ct-muted); }
885
+
886
+ .exit-badge {
887
+ color: var(--ct-muted);
888
+ }
889
+
890
+ .code {
891
+ margin: 0;
892
+ overflow: auto;
893
+ font-family: var(--ct-mono);
894
+ font-size: 12.5px;
895
+ line-height: 1.55;
896
+ color: #dbeafe;
897
+ background: #070a0f;
898
+ }
899
+
900
+ .command-code {
901
+ margin: 12px;
902
+ padding: 12px;
903
+ border: 1px solid #1f2937;
904
+ border-radius: 8px;
905
+ }
906
+
907
+ .output-drawer {
908
+ border-top: 1px solid var(--ct-line);
909
+ }
910
+
911
+ .output-drawer summary {
912
+ cursor: pointer;
913
+ color: var(--ct-muted);
914
+ font-family: var(--ct-mono);
915
+ font-size: 12px;
916
+ padding: 10px 12px;
917
+ }
918
+
919
+ .output-code {
920
+ max-height: 520px;
921
+ padding: 12px;
922
+ border-top: 1px solid #1f2937;
923
+ }
924
+
925
+ .usage-card {
926
+ display: flex;
927
+ flex-wrap: wrap;
928
+ gap: 8px;
929
+ padding: 12px;
930
+ border-left: 3px solid var(--ct-green);
931
+ }
932
+
933
+ .usage-card span {
934
+ font-family: var(--ct-mono);
935
+ font-size: 12px;
936
+ color: var(--ct-text);
937
+ background: #0b1117;
938
+ border: 1px solid var(--ct-line);
939
+ border-radius: 999px;
940
+ padding: 4px 9px;
941
+ }
942
+
943
+ .todo-card,
944
+ .file-card {
945
+ padding: 12px;
946
+ }
947
+
948
+ .todo-card {
949
+ border-left: 3px solid var(--ct-cyan);
950
+ }
951
+
952
+ .file-card {
953
+ border-left: 3px solid #c9d1d9;
954
+ }
955
+
956
+ .todo-card ul,
957
+ .file-card ul {
958
+ list-style: none;
959
+ padding: 0;
960
+ margin: 0;
961
+ display: grid;
962
+ gap: 8px;
963
+ }
964
+
965
+ .todo-row {
966
+ display: grid;
967
+ grid-template-columns: 24px minmax(0, 1fr);
968
+ gap: 8px;
969
+ color: var(--ct-text);
970
+ line-height: 1.45;
971
+ }
972
+
973
+ .todo-check {
974
+ font-family: var(--ct-mono);
975
+ color: var(--ct-muted);
976
+ }
977
+
978
+ .todo-check.done {
979
+ color: var(--ct-green);
980
+ }
981
+
982
+ .file-card li {
983
+ display: flex;
984
+ gap: 10px;
985
+ align-items: baseline;
986
+ min-width: 0;
987
+ }
988
+
989
+ .file-kind {
990
+ color: var(--ct-yellow);
991
+ font-family: var(--ct-mono);
992
+ font-size: 12px;
993
+ min-width: 46px;
994
+ }
995
+
996
+ .file-card code {
997
+ color: var(--ct-text);
998
+ font-family: var(--ct-mono);
999
+ overflow-wrap: anywhere;
1000
+ }
1001
+
1002
+ .empty-state {
1003
+ padding: 32px;
1004
+ color: var(--ct-muted);
1005
+ }
1006
+
1007
+ .empty-state h2 {
1008
+ color: var(--ct-text);
1009
+ margin: 0 0 8px;
1010
+ font-family: var(--ct-mono);
1011
+ font-size: 18px;
1012
+ letter-spacing: 0;
1013
+ }
1014
+
1015
+ .error-panel {
1016
+ border-color: rgba(248, 81, 73, .6);
1017
+ }
1018
+
1019
+ .ct-page-info {
1020
+ color: var(--ct-muted);
1021
+ font-family: var(--ct-mono);
1022
+ font-size: 12px;
1023
+ margin: 8px 0 12px;
1024
+ }
1025
+
1026
+ .ct-pager {
1027
+ display: grid;
1028
+ grid-template-columns: 1fr 1fr;
1029
+ gap: 8px;
1030
+ }
1031
+
1032
+ @media (max-width: 860px) {
1033
+ .ct-shell {
1034
+ grid-template-columns: 1fr;
1035
+ }
1036
+ .ct-sidebar {
1037
+ position: relative;
1038
+ height: auto;
1039
+ border-right: 0;
1040
+ border-bottom: 1px solid var(--ct-line);
1041
+ }
1042
+ .ct-main {
1043
+ padding: 14px;
1044
+ }
1045
+ .event {
1046
+ grid-template-columns: 1fr;
1047
+ }
1048
+ .event-rail {
1049
+ position: relative;
1050
+ top: 0;
1051
+ width: 58px;
1052
+ }
1053
+ .summary-topline {
1054
+ flex-direction: column;
1055
+ }
1056
+ }
1057
+ """
1058
+
1059
+
1060
+ def build_app() -> gr.Blocks:
1061
+ choices = _session_choices()
1062
+ default = choices[0] if choices else None
1063
+ initial_summary, initial_transcript, initial_page_info, initial_page = render_session(default, "", 1)
1064
+
1065
+ with gr.Blocks(title=APP_TITLE, fill_width=True) as demo:
1066
+ with gr.Row(elem_id="ct-app", elem_classes=["ct-shell"]):
1067
+ with gr.Column(elem_classes=["ct-sidebar"], scale=0):
1068
+ gr.HTML(
1069
+ '<div class="ct-brand">'
1070
+ "<h1>Codex Traces</h1>"
1071
+ "<p>Private rollout.jsonl session viewer</p>"
1072
+ "</div>"
1073
+ )
1074
+ session = gr.Dropdown(
1075
+ choices=choices,
1076
+ value=default,
1077
+ label="Session file",
1078
+ interactive=True,
1079
+ allow_custom_value=False,
1080
+ )
1081
+ search = gr.Textbox(
1082
+ label="Search transcript",
1083
+ placeholder="Filter by keyword, command, output, token...",
1084
+ lines=1,
1085
+ max_lines=1,
1086
+ )
1087
+ filter_button = gr.Button("Filter", variant="primary")
1088
+ refresh_button = gr.Button("Refresh file list")
1089
+ page_state = gr.State(initial_page)
1090
+ page_info = gr.Markdown(initial_page_info, elem_classes=["ct-page-info"])
1091
+ with gr.Row(elem_classes=["ct-pager"]):
1092
+ prev_button = gr.Button("Previous")
1093
+ next_button = gr.Button("Next")
1094
+
1095
+ with gr.Column(elem_classes=["ct-main"], scale=1):
1096
+ summary = gr.HTML(initial_summary)
1097
+ transcript = gr.HTML(initial_transcript)
1098
+
1099
+ session.change(
1100
+ fn=load_session,
1101
+ inputs=session,
1102
+ outputs=[summary, transcript, page_info, page_state, search],
1103
+ api_name="load_session",
1104
+ show_progress="minimal",
1105
+ )
1106
+ filter_button.click(
1107
+ fn=filter_session,
1108
+ inputs=[session, search],
1109
+ outputs=[summary, transcript, page_info, page_state],
1110
+ api_name="filter_session",
1111
+ show_progress="minimal",
1112
+ )
1113
+ search.submit(
1114
+ fn=filter_session,
1115
+ inputs=[session, search],
1116
+ outputs=[summary, transcript, page_info, page_state],
1117
+ api_name=False,
1118
+ show_progress="minimal",
1119
+ )
1120
+ prev_button.click(
1121
+ fn=previous_page,
1122
+ inputs=[session, search, page_state],
1123
+ outputs=[summary, transcript, page_info, page_state],
1124
+ api_name="previous_page",
1125
+ show_progress="minimal",
1126
+ )
1127
+ next_button.click(
1128
+ fn=next_page,
1129
+ inputs=[session, search, page_state],
1130
+ outputs=[summary, transcript, page_info, page_state],
1131
+ api_name="next_page",
1132
+ show_progress="minimal",
1133
+ )
1134
+ refresh_button.click(
1135
+ fn=refresh_sessions,
1136
+ inputs=None,
1137
+ outputs=[session, summary, transcript, page_info, page_state, search],
1138
+ api_name="refresh_sessions",
1139
+ show_progress="minimal",
1140
+ )
1141
+
1142
+ return demo
1143
+
1144
+
1145
+ demo = build_app()
1146
+
1147
+ if __name__ == "__main__":
1148
+ demo.launch(
1149
+ theme=gr.themes.Base(
1150
+ primary_hue="blue",
1151
+ secondary_hue="cyan",
1152
+ neutral_hue="slate",
1153
+ radius_size="sm",
1154
+ ),
1155
+ css=CUSTOM_CSS,
1156
+ show_error=True,
1157
+ )
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ gradio==6.19.0
2
+ huggingface_hub==1.7.1