scrollback 0.1.0__tar.gz → 0.1.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 (100) hide show
  1. {scrollback-0.1.0 → scrollback-0.1.2}/CHANGELOG.md +21 -1
  2. {scrollback-0.1.0 → scrollback-0.1.2}/CONTRIBUTING.md +8 -4
  3. {scrollback-0.1.0 → scrollback-0.1.2}/PKG-INFO +10 -3
  4. {scrollback-0.1.0 → scrollback-0.1.2}/README.md +9 -2
  5. scrollback-0.1.2/assets/screenshots/cli.png +0 -0
  6. scrollback-0.1.2/assets/screenshots/cli.svg +58 -0
  7. {scrollback-0.1.0 → scrollback-0.1.2}/scripts/screenshots.py +46 -6
  8. {scrollback-0.1.0 → scrollback-0.1.2}/src/scrollback/__init__.py +1 -1
  9. {scrollback-0.1.0 → scrollback-0.1.2}/src/scrollback/cli.py +80 -0
  10. {scrollback-0.1.0 → scrollback-0.1.2}/src/scrollback/launcher_install.py +40 -0
  11. {scrollback-0.1.0 → scrollback-0.1.2}/tests/test_launcher_install.py +26 -0
  12. scrollback-0.1.0/assets/screenshots/cli.svg +0 -91
  13. {scrollback-0.1.0 → scrollback-0.1.2}/.github/workflows/ci.yml +0 -0
  14. {scrollback-0.1.0 → scrollback-0.1.2}/.github/workflows/publish.yml +0 -0
  15. {scrollback-0.1.0 → scrollback-0.1.2}/.gitignore +0 -0
  16. {scrollback-0.1.0 → scrollback-0.1.2}/LICENSE +0 -0
  17. {scrollback-0.1.0 → scrollback-0.1.2}/ROADMAP.md +0 -0
  18. {scrollback-0.1.0 → scrollback-0.1.2}/assets/icon-512.png +0 -0
  19. {scrollback-0.1.0 → scrollback-0.1.2}/assets/icon.icns +0 -0
  20. {scrollback-0.1.0 → scrollback-0.1.2}/assets/icon.svg +0 -0
  21. {scrollback-0.1.0 → scrollback-0.1.2}/assets/screenshots/web.png +0 -0
  22. {scrollback-0.1.0 → scrollback-0.1.2}/pyproject.toml +0 -0
  23. {scrollback-0.1.0 → scrollback-0.1.2}/scripts/demo_data.py +0 -0
  24. {scrollback-0.1.0 → scrollback-0.1.2}/src/scrollback/assets/icon-256.png +0 -0
  25. {scrollback-0.1.0 → scrollback-0.1.2}/src/scrollback/assets/icon.icns +0 -0
  26. {scrollback-0.1.0 → scrollback-0.1.2}/src/scrollback/clipboard.py +0 -0
  27. {scrollback-0.1.0 → scrollback-0.1.2}/src/scrollback/export.py +0 -0
  28. {scrollback-0.1.0 → scrollback-0.1.2}/src/scrollback/fts.py +0 -0
  29. {scrollback-0.1.0 → scrollback-0.1.2}/src/scrollback/highlight.py +0 -0
  30. {scrollback-0.1.0 → scrollback-0.1.2}/src/scrollback/katexbundle.py +0 -0
  31. {scrollback-0.1.0 → scrollback-0.1.2}/src/scrollback/launchers/scrollback.bat +0 -0
  32. {scrollback-0.1.0 → scrollback-0.1.2}/src/scrollback/launchers/scrollback.command +0 -0
  33. {scrollback-0.1.0 → scrollback-0.1.2}/src/scrollback/launchers/scrollback.desktop +0 -0
  34. {scrollback-0.1.0 → scrollback-0.1.2}/src/scrollback/launchers/scrollback.sh +0 -0
  35. {scrollback-0.1.0 → scrollback-0.1.2}/src/scrollback/mathspan.py +0 -0
  36. {scrollback-0.1.0 → scrollback-0.1.2}/src/scrollback/minimd.py +0 -0
  37. {scrollback-0.1.0 → scrollback-0.1.2}/src/scrollback/models.py +0 -0
  38. {scrollback-0.1.0 → scrollback-0.1.2}/src/scrollback/serialize.py +0 -0
  39. {scrollback-0.1.0 → scrollback-0.1.2}/src/scrollback/serverconfig.py +0 -0
  40. {scrollback-0.1.0 → scrollback-0.1.2}/src/scrollback/sources/__init__.py +0 -0
  41. {scrollback-0.1.0 → scrollback-0.1.2}/src/scrollback/sources/aider.py +0 -0
  42. {scrollback-0.1.0 → scrollback-0.1.2}/src/scrollback/sources/base.py +0 -0
  43. {scrollback-0.1.0 → scrollback-0.1.2}/src/scrollback/sources/claudecode.py +0 -0
  44. {scrollback-0.1.0 → scrollback-0.1.2}/src/scrollback/sources/codex.py +0 -0
  45. {scrollback-0.1.0 → scrollback-0.1.2}/src/scrollback/sources/opencode.py +0 -0
  46. {scrollback-0.1.0 → scrollback-0.1.2}/src/scrollback/sources/registry.py +0 -0
  47. {scrollback-0.1.0 → scrollback-0.1.2}/src/scrollback/store.py +0 -0
  48. {scrollback-0.1.0 → scrollback-0.1.2}/src/scrollback/termrender.py +0 -0
  49. {scrollback-0.1.0 → scrollback-0.1.2}/src/scrollback/web/__init__.py +0 -0
  50. {scrollback-0.1.0 → scrollback-0.1.2}/src/scrollback/web/app.py +0 -0
  51. {scrollback-0.1.0 → scrollback-0.1.2}/src/scrollback/web/static/app.js +0 -0
  52. {scrollback-0.1.0 → scrollback-0.1.2}/src/scrollback/web/static/apple-touch-icon.png +0 -0
  53. {scrollback-0.1.0 → scrollback-0.1.2}/src/scrollback/web/static/favicon.png +0 -0
  54. {scrollback-0.1.0 → scrollback-0.1.2}/src/scrollback/web/static/favicon.svg +0 -0
  55. {scrollback-0.1.0 → scrollback-0.1.2}/src/scrollback/web/static/index.html +0 -0
  56. {scrollback-0.1.0 → scrollback-0.1.2}/src/scrollback/web/static/style.css +0 -0
  57. {scrollback-0.1.0 → scrollback-0.1.2}/src/scrollback/web/static/vendor/highlight.min.js +0 -0
  58. {scrollback-0.1.0 → scrollback-0.1.2}/src/scrollback/web/static/vendor/hljs-dark.min.css +0 -0
  59. {scrollback-0.1.0 → scrollback-0.1.2}/src/scrollback/web/static/vendor/hljs-light.min.css +0 -0
  60. {scrollback-0.1.0 → scrollback-0.1.2}/src/scrollback/web/static/vendor/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  61. {scrollback-0.1.0 → scrollback-0.1.2}/src/scrollback/web/static/vendor/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  62. {scrollback-0.1.0 → scrollback-0.1.2}/src/scrollback/web/static/vendor/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  63. {scrollback-0.1.0 → scrollback-0.1.2}/src/scrollback/web/static/vendor/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  64. {scrollback-0.1.0 → scrollback-0.1.2}/src/scrollback/web/static/vendor/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  65. {scrollback-0.1.0 → scrollback-0.1.2}/src/scrollback/web/static/vendor/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
  66. {scrollback-0.1.0 → scrollback-0.1.2}/src/scrollback/web/static/vendor/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  67. {scrollback-0.1.0 → scrollback-0.1.2}/src/scrollback/web/static/vendor/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
  68. {scrollback-0.1.0 → scrollback-0.1.2}/src/scrollback/web/static/vendor/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
  69. {scrollback-0.1.0 → scrollback-0.1.2}/src/scrollback/web/static/vendor/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  70. {scrollback-0.1.0 → scrollback-0.1.2}/src/scrollback/web/static/vendor/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
  71. {scrollback-0.1.0 → scrollback-0.1.2}/src/scrollback/web/static/vendor/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  72. {scrollback-0.1.0 → scrollback-0.1.2}/src/scrollback/web/static/vendor/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  73. {scrollback-0.1.0 → scrollback-0.1.2}/src/scrollback/web/static/vendor/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  74. {scrollback-0.1.0 → scrollback-0.1.2}/src/scrollback/web/static/vendor/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
  75. {scrollback-0.1.0 → scrollback-0.1.2}/src/scrollback/web/static/vendor/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  76. {scrollback-0.1.0 → scrollback-0.1.2}/src/scrollback/web/static/vendor/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  77. {scrollback-0.1.0 → scrollback-0.1.2}/src/scrollback/web/static/vendor/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  78. {scrollback-0.1.0 → scrollback-0.1.2}/src/scrollback/web/static/vendor/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  79. {scrollback-0.1.0 → scrollback-0.1.2}/src/scrollback/web/static/vendor/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  80. {scrollback-0.1.0 → scrollback-0.1.2}/src/scrollback/web/static/vendor/katex/katex.min.css +0 -0
  81. {scrollback-0.1.0 → scrollback-0.1.2}/src/scrollback/web/static/vendor/katex/katex.min.js +0 -0
  82. {scrollback-0.1.0 → scrollback-0.1.2}/src/scrollback/web/static/vendor/marked.min.js +0 -0
  83. {scrollback-0.1.0 → scrollback-0.1.2}/src/scrollback/web/static/vendor/purify.min.js +0 -0
  84. {scrollback-0.1.0 → scrollback-0.1.2}/src/scrollback/webopen.py +0 -0
  85. {scrollback-0.1.0 → scrollback-0.1.2}/tests/test_aider.py +0 -0
  86. {scrollback-0.1.0 → scrollback-0.1.2}/tests/test_claude_paging.py +0 -0
  87. {scrollback-0.1.0 → scrollback-0.1.2}/tests/test_claude_subagents.py +0 -0
  88. {scrollback-0.1.0 → scrollback-0.1.2}/tests/test_cli_helpers.py +0 -0
  89. {scrollback-0.1.0 → scrollback-0.1.2}/tests/test_codex.py +0 -0
  90. {scrollback-0.1.0 → scrollback-0.1.2}/tests/test_export.py +0 -0
  91. {scrollback-0.1.0 → scrollback-0.1.2}/tests/test_fts.py +0 -0
  92. {scrollback-0.1.0 → scrollback-0.1.2}/tests/test_highlight.py +0 -0
  93. {scrollback-0.1.0 → scrollback-0.1.2}/tests/test_minimd.py +0 -0
  94. {scrollback-0.1.0 → scrollback-0.1.2}/tests/test_models.py +0 -0
  95. {scrollback-0.1.0 → scrollback-0.1.2}/tests/test_serverconfig.py +0 -0
  96. {scrollback-0.1.0 → scrollback-0.1.2}/tests/test_sources_live.py +0 -0
  97. {scrollback-0.1.0 → scrollback-0.1.2}/tests/test_stats_resume.py +0 -0
  98. {scrollback-0.1.0 → scrollback-0.1.2}/tests/test_store_filters.py +0 -0
  99. {scrollback-0.1.0 → scrollback-0.1.2}/tests/test_web_api.py +0 -0
  100. {scrollback-0.1.0 → scrollback-0.1.2}/tests/test_webopen.py +0 -0
