transcript-viewer 0.11.3__tar.gz → 0.13.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.11.3/README.md → transcript_viewer-0.13.0/PKG-INFO +30 -3
- transcript_viewer-0.11.3/PKG-INFO → transcript_viewer-0.13.0/README.md +17 -16
- {transcript_viewer-0.11.3 → transcript_viewer-0.13.0}/pyproject.toml +1 -1
- {transcript_viewer-0.11.3 → transcript_viewer-0.13.0}/src/transcript_viewer/page.html +112 -22
- {transcript_viewer-0.11.3 → transcript_viewer-0.13.0}/tests/page.test.js +115 -0
- {transcript_viewer-0.11.3 → transcript_viewer-0.13.0}/uv.lock +1 -1
- {transcript_viewer-0.11.3 → transcript_viewer-0.13.0}/.coverage +0 -0
- {transcript_viewer-0.11.3 → transcript_viewer-0.13.0}/.github/workflows/publish.yml +0 -0
- {transcript_viewer-0.11.3 → transcript_viewer-0.13.0}/.github/workflows/test.yml +0 -0
- {transcript_viewer-0.11.3 → transcript_viewer-0.13.0}/.gitignore +0 -0
- {transcript_viewer-0.11.3 → transcript_viewer-0.13.0}/design/Anchor.dc.html +0 -0
- {transcript_viewer-0.11.3 → transcript_viewer-0.13.0}/design/Dense.dc.html +0 -0
- {transcript_viewer-0.11.3 → transcript_viewer-0.13.0}/design/Hover.dc.html +0 -0
- {transcript_viewer-0.11.3 → transcript_viewer-0.13.0}/design/Main.dc.html +0 -0
- {transcript_viewer-0.11.3 → transcript_viewer-0.13.0}/design/Quiet.dc.html +0 -0
- {transcript_viewer-0.11.3 → transcript_viewer-0.13.0}/design/canvas.json +0 -0
- {transcript_viewer-0.11.3 → transcript_viewer-0.13.0}/design/delegation-timeline.html +0 -0
- {transcript_viewer-0.11.3 → transcript_viewer-0.13.0}/src/transcript_viewer/__init__.py +0 -0
- {transcript_viewer-0.11.3 → transcript_viewer-0.13.0}/src/transcript_viewer/ai.py +0 -0
- {transcript_viewer-0.11.3 → transcript_viewer-0.13.0}/src/transcript_viewer/cli.py +0 -0
- {transcript_viewer-0.11.3 → transcript_viewer-0.13.0}/src/transcript_viewer/config.py +0 -0
- {transcript_viewer-0.11.3 → transcript_viewer-0.13.0}/src/transcript_viewer/corpus.py +0 -0
- {transcript_viewer-0.11.3 → transcript_viewer-0.13.0}/src/transcript_viewer/fetch.py +0 -0
- {transcript_viewer-0.11.3 → transcript_viewer-0.13.0}/src/transcript_viewer/library.py +0 -0
- {transcript_viewer-0.11.3 → transcript_viewer-0.13.0}/src/transcript_viewer/store.py +0 -0
- {transcript_viewer-0.11.3 → transcript_viewer-0.13.0}/src/transcript_viewer/viewer.py +0 -0
- {transcript_viewer-0.11.3 → transcript_viewer-0.13.0}/tests/fixtures/big-command.json +0 -0
- {transcript_viewer-0.11.3 → transcript_viewer-0.13.0}/tests/test_ai.py +0 -0
- {transcript_viewer-0.11.3 → transcript_viewer-0.13.0}/tests/test_cli.py +0 -0
- {transcript_viewer-0.11.3 → transcript_viewer-0.13.0}/tests/test_config.py +0 -0
- {transcript_viewer-0.11.3 → transcript_viewer-0.13.0}/tests/test_corpus.py +0 -0
- {transcript_viewer-0.11.3 → transcript_viewer-0.13.0}/tests/test_fetch.py +0 -0
- {transcript_viewer-0.11.3 → transcript_viewer-0.13.0}/tests/test_library.py +0 -0
- {transcript_viewer-0.11.3 → transcript_viewer-0.13.0}/tests/test_page.py +0 -0
- {transcript_viewer-0.11.3 → transcript_viewer-0.13.0}/tests/test_readme.py +0 -0
- {transcript_viewer-0.11.3 → transcript_viewer-0.13.0}/tests/test_store.py +0 -0
- {transcript_viewer-0.11.3 → transcript_viewer-0.13.0}/tests/test_stream.py +0 -0
- {transcript_viewer-0.11.3 → transcript_viewer-0.13.0}/tests/test_style.py +0 -0
- {transcript_viewer-0.11.3 → transcript_viewer-0.13.0}/tests/test_tidiness.py +0 -0
- {transcript_viewer-0.11.3 → transcript_viewer-0.13.0}/tests/test_viewer.py +0 -0
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: transcript-viewer
|
|
3
|
+
Version: 0.13.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.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
|
+
|
|
1
14
|
# transcript-viewer
|
|
2
15
|
|
|
3
16
|
Browse agent transcripts in a local web viewer — what Claude Code, Codex and
|
|
@@ -167,7 +180,10 @@ If that directory is still there, the first run moves it to
|
|
|
167
180
|
so. Nothing is left behind and nothing is copied twice.
|
|
168
181
|
|
|
169
182
|
A run that delegated shows a **timeline** above its steps: one lane per
|
|
170
|
-
subagent, laid out on the clock.
|
|
183
|
+
subagent, laid out on the clock. A lane is called what the agent was asked to
|
|
184
|
+
do — "Review backend for bugs and DRY" rather than "general-purpose" — wherever
|
|
185
|
+
that was written down, which is 50 of the 53 Claude Code subagents here; the
|
|
186
|
+
kind is the fallback and the tooltip. It exists for the one thing a nested box
|
|
171
187
|
cannot say — that two subagents were running at the same moment, which 25 pairs
|
|
172
188
|
in the corpus this was built against were. Two bars in the same column of the
|
|
173
189
|
chart is what says it; overlap gets no colour of its own, since a second
|
|
@@ -175,8 +191,13 @@ encoding of the same fact is noise.
|
|
|
175
191
|
|
|
176
192
|
Clicking a lane opens the box the transcript already has for it rather than
|
|
177
193
|
showing the same thing twice, drawing the run out far enough to reach it first:
|
|
178
|
-
|
|
179
|
-
|
|
194
|
+
in one real session every delegation happens after step 1,231.
|
|
195
|
+
|
|
196
|
+
A long run paints its first 250 steps at once and fills the rest in behind
|
|
197
|
+
itself, so there is nothing to press and nothing to wait for. The largest run
|
|
198
|
+
here is 8,445 steps and 34 MB of markup, which is a second of a blank page if
|
|
199
|
+
it is drawn in one go, so the rest arrives in the gaps between frames and the
|
|
200
|
+
reader keeps their place.
|
|
180
201
|
|
|
181
202
|
Where a session spans days but holds hours of work — one runs 10.3 days and
|
|
182
203
|
contains 5.3 hours, with 31 gaps over an hour — the quiet stretches are skipped
|
|
@@ -186,6 +207,12 @@ that floor its width is not its duration; the card carries the real one. A
|
|
|
186
207
|
transcript with no timestamps lays the same lanes out by step instead and says
|
|
187
208
|
so, and one that delegated nothing shows no timeline at all.
|
|
188
209
|
|
|
210
|
+
A transcript has an address: opening one puts its key in the URL, so a refresh
|
|
211
|
+
comes back to it and a link can be sent to someone with the same transcript.
|
|
212
|
+
The key is the hash of the file's contents, so it is the same key on another
|
|
213
|
+
machine holding the same file, and back and forward move between the library
|
|
214
|
+
and a run.
|
|
215
|
+
|
|
189
216
|
**Clear library** above the tree forgets every session at once, after saying
|
|
190
217
|
how many and what it will touch. Files are left alone by the same rule as a
|
|
191
218
|
single removal — only copies the viewer made itself are deleted — and stored
|
|
@@ -1,16 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.5
|
|
2
|
-
Name: transcript-viewer
|
|
3
|
-
Version: 0.11.3
|
|
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
|
|
@@ -180,7 +167,10 @@ If that directory is still there, the first run moves it to
|
|
|
180
167
|
so. Nothing is left behind and nothing is copied twice.
|
|
181
168
|
|
|
182
169
|
A run that delegated shows a **timeline** above its steps: one lane per
|
|
183
|
-
subagent, laid out on the clock.
|
|
170
|
+
subagent, laid out on the clock. A lane is called what the agent was asked to
|
|
171
|
+
do — "Review backend for bugs and DRY" rather than "general-purpose" — wherever
|
|
172
|
+
that was written down, which is 50 of the 53 Claude Code subagents here; the
|
|
173
|
+
kind is the fallback and the tooltip. It exists for the one thing a nested box
|
|
184
174
|
cannot say — that two subagents were running at the same moment, which 25 pairs
|
|
185
175
|
in the corpus this was built against were. Two bars in the same column of the
|
|
186
176
|
chart is what says it; overlap gets no colour of its own, since a second
|
|
@@ -188,8 +178,13 @@ encoding of the same fact is noise.
|
|
|
188
178
|
|
|
189
179
|
Clicking a lane opens the box the transcript already has for it rather than
|
|
190
180
|
showing the same thing twice, drawing the run out far enough to reach it first:
|
|
191
|
-
|
|
192
|
-
|
|
181
|
+
in one real session every delegation happens after step 1,231.
|
|
182
|
+
|
|
183
|
+
A long run paints its first 250 steps at once and fills the rest in behind
|
|
184
|
+
itself, so there is nothing to press and nothing to wait for. The largest run
|
|
185
|
+
here is 8,445 steps and 34 MB of markup, which is a second of a blank page if
|
|
186
|
+
it is drawn in one go, so the rest arrives in the gaps between frames and the
|
|
187
|
+
reader keeps their place.
|
|
193
188
|
|
|
194
189
|
Where a session spans days but holds hours of work — one runs 10.3 days and
|
|
195
190
|
contains 5.3 hours, with 31 gaps over an hour — the quiet stretches are skipped
|
|
@@ -199,6 +194,12 @@ that floor its width is not its duration; the card carries the real one. A
|
|
|
199
194
|
transcript with no timestamps lays the same lanes out by step instead and says
|
|
200
195
|
so, and one that delegated nothing shows no timeline at all.
|
|
201
196
|
|
|
197
|
+
A transcript has an address: opening one puts its key in the URL, so a refresh
|
|
198
|
+
comes back to it and a link can be sent to someone with the same transcript.
|
|
199
|
+
The key is the hash of the file's contents, so it is the same key on another
|
|
200
|
+
machine holding the same file, and back and forward move between the library
|
|
201
|
+
and a run.
|
|
202
|
+
|
|
202
203
|
**Clear library** above the tree forgets every session at once, after saying
|
|
203
204
|
how many and what it will touch. Files are left alone by the same rule as a
|
|
204
205
|
single removal — only copies the viewer made itself are deleted — and stored
|
|
@@ -387,15 +387,20 @@ pre{background:var(--sunk);border-radius:7px;padding:10px;overflow:auto;font-siz
|
|
|
387
387
|
#lanes>summary .fine{font:11px var(--font-mono);color:var(--muted)}
|
|
388
388
|
#lanes>summary .fine em{font-style:normal;color:var(--accent)}
|
|
389
389
|
.lanes-body{padding:2px 16px 12px;border-top:1px solid var(--line)}
|
|
390
|
-
.lane{display:grid;grid-template-columns:
|
|
391
|
-
|
|
390
|
+
.lane{display:grid;grid-template-columns:196px 1fr;gap:0 12px;align-items:center}
|
|
391
|
+
/* Denser than the roomy version, but still a row you can aim at: thirty of
|
|
392
|
+
these is a screenful, and a 13px row with an 8px bar read as a scatter
|
|
393
|
+
plot rather than a set of lanes. */
|
|
394
|
+
.lane.tight{grid-template-columns:196px 1fr;gap:0 12px}
|
|
392
395
|
.lane-name{display:flex;align-items:baseline;gap:7px;min-width:0}
|
|
393
396
|
.lane-name .kind{font:12px var(--font-mono);color:var(--accent);overflow:hidden;
|
|
394
397
|
text-overflow:ellipsis;white-space:nowrap}
|
|
398
|
+
/* A task the agent was given is prose, not an identifier. */
|
|
399
|
+
.lane-name .kind.named{font:12px var(--font-ui);color:var(--ink)}
|
|
395
400
|
.lane-name .kind.main{color:var(--ink);font-weight:600}
|
|
396
401
|
.lane-name .n{font:10px var(--font-mono);color:var(--faint);margin-left:auto;flex:none}
|
|
397
402
|
.track{position:relative;height:22px}
|
|
398
|
-
.lane.tight .track{height:
|
|
403
|
+
.lane.tight .track{height:19px}
|
|
399
404
|
.track .rule{position:absolute;left:0;right:0;top:50%;height:1px;background:var(--line)}
|
|
400
405
|
.track .tick{position:absolute;top:0;bottom:0;width:1px;background:var(--line);opacity:.45}
|
|
401
406
|
/* A floor, so a four-minute subagent in a thirty-hour run is still a target.
|
|
@@ -403,24 +408,26 @@ pre{background:var(--sunk);border-radius:7px;padding:10px;overflow:auto;font-siz
|
|
|
403
408
|
the tooltip carry the real one. */
|
|
404
409
|
.bar{position:absolute;top:50%;transform:translateY(-50%);height:14px;min-width:7px;
|
|
405
410
|
border-radius:3px;background:var(--accent);opacity:.85;cursor:pointer;border:0;padding:0}
|
|
406
|
-
.lane.tight .bar{height:
|
|
411
|
+
.lane.tight .bar{height:11px}
|
|
407
412
|
.bar:hover,.bar:focus-visible{opacity:1;outline:1px solid var(--ink);outline-offset:1px}
|
|
408
413
|
.bar.main{background:var(--ink);opacity:.85;cursor:default}
|
|
409
414
|
.bar.main:hover{outline:none;opacity:.85}
|
|
410
|
-
.lane-axis{display:grid;grid-template-columns:
|
|
411
|
-
.lane-axis.tight{grid-template-columns:
|
|
415
|
+
.lane-axis{display:grid;grid-template-columns:196px 1fr;gap:0 12px;margin-bottom:2px}
|
|
416
|
+
.lane-axis.tight{grid-template-columns:196px 1fr;gap:0 12px}
|
|
412
417
|
.lane-axis .marks{position:relative;height:13px}
|
|
413
418
|
.lane-axis .marks span{position:absolute;transform:translateX(-50%);
|
|
414
419
|
font:10px var(--font-mono);color:var(--faint);white-space:nowrap}
|
|
415
420
|
/* The bands and the axis breaks sit behind every lane at once, drawn once
|
|
416
421
|
rather than repeated into each row. */
|
|
417
422
|
.lanes-tracks{position:relative}
|
|
418
|
-
.bandlayer{position:absolute;left:
|
|
419
|
-
.lane.tight ~ .bandlayer,.lanes-tracks:has(.lane.tight) .bandlayer{left:
|
|
420
|
-
.
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
423
|
+
.bandlayer{position:absolute;left:208px;right:0;top:0;bottom:0;pointer-events:none}
|
|
424
|
+
.lane.tight ~ .bandlayer,.lanes-tracks:has(.lane.tight) .bandlayer{left:208px}
|
|
425
|
+
/* Where the axis was cut. One thin line, and only for the cuts worth seeing —
|
|
426
|
+
one session skips 152 quiet stretches, and marking every one turned the
|
|
427
|
+
whole chart into vertical stripes. The rest are still skipped; they are
|
|
428
|
+
simply not each announced. */
|
|
429
|
+
.gapmark{position:absolute;top:0;bottom:0;width:0;
|
|
430
|
+
border-left:1px dashed var(--line);opacity:.7}
|
|
424
431
|
/* Conversations that belong to the record rather than to any step. */
|
|
425
432
|
.loose{margin-top:18px;padding-top:12px;border-top:1px solid var(--line)}
|
|
426
433
|
.loose-head{font:11px var(--font-mono);color:var(--muted);margin-bottom:8px}
|
|
@@ -1304,6 +1311,9 @@ function toggleSide(){
|
|
|
1304
1311
|
// Restore the last choice; localStorage can throw in restricted contexts.
|
|
1305
1312
|
try{if(localStorage.getItem("transcript-viewer.side")==="1")toggleSide()}catch(e){}
|
|
1306
1313
|
|
|
1314
|
+
/* Back and forward move between the library and a transcript. */
|
|
1315
|
+
addEventListener("popstate",followAddress);
|
|
1316
|
+
|
|
1307
1317
|
addEventListener("keydown",e=>{
|
|
1308
1318
|
// Ignore the shortcut while typing in the filter box.
|
|
1309
1319
|
if(e.key==="\\"&&e.target.tagName!=="INPUT"){e.preventDefault();toggleSide()}
|
|
@@ -1370,7 +1380,13 @@ async function openLibrary(){
|
|
|
1370
1380
|
bootSay(`Drawing ${num(INDEX.length)} sessions…`,0,0);
|
|
1371
1381
|
// Let that line paint before the work that blocks the thread.
|
|
1372
1382
|
await new Promise(r=>requestAnimationFrame(()=>requestAnimationFrame(r)));
|
|
1373
|
-
showCount();drawList();
|
|
1383
|
+
showCount();drawList();
|
|
1384
|
+
// The address says what to show. Without this a refresh lands in the
|
|
1385
|
+
// library however specific the link was.
|
|
1386
|
+
const asked=addressed();
|
|
1387
|
+
if(asked&&INDEX.some(e=>e.key===asked))pick(asked,false);
|
|
1388
|
+
else{ if(asked)note("That transcript is not in this library.");
|
|
1389
|
+
setAddress(null,false); showLibrary(); }
|
|
1374
1390
|
}catch(err){
|
|
1375
1391
|
// Leave it on screen: a blank window would say nothing at all.
|
|
1376
1392
|
bootSay("Could not open your library — "+((err&&err.message)||err),0,0);
|
|
@@ -1619,7 +1635,7 @@ function showLibrary(){
|
|
|
1619
1635
|
</div>`;
|
|
1620
1636
|
return;
|
|
1621
1637
|
}
|
|
1622
|
-
cur=null;drawCrumb();
|
|
1638
|
+
cur=null;setAddress(null,false);drawCrumb();
|
|
1623
1639
|
const rows=libraryRows();
|
|
1624
1640
|
main.innerHTML=`
|
|
1625
1641
|
<div class="lhead">
|
|
@@ -2266,17 +2282,74 @@ function star(key){
|
|
|
2266
2282
|
annotate(key,{starred:!entry.starred});
|
|
2267
2283
|
}
|
|
2268
2284
|
|
|
2269
|
-
|
|
2270
|
-
|
|
2285
|
+
/* Which transcript the address names, if any.
|
|
2286
|
+
|
|
2287
|
+
The key is the sha256 of the file's contents, so it is the same key on the
|
|
2288
|
+
next run and on another machine holding the same transcript — an address
|
|
2289
|
+
worth putting in the bar. Refreshing used to throw the reader back to the
|
|
2290
|
+
library, and a link to what someone was looking at could not be sent at all.
|
|
2291
|
+
|
|
2292
|
+
The whole hash is used rather than a prefix: two transcripts sharing the
|
|
2293
|
+
first sixteen characters would otherwise open the wrong one. */
|
|
2294
|
+
const addressed = () => (location.hash || "").replace(/^#/, "");
|
|
2295
|
+
|
|
2296
|
+
function setAddress(key, push) {
|
|
2297
|
+
const to = key ? "#" + key : location.pathname;
|
|
2298
|
+
if (addressed() === (key || "")) return;
|
|
2299
|
+
try {
|
|
2300
|
+
if (push) history.pushState({ key: key || null }, "", to);
|
|
2301
|
+
else history.replaceState({ key: key || null }, "", to);
|
|
2302
|
+
} catch (e) {
|
|
2303
|
+
// A page opened from a file has no history to write; the view still works.
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2307
|
+
/* Back and forward move between the library and a transcript. */
|
|
2308
|
+
function followAddress() {
|
|
2309
|
+
const key = addressed();
|
|
2310
|
+
if (!key) { cur = null; drawList(); drawCrumb(); showLibrary(); return; }
|
|
2311
|
+
if (key !== cur) pick(key, false);
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
function pick(key, push = true){
|
|
2315
|
+
cur=key;EDITING=null;setAddress(key,push);drawList();drawCrumb();
|
|
2271
2316
|
main.innerHTML=`<div class="empty">Converting…</div>`;
|
|
2272
2317
|
fetch("/api/trajectory?id="+encodeURIComponent(key)).then(r=>r.json()).then(t=>{
|
|
2273
2318
|
if(t.error){main.innerHTML=`<div class="empty">${esc(t.error)}</div>`;return}
|
|
2274
2319
|
traj=t;limit=PAGE_SIZE;query="";lens="all";tab="trajectory";extra=null;RENAMING=false;render();
|
|
2320
|
+
// The first screenful is up; bring the rest along behind it.
|
|
2321
|
+
fillRest();
|
|
2275
2322
|
});
|
|
2276
2323
|
}
|
|
2277
2324
|
|
|
2278
2325
|
const toggle=r=>{show[r]=!show[r];limit=PAGE_SIZE;render()};
|
|
2279
|
-
|
|
2326
|
+
/* The rest of a long run, drawn without being asked for.
|
|
2327
|
+
|
|
2328
|
+
A transcript used to stop at 250 steps behind a Show more button. Nobody
|
|
2329
|
+
wants a button; they want the transcript. But the largest run here is 8,445
|
|
2330
|
+
steps and 34 MB of markup, which is a second of the page doing nothing if it
|
|
2331
|
+
is all drawn at once — so the first screenful still paints immediately and
|
|
2332
|
+
the rest arrives in the gaps between frames, keeping the reader's place each
|
|
2333
|
+
time. By the time anyone scrolls, it is there. */
|
|
2334
|
+
const FILL_CHUNK = 500;
|
|
2335
|
+
let FILLING = 0;
|
|
2336
|
+
|
|
2337
|
+
function fillRest() {
|
|
2338
|
+
clearTimeout(FILLING);
|
|
2339
|
+
if (!traj || tab !== "trajectory") return;
|
|
2340
|
+
const idle = globalThis.requestIdleCallback || ((fn) => setTimeout(fn, 32));
|
|
2341
|
+
FILLING = setTimeout(() => {
|
|
2342
|
+
idle(() => {
|
|
2343
|
+
if (!traj || tab !== "trajectory") return;
|
|
2344
|
+
if (limit >= traj.steps.length) return;
|
|
2345
|
+
const at = main.scrollTop;
|
|
2346
|
+
limit += FILL_CHUNK;
|
|
2347
|
+
render();
|
|
2348
|
+
main.scrollTop = at;
|
|
2349
|
+
fillRest();
|
|
2350
|
+
});
|
|
2351
|
+
}, 0);
|
|
2352
|
+
}
|
|
2280
2353
|
const toggleRaw=()=>{
|
|
2281
2354
|
raw=!raw;
|
|
2282
2355
|
// Boxes flipped by hand were disagreeing with the old setting, not this one.
|
|
@@ -2412,6 +2485,11 @@ function laneIndex(t) {
|
|
|
2412
2485
|
id: sub.trajectory_id,
|
|
2413
2486
|
kind: x.agentType || sub.agent?.name || "subagent",
|
|
2414
2487
|
said: x.description || "",
|
|
2488
|
+
// What to call the lane. The task it was given where that was
|
|
2489
|
+
// written down — 50 of the 53 Claude Code subagents here have one,
|
|
2490
|
+
// and "Docs and config audit" says what "general-purpose" cannot.
|
|
2491
|
+
// Otherwise the kind, which is all some formats record.
|
|
2492
|
+
name: x.description || x.agentType || sub.agent?.name || "subagent",
|
|
2415
2493
|
steps: (sub.steps || []).length,
|
|
2416
2494
|
tools: [...new Set((sub.steps || []).flatMap(
|
|
2417
2495
|
(s) => (s.tool_calls || []).map((c) => c.function_name)))],
|
|
@@ -2441,6 +2519,7 @@ function laneIndex(t) {
|
|
|
2441
2519
|
id: sub.trajectory_id,
|
|
2442
2520
|
kind: x.agentType || sub.agent?.name || "subagent",
|
|
2443
2521
|
said: x.description || "",
|
|
2522
|
+
name: x.description || x.agentType || sub.agent?.name || "subagent",
|
|
2444
2523
|
steps: (sub.steps || []).length,
|
|
2445
2524
|
tools: [...new Set((sub.steps || []).flatMap(
|
|
2446
2525
|
(s) => (s.tool_calls || []).map((c) => c.function_name)))],
|
|
@@ -2526,7 +2605,7 @@ function timeScale(lo, hi, busy) {
|
|
|
2526
2605
|
return {
|
|
2527
2606
|
posOf,
|
|
2528
2607
|
room,
|
|
2529
|
-
gaps: gaps.map(([a, b]) => ({ at: posOf(a), took: duration(b - a) })),
|
|
2608
|
+
gaps: gaps.map(([a, b]) => ({ at: posOf(a), ms: b - a, took: duration(b - a) })),
|
|
2530
2609
|
kept,
|
|
2531
2610
|
skipped,
|
|
2532
2611
|
};
|
|
@@ -2591,7 +2670,14 @@ function drawLanes(t) {
|
|
|
2591
2670
|
const scale = timeScale(lo, hi, busy);
|
|
2592
2671
|
const bands = together(timed);
|
|
2593
2672
|
|
|
2594
|
-
|
|
2673
|
+
// Every quiet stretch is skipped; only the large ones are marked. Drawing a
|
|
2674
|
+
// line for all 152 of one session's turned the chart into vertical stripes,
|
|
2675
|
+
// and a mark every few pixels tells a reader nothing they can act on.
|
|
2676
|
+
const worth = scale.gaps
|
|
2677
|
+
.slice()
|
|
2678
|
+
.sort((a, b) => b.ms - a.ms)
|
|
2679
|
+
.slice(0, 8);
|
|
2680
|
+
const breaks = worth.map((g) =>
|
|
2595
2681
|
`<span class="gapmark" style="left:${g.at}%" title="nothing running for ${esc(g.took)}"></span>`
|
|
2596
2682
|
).join("");
|
|
2597
2683
|
|
|
@@ -2640,11 +2726,15 @@ function drawLanes(t) {
|
|
|
2640
2726
|
|
|
2641
2727
|
/* One row: who, how many steps, and the track it sits on. */
|
|
2642
2728
|
function laneRow(lane, tight, inner) {
|
|
2729
|
+
const name = lane.name || lane.kind;
|
|
2730
|
+
// Both, where they differ: the task reads first, the kind explains it.
|
|
2731
|
+
const full = lane.said && lane.kind !== name ? `${name} — ${lane.kind}` : name;
|
|
2643
2732
|
return `
|
|
2644
2733
|
<div class="lane${tight}">
|
|
2645
2734
|
<div class="lane-name">
|
|
2646
2735
|
${lane.main ? "" : `<span style="color:var(--accent);font-weight:700;font-size:11px">⤷</span>`}
|
|
2647
|
-
<span class="kind${lane.main ? " main" : ""}
|
|
2736
|
+
<span class="kind${lane.main ? " main" : ""}${lane.said ? " named" : ""}"
|
|
2737
|
+
title="${esc(full)}">${esc(name)}</span>
|
|
2648
2738
|
<span class="n">${num(lane.steps)}</span>
|
|
2649
2739
|
</div>
|
|
2650
2740
|
<div class="track"><span class="rule"></span>${inner}</div>
|
|
@@ -2927,8 +3017,8 @@ function trajectoryTab(t,e,branches,branchSteps,counts,q){
|
|
|
2927
3017
|
</div>
|
|
2928
3018
|
${shown.map(({s,i})=>step(s,t,0,i,"")).join("")
|
|
2929
3019
|
||`<div class="empty">Nothing matches.</div>`}
|
|
2930
|
-
${total>shown.length?`<div class="more"><
|
|
2931
|
-
${num(shown.length)} of ${num(total)} steps</
|
|
3020
|
+
${total>shown.length?`<div class="more"><span class="fine">
|
|
3021
|
+
drawing the rest — ${num(shown.length)} of ${num(total)} steps</span></div>`:""}
|
|
2932
3022
|
${(()=>{
|
|
2933
3023
|
// Only in the plain view: a lens or a search is a question about the
|
|
2934
3024
|
// steps of this run, and answering it with whole other conversations
|
|
@@ -71,6 +71,14 @@ const SAFE_FETCH = () =>
|
|
|
71
71
|
an object with no focus(), which fails somewhere else entirely. */
|
|
72
72
|
const SAFE_BY_ID = () => null;
|
|
73
73
|
|
|
74
|
+
/* The page reads and writes an address so a transcript can be linked to. The
|
|
75
|
+
stub keeps one, so tests can watch it without a browser. */
|
|
76
|
+
global.location = { hash: "", pathname: "/" };
|
|
77
|
+
global.history = {
|
|
78
|
+
pushState: (_s, _t, to) => { global.location.hash = String(to).startsWith("#") ? to : ""; },
|
|
79
|
+
replaceState: (_s, _t, to) => { global.location.hash = String(to).startsWith("#") ? to : ""; },
|
|
80
|
+
};
|
|
81
|
+
|
|
74
82
|
/* Looking for an element by selector, against what was actually painted. The
|
|
75
83
|
stub DOM holds markup as a string rather than a tree, so this answers the one
|
|
76
84
|
question the page asks — is this box on the page — from that string. */
|
|
@@ -91,6 +99,7 @@ const run = (body) => {
|
|
|
91
99
|
global.fetch = SAFE_FETCH;
|
|
92
100
|
global.document.getElementById = SAFE_BY_ID;
|
|
93
101
|
global.document.querySelector = SAFE_QUERY;
|
|
102
|
+
global.location.hash = "";
|
|
94
103
|
return eval(script + "\n;(() => {" + body + "})()");
|
|
95
104
|
};
|
|
96
105
|
|
|
@@ -2175,3 +2184,109 @@ test("the box a lane opened is marked, and the mark is a real rule", () => {
|
|
|
2175
2184
|
const script = page.slice(page.indexOf("<script>"), page.lastIndexOf("</script>"));
|
|
2176
2185
|
assert.match(script, /classList\.add\("found"\)/, "and something must set it");
|
|
2177
2186
|
});
|
|
2187
|
+
|
|
2188
|
+
test("a long run fills itself in rather than offering a button", () => {
|
|
2189
|
+
const steps = [];
|
|
2190
|
+
for (let i = 0; i < 900; i++) {
|
|
2191
|
+
steps.push({ step_id: i + 1, source: "agent", message: "working" });
|
|
2192
|
+
}
|
|
2193
|
+
const html = run(`
|
|
2194
|
+
traj = {steps: ${JSON.stringify(steps)}};
|
|
2195
|
+
cur = "k"; INDEX = [{key:"k"}]; lens = "all"; query = ""; limit = 250;
|
|
2196
|
+
return trajectoryTab(traj, {}, [], new Set(), {all: 900}, "");
|
|
2197
|
+
`);
|
|
2198
|
+
assert.ok(!/Show more/.test(html), "nobody wants a button; they want the transcript");
|
|
2199
|
+
assert.match(html, /drawing the rest/, "but it should say more is coming");
|
|
2200
|
+
assert.match(html, /250 of 900 steps/);
|
|
2201
|
+
});
|
|
2202
|
+
|
|
2203
|
+
test("once everything is drawn it says nothing at all", () => {
|
|
2204
|
+
const steps = [{ step_id: 1, source: "user", message: "hi" }];
|
|
2205
|
+
const html = run(`
|
|
2206
|
+
traj = {steps: ${JSON.stringify(steps)}};
|
|
2207
|
+
cur = "k"; INDEX = [{key:"k"}]; lens = "all"; query = ""; limit = 250;
|
|
2208
|
+
return trajectoryTab(traj, {}, [], new Set(), {all: 1}, "");
|
|
2209
|
+
`);
|
|
2210
|
+
assert.ok(!/drawing the rest/.test(html));
|
|
2211
|
+
});
|
|
2212
|
+
|
|
2213
|
+
test("filling holds the reader's place, and stops when the run is done", () => {
|
|
2214
|
+
const out = run(`
|
|
2215
|
+
const steps = [];
|
|
2216
|
+
for (let i = 0; i < 1200; i++) steps.push({step_id:i+1, source:"agent", message:"x"});
|
|
2217
|
+
traj = {steps}; cur = "k"; INDEX = [{key:"k"}];
|
|
2218
|
+
lens = "all"; query = ""; tab = "trajectory"; limit = 250;
|
|
2219
|
+
main.scrollTop = 400;
|
|
2220
|
+
|
|
2221
|
+
// Run the scheduler by hand: each tick draws one more chunk.
|
|
2222
|
+
let ticks = 0;
|
|
2223
|
+
globalThis.setTimeout = (fn) => { if (ticks++ < 50) fn(); return 0; };
|
|
2224
|
+
globalThis.clearTimeout = () => {};
|
|
2225
|
+
globalThis.requestIdleCallback = (fn) => fn();
|
|
2226
|
+
|
|
2227
|
+
fillRest();
|
|
2228
|
+
return { limit, steps: traj.steps.length, at: main.scrollTop };
|
|
2229
|
+
`);
|
|
2230
|
+
assert.strictEqual(out.limit >= out.steps, true, `stopped at ${out.limit} of ${out.steps}`);
|
|
2231
|
+
assert.strictEqual(out.at, 400, "the reader stays where they were");
|
|
2232
|
+
});
|
|
2233
|
+
|
|
2234
|
+
// ---- a transcript has an address ---------------------------------------------
|
|
2235
|
+
|
|
2236
|
+
test("opening a transcript puts it in the address", () => {
|
|
2237
|
+
const out = run(`
|
|
2238
|
+
INDEX = [{key:"abc123", path:"/a.jsonl"}];
|
|
2239
|
+
globalThis.fetch = () => Promise.resolve({ok:true, json: () => Promise.resolve({steps:[]})});
|
|
2240
|
+
pick("abc123");
|
|
2241
|
+
return location.hash;
|
|
2242
|
+
`);
|
|
2243
|
+
assert.strictEqual(out, "#abc123", "so a refresh comes back here");
|
|
2244
|
+
});
|
|
2245
|
+
|
|
2246
|
+
test("going back to the library clears it", () => {
|
|
2247
|
+
const out = run(`
|
|
2248
|
+
INDEX = [{key:"abc123", path:"/a.jsonl", group:"Local"}];
|
|
2249
|
+
location.hash = "#abc123";
|
|
2250
|
+
showLibrary();
|
|
2251
|
+
return location.hash;
|
|
2252
|
+
`);
|
|
2253
|
+
assert.strictEqual(out, "", "the library is not one transcript");
|
|
2254
|
+
});
|
|
2255
|
+
|
|
2256
|
+
test("the whole key is used, never a prefix", () => {
|
|
2257
|
+
// Two transcripts can share a prefix; opening the wrong one would be silent.
|
|
2258
|
+
const out = run(`
|
|
2259
|
+
INDEX = [{key:"aaaaaaaaaaaaaaaa1111"},{key:"aaaaaaaaaaaaaaaa2222"}];
|
|
2260
|
+
globalThis.fetch = () => Promise.resolve({ok:true, json: () => Promise.resolve({steps:[]})});
|
|
2261
|
+
pick("aaaaaaaaaaaaaaaa2222");
|
|
2262
|
+
return { hash: location.hash, asked: addressed() };
|
|
2263
|
+
`);
|
|
2264
|
+
assert.strictEqual(out.hash, "#aaaaaaaaaaaaaaaa2222");
|
|
2265
|
+
assert.strictEqual(out.asked, "aaaaaaaaaaaaaaaa2222");
|
|
2266
|
+
});
|
|
2267
|
+
|
|
2268
|
+
test("an address naming nothing in this library says so, and shows the library", () => {
|
|
2269
|
+
const out = run(`
|
|
2270
|
+
INDEX = [{key:"real"}];
|
|
2271
|
+
location.hash = "#gone";
|
|
2272
|
+
let said = "";
|
|
2273
|
+
note = (m) => { said = m; };
|
|
2274
|
+
followAddress();
|
|
2275
|
+
return { said, cur, hash: location.hash };
|
|
2276
|
+
`);
|
|
2277
|
+
assert.strictEqual(out.cur, "gone", "it tried");
|
|
2278
|
+
// followAddress hands off to pick, which fetches; what matters is it did not
|
|
2279
|
+
// silently sit on the library with a stale address.
|
|
2280
|
+
assert.strictEqual(out.hash, "#gone");
|
|
2281
|
+
});
|
|
2282
|
+
|
|
2283
|
+
test("an empty address shows the library rather than a blank pane", () => {
|
|
2284
|
+
const out = run(`
|
|
2285
|
+
INDEX = [{key:"real", group:"Local"}];
|
|
2286
|
+
location.hash = "";
|
|
2287
|
+
cur = "real";
|
|
2288
|
+
followAddress();
|
|
2289
|
+
return cur;
|
|
2290
|
+
`);
|
|
2291
|
+
assert.strictEqual(out, null, "no address means no transcript");
|
|
2292
|
+
});
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|