transcript-viewer 0.11.1__tar.gz → 0.11.2__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.1 → transcript_viewer-0.11.2}/PKG-INFO +9 -3
  2. {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/README.md +8 -2
  3. {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/pyproject.toml +1 -1
  4. {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/src/transcript_viewer/page.html +44 -11
  5. {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/tests/page.test.js +83 -1
  6. {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/uv.lock +1 -1
  7. {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/.coverage +0 -0
  8. {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/.github/workflows/publish.yml +0 -0
  9. {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/.github/workflows/test.yml +0 -0
  10. {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/.gitignore +0 -0
  11. {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/design/Anchor.dc.html +0 -0
  12. {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/design/Dense.dc.html +0 -0
  13. {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/design/Hover.dc.html +0 -0
  14. {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/design/Main.dc.html +0 -0
  15. {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/design/Quiet.dc.html +0 -0
  16. {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/design/canvas.json +0 -0
  17. {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/design/delegation-timeline.html +0 -0
  18. {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/src/transcript_viewer/__init__.py +0 -0
  19. {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/src/transcript_viewer/ai.py +0 -0
  20. {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/src/transcript_viewer/cli.py +0 -0
  21. {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/src/transcript_viewer/config.py +0 -0
  22. {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/src/transcript_viewer/corpus.py +0 -0
  23. {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/src/transcript_viewer/fetch.py +0 -0
  24. {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/src/transcript_viewer/library.py +0 -0
  25. {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/src/transcript_viewer/store.py +0 -0
  26. {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/src/transcript_viewer/viewer.py +0 -0
  27. {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/tests/fixtures/big-command.json +0 -0
  28. {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/tests/test_ai.py +0 -0
  29. {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/tests/test_cli.py +0 -0
  30. {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/tests/test_config.py +0 -0
  31. {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/tests/test_corpus.py +0 -0
  32. {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/tests/test_fetch.py +0 -0
  33. {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/tests/test_library.py +0 -0
  34. {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/tests/test_page.py +0 -0
  35. {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/tests/test_readme.py +0 -0
  36. {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/tests/test_store.py +0 -0
  37. {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/tests/test_stream.py +0 -0
  38. {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/tests/test_style.py +0 -0
  39. {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/tests/test_tidiness.py +0 -0
  40. {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/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.1
3
+ Version: 0.11.2
4
4
  Summary: Browse agent transcripts in a local, dependency-free web viewer.
5
5
  License: MIT
6
6
  Requires-Python: >=3.12
@@ -182,8 +182,14 @@ so. Nothing is left behind and nothing is copied twice.
182
182
  A run that delegated shows a **timeline** above its steps: one lane per
183
183
  subagent, laid out on the clock. It exists for the one thing a nested box
184
184
  cannot say — that two subagents were running at the same moment, which 25 pairs
185
- in the corpus this was built against were. Clicking a lane opens the box the
186
- transcript already has for it rather than showing the same thing twice.
185
+ in the corpus this was built against were. Two bars in the same column of the
186
+ chart is what says it; overlap gets no colour of its own, since a second
187
+ encoding of the same fact is noise.
188
+
189
+ Clicking a lane opens the box the transcript already has for it rather than
190
+ showing the same thing twice, drawing the run out far enough to reach it first:
191
+ a long run paints 250 steps at a time, and in one real session every delegation
192
+ happens after step 1,231.
187
193
 
188
194
  Where a session spans days but holds hours of work — one runs 10.3 days and
189
195
  contains 5.3 hours, with 31 gaps over an hour — the quiet stretches are skipped
@@ -169,8 +169,14 @@ so. Nothing is left behind and nothing is copied twice.
169
169
  A run that delegated shows a **timeline** above its steps: one lane per
170
170
  subagent, laid out on the clock. It exists for the one thing a nested box
171
171
  cannot say — that two subagents were running at the same moment, which 25 pairs
172
- in the corpus this was built against were. Clicking a lane opens the box the
173
- transcript already has for it rather than showing the same thing twice.
172
+ in the corpus this was built against were. Two bars in the same column of the
173
+ chart is what says it; overlap gets no colour of its own, since a second
174
+ encoding of the same fact is noise.
175
+
176
+ Clicking a lane opens the box the transcript already has for it rather than
177
+ showing the same thing twice, drawing the run out far enough to reach it first:
178
+ a long run paints 250 steps at a time, and in one real session every delegation
179
+ happens after step 1,231.
174
180
 
175
181
  Where a session spans days but holds hours of work — one runs 10.3 days and
176
182
  contains 5.3 hours, with 31 gaps over an hour — the quiet stretches are skipped
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "transcript-viewer"
3
- version = "0.11.1"
3
+ version = "0.11.2"
4
4
  description = "Browse agent transcripts in a local, dependency-free web viewer."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -371,6 +371,8 @@ pre{background:var(--sunk);border-radius:7px;padding:10px;overflow:auto;font-siz
371
371
  .branch .inner{padding:6px 14px 4px 22px;border-top:1px solid var(--line);background:var(--sunk)}
372
372
  .branch .inner .step{border-left-color:var(--line)}
373
373
  .miss{padding:9px 12px;color:var(--dim);font-size:12.5px}
374
+ /* Where a lane just sent you. Long enough to notice, short enough to forget. */
375
+ .branch.found{box-shadow:0 0 0 2px var(--accent)}
374
376
 
375
377
  /* ---- delegation timeline ----
376
378
  Nested boxes cannot show that two subagents ran at once, and 25 pairs in the
@@ -396,7 +398,6 @@ pre{background:var(--sunk);border-radius:7px;padding:10px;overflow:auto;font-siz
396
398
  .lane.tight .track{height:13px}
397
399
  .track .rule{position:absolute;left:0;right:0;top:50%;height:1px;background:var(--line)}
398
400
  .track .tick{position:absolute;top:0;bottom:0;width:1px;background:var(--line);opacity:.45}
399
- .track .band{position:absolute;top:0;bottom:0;background:var(--hl)}
400
401
  /* A floor, so a four-minute subagent in a thirty-hour run is still a target.
401
402
  Where a bar is at the floor its width is not its duration — the card and
402
403
  the tooltip carry the real one. */
@@ -2590,11 +2591,6 @@ function drawLanes(t) {
2590
2591
  const scale = timeScale(lo, hi, busy);
2591
2592
  const bands = together(timed);
2592
2593
 
2593
- const bandLayer = bands.map(([a, b]) => {
2594
- const left = scale.posOf(a);
2595
- return `<span class="band" style="left:${left}%;width:${Math.max(scale.posOf(b) - left, 0.3)}%"></span>`;
2596
- }).join("");
2597
-
2598
2594
  const breaks = scale.gaps.map((g) =>
2599
2595
  `<span class="gapmark" style="left:${g.at}%" title="nothing running for ${esc(g.took)}"></span>`
2600
2596
  ).join("");
@@ -2637,7 +2633,7 @@ function drawLanes(t) {
2637
2633
  mainRow,
2638
2634
  marks: marks.map((m) => `<span style="left:${m.pos}%">${esc(m.label)}</span>`).join(""),
2639
2635
  note,
2640
- layer: bandLayer + breaks,
2636
+ layer: breaks,
2641
2637
  overlaps: bands.length,
2642
2638
  });
2643
2639
  }
@@ -2698,7 +2694,6 @@ function laneShell({ count, shown, whole, rows, mainRow, marks, note, layer, ove
2698
2694
  <div class="lanes-legend">
2699
2695
  <span><i style="background:var(--ink);opacity:.85"></i>main thread</span>
2700
2696
  <span><i style="background:var(--accent);opacity:.85"></i>subagent</span>
2701
- ${overlaps ? `<span><i style="background:var(--hl)"></i>running at the same time</span>` : ""}
2702
2697
  <span class="lanes-note">${esc(note)}</span>
2703
2698
  </div>
2704
2699
  </div>
@@ -2746,13 +2741,51 @@ function laneCard(ev, id) {
2746
2741
  /* Open the subagent a lane stands for: scroll to the step that called it and
2747
2742
  unfold the box that is already there, rather than inventing a second place
2748
2743
  for the same thing to live. */
2744
+ /* Open the subagent a lane stands for: reach the step that called it, unfold
2745
+ the box already there, and go to it.
2746
+
2747
+ A long run is drawn 250 steps at a time. In one real session all thirty
2748
+ delegations happen after step 1,231, so the box a lane pointed at had never
2749
+ been painted and the click did nothing at all. The run is drawn far enough
2750
+ first. A lane from a record where nothing spawned it is found in the block
2751
+ after the run instead — which only exists in the plain view, so a lens or a
2752
+ search is cleared to get back to it rather than failing quietly. */
2749
2753
  function openLane(id) {
2750
2754
  laneCard(null);
2751
- const box = document.querySelector(`details.branch[data-lane="${CSS.escape(id)}"]`);
2752
- if (!box) return;
2755
+ const lane = LANES.find((l) => l.id === id);
2756
+ if (!lane) return;
2757
+
2758
+ const find = () =>
2759
+ document.querySelector(`details.branch[data-lane="${CSS.escape(id)}"]`);
2760
+
2761
+ let box = find();
2762
+ if (!box) {
2763
+ let repaint = false;
2764
+ if (lane.fromStep === null && (lens !== "all" || query)) {
2765
+ lens = "all";
2766
+ query = "";
2767
+ repaint = true;
2768
+ }
2769
+ if (lane.fromIndex >= limit) {
2770
+ // Far enough to include it, and a little beyond so it is not the last row.
2771
+ limit = lane.fromIndex + PAGE_SIZE;
2772
+ repaint = true;
2773
+ }
2774
+ if (repaint) render();
2775
+ box = find();
2776
+ }
2777
+ if (!box) {
2778
+ note("That subagent is not in this view.");
2779
+ return;
2780
+ }
2781
+
2753
2782
  box.open = true;
2754
- box.scrollIntoView({ behavior: "smooth", block: "center" });
2755
2783
  box.classList.add("found");
2784
+ // After the fold, not before: opening changes the height, and scrolling to
2785
+ // where the box used to end leaves the reader above what they asked for.
2786
+ requestAnimationFrame(() =>
2787
+ box.scrollIntoView({ behavior: "smooth", block: "center" }),
2788
+ );
2756
2789
  setTimeout(() => box.classList.remove("found"), 1400);
2757
2790
  }
2758
2791
 
@@ -66,8 +66,31 @@ for (const id of [
66
66
  const SAFE_FETCH = () =>
67
67
  Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
68
68
 
69
+ /* Lookup by id, as the page expects. Reset with fetch on every run: a test that
70
+ stubs it to watch one element used to leave the next test's render() holding
71
+ an object with no focus(), which fails somewhere else entirely. */
72
+ const SAFE_BY_ID = () => null;
73
+
74
+ /* Looking for an element by selector, against what was actually painted. The
75
+ stub DOM holds markup as a string rather than a tree, so this answers the one
76
+ question the page asks — is this box on the page — from that string. */
77
+ const SAFE_QUERY = (sel) => {
78
+ const m = /\[data-lane="([^"]+)"\]/.exec(sel);
79
+ if (!m) return null;
80
+ const html = (global.main && global.main.innerHTML) || "";
81
+ return html.includes(`data-lane="${m[1]}"`)
82
+ ? { open: false, classList: { add() {}, remove() {} }, scrollIntoView() {} }
83
+ : null;
84
+ };
85
+
86
+ /* The page escapes an id before putting it in a selector; the stub DOM has no
87
+ CSS object, so provide the one method that is used. */
88
+ global.CSS = { escape: (v) => String(v).replace(/["\\]/g, "\\$&") };
89
+
69
90
  const run = (body) => {
70
91
  global.fetch = SAFE_FETCH;
92
+ global.document.getElementById = SAFE_BY_ID;
93
+ global.document.querySelector = SAFE_QUERY;
71
94
  return eval(script + "\n;(() => {" + body + "})()");
72
95
  };
73
96
 
@@ -1841,7 +1864,9 @@ test("the timeline says how many ran at once", () => {
1841
1864
  assert.match(html, /id="lanes"/);
1842
1865
  assert.match(html, /2 subagents/);
1843
1866
  assert.match(html, /overlapped once/, "one overlap reads as once, not '1 times'");
1844
- assert.match(html, /class="band"/, "the overlap is drawn, not only counted");
1867
+ // Overlap is not given a colour of its own: two bars in the same column of
1868
+ // the chart already say it, and a second encoding of the same fact is noise.
1869
+ assert.ok(!/class="band"/.test(html), "the lanes are the encoding");
1845
1870
  });
1846
1871
 
1847
1872
  test("the header counts, and counts in English", () => {
@@ -2042,3 +2067,60 @@ test("two durations read against each other share a unit", () => {
2042
2067
  const wide = run(`return durationPair(31*3600000, 10*86400000)`);
2043
2068
  assert.deepStrictEqual(wide, ["1.3d", "10d"]);
2044
2069
  });
2070
+
2071
+ test("a lane reaches its box even when the run has not been drawn that far", () => {
2072
+ // The bug: a long run paints 250 steps at a time, and in one real session all
2073
+ // thirty delegations happen after step 1,231 — so the box a lane pointed at
2074
+ // had never been painted and clicking did nothing whatever.
2075
+ const steps = [];
2076
+ for (let i = 0; i < 400; i++) {
2077
+ steps.push({ step_id: i + 1, source: "agent", message: "working" });
2078
+ }
2079
+ steps[300] = {
2080
+ step_id: 301, source: "agent",
2081
+ tool_calls: [{ tool_call_id: "c1", function_name: "Task" }],
2082
+ observation: { results: [{ source_call_id: "c1",
2083
+ subagent_trajectory_ref: [{ trajectory_id: "far" }] }] },
2084
+ };
2085
+ const t = {
2086
+ steps,
2087
+ subagent_trajectories: [{ trajectory_id: "far", agent: { name: "claude-code" },
2088
+ extra: { agentType: "Explore" }, steps: [] }],
2089
+ };
2090
+
2091
+ const out = run(
2092
+ setup({ ...ROW, key: "k" }) + `
2093
+ traj = ${JSON.stringify(t)};
2094
+ cur = "k"; lens = "all"; query = ""; limit = 250;
2095
+ render();
2096
+ const before = !!document.querySelector('details.branch[data-lane="far"]');
2097
+ LANES = laneIndex(traj);
2098
+ openLane("far");
2099
+ const after = !!document.querySelector('details.branch[data-lane="far"]');
2100
+ return { before, after, limit };
2101
+ `);
2102
+
2103
+ assert.strictEqual(out.before, false, "the box starts undrawn — this is the bug");
2104
+ assert.strictEqual(out.after, true, "and the click has to reach it");
2105
+ assert.ok(out.limit > 300, `the run was drawn far enough (limit ${out.limit})`);
2106
+ });
2107
+
2108
+ test("a lane whose box is filtered away brings the view back rather than failing", () => {
2109
+ const t = {
2110
+ steps: [{ step_id: 1, source: "user", message: "go" }],
2111
+ subagent_trajectories: [{ trajectory_id: "loose", agent: { name: "metr-malt" },
2112
+ extra: {}, steps: [] }],
2113
+ };
2114
+ const out = run(
2115
+ setup({ ...ROW, key: "k" }) + `
2116
+ traj = ${JSON.stringify(t)};
2117
+ cur = "k"; lens = "tools"; query = "nothing matches this"; limit = 250;
2118
+ render();
2119
+ LANES = laneIndex(traj);
2120
+ openLane("loose");
2121
+ return { lens, query, found: !!document.querySelector('details.branch[data-lane="loose"]') };
2122
+ `);
2123
+ assert.strictEqual(out.lens, "all", "the lens is cleared to reach it");
2124
+ assert.strictEqual(out.query, "");
2125
+ assert.strictEqual(out.found, true);
2126
+ });
@@ -388,7 +388,7 @@ wheels = [
388
388
 
389
389
  [[package]]
390
390
  name = "transcript-viewer"
391
- version = "0.11.1"
391
+ version = "0.11.2"
392
392
  source = { editable = "." }
393
393
  dependencies = [
394
394
  { name = "atif-make" },