transcript-viewer 0.11.3__tar.gz → 0.12.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.
Files changed (40) hide show
  1. {transcript_viewer-0.11.3 → transcript_viewer-0.12.0}/PKG-INFO +1 -1
  2. {transcript_viewer-0.11.3 → transcript_viewer-0.12.0}/pyproject.toml +1 -1
  3. {transcript_viewer-0.11.3 → transcript_viewer-0.12.0}/src/transcript_viewer/page.html +53 -13
  4. {transcript_viewer-0.11.3 → transcript_viewer-0.12.0}/tests/page.test.js +46 -0
  5. {transcript_viewer-0.11.3 → transcript_viewer-0.12.0}/uv.lock +1 -1
  6. {transcript_viewer-0.11.3 → transcript_viewer-0.12.0}/.coverage +0 -0
  7. {transcript_viewer-0.11.3 → transcript_viewer-0.12.0}/.github/workflows/publish.yml +0 -0
  8. {transcript_viewer-0.11.3 → transcript_viewer-0.12.0}/.github/workflows/test.yml +0 -0
  9. {transcript_viewer-0.11.3 → transcript_viewer-0.12.0}/.gitignore +0 -0
  10. {transcript_viewer-0.11.3 → transcript_viewer-0.12.0}/README.md +0 -0
  11. {transcript_viewer-0.11.3 → transcript_viewer-0.12.0}/design/Anchor.dc.html +0 -0
  12. {transcript_viewer-0.11.3 → transcript_viewer-0.12.0}/design/Dense.dc.html +0 -0
  13. {transcript_viewer-0.11.3 → transcript_viewer-0.12.0}/design/Hover.dc.html +0 -0
  14. {transcript_viewer-0.11.3 → transcript_viewer-0.12.0}/design/Main.dc.html +0 -0
  15. {transcript_viewer-0.11.3 → transcript_viewer-0.12.0}/design/Quiet.dc.html +0 -0
  16. {transcript_viewer-0.11.3 → transcript_viewer-0.12.0}/design/canvas.json +0 -0
  17. {transcript_viewer-0.11.3 → transcript_viewer-0.12.0}/design/delegation-timeline.html +0 -0
  18. {transcript_viewer-0.11.3 → transcript_viewer-0.12.0}/src/transcript_viewer/__init__.py +0 -0
  19. {transcript_viewer-0.11.3 → transcript_viewer-0.12.0}/src/transcript_viewer/ai.py +0 -0
  20. {transcript_viewer-0.11.3 → transcript_viewer-0.12.0}/src/transcript_viewer/cli.py +0 -0
  21. {transcript_viewer-0.11.3 → transcript_viewer-0.12.0}/src/transcript_viewer/config.py +0 -0
  22. {transcript_viewer-0.11.3 → transcript_viewer-0.12.0}/src/transcript_viewer/corpus.py +0 -0
  23. {transcript_viewer-0.11.3 → transcript_viewer-0.12.0}/src/transcript_viewer/fetch.py +0 -0
  24. {transcript_viewer-0.11.3 → transcript_viewer-0.12.0}/src/transcript_viewer/library.py +0 -0
  25. {transcript_viewer-0.11.3 → transcript_viewer-0.12.0}/src/transcript_viewer/store.py +0 -0
  26. {transcript_viewer-0.11.3 → transcript_viewer-0.12.0}/src/transcript_viewer/viewer.py +0 -0
  27. {transcript_viewer-0.11.3 → transcript_viewer-0.12.0}/tests/fixtures/big-command.json +0 -0
  28. {transcript_viewer-0.11.3 → transcript_viewer-0.12.0}/tests/test_ai.py +0 -0
  29. {transcript_viewer-0.11.3 → transcript_viewer-0.12.0}/tests/test_cli.py +0 -0
  30. {transcript_viewer-0.11.3 → transcript_viewer-0.12.0}/tests/test_config.py +0 -0
  31. {transcript_viewer-0.11.3 → transcript_viewer-0.12.0}/tests/test_corpus.py +0 -0
  32. {transcript_viewer-0.11.3 → transcript_viewer-0.12.0}/tests/test_fetch.py +0 -0
  33. {transcript_viewer-0.11.3 → transcript_viewer-0.12.0}/tests/test_library.py +0 -0
  34. {transcript_viewer-0.11.3 → transcript_viewer-0.12.0}/tests/test_page.py +0 -0
  35. {transcript_viewer-0.11.3 → transcript_viewer-0.12.0}/tests/test_readme.py +0 -0
  36. {transcript_viewer-0.11.3 → transcript_viewer-0.12.0}/tests/test_store.py +0 -0
  37. {transcript_viewer-0.11.3 → transcript_viewer-0.12.0}/tests/test_stream.py +0 -0
  38. {transcript_viewer-0.11.3 → transcript_viewer-0.12.0}/tests/test_style.py +0 -0
  39. {transcript_viewer-0.11.3 → transcript_viewer-0.12.0}/tests/test_tidiness.py +0 -0
  40. {transcript_viewer-0.11.3 → transcript_viewer-0.12.0}/tests/test_viewer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: transcript-viewer