@@ -6,6 +6,24 @@ follow [Semantic Versioning](https://semver.org/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.1.2] - 2026-06-30
10
+
11
+ ### Added
12
+
13
+ - `scrollback uninstall`: removes the artifacts scrollback created (Desktop
14
+ launcher, macOS `.app`, optional search index, launcher log) with a
15
+ confirmation prompt (`--yes` / `--dry-run`). It never touches agent data
16
+ and never self-removes the package; it prints the right `pip`/`pipx
17
+ uninstall` command instead.
18
+
19
+ ## [0.1.1] - 2026-06-30
20
+
21
+ ### Fixed
22
+
23
+ - README images now render on PyPI: use absolute, release-pinned PNG URLs
24
+ (PyPI does not resolve relative paths or display SVGs). Adds a PyPI-
25
+ friendly `cli.png` alongside the GitHub SVG.
26
+
9
27
  ## [0.1.0] - 2026-06-30
10
28
 
11
29
  The first release. scrollback reads AI coding-agent session history
@@ -82,5 +100,7 @@ export it from a CLI and a local web app.
82
100
  - Negative pagination arguments are rejected; clearer errors for unknown
83
101
  sources, failed exports, and unavailable data sources.
84
102
 
85
- [Unreleased]: https://github.com/a-attia/scrollback/compare/v0.1.0...HEAD
103
+ [Unreleased]: https://github.com/a-attia/scrollback/compare/v0.1.2...HEAD
104
+ [0.1.2]: https://github.com/a-attia/scrollback/compare/v0.1.1...v0.1.2
105
+ [0.1.1]: https://github.com/a-attia/scrollback/compare/v0.1.0...v0.1.1
86
106
  [0.1.0]: https://github.com/a-attia/scrollback/releases/tag/v0.1.0
@@ -55,12 +55,16 @@ publish. To regenerate them after a UI change:
55
55
  ```bash
56
56
  pip install -e ".[screenshots]"
57
57
  playwright install chromium # one-time headless-browser download
58
- python scripts/screenshots.py # writes assets/screenshots/{cli.svg,web.png}
58
+ python scripts/screenshots.py # writes assets/screenshots/{cli.svg,cli.png,web.png}
59
59
  ```
60
60
 
61
- The CLI image is rendered with `rich` (no browser); the web image is
62
- captured with headless Chromium via Playwright. Neither the `screenshots`
63
- extra nor the browser is needed to run scrollback.
61
+ The CLI image is rendered with `rich` (SVG for GitHub, plus a PNG for PyPI,
62
+ which does not display SVGs); the web image is captured with headless
63
+ Chromium via Playwright. The README embeds the PNGs via absolute,
64
+ release-pinned `raw.githubusercontent.com` URLs so they render on both
65
+ GitHub and PyPI (relative paths only work on GitHub). When cutting a new
66
+ release, bump the version in those URLs. Neither the `screenshots` extra nor
67
+ the browser is needed to run scrollback.
64
68
 
65
69
  ## Submitting changes
66
70
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scrollback
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: Navigate, search, copy, and export your AI coding-agent sessions (opencode, Claude Code, ...) from one local, read-only tool.
5
5
  Project-URL: Homepage, https://github.com/a-attia/scrollback
6
6
  Project-URL: Repository, https://github.com/a-attia/scrollback
@@ -73,13 +73,13 @@ modifies, locks for writing, or uploads your data.
73
73
  You can use it two ways. From the **command line**, list, search, and export
74
74
  your sessions in a single scriptable tool:
75
75
 
76
- ![scrollback listing recent sessions in the terminal.](assets/screenshots/cli.svg)
76
+ ![scrollback listing recent sessions in the terminal.](https://raw.githubusercontent.com/a-attia/scrollback/v0.1.2/assets/screenshots/cli.png)
77
77
 
78
78
  Or open the **local web app** to read a transcript in full — with rendered
79
79
  Markdown, syntax-highlighted code, and typeset LaTeX math:
80
80
 
81
81
  ![The scrollback web app showing a session list beside a transcript with
82
- rendered Markdown, highlighted code, and typeset equations.](assets/screenshots/web.png)
82
+ rendered Markdown, highlighted code, and typeset equations.](https://raw.githubusercontent.com/a-attia/scrollback/v0.1.2/assets/screenshots/web.png)
83
83
 
84
84
  Both views read the same on-disk session stores, so you can jump between
85
85
  them freely. (The screenshots above use synthetic demo data.)
@@ -299,6 +299,13 @@ window closes. All of this behaviour is decided in Python, so the launcher
299
299
  scripts stay free of OS-specific assumptions and ship inside the package
300
300
  for `pip install` users.
301
301
 
302
+ To clean up, `scrollback uninstall` removes the artifacts scrollback
303
+ created — the launchers, the macOS `.app`, the optional search index, and the
304
+ launcher log — after a confirmation (`--yes` to skip it, `--dry-run` to
305
+ preview). It never touches your agent data, and it does not remove the Python
306
+ package itself: it prints the right `pip`/`pipx uninstall` command to finish
307
+ the job (a program can't reliably uninstall the package it is running from).
308
+
302
309
  ## Fast search (optional index)
303
310
 
304
311
  By default, search is a lexical scan over your live data: zero setup,
@@ -14,13 +14,13 @@ modifies, locks for writing, or uploads your data.
14
14
  You can use it two ways. From the **command line**, list, search, and export
15
15
  your sessions in a single scriptable tool:
16
16
 
17
- ![scrollback listing recent sessions in the terminal.](assets/screenshots/cli.svg)
17
+ ![scrollback listing recent sessions in the terminal.](https://raw.githubusercontent.com/a-attia/scrollback/v0.1.2/assets/screenshots/cli.png)
18
18
 
19
19
  Or open the **local web app** to read a transcript in full — with rendered
20
20
  Markdown, syntax-highlighted code, and typeset LaTeX math:
21
21
 
22
22
  ![The scrollback web app showing a session list beside a transcript with
23
- rendered Markdown, highlighted code, and typeset equations.](assets/screenshots/web.png)
23
+ rendered Markdown, highlighted code, and typeset equations.](https://raw.githubusercontent.com/a-attia/scrollback/v0.1.2/assets/screenshots/web.png)
24
24
 
25
25
  Both views read the same on-disk session stores, so you can jump between
26
26
  them freely. (The screenshots above use synthetic demo data.)
@@ -240,6 +240,13 @@ window closes. All of this behaviour is decided in Python, so the launcher
240
240
  scripts stay free of OS-specific assumptions and ship inside the package
241
241
  for `pip install` users.
242
242
 
243
+ To clean up, `scrollback uninstall` removes the artifacts scrollback
244
+ created — the launchers, the macOS `.app`, the optional search index, and the
245
+ launcher log — after a confirmation (`--yes` to skip it, `--dry-run` to
246
+ preview). It never touches your agent data, and it does not remove the Python
247
+ package itself: it prints the right `pip`/`pipx uninstall` command to finish
248
+ the job (a program can't reliably uninstall the package it is running from).
249
+
243
250
  ## Fast search (optional index)
244
251
 
245
252
  By default, search is a lexical scan over your live data: zero setup,
@@ -0,0 +1,58 @@
1
+ <svg class="rich-terminal" viewBox="0 0 1141 74.4" xmlns="http://www.w3.org/2000/svg">
2
+ <!-- Generated with Rich https://www.textualize.io -->
3
+ <style>
4
+
5
+ @font-face {
6
+ font-family: "Fira Code";
7
+ src: local("FiraCode-Regular"),
8
+ url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"),
9
+ url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff");
10
+ font-style: normal;
11
+ font-weight: 400;
12
+ }
13
+ @font-face {
14
+ font-family: "Fira Code";
15
+ src: local("FiraCode-Bold"),
16
+ url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"),
17
+ url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff");
18
+ font-style: bold;
19
+ font-weight: 700;
20
+ }
21
+
22
+ .terminal-387712033-matrix {
23
+ font-family: Fira Code, monospace;
24
+ font-size: 20px;
25
+ line-height: 24.4px;
26
+ font-variant-east-asian: full-width;
27
+ }
28
+
29
+ .terminal-387712033-title {
30
+ font-size: 18px;
31
+ font-weight: bold;
32
+ font-family: arial;
33
+ }
34
+
35
+
36
+ </style>
37
+
38
+ <defs>
39
+ <clipPath id="terminal-387712033-clip-terminal">
40
+ <rect x="0" y="0" width="1121.3999999999999" height="23.4" />
41
+ </clipPath>
42
+
43
+ </defs>
44
+
45
+ <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="1139" height="72.4" rx="8"/><text class="terminal-387712033-title" fill="#c5c8c6" text-anchor="middle" x="569" y="27">scrollback</text>
46
+ <g transform="translate(26,22)">
47
+ <circle cx="0" cy="0" r="7" fill="#ff5f57"/>
48
+ <circle cx="22" cy="0" r="7" fill="#febc2e"/>
49
+ <circle cx="44" cy="0" r="7" fill="#28c840"/>
50
+ </g>
51
+
52
+ <g transform="translate(9, 41)" clip-path="url(#terminal-387712033-clip-terminal)">
53
+
54
+ <g class="terminal-387712033-matrix">
55
+
56
+ </g>
57
+ </g>
58
+ </svg>
@@ -32,7 +32,12 @@ OUT.mkdir(parents=True, exist_ok=True)
32
32
 
33
33
  # -- CLI screenshot (rich -> SVG, no browser) ----------------------------
34
34
 
35
- def render_cli_svg() -> Path:
35
+ def render_cli() -> list[Path]:
36
+ """Render the CLI list to SVG (crisp on GitHub) and PNG (for PyPI).
37
+
38
+ PyPI's README renderer does not display SVG images, so a PNG is needed
39
+ there; GitHub renders both, and SVG stays sharp at any zoom.
40
+ """
36
41
  from rich.console import Console
37
42
  from rich.table import Table
38
43
  from rich.text import Text
@@ -66,10 +71,45 @@ def render_cli_svg() -> Path:
66
71
 
67
72
  console.print(Text("$ scrollback list --usage", style="bold green"))
68
73
  console.print(table)
69
- out = OUT / "cli.svg"
70
- console.save_svg(str(out), title="scrollback")
71
- print(f"wrote {out}")
72
- return out
74
+
75
+ # PyPI-friendly PNG first: rich's save_svg() clears the record buffer, so
76
+ # the HTML export must happen before we write the SVG.
77
+ png_path = OUT / "cli.png"
78
+ try:
79
+ _svg_html_to_png(console, png_path)
80
+ print(f"wrote {png_path}")
81
+ except Exception as exc: # noqa: BLE001
82
+ print(f"cli.png skipped: {exc}", file=sys.stderr)
83
+
84
+ svg_path = OUT / "cli.svg"
85
+ console.save_svg(str(svg_path), title="scrollback")
86
+ print(f"wrote {svg_path}")
87
+ return [png_path, svg_path]
88
+
89
+
90
+ def _svg_html_to_png(console, out: Path) -> None:
91
+ from playwright.sync_api import sync_playwright
92
+
93
+ # rich returns a full HTML document; give it a dark terminal-like page and
94
+ # screenshot the <pre> block (which carries the real content dimensions).
95
+ html = console.export_html(
96
+ inline_styles=True,
97
+ code_format=(
98
+ "<!DOCTYPE html><html><head><meta charset='utf-8'>"
99
+ "<style>html,body{{margin:0}}"
100
+ "body{{background:#0d1117;display:inline-block;padding:22px 26px}}"
101
+ "pre{{margin:0;color:#c9d1d9;font:15px/1.5 Menlo,Consolas,monospace}}"
102
+ "</style></head>"
103
+ "<body><pre><code>{code}</code></pre></body></html>"
104
+ ),
105
+ )
106
+ with sync_playwright() as p:
107
+ browser = p.chromium.launch()
108
+ page = browser.new_page(device_scale_factor=2)
109
+ page.set_content(html, wait_until="load")
110
+ # The inline-block body wraps tightly around the terminal block.
111
+ page.query_selector("body").screenshot(path=str(out))
112
+ browser.close()
73
113
 
74
114
 
75
115
  # -- web screenshot (headless Chromium via Playwright) -------------------
@@ -132,7 +172,7 @@ def render_web_png() -> Path:
132
172
 
133
173
 
134
174
  def main() -> int:
135
- render_cli_svg()
175
+ render_cli()
136
176
  try:
137
177
  render_web_png()
138
178
  except Exception as exc: # noqa: BLE001
@@ -5,4 +5,4 @@ coding agents (opencode, Claude Code, ...) through a common adapter
5
5
  interface and lets you list, view, search, and export those sessions.
6
6
  """
7
7
 
8
- __version__ = "0.1.0"
8
+ __version__ = "0.1.2"
@@ -844,6 +844,72 @@ def cmd_install_launcher(args: argparse.Namespace) -> int:
844
844
  return 0
845
845
 
846
846
 
847
+ def _detect_install_tool() -> str:
848
+ """Best-effort guess of how scrollback was installed, for the hint.
849
+
850
+ Returns the command the user should run to remove the package itself.
851
+ We never run it: a process cannot reliably uninstall the package it is
852
+ executing from, and the right tool (pip / pipx / conda) depends on how
853
+ it was installed.
854
+ """
855
+ exe = (sys.executable or "").replace("\\", "/")
856
+ if "/pipx/" in exe or "/.local/pipx/" in exe:
857
+ return "pipx uninstall scrollback"
858
+ return "pip uninstall scrollback"
859
+
860
+
861
+ def cmd_uninstall(args: argparse.Namespace) -> int:
862
+ """Remove scrollback-created artifacts; explain how to remove the package.
863
+
864
+ Removes only files scrollback itself created (launchers, the macOS .app,
865
+ the optional search index, the launcher log). It never touches your agent
866
+ data, and it never tries to uninstall the Python package -- that is left
867
+ to pip/pipx, with the exact command printed at the end.
868
+ """
869
+ from . import fts, launcher_install
870
+
871
+ targets: list = list(launcher_install.installed_artifacts())
872
+ index_path = fts.default_index_path()
873
+ if index_path.exists():
874
+ targets.append(index_path)
875
+
876
+ if not targets:
877
+ _eprint("no scrollback-created artifacts found.")
878
+ _eprint(f"to remove the package itself, run:\n {_detect_install_tool()}")
879
+ return 0
880
+
881
+ label = "would remove" if args.dry_run else "about to remove"
882
+ _eprint(f"{label}:")
883
+ for p in targets:
884
+ _eprint(f" {p}")
885
+
886
+ if args.dry_run:
887
+ return 0
888
+
889
+ if not args.yes:
890
+ try:
891
+ reply = input("remove these? [y/N] ").strip().lower()
892
+ except (EOFError, KeyboardInterrupt):
893
+ reply = ""
894
+ if reply not in ("y", "yes"):
895
+ _eprint("aborted; nothing removed.")
896
+ return 1
897
+
898
+ removed, failed = 0, 0
899
+ for p in targets:
900
+ try:
901
+ launcher_install.remove_path(p)
902
+ _eprint(f"removed {p}")
903
+ removed += 1
904
+ except OSError as exc:
905
+ _eprint(f"could not remove {p}: {exc}")
906
+ failed += 1
907
+
908
+ _eprint(f"\nremoved {removed} item(s)" + (f", {failed} failed" if failed else ""))
909
+ _eprint(f"to remove the package itself, run:\n {_detect_install_tool()}")
910
+ return 1 if failed else 0
911
+
912
+
847
913
  class _AppBridge:
848
914
  """JS<->Python API exposed to the pywebview window.
849
915
 
@@ -1093,6 +1159,20 @@ def build_parser() -> argparse.ArgumentParser:
1093
1159
  "falls back to the Desktop launcher elsewhere)")
1094
1160
  sp.set_defaults(func=cmd_install_launcher)
1095
1161
 
1162
+ # uninstall
1163
+ sp = sub.add_parser(
1164
+ "uninstall",
1165
+ help="remove scrollback-created artifacts (launchers, app, index)",
1166
+ description="Remove files scrollback created (Desktop launcher, macOS "
1167
+ ".app, search index, launcher log). Your agent data is never "
1168
+ "touched. The Python package itself is removed with pip/pipx "
1169
+ "-- the exact command is printed at the end.",
1170
+ )
1171
+ sp.add_argument("-y", "--yes", action="store_true", help="skip the confirmation prompt")
1172
+ sp.add_argument("--dry-run", action="store_true",
1173
+ help="show what would be removed, then exit")
1174
+ sp.set_defaults(func=cmd_uninstall)
1175
+
1096
1176
  return p
1097
1177
 
1098
1178
 
@@ -207,3 +207,43 @@ def _install_linux(dest: Path | None) -> list[Path]:
207
207
  _write(sh, _read_bundled("scrollback.sh"), executable=True)
208
208
  created.append(sh)
209
209
  return created
210
+
211
+
212
+ # -- uninstall: locate + remove the artifacts install creates --------------
213
+
214
+ def installed_artifacts() -> list[Path]:
215
+ """Return the launcher/app/log paths scrollback may have created.
216
+
217
+ Covers the default install locations on every platform (a custom
218
+ ``--dest`` is not tracked, so those must be removed by hand). Only paths
219
+ that currently exist are returned. Never includes the user's agent data.
220
+ """
221
+ home = Path.home()
222
+ candidates: list[Path] = []
223
+
224
+ if sys.platform == "darwin":
225
+ candidates += [
226
+ _desktop_dir() / "scrollback.command",
227
+ home / "Applications" / "scrollback.app",
228
+ home / "Library" / "Logs" / "scrollback-launcher.log",
229
+ ]
230
+ elif sys.platform == "win32":
231
+ candidates += [_desktop_dir() / "scrollback.bat"]
232
+ else:
233
+ apps = Path(os.environ.get("XDG_DATA_HOME", home / ".local" / "share")) / "applications"
234
+ candidates += [
235
+ apps / "scrollback.desktop",
236
+ _desktop_dir() / "scrollback.sh",
237
+ ]
238
+
239
+ return [p for p in candidates if p.exists()]
240
+
241
+
242
+ def remove_path(path: Path) -> None:
243
+ """Delete a file or directory (used to remove a .app bundle)."""
244
+ import shutil
245
+
246
+ if path.is_dir() and not path.is_symlink():
247
+ shutil.rmtree(path)
248
+ else:
249
+ path.unlink()
@@ -99,3 +99,29 @@ def test_command_script_bakes_absolute_interpreter_path():
99
99
  script = launcher_install._command_script()
100
100
  assert sys.executable in script
101
101
  assert "scrollback.cli web" in script
102
+
103
+
104
+ def test_installed_artifacts_finds_what_install_created(tmp_path, monkeypatch):
105
+ # Point HOME at a temp dir, install to the (temp) Desktop, then confirm
106
+ # installed_artifacts() reports the created launcher and that remove_path
107
+ # actually deletes it. Never touches the real home.
108
+ monkeypatch.setattr(launcher_install.Path, "home", classmethod(lambda cls: tmp_path))
109
+ desktop = tmp_path / "Desktop"
110
+ desktop.mkdir()
111
+
112
+ created = launcher_install.install(desktop, desktop=True)
113
+ assert created and all(p.exists() for p in created)
114
+
115
+ found = launcher_install.installed_artifacts()
116
+ # The platform's Desktop launcher should be discovered.
117
+ assert any(p in found for p in created), (found, created)
118
+
119
+ for p in found:
120
+ launcher_install.remove_path(p)
121
+ assert not p.exists()
122
+
123
+
124
+ def test_installed_artifacts_empty_when_nothing_installed(tmp_path, monkeypatch):
125
+ monkeypatch.setattr(launcher_install.Path, "home", classmethod(lambda cls: tmp_path))
126
+ (tmp_path / "Desktop").mkdir()
127
+ assert launcher_install.installed_artifacts() == []
@@ -1,91 +0,0 @@
1
- <svg class="rich-terminal" viewBox="0 0 1141 245.2" xmlns="http://www.w3.org/2000/svg">
2
- <!-- Generated with Rich https://www.textualize.io -->
3
- <style>
4
-
5
- @font-face {
6
- font-family: "Fira Code";
7
- src: local("FiraCode-Regular"),
8
- url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"),
9
- url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff");
10
- font-style: normal;
11
- font-weight: 400;
12
- }
13
- @font-face {
14
- font-family: "Fira Code";
15
- src: local("FiraCode-Bold"),
16
- url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"),
17
- url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff");
18
- font-style: bold;
19
- font-weight: 700;
20
- }
21
-
22
- .terminal-4113685748-matrix {
23
- font-family: Fira Code, monospace;
24
- font-size: 20px;
25
- line-height: 24.4px;
26
- font-variant-east-asian: full-width;
27
- }
28
-
29
- .terminal-4113685748-title {
30
- font-size: 18px;
31
- font-weight: bold;
32
- font-family: arial;
33
- }
34
-
35
- .terminal-4113685748-r1 { fill: #98a84b;font-weight: bold }
36
- .terminal-4113685748-r2 { fill: #c5c8c6 }
37
- .terminal-4113685748-r3 { fill: #c5c8c6;font-weight: bold }
38
- .terminal-4113685748-r4 { fill: #d0b344 }
39
- .terminal-4113685748-r5 { fill: #868887 }
40
- .terminal-4113685748-r6 { fill: #d75f00 }
41
- </style>
42
-
43
- <defs>
44
- <clipPath id="terminal-4113685748-clip-terminal">
45
- <rect x="0" y="0" width="1121.3999999999999" height="194.2" />
46
- </clipPath>
47
- <clipPath id="terminal-4113685748-line-0">
48
- <rect x="0" y="1.5" width="1122.4" height="24.65"/>
49
- </clipPath>
50
- <clipPath id="terminal-4113685748-line-1">
51
- <rect x="0" y="25.9" width="1122.4" height="24.65"/>
52
- </clipPath>
53
- <clipPath id="terminal-4113685748-line-2">
54
- <rect x="0" y="50.3" width="1122.4" height="24.65"/>
55
- </clipPath>
56
- <clipPath id="terminal-4113685748-line-3">
57
- <rect x="0" y="74.7" width="1122.4" height="24.65"/>
58
- </clipPath>
59
- <clipPath id="terminal-4113685748-line-4">
60
- <rect x="0" y="99.1" width="1122.4" height="24.65"/>
61
- </clipPath>
62
- <clipPath id="terminal-4113685748-line-5">
63
- <rect x="0" y="123.5" width="1122.4" height="24.65"/>
64
- </clipPath>
65
- <clipPath id="terminal-4113685748-line-6">
66
- <rect x="0" y="147.9" width="1122.4" height="24.65"/>
67
- </clipPath>
68
- </defs>
69
-
70
- <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="1139" height="243.2" rx="8"/><text class="terminal-4113685748-title" fill="#c5c8c6" text-anchor="middle" x="569" y="27">scrollback</text>
71
- <g transform="translate(26,22)">
72
- <circle cx="0" cy="0" r="7" fill="#ff5f57"/>
73
- <circle cx="22" cy="0" r="7" fill="#febc2e"/>
74
- <circle cx="44" cy="0" r="7" fill="#28c840"/>
75
- </g>
76
-
77
- <g transform="translate(9, 41)" clip-path="url(#terminal-4113685748-clip-terminal)">
78
-
79
- <g class="terminal-4113685748-matrix">
80
- <text class="terminal-4113685748-r1" x="0" y="20" textLength="305" clip-path="url(#terminal-4113685748-line-0)">$&#160;scrollback&#160;list&#160;--usage</text><text class="terminal-4113685748-r2" x="1122.4" y="20" textLength="12.2" clip-path="url(#terminal-4113685748-line-0)">
81
- </text><text class="terminal-4113685748-r3" x="0" y="44.4" textLength="122" clip-path="url(#terminal-4113685748-line-1)">source&#160;&#160;&#160;&#160;</text><text class="terminal-4113685748-r3" x="146.4" y="44.4" textLength="146.4" clip-path="url(#terminal-4113685748-line-1)">id&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text class="terminal-4113685748-r3" x="317.2" y="44.4" textLength="195.2" clip-path="url(#terminal-4113685748-line-1)">updated&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text class="terminal-4113685748-r3" x="536.8" y="44.4" textLength="48.8" clip-path="url(#terminal-4113685748-line-1)">msgs</text><text class="terminal-4113685748-r3" x="610" y="44.4" textLength="61" clip-path="url(#terminal-4113685748-line-1)">&#160;cost</text><text class="terminal-4113685748-r3" x="695.4" y="44.4" textLength="122" clip-path="url(#terminal-4113685748-line-1)">tok&#160;in/out</text><text class="terminal-4113685748-r3" x="841.8" y="44.4" textLength="280.6" clip-path="url(#terminal-4113685748-line-1)">title&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text class="terminal-4113685748-r2" x="1122.4" y="44.4" textLength="12.2" clip-path="url(#terminal-4113685748-line-1)">
82
- </text><text class="terminal-4113685748-r4" x="0" y="68.8" textLength="122" clip-path="url(#terminal-4113685748-line-2)">opencode&#160;&#160;</text><text class="terminal-4113685748-r5" x="146.4" y="68.8" textLength="146.4" clip-path="url(#terminal-4113685748-line-2)">ses_demo_hea</text><text class="terminal-4113685748-r5" x="317.2" y="68.8" textLength="195.2" clip-path="url(#terminal-4113685748-line-2)">2026-03-14&#160;09:32</text><text class="terminal-4113685748-r2" x="536.8" y="68.8" textLength="48.8" clip-path="url(#terminal-4113685748-line-2)">&#160;&#160;&#160;2</text><text class="terminal-4113685748-r2" x="610" y="68.8" textLength="61" clip-path="url(#terminal-4113685748-line-2)">$0.02</text><text class="terminal-4113685748-r2" x="695.4" y="68.8" textLength="122" clip-path="url(#terminal-4113685748-line-2)">&#160;1.8k/2.3k</text><text class="terminal-4113685748-r2" x="841.8" y="68.8" textLength="280.6" clip-path="url(#terminal-4113685748-line-2)">Derive&#160;a&#160;stable&#160;scheme&#160;</text><text class="terminal-4113685748-r2" x="1122.4" y="68.8" textLength="12.2" clip-path="url(#terminal-4113685748-line-2)">
83
- </text><text class="terminal-4113685748-r2" x="841.8" y="93.2" textLength="280.6" clip-path="url(#terminal-4113685748-line-3)">for&#160;the&#160;heat&#160;equation&#160;&#160;</text><text class="terminal-4113685748-r2" x="1122.4" y="93.2" textLength="12.2" clip-path="url(#terminal-4113685748-line-3)">
84
- </text><text class="terminal-4113685748-r6" x="0" y="117.6" textLength="122" clip-path="url(#terminal-4113685748-line-4)">claudecode</text><text class="terminal-4113685748-r5" x="146.4" y="117.6" textLength="146.4" clip-path="url(#terminal-4113685748-line-4)">ses_demo_ref</text><text class="terminal-4113685748-r5" x="317.2" y="117.6" textLength="195.2" clip-path="url(#terminal-4113685748-line-4)">2026-03-14&#160;08:10</text><text class="terminal-4113685748-r2" x="536.8" y="117.6" textLength="48.8" clip-path="url(#terminal-4113685748-line-4)">&#160;&#160;&#160;2</text><text class="terminal-4113685748-r2" x="610" y="117.6" textLength="61" clip-path="url(#terminal-4113685748-line-4)">$0.01</text><text class="terminal-4113685748-r2" x="695.4" y="117.6" textLength="122" clip-path="url(#terminal-4113685748-line-4)">&#160;&#160;1.2k/900</text><text class="terminal-4113685748-r2" x="841.8" y="117.6" textLength="280.6" clip-path="url(#terminal-4113685748-line-4)">Refactor&#160;load_config&#160;&#160;&#160;</text><text class="terminal-4113685748-r2" x="1122.4" y="117.6" textLength="12.2" clip-path="url(#terminal-4113685748-line-4)">
85
- </text><text class="terminal-4113685748-r2" x="841.8" y="142" textLength="280.6" clip-path="url(#terminal-4113685748-line-5)">with&#160;typed&#160;validation&#160;&#160;</text><text class="terminal-4113685748-r2" x="1122.4" y="142" textLength="12.2" clip-path="url(#terminal-4113685748-line-5)">
86
- </text><text class="terminal-4113685748-r4" x="0" y="166.4" textLength="122" clip-path="url(#terminal-4113685748-line-6)">opencode&#160;&#160;</text><text class="terminal-4113685748-r5" x="146.4" y="166.4" textLength="146.4" clip-path="url(#terminal-4113685748-line-6)">ses_demo_bug</text><text class="terminal-4113685748-r5" x="317.2" y="166.4" textLength="195.2" clip-path="url(#terminal-4113685748-line-6)">2026-03-13&#160;09:32</text><text class="terminal-4113685748-r2" x="536.8" y="166.4" textLength="48.8" clip-path="url(#terminal-4113685748-line-6)">&#160;&#160;&#160;2</text><text class="terminal-4113685748-r2" x="610" y="166.4" textLength="61" clip-path="url(#terminal-4113685748-line-6)">$0.01</text><text class="terminal-4113685748-r2" x="695.4" y="166.4" textLength="122" clip-path="url(#terminal-4113685748-line-6)">&#160;&#160;&#160;640/410</text><text class="terminal-4113685748-r2" x="841.8" y="166.4" textLength="280.6" clip-path="url(#terminal-4113685748-line-6)">Fix&#160;timezone-naive&#160;&#160;&#160;&#160;&#160;</text><text class="terminal-4113685748-r2" x="1122.4" y="166.4" textLength="12.2" clip-path="url(#terminal-4113685748-line-6)">
87
- </text><text class="terminal-4113685748-r2" x="841.8" y="190.8" textLength="280.6" clip-path="url(#terminal-4113685748-line-7)">timestamp&#160;crash&#160;in&#160;sort</text><text class="terminal-4113685748-r2" x="1122.4" y="190.8" textLength="12.2" clip-path="url(#terminal-4113685748-line-7)">
88
- </text>
89
- </g>
90
- </g>
91
- </svg>
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes