transcript-viewer 0.11.2__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.2 → transcript_viewer-0.12.0}/PKG-INFO +1 -1
  2. {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/pyproject.toml +1 -1
  3. {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/src/transcript_viewer/page.html +70 -16
  4. {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/tests/page.test.js +97 -0
  5. {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/uv.lock +1 -1
  6. {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/.coverage +0 -0
  7. {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/.github/workflows/publish.yml +0 -0
  8. {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/.github/workflows/test.yml +0 -0
  9. {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/.gitignore +0 -0
  10. {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/README.md +0 -0
  11. {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/design/Anchor.dc.html +0 -0
  12. {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/design/Dense.dc.html +0 -0
  13. {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/design/Hover.dc.html +0 -0
  14. {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/design/Main.dc.html +0 -0
  15. {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/design/Quiet.dc.html +0 -0
  16. {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/design/canvas.json +0 -0
  17. {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/design/delegation-timeline.html +0 -0
  18. {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/src/transcript_viewer/__init__.py +0 -0
  19. {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/src/transcript_viewer/ai.py +0 -0
  20. {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/src/transcript_viewer/cli.py +0 -0
  21. {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/src/transcript_viewer/config.py +0 -0
  22. {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/src/transcript_viewer/corpus.py +0 -0
  23. {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/src/transcript_viewer/fetch.py +0 -0
  24. {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/src/transcript_viewer/library.py +0 -0
  25. {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/src/transcript_viewer/store.py +0 -0
  26. {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/src/transcript_viewer/viewer.py +0 -0
  27. {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/tests/fixtures/big-command.json +0 -0
  28. {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/tests/test_ai.py +0 -0
  29. {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/tests/test_cli.py +0 -0
  30. {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/tests/test_config.py +0 -0
  31. {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/tests/test_corpus.py +0 -0
  32. {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/tests/test_fetch.py +0 -0
  33. {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/tests/test_library.py +0 -0
  34. {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/tests/test_page.py +0 -0
  35. {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/tests/test_readme.py +0 -0
  36. {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/tests/test_store.py +0 -0
  37. {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/tests/test_stream.py +0 -0
  38. {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/tests/test_style.py +0 -0
  39. {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/tests/test_tidiness.py +0 -0
  40. {transcript_viewer-0.11.2 → 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.2
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.2"
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
 
@@ -2750,6 +2790,18 @@ function laneCard(ev, id) {
2750
2790
  first. A lane from a record where nothing spawned it is found in the block
2751
2791
  after the run instead — which only exists in the plain view, so a lens or a
2752
2792
  search is cleared to get back to it rather than failing quietly. */
2793
+ /* Put an element at the top of the reading area, with room to breathe. */
2794
+ function laneScroll(el) {
2795
+ const main = document.getElementById("main");
2796
+ if (!main || !el.getBoundingClientRect || !main.getBoundingClientRect) {
2797
+ if (el.scrollIntoView) el.scrollIntoView({ behavior: "smooth", block: "start" });
2798
+ return;
2799
+ }
2800
+ const top =
2801
+ main.scrollTop + el.getBoundingClientRect().top - main.getBoundingClientRect().top - 16;
2802
+ main.scrollTo({ top: Math.max(0, top), behavior: "smooth" });
2803
+ }
2804
+
2753
2805
  function openLane(id) {
2754
2806
  laneCard(null);
2755
2807
  const lane = LANES.find((l) => l.id === id);
@@ -2783,9 +2835,11 @@ function openLane(id) {
2783
2835
  box.classList.add("found");
2784
2836
  // After the fold, not before: opening changes the height, and scrolling to
2785
2837
  // where the box used to end leaves the reader above what they asked for.
2786
- requestAnimationFrame(() =>
2787
- box.scrollIntoView({ behavior: "smooth", block: "center" }),
2788
- );
2838
+ //
2839
+ // To the top of it, not the middle. Centring a box puts a subagent of any
2840
+ // length halfway through itself, when what was asked for was the subagent —
2841
+ // which starts at its first step.
2842
+ requestAnimationFrame(() => laneScroll(box));
2789
2843
  setTimeout(() => box.classList.remove("found"), 1400);
2790
2844
  }
2791
2845
 
@@ -2913,8 +2967,8 @@ function trajectoryTab(t,e,branches,branchSteps,counts,q){
2913
2967
  </div>
2914
2968
  ${shown.map(({s,i})=>step(s,t,0,i,"")).join("")
2915
2969
  ||`<div class="empty">Nothing matches.</div>`}
2916
- ${total>shown.length?`<div class="more"><button onclick="more()">Show more —
2917
- ${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>`:""}
2918
2972
  ${(()=>{
2919
2973
  // Only in the plain view: a lens or a search is a question about the
2920
2974
  // steps of this run, and answering it with whole other conversations
@@ -2124,3 +2124,100 @@ test("a lane whose box is filtered away brings the view back rather than failing
2124
2124
  assert.strictEqual(out.query, "");
2125
2125
  assert.strictEqual(out.found, true);
2126
2126
  });
2127
+
2128
+ test("a lane lands on the top of what it opened, not the middle of it", () => {
2129
+ // Centring put a long subagent halfway through itself. What was asked for is
2130
+ // the subagent, which starts at its first step.
2131
+ const moved = [];
2132
+ const box = {
2133
+ open: false,
2134
+ classList: { add() {}, remove() {} },
2135
+ getBoundingClientRect: () => ({ top: 900 }),
2136
+ };
2137
+ const main = {
2138
+ scrollTop: 200,
2139
+ getBoundingClientRect: () => ({ top: 60 }),
2140
+ scrollTo: (opts) => moved.push(opts),
2141
+ };
2142
+ global.__el = { main, box };
2143
+
2144
+ run(`
2145
+ document.getElementById = (id) => (id === "main" ? globalThis.__el.main : null);
2146
+ laneScroll(globalThis.__el.box);
2147
+ `);
2148
+
2149
+ assert.strictEqual(moved.length, 1, "it should scroll the reading area itself");
2150
+ // 200 + 900 - 60 - 16 of breathing room.
2151
+ assert.strictEqual(moved[0].top, 1024);
2152
+ assert.strictEqual(moved[0].behavior, "smooth");
2153
+ });
2154
+
2155
+ test("landing does not scroll above the top of the run", () => {
2156
+ const moved = [];
2157
+ global.__el2 = {
2158
+ main: {
2159
+ scrollTop: 0,
2160
+ getBoundingClientRect: () => ({ top: 60 }),
2161
+ scrollTo: (o) => moved.push(o),
2162
+ },
2163
+ box: { getBoundingClientRect: () => ({ top: 10 }) },
2164
+ };
2165
+ run(`
2166
+ document.getElementById = (id) => (id === "main" ? globalThis.__el2.main : null);
2167
+ laneScroll(globalThis.__el2.box);
2168
+ `);
2169
+ assert.strictEqual(moved[0].top, 0, "never a negative offset");
2170
+ });
2171
+
2172
+ test("the box a lane opened is marked, and the mark is a real rule", () => {
2173
+ const style = page.slice(page.indexOf("<style>"), page.indexOf("</style>"));
2174
+ assert.match(style, /\.branch\.found\{/, "the class must style something");
2175
+ const script = page.slice(page.indexOf("<script>"), page.lastIndexOf("</script>"));
2176
+ assert.match(script, /classList\.add\("found"\)/, "and something must set it");
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.2"
391
+ version = "0.12.0"
392
392
  source = { editable = "." }
393
393
  dependencies = [
394
394
  { name = "atif-make" },