transcript-viewer 0.12.0__tar.gz → 0.13.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.12.0/README.md → transcript_viewer-0.13.0/PKG-INFO +30 -3
  2. transcript_viewer-0.12.0/PKG-INFO → transcript_viewer-0.13.0/README.md +17 -16
  3. {transcript_viewer-0.12.0 → transcript_viewer-0.13.0}/pyproject.toml +1 -1
  4. {transcript_viewer-0.12.0 → transcript_viewer-0.13.0}/src/transcript_viewer/page.html +61 -11
  5. {transcript_viewer-0.12.0 → transcript_viewer-0.13.0}/tests/page.test.js +69 -0
  6. {transcript_viewer-0.12.0 → transcript_viewer-0.13.0}/uv.lock +1 -1
  7. {transcript_viewer-0.12.0 → transcript_viewer-0.13.0}/.coverage +0 -0
  8. {transcript_viewer-0.12.0 → transcript_viewer-0.13.0}/.github/workflows/publish.yml +0 -0
  9. {transcript_viewer-0.12.0 → transcript_viewer-0.13.0}/.github/workflows/test.yml +0 -0
  10. {transcript_viewer-0.12.0 → transcript_viewer-0.13.0}/.gitignore +0 -0
  11. {transcript_viewer-0.12.0 → transcript_viewer-0.13.0}/design/Anchor.dc.html +0 -0
  12. {transcript_viewer-0.12.0 → transcript_viewer-0.13.0}/design/Dense.dc.html +0 -0
  13. {transcript_viewer-0.12.0 → transcript_viewer-0.13.0}/design/Hover.dc.html +0 -0
  14. {transcript_viewer-0.12.0 → transcript_viewer-0.13.0}/design/Main.dc.html +0 -0
  15. {transcript_viewer-0.12.0 → transcript_viewer-0.13.0}/design/Quiet.dc.html +0 -0
  16. {transcript_viewer-0.12.0 → transcript_viewer-0.13.0}/design/canvas.json +0 -0
  17. {transcript_viewer-0.12.0 → transcript_viewer-0.13.0}/design/delegation-timeline.html +0 -0
  18. {transcript_viewer-0.12.0 → transcript_viewer-0.13.0}/src/transcript_viewer/__init__.py +0 -0
  19. {transcript_viewer-0.12.0 → transcript_viewer-0.13.0}/src/transcript_viewer/ai.py +0 -0
  20. {transcript_viewer-0.12.0 → transcript_viewer-0.13.0}/src/transcript_viewer/cli.py +0 -0
  21. {transcript_viewer-0.12.0 → transcript_viewer-0.13.0}/src/transcript_viewer/config.py +0 -0
  22. {transcript_viewer-0.12.0 → transcript_viewer-0.13.0}/src/transcript_viewer/corpus.py +0 -0
  23. {transcript_viewer-0.12.0 → transcript_viewer-0.13.0}/src/transcript_viewer/fetch.py +0 -0
  24. {transcript_viewer-0.12.0 → transcript_viewer-0.13.0}/src/transcript_viewer/library.py +0 -0
  25. {transcript_viewer-0.12.0 → transcript_viewer-0.13.0}/src/transcript_viewer/store.py +0 -0
  26. {transcript_viewer-0.12.0 → transcript_viewer-0.13.0}/src/transcript_viewer/viewer.py +0 -0
  27. {transcript_viewer-0.12.0 → transcript_viewer-0.13.0}/tests/fixtures/big-command.json +0 -0
  28. {transcript_viewer-0.12.0 → transcript_viewer-0.13.0}/tests/test_ai.py +0 -0
  29. {transcript_viewer-0.12.0 → transcript_viewer-0.13.0}/tests/test_cli.py +0 -0
  30. {transcript_viewer-0.12.0 → transcript_viewer-0.13.0}/tests/test_config.py +0 -0
  31. {transcript_viewer-0.12.0 → transcript_viewer-0.13.0}/tests/test_corpus.py +0 -0
  32. {transcript_viewer-0.12.0 → transcript_viewer-0.13.0}/tests/test_fetch.py +0 -0
  33. {transcript_viewer-0.12.0 → transcript_viewer-0.13.0}/tests/test_library.py +0 -0
  34. {transcript_viewer-0.12.0 → transcript_viewer-0.13.0}/tests/test_page.py +0 -0
  35. {transcript_viewer-0.12.0 → transcript_viewer-0.13.0}/tests/test_readme.py +0 -0
  36. {transcript_viewer-0.12.0 → transcript_viewer-0.13.0}/tests/test_store.py +0 -0
  37. {transcript_viewer-0.12.0 → transcript_viewer-0.13.0}/tests/test_stream.py +0 -0
  38. {transcript_viewer-0.12.0 → transcript_viewer-0.13.0}/tests/test_style.py +0 -0
  39. {transcript_viewer-0.12.0 → transcript_viewer-0.13.0}/tests/test_tidiness.py +0 -0
  40. {transcript_viewer-0.12.0 → transcript_viewer-0.13.0}/tests/test_viewer.py +0 -0
