transcript-viewer 0.7.1__tar.gz → 0.8.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.7.1 → transcript_viewer-0.8.0}/PKG-INFO +1 -1
- {transcript_viewer-0.7.1 → transcript_viewer-0.8.0}/pyproject.toml +1 -1
- {transcript_viewer-0.7.1 → transcript_viewer-0.8.0}/src/transcript_viewer/page.html +104 -9
- {transcript_viewer-0.7.1 → transcript_viewer-0.8.0}/tests/page.test.js +100 -0
- {transcript_viewer-0.7.1 → transcript_viewer-0.8.0}/tests/test_style.py +31 -0
- {transcript_viewer-0.7.1 → transcript_viewer-0.8.0}/uv.lock +1 -1
- {transcript_viewer-0.7.1 → transcript_viewer-0.8.0}/.coverage +0 -0
- {transcript_viewer-0.7.1 → transcript_viewer-0.8.0}/.github/workflows/publish.yml +0 -0
- {transcript_viewer-0.7.1 → transcript_viewer-0.8.0}/.github/workflows/test.yml +0 -0
- {transcript_viewer-0.7.1 → transcript_viewer-0.8.0}/.gitignore +0 -0
- {transcript_viewer-0.7.1 → transcript_viewer-0.8.0}/README.md +0 -0
- {transcript_viewer-0.7.1 → transcript_viewer-0.8.0}/src/transcript_viewer/__init__.py +0 -0
- {transcript_viewer-0.7.1 → transcript_viewer-0.8.0}/src/transcript_viewer/ai.py +0 -0
- {transcript_viewer-0.7.1 → transcript_viewer-0.8.0}/src/transcript_viewer/cli.py +0 -0
- {transcript_viewer-0.7.1 → transcript_viewer-0.8.0}/src/transcript_viewer/config.py +0 -0
- {transcript_viewer-0.7.1 → transcript_viewer-0.8.0}/src/transcript_viewer/corpus.py +0 -0
- {transcript_viewer-0.7.1 → transcript_viewer-0.8.0}/src/transcript_viewer/fetch.py +0 -0
- {transcript_viewer-0.7.1 → transcript_viewer-0.8.0}/src/transcript_viewer/library.py +0 -0
- {transcript_viewer-0.7.1 → transcript_viewer-0.8.0}/src/transcript_viewer/store.py +0 -0
- {transcript_viewer-0.7.1 → transcript_viewer-0.8.0}/src/transcript_viewer/viewer.py +0 -0
- {transcript_viewer-0.7.1 → transcript_viewer-0.8.0}/tests/fixtures/big-command.json +0 -0
- {transcript_viewer-0.7.1 → transcript_viewer-0.8.0}/tests/test_ai.py +0 -0
- {transcript_viewer-0.7.1 → transcript_viewer-0.8.0}/tests/test_cli.py +0 -0
- {transcript_viewer-0.7.1 → transcript_viewer-0.8.0}/tests/test_config.py +0 -0
- {transcript_viewer-0.7.1 → transcript_viewer-0.8.0}/tests/test_corpus.py +0 -0
- {transcript_viewer-0.7.1 → transcript_viewer-0.8.0}/tests/test_fetch.py +0 -0
- {transcript_viewer-0.7.1 → transcript_viewer-0.8.0}/tests/test_library.py +0 -0
- {transcript_viewer-0.7.1 → transcript_viewer-0.8.0}/tests/test_page.py +0 -0
- {transcript_viewer-0.7.1 → transcript_viewer-0.8.0}/tests/test_readme.py +0 -0
- {transcript_viewer-0.7.1 → transcript_viewer-0.8.0}/tests/test_store.py +0 -0
- {transcript_viewer-0.7.1 → transcript_viewer-0.8.0}/tests/test_stream.py +0 -0
- {transcript_viewer-0.7.1 → transcript_viewer-0.8.0}/tests/test_tidiness.py +0 -0
- {transcript_viewer-0.7.1 → transcript_viewer-0.8.0}/tests/test_viewer.py +0 -0
|
@@ -412,20 +412,45 @@ pre{background:var(--sunk);border-radius:7px;padding:10px;overflow:auto;font-siz
|
|
|
412
412
|
/* JSON syntax colours */
|
|
413
413
|
.j-key{color:var(--j-key)}
|
|
414
414
|
/* A command reads as a command: one gutter mark, then the script as written. */
|
|
415
|
+
/* Wrapped rather than scrolled: a command is read, and a line that runs off the
|
|
416
|
+
side is a line nobody reads.
|
|
417
|
+
|
|
418
|
+
No hanging indent. A negative text-indent pulls the *first* line out of the
|
|
419
|
+
content box to the left, and `pre` carries overflow:auto, so the box scrolled
|
|
420
|
+
itself sideways to chase it — which hid the first word of every command and
|
|
421
|
+
left the prompt mark stranded mid-block. Wrapped lines simply align with the
|
|
422
|
+
rest; which lines the shell sees as separate is legible from the text. */
|
|
415
423
|
.sh{background:var(--bg);border-radius:7px;padding:10px 12px 10px 26px;margin:0;
|
|
416
424
|
position:relative;font:12px/1.6 var(--font-mono);color:var(--ink);
|
|
417
|
-
|
|
418
|
-
the side is a line nobody reads. A continued line is indented so it stays
|
|
419
|
-
obvious which lines the shell would actually see as separate. */
|
|
420
|
-
white-space:pre-wrap;overflow-wrap:anywhere;text-indent:-1.4em;padding-left:calc(26px + 1.4em)}
|
|
425
|
+
white-space:pre-wrap;overflow-wrap:anywhere;overflow-x:hidden}
|
|
421
426
|
.sh::before{content:"$";position:absolute;left:11px;top:10px;color:var(--faint);
|
|
422
|
-
user-select:none
|
|
427
|
+
user-select:none}
|
|
423
428
|
.sh-cmd{color:var(--sh-cmd)}
|
|
424
429
|
.sh-str{color:var(--sh-str)}
|
|
425
430
|
.sh-flag{color:var(--sh-flag)}
|
|
426
431
|
.sh-op{color:var(--sh-op);font-weight:500}
|
|
427
432
|
.sh-var{color:var(--sh-var)}
|
|
428
433
|
.sh-com{color:var(--sh-com);font-style:italic}
|
|
434
|
+
|
|
435
|
+
/* A file, shown as a file. The numbers sit in a gutter and are not selectable,
|
|
436
|
+
so copying the block gives back the file rather than the listing of it. */
|
|
437
|
+
.fpath{font:11.5px/1.5 var(--font-mono);color:var(--muted);margin-bottom:6px;
|
|
438
|
+
overflow-wrap:anywhere}
|
|
439
|
+
.fmeta{font:11px/1.5 var(--font-mono);color:var(--faint);margin-top:6px}
|
|
440
|
+
.filelines,.filebody{background:var(--bg);border-radius:7px;padding:10px 12px;
|
|
441
|
+
margin:0;max-height:340px;overflow:auto;font:12px/1.6 var(--font-mono);
|
|
442
|
+
color:var(--ink);white-space:pre-wrap;overflow-wrap:anywhere}
|
|
443
|
+
.ln{display:inline-block;width:3.2em;margin-right:.8em;text-align:right;
|
|
444
|
+
color:var(--faint);user-select:none;-webkit-user-select:none}
|
|
445
|
+
/* What a change replaced, and what it put there. */
|
|
446
|
+
.epair{display:grid;gap:8px}
|
|
447
|
+
.eside{border-radius:7px;padding:8px 10px;background:var(--bg);
|
|
448
|
+
font:12px/1.6 var(--font-mono);white-space:pre-wrap;overflow-wrap:anywhere;
|
|
449
|
+
max-height:260px;overflow:auto}
|
|
450
|
+
.eside b{display:block;font:11px/1.5 var(--font-mono);font-weight:500;
|
|
451
|
+
margin-bottom:4px;letter-spacing:.04em;text-transform:uppercase}
|
|
452
|
+
.eside.out b{color:var(--danger)}
|
|
453
|
+
.eside.in b{color:var(--ok)}
|
|
429
454
|
/* What the call was for, beside what it was called. */
|
|
430
455
|
.tdesc{color:var(--muted);font-size:12px;overflow:hidden;text-overflow:ellipsis;
|
|
431
456
|
white-space:nowrap;flex:1;min-width:0}
|
|
@@ -884,6 +909,44 @@ function sh(src) {
|
|
|
884
909
|
|
|
885
910
|
const shBlock = (command) => `<pre class="sh">${autolink(sh(command))}</pre>`;
|
|
886
911
|
|
|
912
|
+
/* Where a file is, as something you can open.
|
|
913
|
+
|
|
914
|
+
Linked whole rather than through autolink: this value is a path in its
|
|
915
|
+
entirety, where autolink has to guess boundaries in prose and stops at the
|
|
916
|
+
first space. A real one here — "/Users/…/Redwood Research/…" — linked only
|
|
917
|
+
as far as "Redwood", which pointed the link at a directory that is not the
|
|
918
|
+
file. PATH_EXACT is the same test the JSON view uses for a quoted value. */
|
|
919
|
+
const fileRef = (path) => {
|
|
920
|
+
const text = String(path ?? "");
|
|
921
|
+
if (!text) return "";
|
|
922
|
+
const shown = PATH_EXACT.test(text) ? pathAnchor(text, esc(text)) : esc(text);
|
|
923
|
+
return `<div class="fpath">${shown}</div>`;
|
|
924
|
+
};
|
|
925
|
+
|
|
926
|
+
const countOf = (text) => {
|
|
927
|
+
const s = String(text ?? "");
|
|
928
|
+
const lines = s ? s.split("\n").length : 0;
|
|
929
|
+
return `${num(lines)} line${lines === 1 ? "" : "s"} · ${bytes(s.length)}`;
|
|
930
|
+
};
|
|
931
|
+
|
|
932
|
+
/* A file read back arrives numbered, one "12\tline" per line. Put the numbers
|
|
933
|
+
in a gutter so the file reads as a file, and leave them unselectable so that
|
|
934
|
+
copying gives the contents rather than the listing. */
|
|
935
|
+
function numbered(text) {
|
|
936
|
+
const rows = String(text ?? "")
|
|
937
|
+
.split("\n")
|
|
938
|
+
.map((line) => {
|
|
939
|
+
const m = /^(\s*\d+)\t([\s\S]*)$/.exec(line);
|
|
940
|
+
return m
|
|
941
|
+
? `<span class="ln">${esc(m[1].trim())}</span>${esc(m[2])}`
|
|
942
|
+
: `<span class="ln"></span>${esc(line)}`;
|
|
943
|
+
});
|
|
944
|
+
return `<pre class="filelines">${autolink(rows.join("\n"))}</pre>`;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
const fileBlock = (text) =>
|
|
948
|
+
`<pre class="filebody">${autolink(esc(String(text ?? "")))}</pre>`;
|
|
949
|
+
|
|
887
950
|
/* What a call is worth showing as, when it is not worth showing as JSON. Only
|
|
888
951
|
the shapes that carry a command or an address get a body of their own; the
|
|
889
952
|
rest gain a title, which is the part that was missing everywhere. */
|
|
@@ -905,9 +968,37 @@ const TOOLVIEW = {
|
|
|
905
968
|
},
|
|
906
969
|
WebSearch: { title: (a) => a.query || "" },
|
|
907
970
|
// The end of a path is the part worth reading; the full one is on hover.
|
|
908
|
-
Read: {
|
|
909
|
-
|
|
910
|
-
|
|
971
|
+
Read: {
|
|
972
|
+
title: (a) => short(a.file_path),
|
|
973
|
+
body: (a) =>
|
|
974
|
+
fileRef(a.file_path) +
|
|
975
|
+
(a.offset || a.limit || a.pages
|
|
976
|
+
? `<div class="fmeta">${
|
|
977
|
+
a.pages
|
|
978
|
+
? `pages ${esc(String(a.pages))}`
|
|
979
|
+
: `from line ${num(a.offset || 1)}${a.limit ? `, ${num(a.limit)} lines` : ""}`
|
|
980
|
+
}</div>`
|
|
981
|
+
: ""),
|
|
982
|
+
// What came back is the file, numbered as the reader saw it.
|
|
983
|
+
output: (content) => numbered(content),
|
|
984
|
+
},
|
|
985
|
+
Write: {
|
|
986
|
+
title: (a) => short(a.file_path),
|
|
987
|
+
body: (a) =>
|
|
988
|
+
fileRef(a.file_path) +
|
|
989
|
+
fileBlock(a.content) +
|
|
990
|
+
`<div class="fmeta">${countOf(a.content)}</div>`,
|
|
991
|
+
},
|
|
992
|
+
Edit: {
|
|
993
|
+
title: (a) => short(a.file_path),
|
|
994
|
+
body: (a) =>
|
|
995
|
+
fileRef(a.file_path) +
|
|
996
|
+
`<div class="epair">
|
|
997
|
+
<div class="eside out"><b>replaced</b>${autolink(esc(a.old_string ?? ""))}</div>
|
|
998
|
+
<div class="eside in"><b>with</b>${autolink(esc(a.new_string ?? ""))}</div>
|
|
999
|
+
</div>` +
|
|
1000
|
+
(a.replace_all ? `<div class="fmeta">every occurrence</div>` : ""),
|
|
1001
|
+
},
|
|
911
1002
|
NotebookEdit: { title: (a) => short(a.notebook_path) },
|
|
912
1003
|
Skill: { title: (a) => a.skill || "" },
|
|
913
1004
|
Task: { title: (a) => a.description || "" },
|
|
@@ -938,7 +1029,11 @@ function toolBody(id) {
|
|
|
938
1029
|
`<div class="io in"><span class="iol">call</span>${shown}</div>` +
|
|
939
1030
|
(r && r.content
|
|
940
1031
|
? `<div class="io out"><span class="iol">output</span>${
|
|
941
|
-
typeof r.content
|
|
1032
|
+
typeof r.content !== "string"
|
|
1033
|
+
? body(r.content)
|
|
1034
|
+
: !toolIsRaw(id) && view && view.output
|
|
1035
|
+
? view.output(r.content, c.arguments || {})
|
|
1036
|
+
: pre(r.content)
|
|
942
1037
|
}</div>`
|
|
943
1038
|
: "") +
|
|
944
1039
|
(aiOn() ? callSummary(c.tool_call_id) : "")
|
|
@@ -1673,3 +1673,103 @@ test("a real 20,000-character command draws, and quickly", () => {
|
|
|
1673
1673
|
assert.ok(html.includes("class=\"sh\""), "it should render");
|
|
1674
1674
|
assert.ok(took < 2000, `took ${took}ms, which is a page nobody waits for`);
|
|
1675
1675
|
});
|
|
1676
|
+
|
|
1677
|
+
// ---- a file read, written or changed -----------------------------------------
|
|
1678
|
+
|
|
1679
|
+
test("a file read back keeps its line numbers in a gutter", () => {
|
|
1680
|
+
const html = run(`return TOOLVIEW.Read.output('1\\tfirst\\n2\\tsecond')`);
|
|
1681
|
+
assert.match(html, /class="filelines"/);
|
|
1682
|
+
assert.match(html, /<span class="ln">1<\/span>first/);
|
|
1683
|
+
assert.match(html, /<span class="ln">2<\/span>second/);
|
|
1684
|
+
assert.ok(!html.includes("\t"), "the tab becomes the gutter, not whitespace");
|
|
1685
|
+
});
|
|
1686
|
+
|
|
1687
|
+
test("a line that is not numbered still shows", () => {
|
|
1688
|
+
const html = run(`return TOOLVIEW.Read.output('no number here')`);
|
|
1689
|
+
assert.match(html, /no number here/);
|
|
1690
|
+
});
|
|
1691
|
+
|
|
1692
|
+
test("a read of part of a file says which part", () => {
|
|
1693
|
+
const html = run(`return TOOLVIEW.Read.body({file_path:"/a/b.py",offset:40,limit:120})`);
|
|
1694
|
+
assert.match(html, /from line 40/);
|
|
1695
|
+
assert.match(html, /120 lines/);
|
|
1696
|
+
});
|
|
1697
|
+
|
|
1698
|
+
test("a whole-file read says nothing about ranges", () => {
|
|
1699
|
+
const html = run(`return TOOLVIEW.Read.body({file_path:"/a/b.py"})`);
|
|
1700
|
+
assert.ok(!/from line/.test(html));
|
|
1701
|
+
});
|
|
1702
|
+
|
|
1703
|
+
test("a path in a file view still opens in Finder", () => {
|
|
1704
|
+
const html = run(`return TOOLVIEW.Read.body({file_path:"/Users/someone/a.py"})`);
|
|
1705
|
+
assert.match(html, /api\/reveal\?path=/);
|
|
1706
|
+
});
|
|
1707
|
+
|
|
1708
|
+
test("a write shows what it wrote, and how much", () => {
|
|
1709
|
+
const html = run(
|
|
1710
|
+
`return TOOLVIEW.Write.body({file_path:"/a/b.js",content:"one\\ntwo\\nthree"})`,
|
|
1711
|
+
);
|
|
1712
|
+
assert.match(html, /class="filebody"/);
|
|
1713
|
+
assert.match(html, /three/);
|
|
1714
|
+
assert.match(html, /3 lines/);
|
|
1715
|
+
});
|
|
1716
|
+
|
|
1717
|
+
test("an edit shows what it replaced and what it put there", () => {
|
|
1718
|
+
const html = run(
|
|
1719
|
+
`return TOOLVIEW.Edit.body({file_path:"/a/b.js",old_string:"before",new_string:"after",replace_all:true})`,
|
|
1720
|
+
);
|
|
1721
|
+
assert.match(html, /class="eside out"[^>]*>.*replaced.*before/s);
|
|
1722
|
+
assert.match(html, /class="eside in"[^>]*>.*with.*after/s);
|
|
1723
|
+
assert.match(html, /every occurrence/);
|
|
1724
|
+
});
|
|
1725
|
+
|
|
1726
|
+
test("an edit that replaces once does not claim otherwise", () => {
|
|
1727
|
+
const html = run(
|
|
1728
|
+
`return TOOLVIEW.Edit.body({file_path:"/a/b.js",old_string:"x",new_string:"y",replace_all:false})`,
|
|
1729
|
+
);
|
|
1730
|
+
assert.ok(!/every occurrence/.test(html));
|
|
1731
|
+
});
|
|
1732
|
+
|
|
1733
|
+
test("file contents cannot reach the page as markup", () => {
|
|
1734
|
+
for (const expr of [
|
|
1735
|
+
`TOOLVIEW.Write.body({file_path:"/a",content:"<img src=x onerror=alert(1)>"})`,
|
|
1736
|
+
`TOOLVIEW.Edit.body({file_path:"/a",old_string:"<script>",new_string:"<b>"})`,
|
|
1737
|
+
`TOOLVIEW.Read.output("1\\t<img src=x onerror=alert(1)>")`,
|
|
1738
|
+
]) {
|
|
1739
|
+
const html = run(`return ${expr}`);
|
|
1740
|
+
assert.ok(!/<img|<script>/.test(html), `escaped badly: ${expr}`);
|
|
1741
|
+
}
|
|
1742
|
+
});
|
|
1743
|
+
|
|
1744
|
+
test("asking for the recording gives the recording, not the file view", () => {
|
|
1745
|
+
const html = run(`
|
|
1746
|
+
raw = false;
|
|
1747
|
+
CALLS["r1"] = {
|
|
1748
|
+
c:{function_name:"Read",tool_call_id:"r1",arguments:{file_path:"/a/b.py"}},
|
|
1749
|
+
r:{source_call_id:"r1",content:"1\\thello"},
|
|
1750
|
+
};
|
|
1751
|
+
TOOLRAW["r1"] = true;
|
|
1752
|
+
return toolBody("r1");
|
|
1753
|
+
`);
|
|
1754
|
+
assert.match(html, /class="json"/, "the call shows as recorded");
|
|
1755
|
+
assert.ok(!/class="filelines"/.test(html), "and so does the output");
|
|
1756
|
+
});
|
|
1757
|
+
|
|
1758
|
+
test("a path with a space in it links to the whole path", () => {
|
|
1759
|
+
// A real one: "/Users/girish/Documents/Redwood Research/…" linked only as far
|
|
1760
|
+
// as "Redwood", so the link opened a directory that was not the file.
|
|
1761
|
+
const html = run(
|
|
1762
|
+
`return TOOLVIEW.Read.body({file_path:"/Users/girish/Documents/Redwood Research/app/mentions.py"})`,
|
|
1763
|
+
);
|
|
1764
|
+
assert.match(html, /Redwood%20Research/, "the space must be inside the link");
|
|
1765
|
+
assert.ok(
|
|
1766
|
+
!/<\/a>\s*Research/.test(html),
|
|
1767
|
+
"the link must not stop at the space",
|
|
1768
|
+
);
|
|
1769
|
+
});
|
|
1770
|
+
|
|
1771
|
+
test("a relative path is shown but not offered as a link", () => {
|
|
1772
|
+
const html = run(`return TOOLVIEW.Read.body({file_path:"src/thing.py"})`);
|
|
1773
|
+
assert.match(html, /src\/thing\.py/);
|
|
1774
|
+
assert.ok(!/api\/reveal/.test(html), "revealing a relative path would guess");
|
|
1775
|
+
});
|
|
@@ -147,3 +147,34 @@ def test_the_session_count_is_said_in_one_place():
|
|
|
147
147
|
assert script.count('count.textContent=') <= 2, (
|
|
148
148
|
"the session count is being written in more than one place again"
|
|
149
149
|
)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _rule(selector: str) -> str:
|
|
153
|
+
"""The declarations of one rule, as written."""
|
|
154
|
+
start = STYLE.index(selector + "{")
|
|
155
|
+
return STYLE[start + len(selector) + 1 : STYLE.index("}", start)]
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def test_a_command_block_keeps_its_first_line_inside_itself():
|
|
159
|
+
"""A negative text-indent pulls the first line out of the box.
|
|
160
|
+
|
|
161
|
+
`pre` carries overflow:auto, so the box then scrolls sideways to chase it:
|
|
162
|
+
the first word of every command disappeared and the prompt mark was left
|
|
163
|
+
stranded mid-block. Both symptoms, one declaration.
|
|
164
|
+
"""
|
|
165
|
+
rule = _rule(".sh")
|
|
166
|
+
assert "text-indent:-" not in rule.replace(" ", ""), (
|
|
167
|
+
"a negative text-indent on .sh hides the start of the command"
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def test_a_command_wraps_rather_than_scrolling():
|
|
172
|
+
rule = _rule(".sh").replace(" ", "").replace("\n", "")
|
|
173
|
+
assert "white-space:pre-wrap" in rule, "a command that runs off the side is not read"
|
|
174
|
+
assert "overflow-x:auto" not in rule, "it should wrap, not scroll"
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def test_the_prompt_mark_stays_at_the_top_left():
|
|
178
|
+
rule = _rule(".sh::before").replace(" ", "").replace("\n", "")
|
|
179
|
+
assert "position:absolute" in rule
|
|
180
|
+
assert "left:11px" in rule and "top:10px" in rule
|
|
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
|