3
- Version: 0.11.3
3
+ Version: 0.12.0
4
4
  Summary: Browse agent transcripts in a local, dependency-free web viewer.
5
5
  License: MIT
6
6
  Requires-Python: >=3.12
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "transcript-viewer"
3
- version = "0.11.3"
3
+ version = "0.12.0"
4
4
  description = "Browse agent transcripts in a local, dependency-free web viewer."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -388,14 +388,17 @@ pre{background:var(--sunk);border-radius:7px;padding:10px;overflow:auto;font-siz
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
390
  .lane{display:grid;grid-template-columns:150px 1fr;gap:0 12px;align-items:center}
391
- .lane.tight{grid-template-columns:118px 1fr;gap:0 10px}
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:150px 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}
395
398
  .lane-name .kind.main{color:var(--ink);font-weight:600}
396
399
  .lane-name .n{font:10px var(--font-mono);color:var(--faint);margin-left:auto;flex:none}
397
400
  .track{position:relative;height:22px}
398
- .lane.tight .track{height:13px}
401
+ .lane.tight .track{height:19px}
399
402
  .track .rule{position:absolute;left:0;right:0;top:50%;height:1px;background:var(--line)}
400
403
  .track .tick{position:absolute;top:0;bottom:0;width:1px;background:var(--line);opacity:.45}
401
404
  /* A floor, so a four-minute subagent in a thirty-hour run is still a target.
@@ -403,7 +406,7 @@ pre{background:var(--sunk);border-radius:7px;padding:10px;overflow:auto;font-siz
403
406
  the tooltip carry the real one. */
404
407
  .bar{position:absolute;top:50%;transform:translateY(-50%);height:14px;min-width:7px;
405
408
  border-radius:3px;background:var(--accent);opacity:.85;cursor:pointer;border:0;padding:0}
406
- .lane.tight .bar{height:8px}
409
+ .lane.tight .bar{height:11px}
407
410
  .bar:hover,.bar:focus-visible{opacity:1;outline:1px solid var(--ink);outline-offset:1px}
408
411
  .bar.main{background:var(--ink);opacity:.85;cursor:default}
409
412
  .bar.main:hover{outline:none;opacity:.85}