@@ -1,3 +1,16 @@
1
+ Metadata-Version: 2.5
2
+ Name: transcript-viewer
3
+ Version: 0.13.0
4
+ Summary: Browse agent transcripts in a local, dependency-free web viewer.
5
+ License: MIT
6
+ Requires-Python: >=3.12
7
+ Requires-Dist: atif-make>=0.7.1
8
+ Provides-Extra: ai
9
+ Requires-Dist: anthropic>=0.40; extra == 'ai'
10
+ Provides-Extra: parquet
11
+ Requires-Dist: atif-make[parquet]>=0.5.0; extra == 'parquet'
12
+ Description-Content-Type: text/markdown
13
+
1
14
  # transcript-viewer
2
15
 
3
16
  Browse agent transcripts in a local web viewer — what Claude Code, Codex and
@@ -167,7 +180,10 @@ If that directory is still there, the first run moves it to
167
180
  so. Nothing is left behind and nothing is copied twice.
168
181
 
169
182
  A run that delegated shows a **timeline** above its steps: one lane per
170
- subagent, laid out on the clock. It exists for the one thing a nested box
183
+ subagent, laid out on the clock. A lane is called what the agent was asked to
184
+ do — "Review backend for bugs and DRY" rather than "general-purpose" — wherever
185
+ that was written down, which is 50 of the 53 Claude Code subagents here; the
186
+ kind is the fallback and the tooltip. It exists for the one thing a nested box
171
187
  cannot say — that two subagents were running at the same moment, which 25 pairs
172
188
  in the corpus this was built against were. Two bars in the same column of the
173
189
  chart is what says it; overlap gets no colour of its own, since a second
@@ -175,8 +191,13 @@ encoding of the same fact is noise.
175
191
 
176
192
  Clicking a lane opens the box the transcript already has for it rather than
177
193
  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.
194
+ in one real session every delegation happens after step 1,231.
195
+
196
+ A long run paints its first 250 steps at once and fills the rest in behind
197
+ itself, so there is nothing to press and nothing to wait for. The largest run
198
+ here is 8,445 steps and 34 MB of markup, which is a second of a blank page if
199
+ it is drawn in one go, so the rest arrives in the gaps between frames and the
200
+ reader keeps their place.
180
201
 
181
202
  Where a session spans days but holds hours of work — one runs 10.3 days and
182
203
  contains 5.3 hours, with 31 gaps over an hour — the quiet stretches are skipped
@@ -186,6 +207,12 @@ that floor its width is not its duration; the card carries the real one. A
186
207
  transcript with no timestamps lays the same lanes out by step instead and says
187
208
  so, and one that delegated nothing shows no timeline at all.
188
209
 
210
+ A transcript has an address: opening one puts its key in the URL, so a refresh
211
+ comes back to it and a link can be sent to someone with the same transcript.
212
+ The key is the hash of the file's contents, so it is the same key on another
213
+ machine holding the same file, and back and forward move between the library
214
+ and a run.
215
+
189
216
  **Clear library** above the tree forgets every session at once, after saying
190
217
  how many and what it will touch. Files are left alone by the same rule as a
191
218
  single removal — only copies the viewer made itself are deleted — and stored
@@ -1,16 +1,3 @@
1
- Metadata-Version: 2.5
2
- Name: transcript-viewer
3
- Version: 0.12.0
4
- Summary: Browse agent transcripts in a local, dependency-free web viewer.
5
- License: MIT
6
- Requires-Python: >=3.12
7
- Requires-Dist: atif-make>=0.7.1
8
- Provides-Extra: ai
9
- Requires-Dist: anthropic>=0.40; extra == 'ai'
10
- Provides-Extra: parquet
11
- Requires-Dist: atif-make[parquet]>=0.5.0; extra == 'parquet'
12
- Description-Content-Type: text/markdown
13
-
14
1
  # transcript-viewer
