feat(html): add event permalinks
All checks were successful
Security Scan / security (push) Successful in 9s
Test / test (push) Successful in 1m10s

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-01 04:47:53 +00:00
parent 0ab8627447
commit 36c184e7dd
5 changed files with 21 additions and 3 deletions

View file

@ -239,10 +239,11 @@ Success or failure of a `tool_use` event is indicated by a Unicode check mark (U
Each event is rendered as a `<div class="event">` containing:
- `.event-header`: always visible; shows timestamp, tool label, truncated input (120 chars), duration, and status icon.
- `.event-header`: always visible; shows timestamp, tool label, truncated input (120 chars), duration, status icon, and a permalink anchor.
- `.event-body`: hidden by default; shown on click via the `toggle(i)` JavaScript function which toggles the `open` class.
The arrow indicator rotates 90 degrees (CSS `transform: rotate(90deg)`) when the panel is open. Output text in `.event-body` is capped at 400px height with `overflow-y: auto`.
If the page loads with an `#evt-N` fragment, that event is opened automatically and scrolled into view.
Input label semantics vary per tool:

View file

@ -76,5 +76,5 @@ The following have been identified as potential improvements but are not current
- **Parallel search**: fan out `ParseTranscript` calls across goroutines with a result channel to reduce wall time for large directories.
- **Persistent index**: a lightweight SQLite index or binary cache per session file to avoid re-parsing on every `Search` or `ListSessions` call.
- **Additional tool types**: the parser's `extractToolInput` fallback handles any unknown tool by listing its JSON keys. Dedicated handling could be added for `WebFetch`, `WebSearch`, `NotebookEdit`, and other tools that appear in Claude Code sessions.
- **HTML export options**: configurable truncation limits, optional full-output display, and per-event direct links (anchor IDs already exist as `evt-{i}`).
- **HTML export options**: configurable truncation limits and optional full-output display remain open; per-event direct links are now available via `#evt-{i}` permalinks.
- **VHS alternative**: a pure-Go terminal animation renderer to eliminate the `vhs` dependency for MP4 output.

16
html.go
View file

@ -71,6 +71,8 @@ body { background: var(--bg); color: var(--fg); font-family: var(--font); font-s
.event-header .input { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.event-header .dur { color: var(--dim); font-size: 11px; min-width: 50px; text-align: right; }
.event-header .status { font-size: 14px; min-width: 20px; text-align: center; }
.event-header .permalink { color: var(--dim); font-size: 12px; min-width: 16px; text-align: center; text-decoration: none; }
.event-header .permalink:hover { color: var(--accent); }
.event-header .arrow { color: var(--dim); font-size: 10px; transition: transform 0.15s; min-width: 16px; }
.event.open .arrow { transform: rotate(90deg); }
.event-body { display: none; padding: 12px; background: var(--bg); border-top: 1px solid var(--border); }
@ -160,6 +162,7 @@ body { background: var(--bg); color: var(--fg); font-family: var(--font); font-s
<span class="input">%s</span>
<span class="dur">%s</span>
<span class="status">%s</span>
<a class="permalink" href="#evt-%d" aria-label="Direct link to this event" onclick="event.stopPropagation()">#</a>
</div>
<div class="event-body">
`,
@ -174,7 +177,8 @@ body { background: var(--bg); color: var(--fg); font-family: var(--font); font-s
html.EscapeString(toolLabel),
html.EscapeString(truncate(evt.Input, 120)),
durStr,
statusIcon))
statusIcon,
i))
if evt.Input != "" {
label := "Command"
@ -227,12 +231,22 @@ function filterEvents() {
el.classList.toggle('hidden', !show);
});
}
function openHashEvent() {
const hash = window.location.hash;
if (!hash || !hash.startsWith('#evt-')) return;
const el = document.getElementById(hash.slice(1));
if (!el) return;
el.classList.add('open');
el.scrollIntoView({block: 'start'});
}
document.addEventListener('keydown', e => {
if (e.key === '/' && document.activeElement.tagName !== 'INPUT') {
e.preventDefault();
document.getElementById('search').focus();
}
});
window.addEventListener('hashchange', openHashEvent);
document.addEventListener('DOMContentLoaded', openHashEvent);
</script>
</body>
</html>

View file

@ -73,6 +73,8 @@ func TestHTML_RenderHTMLBasicSession_Good(t *testing.T) {
assert.Contains(t, html, "Claude") // assistant event label
assert.Contains(t, html, "Bash")
assert.Contains(t, html, "Read")
assert.Contains(t, html, `href="#evt-0"`)
assert.Contains(t, html, "openHashEvent")
// Should contain JS for toggle and filter
assert.Contains(t, html, "function toggle")
assert.Contains(t, html, "function filterEvents")

View file

@ -15,6 +15,7 @@ go-session provides two output formats for visualising parsed sessions: a self-c
- Yellow: User messages
- Grey: Assistant responses
- Red border: Failed tool calls
- **Permalinks** on each event card for direct `#evt-N` links
### Usage