@@ -416,11 +419,13 @@ pre{background:var(--sunk);border-radius:7px;padding:10px;overflow:auto;font-siz
416
419
  rather than repeated into each row. */
417
420
  .lanes-tracks{position:relative}
418
421
  .bandlayer{position:absolute;left:162px;right:0;top:0;bottom:0;pointer-events:none}
419
- .lane.tight ~ .bandlayer,.lanes-tracks:has(.lane.tight) .bandlayer{left:128px}
420
- .gapmark{position:absolute;top:0;bottom:0;width:0;border-left:1px dashed var(--line)}
421
- .gapmark:after{content:"";position:absolute;left:-2px;top:0;bottom:0;width:5px;
422
- background:var(--bg);opacity:.9;border-left:1px dashed var(--muted);
423
- border-right:1px dashed var(--muted);opacity:.35}
422
+ .lane.tight ~ .bandlayer,.lanes-tracks:has(.lane.tight) .bandlayer{left:162px}
423
+ /* Where the axis was cut. One thin line, and only for the cuts worth seeing —
424
+ one session skips 152 quiet stretches, and marking every one turned the
425
+ whole chart into vertical stripes. The rest are still skipped; they are
426
+ simply not each announced. */
427
+ .gapmark{position:absolute;top:0;bottom:0;width:0;
428
+ border-left:1px dashed var(--line);opacity:.7}
424
429
  /* Conversations that belong to the record rather than to any step. */
425
430
  .loose{margin-top:18px;padding-top:12px;border-top:1px solid var(--line)}
426
431
  .loose-head{font:11px var(--font-mono);color:var(--muted);margin-bottom:8px}
@@ -2272,11 +2277,39 @@ function pick(key){
2272
2277
  fetch("/api/trajectory?id="+encodeURIComponent(key)).then(r=>r.json()).then(t=>{
2273
2278
  if(t.error){main.innerHTML=`<div class="empty">${esc(t.error)}</div>`;return}
2274
2279
  traj=t;limit=PAGE_SIZE;query="";lens="all";tab="trajectory";extra=null;RENAMING=false;render();
2280
+ // The first screenful is up; bring the rest along behind it.
2281
+ fillRest();
2275
2282
  });
2276
2283
  }
2277
2284
 
2278
2285
  const toggle=r=>{show[r]=!show[r];limit=PAGE_SIZE;render()};
2279
- const more=()=>{limit+=PAGE_SIZE*4;render()};
2286
+ /* The rest of a long run, drawn without being asked for.
2287
+
2288
+ A transcript used to stop at 250 steps behind a Show more button. Nobody
2289
+ wants a button; they want the transcript. But the largest run here is 8,445
2290
+ steps and 34 MB of markup, which is a second of the page doing nothing if it
2291
+ is all drawn at once — so the first screenful still paints immediately and
2292
+ the rest arrives in the gaps between frames, keeping the reader's place each
2293
+ time. By the time anyone scrolls, it is there. */
2294
+ const FILL_CHUNK = 500;
2295
+ let FILLING = 0;
2296
+
2297
+ function fillRest() {
2298
+ clearTimeout(FILLING);
2299
+ if (!traj || tab !== "trajectory") return;
2300
+ const idle = globalThis.requestIdleCallback || ((fn) => setTimeout(fn, 32));
2301
+ FILLING = setTimeout(() => {
2302
+ idle(() => {
2303
+ if (!traj || tab !== "trajectory") return;
2304
+ if (limit >= traj.steps.length) return;
2305
+ const at = main.scrollTop;
2306
+ limit += FILL_CHUNK;
2307
+ render();
2308
+ main.scrollTop = at;
2309
+ fillRest();
2310
+ });
2311
+ }, 0);
2312
+ }
2280
2313
  const toggleRaw=()=>{
2281
2314
  raw=!raw;
2282
2315
  // Boxes flipped by hand were disagreeing with the old setting, not this one.
@@ -2526,7 +2559,7 @@ function timeScale(lo, hi, busy) {
2526
2559
  return {
2527
2560
  posOf,
2528
2561
  room,
2529
- gaps: gaps.map(([a, b]) => ({ at: posOf(a), took: duration(b - a) })),
2562
+ gaps: gaps.map(([a, b]) => ({ at: posOf(a), ms: b - a, took: duration(b - a) })),
2530
2563
  kept,
2531
2564
  skipped,
2532
2565
  };
@@ -2591,7 +2624,14 @@ function drawLanes(t) {
2591
2624
  const scale = timeScale(lo, hi, busy);
2592
2625
  const bands = together(timed);
2593
2626
 
2594
- const breaks = scale.gaps.map((g) =>
2627
+ // Every quiet stretch is skipped; only the large ones are marked. Drawing a
2628
+ // line for all 152 of one session's turned the chart into vertical stripes,
2629
+ // and a mark every few pixels tells a reader nothing they can act on.
2630
+ const worth = scale.gaps
2631
+ .slice()
2632
+ .sort((a, b) => b.ms - a.ms)
2633
+ .slice(0, 8);
2634
+ const breaks = worth.map((g) =>
2595
2635
  `<span class="gapmark" style="left:${g.at}%" title="nothing running for ${esc(g.took)}"></span>`
2596
2636
  ).join("");
2597
2637
 
@@ -2927,8 +2967,8 @@ function trajectoryTab(t,e,branches,branchSteps,counts,q){
2927
2967
  </div>
2928
2968
  ${shown.map(({s,i})=>step(s,t,0,i,"")).join("")
2929
2969
  ||`<div class="empty">Nothing matches.</div>`}
2930
- ${total>shown.length?`<div class="more"><button onclick="more()">Show more —
2931
- ${num(shown.length)} of ${num(total)} steps</button></div>`:""}
2970
+ ${total>shown.length?`<div class="more"><span class="fine">
2971
+ drawing the rest — ${num(shown.length)} of ${num(total)} steps</span></div>`:""}
2932
2972
  ${(()=>{
2933
2973
  // Only in the plain view: a lens or a search is a question about the
2934
2974
  // steps of this run, and answering it with whole other conversations
@@ -2175,3 +2175,49 @@ test("the box a lane opened is marked, and the mark is a real rule", () => {
2175
2175
  const script = page.slice(page.indexOf("<script>"), page.lastIndexOf("</script>"));
2176
2176
  assert.match(script, /classList\.add\("found"\)/, "and something must set it");
2177
2177
  });
2178
+
2179
+ test("a long run fills itself in rather than offering a button", () => {
2180
+ const steps = [];
2181
+ for (let i = 0; i < 900; i++) {
2182
+ steps.push({ step_id: i + 1, source: "agent", message: "working" });
2183
+ }
2184
+ const html = run(`
2185
+ traj = {steps: ${JSON.stringify(steps)}};
2186
+ cur = "k"; INDEX = [{key:"k"}]; lens = "all"; query = ""; limit = 250;
2187
+ return trajectoryTab(traj, {}, [], new Set(), {all: 900}, "");
2188
+ `);
2189
+ assert.ok(!/Show more/.test(html), "nobody wants a button; they want the transcript");
2190
+ assert.match(html, /drawing the rest/, "but it should say more is coming");
2191
+ assert.match(html, /250 of 900 steps/);
2192
+ });
2193
+
2194
+ test("once everything is drawn it says nothing at all", () => {
2195
+ const steps = [{ step_id: 1, source: "user", message: "hi" }];
2196
+ const html = run(`
2197
+ traj = {steps: ${JSON.stringify(steps)}};
2198
+ cur = "k"; INDEX = [{key:"k"}]; lens = "all"; query = ""; limit = 250;
2199
+ return trajectoryTab(traj, {}, [], new Set(), {all: 1}, "");
2200
+ `);
2201
+ assert.ok(!/drawing the rest/.test(html));
2202
+ });
2203
+
2204
+ test("filling holds the reader's place, and stops when the run is done", () => {
2205
+ const out = run(`
2206
+ const steps = [];
2207
+ for (let i = 0; i < 1200; i++) steps.push({step_id:i+1, source:"agent", message:"x"});
2208
+ traj = {steps}; cur = "k"; INDEX = [{key:"k"}];
2209
+ lens = "all"; query = ""; tab = "trajectory"; limit = 250;
2210
+ main.scrollTop = 400;
2211
+
2212
+ // Run the scheduler by hand: each tick draws one more chunk.
2213
+ let ticks = 0;
2214
+ globalThis.setTimeout = (fn) => { if (ticks++ < 50) fn(); return 0; };
2215
+ globalThis.clearTimeout = () => {};
2216
+ globalThis.requestIdleCallback = (fn) => fn();
2217
+
2218
+ fillRest();
2219
+ return { limit, steps: traj.steps.length, at: main.scrollTop };
2220
+ `);
2221
+ assert.strictEqual(out.limit >= out.steps, true, `stopped at ${out.limit} of ${out.steps}`);
2222
+ assert.strictEqual(out.at, 400, "the reader stays where they were");
2223
+ });
@@ -388,7 +388,7 @@ wheels = [
388
388
 
389
389
  [[package]]
390
390
  name = "transcript-viewer"
391
- version = "0.11.3"
391
+ version = "0.12.0"
392
392
  source = { editable = "." }
393
393
  dependencies = [
394
394
  { name = "atif-make" },