transcript-viewer 0.14.1__tar.gz → 0.16.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- transcript_viewer-0.14.1/README.md → transcript_viewer-0.16.0/PKG-INFO +158 -29
- transcript_viewer-0.14.1/PKG-INFO → transcript_viewer-0.16.0/README.md +143 -42
- transcript_viewer-0.16.0/TODO.md +40 -0
- transcript_viewer-0.16.0/postgresql:/postgres:x@localhost/postgres +0 -0
- transcript_viewer-0.16.0/pyproject.toml +61 -0
- {transcript_viewer-0.14.1 → transcript_viewer-0.16.0}/src/transcript_viewer/ai.py +8 -1
- transcript_viewer-0.16.0/src/transcript_viewer/cache.py +115 -0
- {transcript_viewer-0.14.1 → transcript_viewer-0.16.0}/src/transcript_viewer/cli.py +72 -2
- {transcript_viewer-0.14.1 → transcript_viewer-0.16.0}/src/transcript_viewer/config.py +10 -4
- transcript_viewer-0.16.0/src/transcript_viewer/db.py +369 -0
- transcript_viewer-0.16.0/src/transcript_viewer/deploy.py +177 -0
- {transcript_viewer-0.14.1 → transcript_viewer-0.16.0}/src/transcript_viewer/fetch.py +20 -0
- transcript_viewer-0.16.0/src/transcript_viewer/identity.py +54 -0
- transcript_viewer-0.16.0/src/transcript_viewer/ingest.py +119 -0
- transcript_viewer-0.16.0/src/transcript_viewer/library.py +301 -0
- transcript_viewer-0.16.0/src/transcript_viewer/page.html +7208 -0
- {transcript_viewer-0.14.1 → transcript_viewer-0.16.0}/src/transcript_viewer/viewer.py +231 -124
- {transcript_viewer-0.14.1 → transcript_viewer-0.16.0}/tests/page.test.js +2011 -188
- transcript_viewer-0.16.0/tests/test_authorisation.py +186 -0
- transcript_viewer-0.16.0/tests/test_cache.py +85 -0
- transcript_viewer-0.16.0/tests/test_db.py +409 -0
- transcript_viewer-0.16.0/tests/test_deploy.py +135 -0
- {transcript_viewer-0.14.1 → transcript_viewer-0.16.0}/tests/test_fetch.py +64 -0
- transcript_viewer-0.16.0/tests/test_identity.py +76 -0
- {transcript_viewer-0.14.1 → transcript_viewer-0.16.0}/tests/test_library.py +56 -21
- {transcript_viewer-0.14.1 → transcript_viewer-0.16.0}/tests/test_readme.py +5 -1
- {transcript_viewer-0.14.1 → transcript_viewer-0.16.0}/tests/test_style.py +75 -6
- transcript_viewer-0.16.0/tests/test_sync.py +95 -0
- {transcript_viewer-0.14.1 → transcript_viewer-0.16.0}/tests/test_tidiness.py +24 -0
- {transcript_viewer-0.14.1 → transcript_viewer-0.16.0}/tests/test_viewer.py +43 -22
- {transcript_viewer-0.14.1 → transcript_viewer-0.16.0}/uv.lock +87 -10
- transcript_viewer-0.14.1/pyproject.toml +0 -38
- transcript_viewer-0.14.1/src/transcript_viewer/library.py +0 -201
- transcript_viewer-0.14.1/src/transcript_viewer/page.html +0 -3463
- {transcript_viewer-0.14.1 → transcript_viewer-0.16.0}/.coverage +0 -0
- {transcript_viewer-0.14.1 → transcript_viewer-0.16.0}/.github/workflows/publish.yml +0 -0
- {transcript_viewer-0.14.1 → transcript_viewer-0.16.0}/.github/workflows/test.yml +0 -0
- {transcript_viewer-0.14.1 → transcript_viewer-0.16.0}/.gitignore +0 -0
- {transcript_viewer-0.14.1 → transcript_viewer-0.16.0}/design/Anchor.dc.html +0 -0
- {transcript_viewer-0.14.1 → transcript_viewer-0.16.0}/design/Dense.dc.html +0 -0
- {transcript_viewer-0.14.1 → transcript_viewer-0.16.0}/design/Hover.dc.html +0 -0
- {transcript_viewer-0.14.1 → transcript_viewer-0.16.0}/design/Main.dc.html +0 -0
- {transcript_viewer-0.14.1 → transcript_viewer-0.16.0}/design/Quiet.dc.html +0 -0
- {transcript_viewer-0.14.1 → transcript_viewer-0.16.0}/design/canvas.json +0 -0
- {transcript_viewer-0.14.1 → transcript_viewer-0.16.0}/design/delegation-timeline.html +0 -0
- {transcript_viewer-0.14.1 → transcript_viewer-0.16.0}/src/transcript_viewer/__init__.py +0 -0
- {transcript_viewer-0.14.1 → transcript_viewer-0.16.0}/src/transcript_viewer/corpus.py +0 -0
- {transcript_viewer-0.14.1 → transcript_viewer-0.16.0}/src/transcript_viewer/store.py +0 -0
- {transcript_viewer-0.14.1 → transcript_viewer-0.16.0}/tests/fixtures/big-command.json +0 -0
- {transcript_viewer-0.14.1 → transcript_viewer-0.16.0}/tests/test_ai.py +0 -0
- {transcript_viewer-0.14.1 → transcript_viewer-0.16.0}/tests/test_cli.py +0 -0
- {transcript_viewer-0.14.1 → transcript_viewer-0.16.0}/tests/test_config.py +0 -0
- {transcript_viewer-0.14.1 → transcript_viewer-0.16.0}/tests/test_corpus.py +0 -0
- {transcript_viewer-0.14.1 → transcript_viewer-0.16.0}/tests/test_page.py +0 -0
- {transcript_viewer-0.14.1 → transcript_viewer-0.16.0}/tests/test_store.py +0 -0
- {transcript_viewer-0.14.1 → transcript_viewer-0.16.0}/tests/test_stream.py +0 -0
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: transcript-viewer
|
|
3
|
+
Version: 0.16.0
|
|
4
|
+
Summary: Browse agent transcripts in a local, dependency-free web viewer.
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.12
|
|
7
|
+
Requires-Dist: atif-make>=0.8.0
|
|
8
|
+
Provides-Extra: ai
|
|
9
|
+
Requires-Dist: anthropic>=0.40; extra == 'ai'
|
|
10
|
+
Provides-Extra: parquet
|
|
11
|
+
Requires-Dist: atif-make[parquet]>=0.8.0; extra == 'parquet'
|
|
12
|
+
Provides-Extra: postgres
|
|
13
|
+
Requires-Dist: psycopg[binary]>=3.1; extra == 'postgres'
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
1
16
|
# transcript-viewer
|
|
2
17
|
|
|
3
18
|
Browse agent transcripts in a local web viewer — what Claude Code, Codex and
|
|
@@ -10,7 +25,20 @@ all of the reading. What is added here is only the browser interface, so a
|
|
|
10
25
|
format this cannot open is a parser missing from `atif-make` rather than
|
|
11
26
|
anything to change in the viewer.
|
|
12
27
|
|
|
13
|
-
##
|
|
28
|
+
## Run it
|
|
29
|
+
|
|
30
|
+
No install, nothing to undo:
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
uvx transcript-viewer # opens on 127.0.0.1, and that is it
|
|
34
|
+
uvx transcript-viewer path/to/session.jsonl
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`uvx` fetches it, runs it, and leaves nothing behind. It is the honest way to
|
|
38
|
+
try a tool that is going to read your agent logs — have a look first, and keep
|
|
39
|
+
it only if it earns the disk.
|
|
40
|
+
|
|
41
|
+
To keep it:
|
|
14
42
|
|
|
15
43
|
```sh
|
|
16
44
|
uv tool install transcript-viewer # pulls atif-make automatically
|
|
@@ -20,15 +48,23 @@ uv tool install "transcript-viewer[ai,parquet]" # + the optional Claude fea
|
|
|
20
48
|
|
|
21
49
|
The extras are only needed for what they name: `parquet` for a dataset that
|
|
22
50
|
ships as Parquet rather than JSON, `ai` for the summarise and ask features. The
|
|
23
|
-
viewer works without either.
|
|
51
|
+
viewer works without either. With `uvx`, ask for one with
|
|
52
|
+
`uvx --from "transcript-viewer[parquet]" transcript-viewer`.
|
|
24
53
|
|
|
25
54
|
```sh
|
|
26
55
|
transcript-viewer # the library, empty on a first run
|
|
27
56
|
transcript-viewer path/to/session.jsonl # one log
|
|
28
57
|
transcript-viewer bundle.zip # a bundle someone sent you
|
|
29
58
|
transcript-viewer --port 8080 --no-open
|
|
59
|
+
transcript-viewer sync s3://bucket/runs # take what is new, and only what is new
|
|
30
60
|
```
|
|
31
61
|
|
|
62
|
+
Everything is yours and stays here. It binds `127.0.0.1` and nothing else, the
|
|
63
|
+
library lives in `~/.transcript-viewer`, no account is involved, and nothing
|
|
64
|
+
leaves the machine unless you ask it to — a fetch you typed, or a summary you
|
|
65
|
+
pressed for with your own API key. There is no telemetry to turn off because
|
|
66
|
+
there is none to write.
|
|
67
|
+
|
|
32
68
|
Nothing appears on a first run. Sessions arrive two ways:
|
|
33
69
|
|
|
34
70
|
- **⟳ on the Local folder** finds what Claude Code, Codex and Copilot have
|
|
@@ -185,9 +221,33 @@ in the corpus this was built against were. Two bars in the same column of the
|
|
|
185
221
|
chart is what says it; overlap gets no colour of its own, since a second
|
|
186
222
|
encoding of the same fact is noise.
|
|
187
223
|
|
|
224
|
+
A subagent can delegate in turn, and in some harnesses that does not stop at two
|
|
225
|
+
levels. One that did gets a row to itself, labelled, with the number it spawned
|
|
226
|
+
and a control that opens them: its children appear beneath it, indented, packed
|
|
227
|
+
among themselves rather than against the rest of the run. The list does the same
|
|
228
|
+
with indented rows. Shut by default and remembered per subagent, so a run where
|
|
229
|
+
nothing nested — which is every run in the library this was built against — is
|
|
230
|
+
drawn exactly as it was before. The header says how deep the delegation went
|
|
231
|
+
only when it went further than one hop.
|
|
232
|
+
|
|
233
|
+
Under the chart is the shape of the delegation as numbers: every link in it
|
|
234
|
+
sorted into the three kinds it can be — from the main thread, from another
|
|
235
|
+
subagent, between subagents — to scale, with the counts and the ratio. A run is
|
|
236
|
+
not described by its subagent count alone; five spawned by the main thread is a
|
|
237
|
+
fan-out and five where two of them spawned the rest is a hierarchy, and the two
|
|
238
|
+
read identically until the links are counted. Each kind is counted rather than
|
|
239
|
+
assumed, so a format that starts recording one starts showing it here. Across
|
|
240
|
+
the library this was built against the ratio is 100 / 0 / 0 — 52 links in 17
|
|
241
|
+
transcripts, every one a call from the main thread — and 92 further subagents
|
|
242
|
+
that nothing links at all, which is the fact the line exists to surface. Beside
|
|
243
|
+
it: how many delegated in turn, the widest fan-out, and how many were spawned by
|
|
244
|
+
nothing.
|
|
245
|
+
|
|
188
246
|
Clicking a lane opens the box the transcript already has for it rather than
|
|
189
247
|
showing the same thing twice, drawing the run out far enough to reach it first:
|
|
190
|
-
in one real session every delegation happens after step 1,231.
|
|
248
|
+
in one real session every delegation happens after step 1,231. A nested subagent
|
|
249
|
+
is drawn inside its orchestrator's box, so it is the orchestrator's step that
|
|
250
|
+
has to be painted and the boxes above it are unfolded on the way.
|
|
191
251
|
|
|
192
252
|
A long run paints its first 250 steps at once and fills the rest in behind
|
|
193
253
|
itself, so there is nothing to press and nothing to wait for. The largest run
|
|
@@ -298,17 +358,17 @@ which of the two is in use, and **Remove** clears the stored one.
|
|
|
298
358
|
|
|
299
359
|
With no key configured, every AI control is hidden and the endpoint refuses.
|
|
300
360
|
|
|
301
|
-
Each transcript also has its own **With AI support** switch. Turn it off and
|
|
302
|
-
that session's controls disappear — useful when a transcript holds something that should not
|
|
303
|
-
leave the machine. The server enforces it too, so a switched-off transcript is
|
|
304
|
-
refused even if a request is made directly.
|
|
305
|
-
|
|
306
361
|
## Themes
|
|
307
362
|
|
|
308
|
-
Three
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
363
|
+
Three from Diwan — `paper`, `cool` (both light) and `dark` — and a pair,
|
|
364
|
+
`redwood` and `redwoodDark`, taken from Redwood Research's own design tokens for
|
|
365
|
+
transcripts that end up in a Redwood deck. Both come from that file as written:
|
|
366
|
+
the dark one is Redwood's own dark palette, not the light one dimmed, which is
|
|
367
|
+
why its green is `#43b184` where the light one's is `#1a7d5c`. The control in
|
|
368
|
+
the top bar cycles them, as Diwan's own header does, and the choice is
|
|
369
|
+
remembered per browser; a remembered name this build no longer has falls back to
|
|
370
|
+
`paper` rather than to an unstyled page. Note that "light and dark" is really
|
|
371
|
+
five modes here, three of them light.
|
|
312
372
|
|
|
313
373
|
## What it shows
|
|
314
374
|
|
|
@@ -406,44 +466,113 @@ an external file (`--split-subagents`) rather than an embedded trajectory, the
|
|
|
406
466
|
viewer says so instead of silently showing nothing.
|
|
407
467
|
|
|
408
468
|
Because branches sit anywhere in a trajectory that may run to thousands of
|
|
409
|
-
steps,
|
|
410
|
-
|
|
469
|
+
steps, there is an "only branches" filter. There was also a jump list above the
|
|
470
|
+
run, until the delegation box came to say everything it said and more — and to
|
|
471
|
+
disagree with it, since the list counted top-level calls while the box counts
|
|
472
|
+
every subagent at every depth. Steps render 250 at a time so
|
|
411
473
|
a 8,000-step session stays responsive.
|
|
412
474
|
|
|
475
|
+
## Running it for more than one person
|
|
476
|
+
|
|
477
|
+
Everything above is the whole tool. This section is for the one case it is not:
|
|
478
|
+
a copy of it serving a team rather than sitting on a laptop.
|
|
479
|
+
|
|
480
|
+
Nothing here is on by default, and a copy told nothing behaves exactly as the
|
|
481
|
+
one you have — loopback, nobody signed in, annotations in SQLite beside the
|
|
482
|
+
index. There is a test that walks every route asserting that, because it is what
|
|
483
|
+
keeps this one program rather than two.
|
|
484
|
+
|
|
485
|
+
A `deploy.toml` beside the library says the rest, and every setting can come
|
|
486
|
+
from the environment instead, which is what a container has:
|
|
487
|
+
|
|
488
|
+
```toml
|
|
489
|
+
[server]
|
|
490
|
+
host = "0.0.0.0"
|
|
491
|
+
|
|
492
|
+
[identity]
|
|
493
|
+
# The header whatever stands in front puts the signed-in person in. Trusted
|
|
494
|
+
# only because it is named here — a header this was not told to expect is one
|
|
495
|
+
# anybody can send.
|
|
496
|
+
header = "x-amzn-oidc-identity"
|
|
497
|
+
admins = ["you@example.com"]
|
|
498
|
+
|
|
499
|
+
[storage]
|
|
500
|
+
database = "postgresql://…" # needs transcript-viewer[postgres]
|
|
501
|
+
|
|
502
|
+
[secrets]
|
|
503
|
+
mode = "server" # the operator's keys, not each person's
|
|
504
|
+
|
|
505
|
+
[ai]
|
|
506
|
+
enabled = "off" # never send a transcript to a model
|
|
507
|
+
|
|
508
|
+
[sources]
|
|
509
|
+
urls = ["s3://bucket/runs"] # what `transcript-viewer sync` follows
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
The sign-in is not implemented here and should not be: a load balancer or a mesh
|
|
513
|
+
in front does it, and hands over one header. That is why the header must be
|
|
514
|
+
named — if the server can be reached around whatever authenticated the request,
|
|
515
|
+
anyone can claim to be anyone.
|
|
516
|
+
|
|
517
|
+
`postgres` is for this and only this. Annotations live in SQLite otherwise,
|
|
518
|
+
which is in the standard library and needs nothing installed — but two copies of
|
|
519
|
+
the server with two SQLite files means one person's stars exist on one of them
|
|
520
|
+
and not the other, which is a wrong answer rather than a slow one.
|
|
521
|
+
|
|
522
|
+
Labels are shared and stars are personal: a title, a tag or a note is the team's
|
|
523
|
+
one description of a transcript, and what you starred is yours.
|
|
524
|
+
|
|
413
525
|
## Developing alongside atif-make
|
|
414
526
|
|
|
415
|
-
The two packages are developed together
|
|
416
|
-
|
|
527
|
+
The two packages are developed together — a viewer feature usually needs the
|
|
528
|
+
parser to record the thing first — so `atif-make` resolves from the directory
|
|
529
|
+
next door rather than from the index:
|
|
417
530
|
|
|
418
|
-
```
|
|
419
|
-
|
|
531
|
+
```toml
|
|
532
|
+
[tool.uv.sources]
|
|
533
|
+
atif-make = { path = "../atif-make", editable = true }
|
|
420
534
|
```
|
|
421
535
|
|
|
422
|
-
|
|
423
|
-
|
|
536
|
+
`uv run transcript-viewer` therefore runs both working copies, with no extra
|
|
537
|
+
flags. uv drops that declaration when the package is built, so what is published
|
|
538
|
+
still depends on the version range; a checkout without `atif-make` beside it can
|
|
539
|
+
pass `--no-sources`.
|
|
540
|
+
|
|
541
|
+
To make the installed command live too:
|
|
424
542
|
|
|
425
543
|
```sh
|
|
426
|
-
uv
|
|
544
|
+
uv tool install --force --editable .
|
|
427
545
|
```
|
|
428
546
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
547
|
+
Reinstall from the index (`uv tool install --force transcript-viewer`) when you
|
|
548
|
+
want to test what users actually get.
|
|
549
|
+
|
|
550
|
+
**The page is re-read on every load; the converter is not.** `page.html` is
|
|
551
|
+
stat'ed per request, so editing it shows on a refresh. Python modules are
|
|
552
|
+
imported once and a converted trajectory is cached in memory for the life of the
|
|
553
|
+
run, so a parser change needs the server restarted. A fix that made a session's
|
|
554
|
+
delegation three levels deep looked like it had done nothing for exactly this
|
|
555
|
+
reason: the page was new, the parser in memory was not, and the viewer drew what
|
|
556
|
+
it was handed.
|
|
432
557
|
|
|
433
558
|
## Layout
|
|
434
559
|
|
|
435
560
|
```
|
|
436
561
|
src/transcript_viewer/
|
|
437
|
-
page.html the whole
|
|
562
|
+
page.html the whole interface: one page, no build step, no dependencies
|
|
438
563
|
viewer.py the HTTP server: sixteen endpoints over the page and the library
|
|
439
564
|
corpus.py the index — what is on this machine, and where it came from
|
|
440
565
|
library.py what you decide about a session: title, tags, stars, summaries
|
|
566
|
+
db.py where those decisions are kept — SQLite, or PostgreSQL when shared
|
|
567
|
+
cache.py what has been converted lately, kept only while there is room
|
|
441
568
|
fetch.py bringing transcripts in from Hugging Face, GitHub, S3 or a link
|
|
569
|
+
ingest.py what happens to them after: unpack, index, record where they came from
|
|
442
570
|
ai.py the optional Claude-backed features
|
|
443
|
-
config.py settings and tokens
|
|
571
|
+
config.py settings and tokens — what a person types in
|
|
572
|
+
deploy.py how this copy is set up — what an operator decided, before anyone arrived
|
|
573
|
+
identity.py who is asking, when anyone is
|
|
444
574
|
store.py small JSON files under ~/.transcript-viewer, written so a crash cannot truncate them
|
|
445
575
|
cli.py the command line
|
|
446
|
-
page.html the whole interface: one page, no build step, no dependencies
|
|
447
576
|
```
|
|
448
577
|
|
|
449
578
|
The page is a file rather than a string inside `viewer.py`, which is where it
|
|
@@ -486,8 +615,8 @@ trajectory pane stuck on "Converting…". Those checks live in
|
|
|
486
615
|
|
|
487
616
|
The AI tests never call the API. Most stub the model call and check the part
|
|
488
617
|
that matters when it is wrong — that nothing is sent unasked, that a summary is
|
|
489
|
-
paid for once, that a stored key never reaches a response, and that
|
|
490
|
-
|
|
618
|
+
paid for once, that a stored key never reaches a response, and that the endpoint
|
|
619
|
+
refuses outright when no credential is configured.
|
|
491
620
|
|
|
492
621
|
`tests/test_stream.py` is the exception: it drives the one function that does
|
|
493
622
|
touch the SDK, using a fake client but the SDK's real exception classes, so a
|
|
@@ -1,16 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.5
|
|
2
|
-
Name: transcript-viewer
|
|
3
|
-
Version: 0.14.1
|
|
4
|
-
Summary: Browse agent transcripts in a local, dependency-free web viewer.
|
|
5
|
-
License: MIT
|
|
6
|
-
Requires-Python: >=3.12
|
|
7
|
-
Requires-Dist: atif-make>=0.7.1
|
|
8
|
-
Provides-Extra: ai
|
|
9
|
-
Requires-Dist: anthropic>=0.40; extra == 'ai'
|
|
10
|
-
Provides-Extra: parquet
|
|
11
|
-
Requires-Dist: atif-make[parquet]>=0.5.0; extra == 'parquet'
|
|
12
|
-
Description-Content-Type: text/markdown
|
|
13
|
-
|
|
14
1
|
# transcript-viewer
|
|
15
2
|
|
|
16
3
|
Browse agent transcripts in a local web viewer — what Claude Code, Codex and
|
|
@@ -23,7 +10,20 @@ all of the reading. What is added here is only the browser interface, so a
|
|
|
23
10
|
format this cannot open is a parser missing from `atif-make` rather than
|
|
24
11
|
anything to change in the viewer.
|
|
25
12
|
|
|
26
|
-
##
|
|
13
|
+
## Run it
|
|
14
|
+
|
|
15
|
+
No install, nothing to undo:
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
uvx transcript-viewer # opens on 127.0.0.1, and that is it
|
|
19
|
+
uvx transcript-viewer path/to/session.jsonl
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`uvx` fetches it, runs it, and leaves nothing behind. It is the honest way to
|
|
23
|
+
try a tool that is going to read your agent logs — have a look first, and keep
|
|
24
|
+
it only if it earns the disk.
|
|
25
|
+
|
|
26
|
+
To keep it:
|
|
27
27
|
|
|
28
28
|
```sh
|
|
29
29
|
uv tool install transcript-viewer # pulls atif-make automatically
|
|
@@ -33,15 +33,23 @@ uv tool install "transcript-viewer[ai,parquet]" # + the optional Claude fea
|
|
|
33
33
|
|
|
34
34
|
The extras are only needed for what they name: `parquet` for a dataset that
|
|
35
35
|
ships as Parquet rather than JSON, `ai` for the summarise and ask features. The
|
|
36
|
-
viewer works without either.
|
|
36
|
+
viewer works without either. With `uvx`, ask for one with
|
|
37
|
+
`uvx --from "transcript-viewer[parquet]" transcript-viewer`.
|
|
37
38
|
|
|
38
39
|
```sh
|
|
39
40
|
transcript-viewer # the library, empty on a first run
|
|
40
41
|
transcript-viewer path/to/session.jsonl # one log
|
|
41
42
|
transcript-viewer bundle.zip # a bundle someone sent you
|
|
42
43
|
transcript-viewer --port 8080 --no-open
|
|
44
|
+
transcript-viewer sync s3://bucket/runs # take what is new, and only what is new
|
|
43
45
|
```
|
|
44
46
|
|
|
47
|
+
Everything is yours and stays here. It binds `127.0.0.1` and nothing else, the
|
|
48
|
+
library lives in `~/.transcript-viewer`, no account is involved, and nothing
|
|
49
|
+
leaves the machine unless you ask it to — a fetch you typed, or a summary you
|
|
50
|
+
pressed for with your own API key. There is no telemetry to turn off because
|
|
51
|
+
there is none to write.
|
|
52
|
+
|
|
45
53
|
Nothing appears on a first run. Sessions arrive two ways:
|
|
46
54
|
|
|
47
55
|
- **⟳ on the Local folder** finds what Claude Code, Codex and Copilot have
|
|
@@ -198,9 +206,33 @@ in the corpus this was built against were. Two bars in the same column of the
|
|
|
198
206
|
chart is what says it; overlap gets no colour of its own, since a second
|
|
199
207
|
encoding of the same fact is noise.
|
|
200
208
|
|
|
209
|
+
A subagent can delegate in turn, and in some harnesses that does not stop at two
|
|
210
|
+
levels. One that did gets a row to itself, labelled, with the number it spawned
|
|
211
|
+
and a control that opens them: its children appear beneath it, indented, packed
|
|
212
|
+
among themselves rather than against the rest of the run. The list does the same
|
|
213
|
+
with indented rows. Shut by default and remembered per subagent, so a run where
|
|
214
|
+
nothing nested — which is every run in the library this was built against — is
|
|
215
|
+
drawn exactly as it was before. The header says how deep the delegation went
|
|
216
|
+
only when it went further than one hop.
|
|
217
|
+
|
|
218
|
+
Under the chart is the shape of the delegation as numbers: every link in it
|
|
219
|
+
sorted into the three kinds it can be — from the main thread, from another
|
|
220
|
+
subagent, between subagents — to scale, with the counts and the ratio. A run is
|
|
221
|
+
not described by its subagent count alone; five spawned by the main thread is a
|
|
222
|
+
fan-out and five where two of them spawned the rest is a hierarchy, and the two
|
|
223
|
+
read identically until the links are counted. Each kind is counted rather than
|
|
224
|
+
assumed, so a format that starts recording one starts showing it here. Across
|
|
225
|
+
the library this was built against the ratio is 100 / 0 / 0 — 52 links in 17
|
|
226
|
+
transcripts, every one a call from the main thread — and 92 further subagents
|
|
227
|
+
that nothing links at all, which is the fact the line exists to surface. Beside
|
|
228
|
+
it: how many delegated in turn, the widest fan-out, and how many were spawned by
|
|
229
|
+
nothing.
|
|
230
|
+
|
|
201
231
|
Clicking a lane opens the box the transcript already has for it rather than
|
|
202
232
|
showing the same thing twice, drawing the run out far enough to reach it first:
|
|
203
|
-
in one real session every delegation happens after step 1,231.
|
|
233
|
+
in one real session every delegation happens after step 1,231. A nested subagent
|
|
234
|
+
is drawn inside its orchestrator's box, so it is the orchestrator's step that
|
|
235
|
+
has to be painted and the boxes above it are unfolded on the way.
|
|
204
236
|
|
|
205
237
|
A long run paints its first 250 steps at once and fills the rest in behind
|
|
206
238
|
itself, so there is nothing to press and nothing to wait for. The largest run
|
|
@@ -311,17 +343,17 @@ which of the two is in use, and **Remove** clears the stored one.
|
|
|
311
343
|
|
|
312
344
|
With no key configured, every AI control is hidden and the endpoint refuses.
|
|
313
345
|
|
|
314
|
-
Each transcript also has its own **With AI support** switch. Turn it off and
|
|
315
|
-
that session's controls disappear — useful when a transcript holds something that should not
|
|
316
|
-
leave the machine. The server enforces it too, so a switched-off transcript is
|
|
317
|
-
refused even if a request is made directly.
|
|
318
|
-
|
|
319
346
|
## Themes
|
|
320
347
|
|
|
321
|
-
Three
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
348
|
+
Three from Diwan — `paper`, `cool` (both light) and `dark` — and a pair,
|
|
349
|
+
`redwood` and `redwoodDark`, taken from Redwood Research's own design tokens for
|
|
350
|
+
transcripts that end up in a Redwood deck. Both come from that file as written:
|
|
351
|
+
the dark one is Redwood's own dark palette, not the light one dimmed, which is
|
|
352
|
+
why its green is `#43b184` where the light one's is `#1a7d5c`. The control in
|
|
353
|
+
the top bar cycles them, as Diwan's own header does, and the choice is
|
|
354
|
+
remembered per browser; a remembered name this build no longer has falls back to
|
|
355
|
+
`paper` rather than to an unstyled page. Note that "light and dark" is really
|
|
356
|
+
five modes here, three of them light.
|
|
325
357
|
|
|
326
358
|
## What it shows
|
|
327
359
|
|
|
@@ -419,44 +451,113 @@ an external file (`--split-subagents`) rather than an embedded trajectory, the
|
|
|
419
451
|
viewer says so instead of silently showing nothing.
|
|
420
452
|
|
|
421
453
|
Because branches sit anywhere in a trajectory that may run to thousands of
|
|
422
|
-
steps,
|
|
423
|
-
|
|
454
|
+
steps, there is an "only branches" filter. There was also a jump list above the
|
|
455
|
+
run, until the delegation box came to say everything it said and more — and to
|
|
456
|
+
disagree with it, since the list counted top-level calls while the box counts
|
|
457
|
+
every subagent at every depth. Steps render 250 at a time so
|
|
424
458
|
a 8,000-step session stays responsive.
|
|
425
459
|
|
|
460
|
+
## Running it for more than one person
|
|
461
|
+
|
|
462
|
+
Everything above is the whole tool. This section is for the one case it is not:
|
|
463
|
+
a copy of it serving a team rather than sitting on a laptop.
|
|
464
|
+
|
|
465
|
+
Nothing here is on by default, and a copy told nothing behaves exactly as the
|
|
466
|
+
one you have — loopback, nobody signed in, annotations in SQLite beside the
|
|
467
|
+
index. There is a test that walks every route asserting that, because it is what
|
|
468
|
+
keeps this one program rather than two.
|
|
469
|
+
|
|
470
|
+
A `deploy.toml` beside the library says the rest, and every setting can come
|
|
471
|
+
from the environment instead, which is what a container has:
|
|
472
|
+
|
|
473
|
+
```toml
|
|
474
|
+
[server]
|
|
475
|
+
host = "0.0.0.0"
|
|
476
|
+
|
|
477
|
+
[identity]
|
|
478
|
+
# The header whatever stands in front puts the signed-in person in. Trusted
|
|
479
|
+
# only because it is named here — a header this was not told to expect is one
|
|
480
|
+
# anybody can send.
|
|
481
|
+
header = "x-amzn-oidc-identity"
|
|
482
|
+
admins = ["you@example.com"]
|
|
483
|
+
|
|
484
|
+
[storage]
|
|
485
|
+
database = "postgresql://…" # needs transcript-viewer[postgres]
|
|
486
|
+
|
|
487
|
+
[secrets]
|
|
488
|
+
mode = "server" # the operator's keys, not each person's
|
|
489
|
+
|
|
490
|
+
[ai]
|
|
491
|
+
enabled = "off" # never send a transcript to a model
|
|
492
|
+
|
|
493
|
+
[sources]
|
|
494
|
+
urls = ["s3://bucket/runs"] # what `transcript-viewer sync` follows
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
The sign-in is not implemented here and should not be: a load balancer or a mesh
|
|
498
|
+
in front does it, and hands over one header. That is why the header must be
|
|
499
|
+
named — if the server can be reached around whatever authenticated the request,
|
|
500
|
+
anyone can claim to be anyone.
|
|
501
|
+
|
|
502
|
+
`postgres` is for this and only this. Annotations live in SQLite otherwise,
|
|
503
|
+
which is in the standard library and needs nothing installed — but two copies of
|
|
504
|
+
the server with two SQLite files means one person's stars exist on one of them
|
|
505
|
+
and not the other, which is a wrong answer rather than a slow one.
|
|
506
|
+
|
|
507
|
+
Labels are shared and stars are personal: a title, a tag or a note is the team's
|
|
508
|
+
one description of a transcript, and what you starred is yours.
|
|
509
|
+
|
|
426
510
|
## Developing alongside atif-make
|
|
427
511
|
|
|
428
|
-
The two packages are developed together
|
|
429
|
-
|
|
512
|
+
The two packages are developed together — a viewer feature usually needs the
|
|
513
|
+
parser to record the thing first — so `atif-make` resolves from the directory
|
|
514
|
+
next door rather than from the index:
|
|
430
515
|
|
|
431
|
-
```
|
|
432
|
-
|
|
516
|
+
```toml
|
|
517
|
+
[tool.uv.sources]
|
|
518
|
+
atif-make = { path = "../atif-make", editable = true }
|
|
433
519
|
```
|
|
434
520
|
|
|
435
|
-
|
|
436
|
-
|
|
521
|
+
`uv run transcript-viewer` therefore runs both working copies, with no extra
|
|
522
|
+
flags. uv drops that declaration when the package is built, so what is published
|
|
523
|
+
still depends on the version range; a checkout without `atif-make` beside it can
|
|
524
|
+
pass `--no-sources`.
|
|
525
|
+
|
|
526
|
+
To make the installed command live too:
|
|
437
527
|
|
|
438
528
|
```sh
|
|
439
|
-
uv
|
|
529
|
+
uv tool install --force --editable .
|
|
440
530
|
```
|
|
441
531
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
532
|
+
Reinstall from the index (`uv tool install --force transcript-viewer`) when you
|
|
533
|
+
want to test what users actually get.
|
|
534
|
+
|
|
535
|
+
**The page is re-read on every load; the converter is not.** `page.html` is
|
|
536
|
+
stat'ed per request, so editing it shows on a refresh. Python modules are
|
|
537
|
+
imported once and a converted trajectory is cached in memory for the life of the
|
|
538
|
+
run, so a parser change needs the server restarted. A fix that made a session's
|
|
539
|
+
delegation three levels deep looked like it had done nothing for exactly this
|
|
540
|
+
reason: the page was new, the parser in memory was not, and the viewer drew what
|
|
541
|
+
it was handed.
|
|
445
542
|
|
|
446
543
|
## Layout
|
|
447
544
|
|
|
448
545
|
```
|
|
449
546
|
src/transcript_viewer/
|
|
450
|
-
page.html the whole
|
|
547
|
+
page.html the whole interface: one page, no build step, no dependencies
|
|
451
548
|
viewer.py the HTTP server: sixteen endpoints over the page and the library
|
|
452
549
|
corpus.py the index — what is on this machine, and where it came from
|
|
453
550
|
library.py what you decide about a session: title, tags, stars, summaries
|
|
551
|
+
db.py where those decisions are kept — SQLite, or PostgreSQL when shared
|
|
552
|
+
cache.py what has been converted lately, kept only while there is room
|
|
454
553
|
fetch.py bringing transcripts in from Hugging Face, GitHub, S3 or a link
|
|
554
|
+
ingest.py what happens to them after: unpack, index, record where they came from
|
|
455
555
|
ai.py the optional Claude-backed features
|
|
456
|
-
config.py settings and tokens
|
|
556
|
+
config.py settings and tokens — what a person types in
|
|
557
|
+
deploy.py how this copy is set up — what an operator decided, before anyone arrived
|
|
558
|
+
identity.py who is asking, when anyone is
|
|
457
559
|
store.py small JSON files under ~/.transcript-viewer, written so a crash cannot truncate them
|
|
458
560
|
cli.py the command line
|
|
459
|
-
page.html the whole interface: one page, no build step, no dependencies
|
|
460
561
|
```
|
|
461
562
|
|
|
462
563
|
The page is a file rather than a string inside `viewer.py`, which is where it
|
|
@@ -499,8 +600,8 @@ trajectory pane stuck on "Converting…". Those checks live in
|
|
|
499
600
|
|
|
500
601
|
The AI tests never call the API. Most stub the model call and check the part
|
|
501
602
|
that matters when it is wrong — that nothing is sent unasked, that a summary is
|
|
502
|
-
paid for once, that a stored key never reaches a response, and that
|
|
503
|
-
|
|
603
|
+
paid for once, that a stored key never reaches a response, and that the endpoint
|
|
604
|
+
refuses outright when no credential is configured.
|
|
504
605
|
|
|
505
606
|
`tests/test_stream.py` is the exception: it drives the one function that does
|
|
506
607
|
touch the SDK, using a fake client but the SDK's real exception classes, so a
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Deliberately not built yet
|
|
2
|
+
|
|
3
|
+
Each of these was considered while building something adjacent and left out on
|
|
4
|
+
purpose. The note says what is missing and what would make it worth doing, so a
|
|
5
|
+
later reader can tell a decision from an oversight.
|
|
6
|
+
|
|
7
|
+
## Inter-agent communication, drawn as arrows
|
|
8
|
+
|
|
9
|
+
The delegation box counts the three kinds of link and shows the ratio
|
|
10
|
+
(`laneShape`). What it does not do is *draw* a sideways link — a subagent
|
|
11
|
+
addressing one that is not its own child would be an arrow between two bars on
|
|
12
|
+
the timeline, and there is nowhere for one to go in the current layout.
|
|
13
|
+
|
|
14
|
+
Worth building when a transcript has one. Across the whole library today the
|
|
15
|
+
ratio reads 100 / 0 / 0 — 52 links in 17 transcripts, all of them a call from
|
|
16
|
+
the main thread — so an arrow layer would have nothing to draw. The count is
|
|
17
|
+
computed rather than assumed, so the day a format records a sibling message the
|
|
18
|
+
number moves on its own and the arrows become the missing half.
|
|
19
|
+
|
|
20
|
+
## Summarising what a subagent did
|
|
21
|
+
|
|
22
|
+
The delegation box says how many steps a subagent took, how long it ran, and
|
|
23
|
+
which tools it reached for. It does not say what it concluded. Doing that
|
|
24
|
+
honestly means either reading the final assistant turn — which is often a wall
|
|
25
|
+
of text — or generating a summary, which the AI features could do but which
|
|
26
|
+
would then have to be marked as generated everywhere it appears.
|
|
27
|
+
|
|
28
|
+
## Fleet scale
|
|
29
|
+
|
|
30
|
+
The timeline is designed for the runs in the library: tens of subagents over
|
|
31
|
+
hours. Reports of runs with **115 agents over ~1,170 minutes** are the case it
|
|
32
|
+
is not designed for. Two things break first:
|
|
33
|
+
|
|
34
|
+
- packed rows stop compressing, because at that many concurrent agents almost
|
|
35
|
+
everything overlaps something;
|
|
36
|
+
- the 7px minimum bar width (`.bar{min-width:7px}`) stops being a floor and
|
|
37
|
+
starts being most of the chart.
|
|
38
|
+
|
|
39
|
+
Both point at the same missing thing: **zoom**, and a way to collapse a group of
|
|
40
|
+
lanes into one summary lane. Neither is a tweak to the current layout.
|
|
Binary file
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "transcript-viewer"
|
|
3
|
+
version = "0.16.0"
|
|
4
|
+
description = "Browse agent transcripts in a local, dependency-free web viewer."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
license = { text = "MIT" }
|
|
8
|
+
# The only dependency is the converter. The viewer itself is stdlib http.server
|
|
9
|
+
# plus one self-contained HTML page.
|
|
10
|
+
# The floor is not politeness: this viewer imports atif_make.container, which
|
|
11
|
+
# is how a benchmark published as one JSON array of a thousand runs becomes a
|
|
12
|
+
# thousand sessions. An older atif-make fails to import at all.
|
|
13
|
+
# 0.8.0 is where a Claude Code session that delegated more than one level deep
|
|
14
|
+
# started converting to a tree. Against an older one the chart still draws, and
|
|
15
|
+
# draws a flat row of subagents that nothing spawned — which reads as the
|
|
16
|
+
# viewer being broken rather than the converter being old.
|
|
17
|
+
dependencies = ["atif-make>=0.8.0"]
|
|
18
|
+
|
|
19
|
+
[project.optional-dependencies]
|
|
20
|
+
# Claude-backed explanations are opt-in, so the default install stays
|
|
21
|
+
# dependency-free: pip install "transcript-viewer[ai]"
|
|
22
|
+
# Datasets published as Parquet — METR's transcripts are the one so far. The
|
|
23
|
+
# reader lives in atif-make, which only needs it to split a shard.
|
|
24
|
+
parquet = ["atif-make[parquet]>=0.8.0"]
|
|
25
|
+
ai = ["anthropic>=0.40"]
|
|
26
|
+
# Only for a deployment serving more than one person. Two copies of the server
|
|
27
|
+
# with two SQLite files means one person's stars exist on one and not the other,
|
|
28
|
+
# which is a wrong answer rather than a slow one.
|
|
29
|
+
postgres = ["psycopg[binary]>=3.1"]
|
|
30
|
+
|
|
31
|
+
[project.scripts]
|
|
32
|
+
transcript-viewer = "transcript_viewer.cli:main"
|
|
33
|
+
|
|
34
|
+
[dependency-groups]
|
|
35
|
+
dev = ["pytest>=8.0"]
|
|
36
|
+
|
|
37
|
+
# Develop against the converter in the next directory, not the release.
|
|
38
|
+
#
|
|
39
|
+
# The two repos change together — a viewer feature usually needs the parser to
|
|
40
|
+
# record the thing first — and resolving `atif-make` from PyPI meant every such
|
|
41
|
+
# change looked like it had failed. A parser fix that made a session's
|
|
42
|
+
# delegation three levels deep was invisible here for exactly that reason: the
|
|
43
|
+
# page was local, the converter was 0.7.1, and the viewer faithfully drew what
|
|
44
|
+
# the old parser handed it.
|
|
45
|
+
#
|
|
46
|
+
# uv drops `tool.uv.sources` when this package is built, so what is published
|
|
47
|
+
# still depends on the version range above. A checkout without atif-make beside
|
|
48
|
+
# it can pass `--no-sources`.
|
|
49
|
+
[tool.uv.sources]
|
|
50
|
+
atif-make = { path = "../atif-make", editable = true }
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
[build-system]
|
|
54
|
+
requires = ["hatchling"]
|
|
55
|
+
build-backend = "hatchling.build"
|
|
56
|
+
|
|
57
|
+
[tool.hatch.build.targets.wheel]
|
|
58
|
+
packages = ["src/transcript_viewer"]
|
|
59
|
+
|
|
60
|
+
[tool.pytest.ini_options]
|
|
61
|
+
testpaths = ["tests"]
|
|
@@ -17,7 +17,7 @@ import re
|
|
|
17
17
|
from collections.abc import Iterator
|
|
18
18
|
from typing import Any
|
|
19
19
|
|
|
20
|
-
from . import config
|
|
20
|
+
from . import config, deploy
|
|
21
21
|
|
|
22
22
|
MODEL = "claude-opus-5"
|
|
23
23
|
|
|
@@ -57,7 +57,14 @@ def status() -> tuple[bool, str]:
|
|
|
57
57
|
|
|
58
58
|
A bare boolean was not enough: a saved key with no SDK installed looks
|
|
59
59
|
exactly like no key at all, which reads as "the save didn't work".
|
|
60
|
+
|
|
61
|
+
A deployment that has turned this off is answered first and without
|
|
62
|
+
qualification. Everything below is about whether a call *could* be made;
|
|
63
|
+
this is about whether it may be, and the two must not be confused — a
|
|
64
|
+
refusal that reads like a missing dependency invites someone to install it.
|
|
60
65
|
"""
|
|
66
|
+
if deploy.current().ai != "on":
|
|
67
|
+
return False, "this viewer does not send transcripts to a model"
|
|
61
68
|
try:
|
|
62
69
|
_client()
|
|
63
70
|
except Unavailable as exc:
|