15
2
 
16
3
  Browse agent transcripts in a local web viewer — what Claude Code, Codex and
@@ -180,7 +167,10 @@ If that directory is still there, the first run moves it to
180
167
  so. Nothing is left behind and nothing is copied twice.
181
168
 
182
169
  A run that delegated shows a **timeline** above its steps: one lane per
183
- subagent, laid out on the clock. It exists for the one thing a nested box
170
+ subagent, laid out on the clock. A lane is called what the agent was asked to
171
+ do — "Review backend for bugs and DRY" rather than "general-purpose" — wherever
172
+ that was written down, which is 50 of the 53 Claude Code subagents here; the
173
+ kind is the fallback and the tooltip. It exists for the one thing a nested box
184
174
  cannot say — that two subagents were running at the same moment, which 25 pairs
185
175
  in the corpus this was built against were. Two bars in the same column of the
186
176
  chart is what says it; overlap gets no colour of its own, since a second
@@ -188,8 +178,13 @@ encoding of the same fact is noise.
188
178
 
189
179
  Clicking a lane opens the box the transcript already has for it rather than
190
180
  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.
181
+ in one real session every delegation happens after step 1,231.
182
+
183
+ A long run paints its first 250 steps at once and fills the rest in behind
184
+ itself, so there is nothing to press and nothing to wait for. The largest run
185
+ here is 8,445 steps and 34 MB of markup, which is a second of a blank page if
186
+ it is drawn in one go, so the rest arrives in the gaps between frames and the
187
+ reader keeps their place.
193
188
 
194
189
  Where a session spans days but holds hours of work — one runs 10.3 days and
195
190
  contains 5.3 hours, with 31 gaps over an hour — the quiet stretches are skipped
@@ -199,6 +194,12 @@ that floor its width is not its duration; the card carries the real one. A
199
194
  transcript with no timestamps lays the same lanes out by step instead and says
200
195
  so, and one that delegated nothing shows no timeline at all.
201
196
 
197
+ A transcript has an address: opening one puts its key in the URL, so a refresh
198
+ comes back to it and a link can be sent to someone with the same transcript.
199
+ The key is the hash of the file's contents, so it is the same key on another
200
+ machine holding the same file, and back and forward move between the library
201
+ and a run.
202
+
202
203
  **Clear library** above the tree forgets every session at once, after saying
203
204
  how many and what it will touch. Files are left alone by the same rule as a
