## Verdict
VERDICT: DO-NOT-SHIP (15 findings) - the core interaction surface (session rows) is mouse-only, a routine launch can freeze the whole app behind a buttonless modal, and the Manage Sessions delete violates the project's own confirm-first requirement.

## Findings

- **[CRITICAL] Session rows are unreachable by keyboard** - every row is a bare `div` with only `click` and `contextmenu` listeners (`src/core/panel.ts:1327-1341`); no `tabindex`, no `role`, no keydown handler. A keyboard user cannot resume, favourite, branch, or delete anything - and since the context menu is only opened from a row's `contextmenu` event on an unfocusable element, the entire action set is unreachable, not merely awkward. The popup repeats it: switching the current conversation is a row-div click (`src/core/popup.ts:295-310`) with no focusable equivalent (Open/copy are real buttons; Switch is not). Fix: give rows `role="button"` (or listbox semantics), `tabindex="0"`, Enter/Space → open, Menu-key/Shift+F10 → context menu; in the popup add Switch to the row's button set or make the row a button.

- **[MAJOR] Launch modal can hang forever with no exit** - `showLaunchSpinner` is a Lumino `Dialog` with `buttons: []` (`src/core/terminals.ts:41`), dismissed only when the request settles, and `requestAPI` (`src/core/request.ts:17-54`) sets no timeout. A stalled `launch` POST leaves the user staring at an undismissable app-wide scrim with a spinner - reads as a crash, and is one (of the UI). It is also over-focusing: a sub-second routine action blocks all of JupyterLab every single time a row is clicked. Fix: add a Cancel button and/or a timeout that dismisses with an error toast; better, replace the modal with an inline busy state on the clicked row (the row already has a spinner pattern, `panel.ts:1260-1266`).

- **[MAJOR] Manage Sessions delete is destructive with no confirmation** - `deleteBtn` fires immediately on click; the code explicitly declines to confirm (`src/core/popup.ts:344-352`) because a nested Lumino dialog renders detached. With `deleteToTrash=false` this is permanent, unrecoverable, and select-all makes bulk loss two clicks. Requirement 6 of record says destructive ops "confirm first" - this reopens that requirement, whatever the technical excuse. Fix that avoids dialog nesting: two-step arming inside the same footer (`Delete (3)` → button turns filled-error `Confirm delete 3`, reverting after ~4 s or on any selection change), or an undo window in the status line.

- **[MAJOR] Gemini's two approval controls silently contradict each other** - `yoloMode` (boolean) and `approvalMode` (enum containing `yolo`) both exist (`src/providers/gemini.ts:57-76`, `schema/plugin.json:66-78`), and `_launchMode` takes the first true boolean before ever reading the enum (`src/core/panel.ts:689-699`). A user who sets `approvalMode: plan` (read-only!) while `yoloMode` is on gets `--yolo` - the exact inverse of their stated intent, with no warning. Fix: drop the `yoloMode` toggle (the enum already expresses it) or validate/grey the enum while the boolean is on.

- **[MAJOR] "retrying" copy on failures that never retry** - `_showError` always renders `Could not reach the server - retrying. …` (`src/core/panel.ts:499`), but it is shared by one-shot actions: favourite toggle rollback (`:541`), project removal (`:580`), branch launch (`:831`). The user waits for a retry that never comes. Fix: split the copy - the poll path keeps "retrying", action paths say what failed and that a re-try is manual.

- **[MAJOR] No absolute time reference anywhere** - the row shows `3d ago` and its tooltip repeats `Last activity: 3d ago` (`src/core/panel.ts:1372`) - the tooltip is exactly where the absolute reference belongs and it only restates the relative value. Granularity also caps at days (`_formatRelativeTime`, `:1433-1448`), so a year-old session reads `365d ago`. Fix: tooltip line becomes `Last activity: 2026-08-04 14:12 (3d ago)`; consider `mo`/`y` buckets for the column.

- **[MINOR] Header icon buttons under minimum target size** - `.jp-AiAssistantsPanel-iconButton` is 20×20 (`style/base.css:48-60`) and `.jp-AiAssistantsPanel-searchClear` 20px wide (`:120-134`) - below the 24×24 WCAG 2.2 AA minimum the popup's own copy button already meets (`:674-689`). Fix: 24px hit box, keep the 14px glyph.

