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.
- {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/PKG-INFO +9 -3
- {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/README.md +8 -2
- {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/pyproject.toml +1 -1
- {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/src/transcript_viewer/page.html +44 -11
- {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/tests/page.test.js +83 -1
- {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/uv.lock +1 -1
- {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/.coverage +0 -0
- {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/.github/workflows/publish.yml +0 -0
- {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/.github/workflows/test.yml +0 -0
- {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/.gitignore +0 -0
- {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/design/Anchor.dc.html +0 -0
- {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/design/Dense.dc.html +0 -0
- {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/design/Hover.dc.html +0 -0
- {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/design/Main.dc.html +0 -0
- {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/design/Quiet.dc.html +0 -0
- {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/design/canvas.json +0 -0
- {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/design/delegation-timeline.html +0 -0
- {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/src/transcript_viewer/__init__.py +0 -0
- {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/src/transcript_viewer/ai.py +0 -0
- {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/src/transcript_viewer/cli.py +0 -0
- {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/src/transcript_viewer/config.py +0 -0
- {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/src/transcript_viewer/corpus.py +0 -0
- {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/src/transcript_viewer/fetch.py +0 -0
- {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/src/transcript_viewer/library.py +0 -0
- {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/src/transcript_viewer/store.py +0 -0
- {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/src/transcript_viewer/viewer.py +0 -0
- {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/tests/fixtures/big-command.json +0 -0
- {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/tests/test_ai.py +0 -0
- {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/tests/test_cli.py +0 -0
- {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/tests/test_config.py +0 -0
- {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/tests/test_corpus.py +0 -0
- {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/tests/test_fetch.py +0 -0
- {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/tests/test_library.py +0 -0
- {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/tests/test_page.py +0 -0
- {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/tests/test_readme.py +0 -0
- {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/tests/test_store.py +0 -0
- {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/tests/test_stream.py +0 -0
- {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/tests/test_style.py +0 -0
- {transcript_viewer-0.11.1 → transcript_viewer-0.11.2}/tests/test_tidiness.py +0 -0
- {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.
|
|
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.
|
|
186
|
-
|
|
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.
|
|
173
|
-
|
|
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
|
|
@@ -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:
|
|
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
|
|
2752
|
-
if (!
|
|
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
|
-
|
|
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
|
+
});
|
|
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
|