Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ All notable user-visible changes to this project are documented in this file.
- Comments not attached to a snippet (e.g. `sideshow comment` without
`--snippet`) were stored and delivered to agents but never shown in the
viewer; they now render in the session thread.
- The viewer is now usable by keyboard and assistive tech: session rows are
focusable and activate with Enter/Space (focus survives live re-renders),
hover-only actions (session delete, card open/delete) are reachable and
shown on focus, the editable session title is labeled and Escape cancels
an edit, snippet iframes carry the snippet title, and toasts are announced
via a polite live region.

## [0.2.0] - 2026-06-11

Expand Down
54 changes: 47 additions & 7 deletions viewer/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@
.sess:hover {
background: var(--hover);
}
.sess:focus-visible {
outline: 2px solid var(--accent);
outline-offset: -2px;
}
.sess.sel {
background: var(--surface);
box-shadow: 0 0 0 0.5px var(--border);
Expand Down Expand Up @@ -132,7 +136,7 @@
position: absolute;
top: 8px;
right: 6px;
display: none;
opacity: 0;
border: none;
background: none;
color: var(--faint);
Expand All @@ -142,10 +146,12 @@
border-radius: 5px;
font-family: inherit;
}
.sess:hover .x {
display: block;
.sess:hover .x,
.sess:focus-within .x {
opacity: 1;
}
.sess:hover .dot {
.sess:hover .dot,
.sess:focus-within .dot {
display: none;
}
.sess .x:hover {
Expand Down Expand Up @@ -259,7 +265,8 @@
opacity: 0;
transition: opacity 0.15s;
}
.card:hover .act {
.card:hover .act,
.card:focus-within .act {
opacity: 1;
}
.card-head .act:hover {
Expand Down Expand Up @@ -459,14 +466,20 @@ <h2>or try it yourself</h2>
</div>
<div id="sessionView" hidden>
<div class="session-head">
<span id="sessTitle" contenteditable="true" spellcheck="false"></span>
<span
id="sessTitle"
contenteditable="true"
spellcheck="false"
role="textbox"
aria-label="Session title"
></span>
<span class="meta" id="sessMeta"></span>
</div>
<div id="stream"></div>
</div>
</main>
</div>
<div id="toast"></div>
<div id="toast" role="status" aria-live="polite"></div>
<script>
const $ = (id) => document.getElementById(id);
const state = {
Expand Down Expand Up @@ -511,14 +524,31 @@ <h2>or try it yourself</h2>

function renderSidebar() {
const list = $("sessionList");
// re-renders replace the rows wholesale; put keyboard focus back where it was
const focused = list.contains(document.activeElement)
? {
id: document.activeElement.closest(".sess")?.dataset.id,
x: document.activeElement.classList.contains("x"),
}
: null;
list.replaceChildren();
for (const s of state.sessions) {
const el = document.createElement("div");
el.className =
"sess" +
(s.id === state.selected ? " sel" : "") +
(state.unread.has(s.id) ? " unread" : "");
el.dataset.id = s.id;
el.setAttribute("role", "button");
el.tabIndex = 0;
if (s.id === state.selected) el.setAttribute("aria-current", "true");
el.onclick = () => select(s.id);
el.onkeydown = (e) => {
if (e.target === el && (e.key === "Enter" || e.key === " ")) {
e.preventDefault();
select(s.id);
}
};

const title = document.createElement("div");
title.className = "sess-title";
Expand All @@ -532,6 +562,7 @@ <h2>or try it yourself</h2>
x.className = "x";
x.textContent = "✕";
x.title = "Delete session";
x.setAttribute("aria-label", `Delete session "${sessionLabel(s)}"`);
x.onclick = async (e) => {
e.stopPropagation();
if (!confirm(`Delete "${sessionLabel(s)}" and its snippets?`)) return;
Expand All @@ -540,6 +571,10 @@ <h2>or try it yourself</h2>
el.append(title, meta, dot, x);
list.appendChild(el);
}
if (focused?.id) {
const row = list.querySelector(`.sess[data-id="${CSS.escape(focused.id)}"]`);
if (row) (focused.x ? row.querySelector(".x") : row).focus();
}
$("onboard").hidden = state.sessions.length > 0;
$("sessionView").hidden = state.sessions.length === 0;
}
Expand Down Expand Up @@ -574,6 +609,10 @@ <h2>or try it yourself</h2>
if (e.key === "Enter") {
e.preventDefault();
titleEl.blur();
} else if (e.key === "Escape") {
e.preventDefault();
titleEl.textContent = sessionLabel(s);
titleEl.blur();
}
};
$("sessMeta").textContent = `${s.agent} · started ${relTime(s.createdAt)}`;
Expand Down Expand Up @@ -721,6 +760,7 @@ <h2>or try it yourself</h2>
}

const iframe = card.querySelector("iframe");
iframe.title = s.title;
const src = `/s/${s.id}?cb=${s.version}`;
if (iframe.getAttribute("src") !== src) iframe.src = src;

Expand Down
Loading