- **[MINOR] No `prefers-reduced-motion` handling** - three infinite spin animations (`style/base.css:406-430`) plus a deliberately manufactured 500 ms minimum spin on refresh (`src/core/panel.ts:193-195`). Spinners on genuine waits are defensible; the artificial hold and the absence of any reduced-motion fallback are not. Fix: `@media (prefers-reduced-motion: reduce)` swaps spinners for a static/opacity indicator and drops the minimum-spin floor.

- **[MINOR] Status signals are silent to screen readers** - the live/remote dot is a `span` with only a `title` (`src/core/panel.ts:1348-1362`), which most screen readers never announce; the inline error strip (`:485-491`) has no `role="alert"`/`aria-live`, so a failed poll is invisible to SR users while sighted users see red. Fix: sr-only text or `aria-label` on the dot; `role="status"` on the error strip (the popup already does this right, `popup.ts:119-123`).

- **[MINOR] Filtered-out and truly-empty sections read identically** - with an active filter, a section shows `Empty.` (`src/core/panel.ts:1227-1228`) - indistinguishable from having no sessions; the popup already distinguishes (`No matching sessions.` vs `No other conversations.`, `popup.ts:219-221`). Fix: `No matches.` when `this._filter` is non-empty.

- **[MINOR] Empty panel state is descriptive, not actionable** - `No Claude Code sessions found.` (`src/core/panel.ts:1155`) leaves a first-time user without the next step, while the `+` button sits 20px away unexplained. Fix: one added line - "Use + to start a session in the current folder."

- **[MINOR] Shield glyph marks the *unsafe* variants** - `shieldIcon` (an intact protective shield, `src/core/icons.ts:51-60`) decorates every skip-permissions/YOLO menu entry (`panel.ts:1529, 1543, 1548`). A shield reads as "protected" - the icon's semantics invert the meaning it marks. The explicit labels save it from MAJOR. Fix: shield-off or warning-triangle glyph.

- **[MINOR] Section carets are raw text glyphs** - `▾`/`▸` as `textContent` (`src/core/panel.ts:1206`) where sibling JupyterLab panels use `caretDown`/`caretRight` LabIcons; rendering varies by font, and screen readers announce "black down-pointing small triangle" mid-button. Fix: use the ui-components caret icons, or at minimum `aria-hidden="true"` on the span (state is already on `aria-expanded`).

- **[MINOR] Search inputs have no accessible name** - both the panel filter (`src/core/panel.ts:299-303`) and popup filter (`popup.ts:73-77`) rely on placeholder alone. Fix: `aria-label="Filter sessions"` on each.

- **[MINOR (taste)] "bg" chip is jargon; popup floor is wide for small screens** - the row chip says `bg` with its tooltip deliberately suppressed (`src/core/panel.ts:1293-1298`) - "agent" would say *what* it is in the same footprint; and `.jp-AiAssistantsPanel-branchPopup { min-width: 360px }` (`style/base.css:552`) overflows a 320-360px viewport inside the Dialog's padding. Both defensible calls; both worth a look.

## What works
- **Theme-token discipline is exemplary** - every colour in `base.css` routes through `--jp-*` vars (the popup even maps a local design language onto them, `:522-547`), so light/dark/custom themes render correctly with zero hardcoded pixels; the dot and star are deliberately softened rather than alarming
- **State preservation across re-renders** - per-section scroll capture/restore (`panel.ts:1136-1191`), disambiguation computed against the full set so labels don't shift under filtering, and poll suppression while the context menu is open are the kind of details users never notice and always feel
- **Failure containment** - one provider's dead server degrades to a quiet inline strip and keeps polling; nothing modal, nothing cross-panel (`panel.ts:484-500`)
- **The delete flow's mechanics** (aside from the missing confirm) are careful: busy-scrim with keyboard re-sync, `aria-busy`, polite re-announcing live region, refetch-from-truth instead of optimistic splice, focus restoration after the focused row is destroyed (`popup.ts:334-407`)

Dimensions passing cleanly: visual hierarchy (title → sections → rows, salience proportional), animation envelope (no looping attention effects - all loops are genuine busy states), comparability/label alignment (fixed time and select columns), conditional affordances (cleanup-parallel and branch submenus appear only when applicable, `panel.ts:1623, 1764`), and output slop (copy is uniformly terse; nothing to delete).

Not logging to journal: review delivered in conversation, no artefact changed.