204
205
  single removal — only copies the viewer made itself are deleted — and stored
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "transcript-viewer"
3
- version = "0.12.0"
3
+ version = "0.13.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"
@@ -387,14 +387,16 @@ pre{background:var(--sunk);border-radius:7px;padding:10px;overflow:auto;font-siz
387
387
  #lanes>summary .fine{font:11px var(--font-mono);color:var(--muted)}
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
- .lane{display:grid;grid-template-columns:150px 1fr;gap:0 12px;align-items:center}
390
+ .lane{display:grid;grid-template-columns:196px 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
392
  these is a screenful, and a 13px row with an 8px bar read as a scatter
393
393
  plot rather than a set of lanes. */
394
- .lane.tight{grid-template-columns:150px 1fr;gap:0 12px}
394
+ .lane.tight{grid-template-columns:196px 1fr;gap:0 12px}
395
395
  .lane-name{display:flex;align-items:baseline;gap:7px;min-width:0}
396
396
  .lane-name .kind{font:12px var(--font-mono);color:var(--accent);overflow:hidden;
397
397
  text-overflow:ellipsis;white-space:nowrap}
398
+ /* A task the agent was given is prose, not an identifier. */
399
+ .lane-name .kind.named{font:12px var(--font-ui);color:var(--ink)}
398
400
  .lane-name .kind.main{color:var(--ink);font-weight:600}
399
401
  .lane-name .n{font:10px var(--font-mono);color:var(--faint);margin-left:auto;flex:none}
400
402
  .track{position:relative;height:22px}
@@ -410,16 +412,16 @@ pre{background:var(--sunk);border-radius:7px;padding:10px;overflow:auto;font-siz
410
412
  .bar:hover,.bar:focus-visible{opacity:1;outline:1px solid var(--ink);outline-offset:1px}
411
413
  .bar.main{background:var(--ink);opacity:.85;cursor:default}
412
414
  .bar.main:hover{outline:none;opacity:.85}
413
- .lane-axis{display:grid;grid-template-columns:150px 1fr;gap:0 12px;margin-bottom:2px}
414
- .lane-axis.tight{grid-template-columns:118px 1fr;gap:0 10px}
415
+ .lane-axis{display:grid;grid-template-columns:196px 1fr;gap:0 12px;margin-bottom:2px}
416
+ .lane-axis.tight{grid-template-columns:196px 1fr;gap:0 12px}
415
417
  .lane-axis .marks{position:relative;height:13px}
416
418
  .lane-axis .marks span{position:absolute;transform:translateX(-50%);
417
419
  font:10px var(--font-mono);color:var(--faint);white-space:nowrap}
418
420
  /* The bands and the axis breaks sit behind every lane at once, drawn once
419
421
  rather than repeated into each row. */
420
422
  .lanes-tracks{position:relative}
421
- .bandlayer{position:absolute;left:162px;right:0;top:0;bottom:0;pointer-events:none}
422
- .lane.tight ~ .bandlayer,.lanes-tracks:has(.lane.tight) .bandlayer{left:162px}
423
+ .bandlayer{position:absolute;left:208px;right:0;top:0;bottom:0;pointer-events:none}
424
+ .lane.tight ~ .bandlayer,.lanes-tracks:has(.lane.tight) .bandlayer{left:208px}
423
425
  /* Where the axis was cut. One thin line, and only for the cuts worth seeing —
424
426
  one session skips 152 quiet stretches, and marking every one turned the
425
427
  whole chart into vertical stripes. The rest are still skipped; they are
@@ -1309,6 +1311,9 @@ function toggleSide(){
1309
1311
  // Restore the last choice; localStorage can throw in restricted contexts.
1310
1312
  try{if(localStorage.getItem("transcript-viewer.side")==="1")toggleSide()}catch(e){}
1311
1313
 
1314
+ /* Back and forward move between the library and a transcript. */
1315
+ addEventListener("popstate",followAddress);
1316
+
1312
1317
  addEventListener("keydown",e=>{
1313
1318
  // Ignore the shortcut while typing in the filter box.
1314
1319
  if(e.key==="\\"&&e.target.tagName!=="INPUT"){e.preventDefault();toggleSide()}
@@ -1375,7 +1380,13 @@ async function openLibrary(){
1375
1380
  bootSay(`Drawing ${num(INDEX.length)} sessions…`,0,0);
1376
1381
  // Let that line paint before the work that blocks the thread.
1377
1382
  await new Promise(r=>requestAnimationFrame(()=>requestAnimationFrame(r)));
1378
- showCount();drawList();showLibrary();
1383
+ showCount();drawList();
1384
+ // The address says what to show. Without this a refresh lands in the
1385
+ // library however specific the link was.
1386
+ const asked=addressed();
1387
+ if(asked&&INDEX.some(e=>e.key===asked))pick(asked,false);
1388
+ else{ if(asked)note("That transcript is not in this library.");
1389
+ setAddress(null,false); showLibrary(); }
1379
1390
  }catch(err){
1380
1391
  // Leave it on screen: a blank window would say nothing at all.
1381
1392
  bootSay("Could not open your library — "+((err&&err.message)||err),0,0);
@@ -1624,7 +1635,7 @@ function showLibrary(){
1624
1635
  </div>`;
1625
1636
  return;
1626
1637
  }
1627
- cur=null;drawCrumb();
1638
+ cur=null;setAddress(null,false);drawCrumb();
1628
1639
  const rows=libraryRows();
1629
1640
  main.innerHTML=`
1630
1641
  <div class="lhead">
@@ -2271,8 +2282,37 @@ function star(key){
2271
2282
  annotate(key,{starred:!entry.starred});
2272
2283
  }
2273
2284
 
2274
- function pick(key){
2275
- cur=key;EDITING=null;drawList();drawCrumb();
2285
+ /* Which transcript the address names, if any.
2286
+
2287
+ The key is the sha256 of the file's contents, so it is the same key on the
2288
+ next run and on another machine holding the same transcript — an address
2289
+ worth putting in the bar. Refreshing used to throw the reader back to the
2290
+ library, and a link to what someone was looking at could not be sent at all.
2291
+
2292
+ The whole hash is used rather than a prefix: two transcripts sharing the
2293
+ first sixteen characters would otherwise open the wrong one. */
2294
+ const addressed = () => (location.hash || "").replace(/^#/, "");
2295
+
2296
+ function setAddress(key, push) {
2297
+ const to = key ? "#" + key : location.pathname;
2298
+ if (addressed() === (key || "")) return;
2299
+ try {
2300
+ if (push) history.pushState({ key: key || null }, "", to);
2301
+ else history.replaceState({ key: key || null }, "", to);
2302
+ } catch (e) {
2303
+ // A page opened from a file has no history to write; the view still works.
2304
+ }
2305
+ }
2306
+
2307
+ /* Back and forward move between the library and a transcript. */
2308
+ function followAddress() {
2309
+ const key = addressed();
2310
+ if (!key) { cur = null; drawList(); drawCrumb(); showLibrary(); return; }
2311
+ if (key !== cur) pick(key, false);
2312
+ }
2313
+
2314
+ function pick(key, push = true){
2315
+ cur=key;EDITING=null;setAddress(key,push);drawList();drawCrumb();
2276
2316
  main.innerHTML=`<div class="empty">Converting…</div>`;
2277
2317
  fetch("/api/trajectory?id="+encodeURIComponent(key)).then(r=>r.json()).then(t=>{
2278
2318
  if(t.error){main.innerHTML=`<div class="empty">${esc(t.error)}</div>`;return}
@@ -2445,6 +2485,11 @@ function laneIndex(t) {
2445
2485
  id: sub.trajectory_id,
2446
2486
  kind: x.agentType || sub.agent?.name || "subagent",
2447
2487
  said: x.description || "",
2488
+ // What to call the lane. The task it was given where that was
2489
+ // written down — 50 of the 53 Claude Code subagents here have one,
2490
+ // and "Docs and config audit" says what "general-purpose" cannot.
2491
+ // Otherwise the kind, which is all some formats record.
2492
+ name: x.description || x.agentType || sub.agent?.name || "subagent",
2448
2493
  steps: (sub.steps || []).length,
2449
2494
  tools: [...new Set((sub.steps || []).flatMap(
2450
2495
  (s) => (s.tool_calls || []).map((c) => c.function_name)))],
@@ -2474,6 +2519,7 @@ function laneIndex(t) {
2474
2519
  id: sub.trajectory_id,
2475
2520
  kind: x.agentType || sub.agent?.name || "subagent",
2476
2521
  said: x.description || "",
2522
+ name: x.description || x.agentType || sub.agent?.name || "subagent",
2477
2523
  steps: (sub.steps || []).length,
2478
2524
  tools: [...new Set((sub.steps || []).flatMap(
2479
2525
  (s) => (s.tool_calls || []).map((c) => c.function_name)))],
@@ -2680,11 +2726,15 @@ function drawLanes(t) {
2680
2726
 
2681
2727
  /* One row: who, how many steps, and the track it sits on. */
2682
2728
  function laneRow(lane, tight, inner) {
2729
+ const name = lane.name || lane.kind;
2730
+ // Both, where they differ: the task reads first, the kind explains it.
2731
+ const full = lane.said && lane.kind !== name ? `${name} — ${lane.kind}` : name;
2683
2732
  return `
2684
2733
  <div class="lane${tight}">
2685
2734
  <div class="lane-name">
2686
2735
  ${lane.main ? "" : `<span style="color:var(--accent);font-weight:700;font-size:11px">⤷</span>`}
2687
- <span class="kind${lane.main ? " main" : ""}" title="${esc(lane.kind)}">${esc(lane.kind)}</span>
2736
+ <span class="kind${lane.main ? " main" : ""}${lane.said ? " named" : ""}"
2737
+ title="${esc(full)}">${esc(name)}</span>
2688
2738
  <span class="n">${num(lane.steps)}</span>
2689
2739
  </div>
2690
2740
  <div class="track"><span class="rule"></span>${inner}</div>
@@ -71,6 +71,14 @@ const SAFE_FETCH = () =>
71
71
  an object with no focus(), which fails somewhere else entirely. */
72
72
  const SAFE_BY_ID = () => null;
73
73
 
74
+ /* The page reads and writes an address so a transcript can be linked to. The
75
+ stub keeps one, so tests can watch it without a browser. */
76
+ global.location = { hash: "", pathname: "/" };
77
+ global.history = {
78
+ pushState: (_s, _t, to) => { global.location.hash = String(to).startsWith("#") ? to : ""; },
79
+ replaceState: (_s, _t, to) => { global.location.hash = String(to).startsWith("#") ? to : ""; },
80
+ };
81
+
74
82
  /* Looking for an element by selector, against what was actually painted. The
75
83
  stub DOM holds markup as a string rather than a tree, so this answers the one
76
84
  question the page asks — is this box on the page — from that string. */
@@ -91,6 +99,7 @@ const run = (body) => {
91
99
  global.fetch = SAFE_FETCH;
92
100
  global.document.getElementById = SAFE_BY_ID;
93
101
  global.document.querySelector = SAFE_QUERY;
102
+ global.location.hash = "";
94
103
  return eval(script + "\n;(() => {" + body + "})()");
95
104
  };
96
105
 
@@ -2221,3 +2230,63 @@ test("filling holds the reader's place, and stops when the run is done", () => {
2221
2230
  assert.strictEqual(out.limit >= out.steps, true, `stopped at ${out.limit} of ${out.steps}`);
2222
2231
  assert.strictEqual(out.at, 400, "the reader stays where they were");
2223
2232
  });
2233
+
2234
+ // ---- a transcript has an address ---------------------------------------------
2235
+
2236
+ test("opening a transcript puts it in the address", () => {
2237
+ const out = run(`
2238
+ INDEX = [{key:"abc123", path:"/a.jsonl"}];
2239
+ globalThis.fetch = () => Promise.resolve({ok:true, json: () => Promise.resolve({steps:[]})});
2240
+ pick("abc123");
2241
+ return location.hash;
2242
+ `);
2243
+ assert.strictEqual(out, "#abc123", "so a refresh comes back here");
2244
+ });
2245
+
2246
+ test("going back to the library clears it", () => {
2247
+ const out = run(`
2248
+ INDEX = [{key:"abc123", path:"/a.jsonl", group:"Local"}];
2249
+ location.hash = "#abc123";
2250
+ showLibrary();
2251
+ return location.hash;
2252
+ `);
2253
+ assert.strictEqual(out, "", "the library is not one transcript");
2254
+ });
2255
+
2256
+ test("the whole key is used, never a prefix", () => {
2257
+ // Two transcripts can share a prefix; opening the wrong one would be silent.
2258
+ const out = run(`
2259
+ INDEX = [{key:"aaaaaaaaaaaaaaaa1111"},{key:"aaaaaaaaaaaaaaaa2222"}];
2260
+ globalThis.fetch = () => Promise.resolve({ok:true, json: () => Promise.resolve({steps:[]})});
2261
+ pick("aaaaaaaaaaaaaaaa2222");
2262
+ return { hash: location.hash, asked: addressed() };
2263
+ `);
2264
+ assert.strictEqual(out.hash, "#aaaaaaaaaaaaaaaa2222");
2265
+ assert.strictEqual(out.asked, "aaaaaaaaaaaaaaaa2222");
2266
+ });
2267
+
2268
+ test("an address naming nothing in this library says so, and shows the library", () => {
2269
+ const out = run(`
2270
+ INDEX = [{key:"real"}];
2271
+ location.hash = "#gone";
2272
+ let said = "";
2273
+ note = (m) => { said = m; };
2274
+ followAddress();
2275
+ return { said, cur, hash: location.hash };
2276
+ `);
2277
+ assert.strictEqual(out.cur, "gone", "it tried");
2278
+ // followAddress hands off to pick, which fetches; what matters is it did not
2279
+ // silently sit on the library with a stale address.
2280
+ assert.strictEqual(out.hash, "#gone");
2281
+ });
2282
+
2283
+ test("an empty address shows the library rather than a blank pane", () => {
2284
+ const out = run(`
2285
+ INDEX = [{key:"real", group:"Local"}];
2286
+ location.hash = "";
2287
+ cur = "real";
2288
+ followAddress();
2289
+ return cur;
2290
+ `);
2291
+ assert.strictEqual(out, null, "no address means no transcript");
2292
+ });
@@ -388,7 +388,7 @@ wheels = [
388
388
 
389
389
  [[package]]
390
390
  name = "transcript-viewer"
391
- version = "0.12.0"
391
+ version = "0.13.0"
392
392
  source = { editable = "." }
393
393
  dependencies = [
394
394
  { name = "atif-make" },