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.
- {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/PKG-INFO +1 -1
- {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/pyproject.toml +1 -1
- {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/src/transcript_viewer/page.html +70 -16
- {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/tests/page.test.js +97 -0
- {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/uv.lock +1 -1
- {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/.coverage +0 -0
- {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/.github/workflows/publish.yml +0 -0
- {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/.github/workflows/test.yml +0 -0
- {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/.gitignore +0 -0
- {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/README.md +0 -0
- {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/design/Anchor.dc.html +0 -0
- {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/design/Dense.dc.html +0 -0
- {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/design/Hover.dc.html +0 -0
- {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/design/Main.dc.html +0 -0
- {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/design/Quiet.dc.html +0 -0
- {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/design/canvas.json +0 -0
- {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/design/delegation-timeline.html +0 -0
- {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/src/transcript_viewer/__init__.py +0 -0
- {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/src/transcript_viewer/ai.py +0 -0
- {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/src/transcript_viewer/cli.py +0 -0
- {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/src/transcript_viewer/config.py +0 -0
- {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/src/transcript_viewer/corpus.py +0 -0
- {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/src/transcript_viewer/fetch.py +0 -0
- {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/src/transcript_viewer/library.py +0 -0
- {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/src/transcript_viewer/store.py +0 -0
- {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/src/transcript_viewer/viewer.py +0 -0
- {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/tests/fixtures/big-command.json +0 -0
- {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/tests/test_ai.py +0 -0
- {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/tests/test_cli.py +0 -0
- {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/tests/test_config.py +0 -0
- {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/tests/test_corpus.py +0 -0
- {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/tests/test_fetch.py +0 -0
- {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/tests/test_library.py +0 -0
- {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/tests/test_page.py +0 -0
- {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/tests/test_readme.py +0 -0
- {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/tests/test_store.py +0 -0
- {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/tests/test_stream.py +0 -0
- {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/tests/test_style.py +0 -0
- {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/tests/test_tidiness.py +0 -0
- {transcript_viewer-0.11.2 → transcript_viewer-0.12.0}/tests/test_viewer.py +0 -0
|
@@ -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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
420
|
-
.
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2787
|
-
|
|
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"><
|
|
2917
|
-
${num(shown.length)} of ${num(total)} steps</
|
|
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
|
+
});
|
|
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
|
|
File without changes
|