transcript-viewer 0.6.1__tar.gz → 0.6.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 (32) hide show
  1. {transcript_viewer-0.6.1 → transcript_viewer-0.6.2}/PKG-INFO +1 -1
  2. {transcript_viewer-0.6.1 → transcript_viewer-0.6.2}/pyproject.toml +1 -1
  3. {transcript_viewer-0.6.1 → transcript_viewer-0.6.2}/src/transcript_viewer/page.html +15 -13
  4. {transcript_viewer-0.6.1 → transcript_viewer-0.6.2}/tests/test_style.py +37 -0
  5. {transcript_viewer-0.6.1 → transcript_viewer-0.6.2}/uv.lock +1 -1
  6. {transcript_viewer-0.6.1 → transcript_viewer-0.6.2}/.coverage +0 -0
  7. {transcript_viewer-0.6.1 → transcript_viewer-0.6.2}/.github/workflows/publish.yml +0 -0
  8. {transcript_viewer-0.6.1 → transcript_viewer-0.6.2}/.github/workflows/test.yml +0 -0
  9. {transcript_viewer-0.6.1 → transcript_viewer-0.6.2}/.gitignore +0 -0
  10. {transcript_viewer-0.6.1 → transcript_viewer-0.6.2}/README.md +0 -0
  11. {transcript_viewer-0.6.1 → transcript_viewer-0.6.2}/src/transcript_viewer/__init__.py +0 -0
  12. {transcript_viewer-0.6.1 → transcript_viewer-0.6.2}/src/transcript_viewer/ai.py +0 -0
  13. {transcript_viewer-0.6.1 → transcript_viewer-0.6.2}/src/transcript_viewer/cli.py +0 -0
  14. {transcript_viewer-0.6.1 → transcript_viewer-0.6.2}/src/transcript_viewer/config.py +0 -0
  15. {transcript_viewer-0.6.1 → transcript_viewer-0.6.2}/src/transcript_viewer/corpus.py +0 -0
  16. {transcript_viewer-0.6.1 → transcript_viewer-0.6.2}/src/transcript_viewer/fetch.py +0 -0
  17. {transcript_viewer-0.6.1 → transcript_viewer-0.6.2}/src/transcript_viewer/library.py +0 -0
  18. {transcript_viewer-0.6.1 → transcript_viewer-0.6.2}/src/transcript_viewer/store.py +0 -0
  19. {transcript_viewer-0.6.1 → transcript_viewer-0.6.2}/src/transcript_viewer/viewer.py +0 -0
  20. {transcript_viewer-0.6.1 → transcript_viewer-0.6.2}/tests/page.test.js +0 -0
  21. {transcript_viewer-0.6.1 → transcript_viewer-0.6.2}/tests/test_ai.py +0 -0
  22. {transcript_viewer-0.6.1 → transcript_viewer-0.6.2}/tests/test_cli.py +0 -0
  23. {transcript_viewer-0.6.1 → transcript_viewer-0.6.2}/tests/test_config.py +0 -0
  24. {transcript_viewer-0.6.1 → transcript_viewer-0.6.2}/tests/test_corpus.py +0 -0
  25. {transcript_viewer-0.6.1 → transcript_viewer-0.6.2}/tests/test_fetch.py +0 -0
  26. {transcript_viewer-0.6.1 → transcript_viewer-0.6.2}/tests/test_library.py +0 -0
  27. {transcript_viewer-0.6.1 → transcript_viewer-0.6.2}/tests/test_page.py +0 -0
  28. {transcript_viewer-0.6.1 → transcript_viewer-0.6.2}/tests/test_readme.py +0 -0
  29. {transcript_viewer-0.6.1 → transcript_viewer-0.6.2}/tests/test_store.py +0 -0
  30. {transcript_viewer-0.6.1 → transcript_viewer-0.6.2}/tests/test_stream.py +0 -0
  31. {transcript_viewer-0.6.1 → transcript_viewer-0.6.2}/tests/test_tidiness.py +0 -0
  32. {transcript_viewer-0.6.1 → transcript_viewer-0.6.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.6.1
3
+ Version: 0.6.2
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.6.1"
3
+ version = "0.6.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"
@@ -838,6 +838,8 @@ function pre(content) {
838
838
 
839
839
  const esc=s=>String(s??"").replace(/[&<>"]/g,c=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[c]));
840
840
  const num=n=>(n??0).toLocaleString();
841
+ /* How many sessions are in the library, said the same way everywhere. */
842
+ const showCount=()=>{count.textContent=`${num(INDEX.length)} session${INDEX.length===1?"":"s"}`};
841
843
  /* One size formatter. Everything used to divide by 1048576 and say MB, so a
842
844
  900-byte log read as "0.0 MB" and a 144 GB bucket as "147456.0 MB". Each step
843
845
  keeps three significant figures, which is as much as anyone reads. */
@@ -870,7 +872,7 @@ function reveal(a){
870
872
  async function openFiles(files){
871
873
  if(!files||!files.length)return;
872
874
  const names=[...files].map(f=>f.name);
873
- count.textContent=`opening ${names.length} file${names.length>1?"s":""}…`;
875
+ count.textContent=`opening ${num(names.length)} file${names.length>1?"s":""}…`;
874
876
  const problems=[];
875
877
  let first=null;
876
878
  for(const file of files){
@@ -884,7 +886,7 @@ async function openFiles(files){
884
886
  }
885
887
  const fresh=await fetch("/api/index").then(r=>r.json());
886
888
  INDEX=fresh.sessions||[];GROUPS=fresh.groups||[];TAGS=fresh.tags||[];AI=fresh.ai||{};DOWNLOADS=fresh.downloads||DOWNLOADS;
887
- count.textContent=INDEX.length+" sessions";drawList();
889
+ showCount();drawList();
888
890
  if(first!==null)pick(first);
889
891
  if(problems.length)note(problems.join("\n"));
890
892
  picker.value="";
@@ -1015,10 +1017,10 @@ async function openLibrary(){
1015
1017
  try{
1016
1018
  const d=await readIndex();
1017
1019
  INDEX=d.sessions||[];GROUPS=d.groups||[];TAGS=d.tags||[];AI=d.ai||{};DOWNLOADS=d.downloads||DOWNLOADS;
1018
- bootSay("Drawing "+INDEX.length+" sessions…",0,0);
1020
+ bootSay(`Drawing ${num(INDEX.length)} sessions…`,0,0);
1019
1021
  // Let that line paint before the work that blocks the thread.
1020
1022
  await new Promise(r=>requestAnimationFrame(()=>requestAnimationFrame(r)));
1021
- count.textContent=INDEX.length+" sessions";drawList();showLibrary();
1023
+ showCount();drawList();showLibrary();
1022
1024
  }catch(err){
1023
1025
  // Leave it on screen: a blank window would say nothing at all.
1024
1026
  bootSay("Could not open your library — "+((err&&err.message)||err),0,0);
@@ -1117,13 +1119,13 @@ function drawList(){
1117
1119
  <button title="Remove these sessions from the library" class="dang"
1118
1120
  onclick="event.stopPropagation();dropGroup('${esc(r.path)}')">✕</button>
1119
1121
  </span>`}
1120
- <span class="rc">${r.count}</span></div>`;
1122
+ <span class="rc">${num(r.count)}</span></div>`;
1121
1123
  }).join("");
1122
1124
 
1123
1125
  tagbar.innerHTML=TAGS.map(t=>{
1124
1126
  const on=!!ACTIVE_TAGS[t.name];
1125
1127
  return `<span class="pill tag${on?" on":""}" onclick="toggleTag('${esc(t.name)}')">
1126
- ${esc(t.name)}<b>${t.count}</b></span>`;
1128
+ ${esc(t.name)}<b>${num(t.count)}</b></span>`;
1127
1129
  }).join("");
1128
1130
  }
1129
1131
 
@@ -1685,7 +1687,7 @@ async function planUrl(){
1685
1687
  showLibrary();
1686
1688
  closeUrl();
1687
1689
  showProgress(0,0);
1688
- if(result)note(`Added ${result.added} session${result.added===1?"":"s"} — ${result.into}`);
1690
+ if(result)note(`Added ${num(result.added)} session${result.added===1?"":"s"} — ${result.into}`);
1689
1691
  return;
1690
1692
  }
1691
1693
 
@@ -1712,8 +1714,8 @@ async function planUrl(){
1712
1714
  const size=p.bytes?` · ${bytes(p.bytes)}`:"";
1713
1715
  const sample=p.names.length?` — ${p.names.slice(0,3).join(", ")}${
1714
1716
  p.count>3?", …":""}`:"";
1715
- setUrlState(`${p.count} file${p.count===1?"":"s"} in ${p.label}${size}${sample}\n`
1716
- +`→ ${p.into}`, `Download ${p.count}`);
1717
+ setUrlState(`${num(p.count)} file${p.count===1?"":"s"} in ${p.label}${size}${sample}\n`
1718
+ +`→ ${p.into}`, `Download ${num(p.count)}`);
1717
1719
  }catch(e){
1718
1720
  err.textContent=e.message;err.hidden=false;
1719
1721
  setUrlState("","Look");PLANNED=null;
@@ -1874,7 +1876,7 @@ async function annotate(key,fields){
1874
1876
  async function refreshIndex(){
1875
1877
  const d=await fetch("/api/index").then(r=>r.json());
1876
1878
  INDEX=d.sessions||[];GROUPS=d.groups||[];TAGS=d.tags||[];AI=d.ai||{};DOWNLOADS=d.downloads||DOWNLOADS;
1877
- count.textContent=INDEX.length+" sessions";
1879
+ showCount();
1878
1880
  drawList();
1879
1881
  if(!cur)showLibrary();
1880
1882
  }
@@ -2277,7 +2279,7 @@ function branchNav(branches){
2277
2279
  <span class="sid">${b.stepId}</span>
2278
2280
  <b>${esc(label)}</b>
2279
2281
  <span>${esc(x.description||"")||"—"}</span>
2280
- <em>${b.sub?b.sub.steps.length+" steps":"external"}</em>
2282
+ <em>${b.sub?num(b.sub.steps.length)+" steps":"external"}</em>
2281
2283
  </button>`;
2282
2284
  }).join("")+`</div></div>`;
2283
2285
  }
@@ -2305,7 +2307,7 @@ function step(s,ctx,depth,idx,prefix){
2305
2307
  const on=starredSteps().includes(key);
2306
2308
  let h=`<div class="step ${s.source}" id="step-${key}">
2307
2309
  <div class="gut">
2308
- <a class="sid" href="#step-${key}" title="step ${s.step_id}">${s.step_id}</a>
2310
+ <a class="sid" href="#step-${key}" title="step ${num(s.step_id)}">${num(s.step_id)}</a>
2309
2311
  <span class="sstar${on?" on":""}" onclick="toggleStep(event,'${key}')"
2310
2312
  title="${on?"Remove from favourites":"Add to favourites"}">
2311
2313
  <svg width="11" height="11" viewBox="0 0 24 24" fill="${on?"currentColor":"none"}"
@@ -2349,7 +2351,7 @@ function branch(ref,ctx,depth){
2349
2351
  return `<details class="branch"${OPEN_ALL?" open":""}><summary>
2350
2352
  <b>${esc(label)}</b>
2351
2353
  <span class="desc">${esc(x.description||"")}</span>
2352
- <span class="pill br">${sub.steps.length} steps</span>
2354
+ <span class="pill br">${num(sub.steps.length)} steps</span>
2353
2355
  ${depth?`<span class="pill">depth ${depth+1}</span>`:""}
2354
2356
  </summary><div class="inner">
2355
2357
  ${sub.steps.map(s=>step(s,sub,depth+1,null,esc(sub.trajectory_id||"sub")+"-")).join("")}
@@ -110,3 +110,40 @@ def test_sizes_go_through_one_formatter():
110
110
  )
111
111
  assert "1048576" not in code, "a size is still being divided by hand"
112
112
  assert code.count("function bytes(") == 1
113
+
114
+
115
+ # Numbers a reader sees are formatted in one place, so a corpus of thousands
116
+ # reads as "2,258 sessions" rather than "2258 sessions". These are the shapes
117
+ # that reached the screen raw before anyone looked.
118
+ RAW_NUMBERS = (
119
+ (r"\.length\s*\+\s*[\"']", "a count concatenated onto a string"),
120
+ (r"\$\{[^}]*\.length\}\s*(?:session|step|file|result|match)", "${…length} beside a noun"),
121
+ (r"\$\{\s*[a-zA-Z_$][\w.$]*\.count\s*\}", "${…count}"),
122
+ (r"\$\{\s*[a-zA-Z_$][\w.$]*\.added\s*\}", "${…added}"),
123
+ (r"\$\{\s*s\.step_id\s*\}", "${s.step_id}"),
124
+ )
125
+
126
+
127
+ @pytest.mark.parametrize(("pattern", "what"), RAW_NUMBERS, ids=[w for _, w in RAW_NUMBERS])
128
+ def test_a_number_on_screen_goes_through_the_formatter(pattern, what):
129
+ script = PAGE[PAGE.index("<script>") : PAGE.rindex("</script>")]
130
+ hits = []
131
+ for i, line in enumerate(script.splitlines(), start=1):
132
+ stripped = line.strip()
133
+ if stripped.startswith("//") or stripped.startswith("*"):
134
+ continue
135
+ for match in re.finditer(pattern, line):
136
+ around = line[max(0, match.start() - 14) : match.end() + 3]
137
+ if "num(" in around or "bytes(" in around:
138
+ continue
139
+ hits.append(f"line {i}: {stripped[:90]}")
140
+ assert not hits, f"{what} without num(): " + "; ".join(hits)
141
+
142
+
143
+ def test_the_session_count_is_said_in_one_place():
144
+ """It was written out three times, and one of them was already wrong."""
145
+ script = PAGE[PAGE.index("<script>") : PAGE.rindex("</script>")]
146
+ assert "const showCount=" in script, "the count should have a single home"
147
+ assert script.count('count.textContent=') <= 2, (
148
+ "the session count is being written in more than one place again"
149
+ )
@@ -388,7 +388,7 @@ wheels = [
388
388
 
389
389
  [[package]]
390
390
  name = "transcript-viewer"
391
- version = "0.6.1"
391
+ version = "0.6.2"
392
392
  source = { editable = "." }
393
393
  dependencies = [
394
394
  { name = "atif-make" },