What task are you trying to do?
Unify the session row left-side region so a single reserved 24px slot displays either the running / permission / error / unseen indicator or the pin icon, instead of two separate reserved slots.
Context
PR #149 (closing #143) stabilized the status slot width and redrew Pin / Filter icons, but kept the two-slot architecture: the pin button is rendered via SessionItem leadingSlot (before SessionRow) and the status indicator lives inside SessionRow. This creates a visible gap between the pin position and the running spinner position, which does not match the original intent ("pin button position displays as running when running").
Deferred from #143 scope during review because the architectural change is medium-risk and deserves its own ticket.
What would a good result look like?
- Top-level session rows have a single reserved
size-6 slot with priority: running > permission > error > unseen > pin (if pinned or hover) > empty.
- Child sessions keep the existing in-row status slot (no pin button at nested levels).
- Pin button a11y contract preserved:
tabIndex={isPinned() ? 0 : -1}, aria-hidden hover-to-discover, stopPropagation() on click.
- When pinned + running, the spinner shows visually but the pin button must stay keyboard-reachable so the user can still unpin.
- Top-level title baseline shifts left by ~28px (24px slot + gap-1) after the merge, consistent across all top-level rows. Expected, not a regression.
Which audience does this matter to most?
Both
Extra context
Implementation sketch
packages/app/src/pages/layout/sidebar-items.tsx:
- Add
leadingSlot?: JSX.Element prop on SessionRow.
- Append a final
<Match when={props.leadingSlot}>{props.leadingSlot}</Match> to the status <Switch> so the pin button renders only when no status is active.
- Remove the outer leadingSlot wrapper at
sidebar-items.tsx:229-231.
- In
SessionItem, pass leadingSlot={!props.level && props.leadingSlot ? props.leadingSlot(props.session) : undefined} to SessionRow.
packages/app/src/pages/layout/pawwork-sidebar.tsx:
- Pin button implementation mostly unchanged (size-6 wrapper, click handler, tint).
- May need a11y tweak so the pin button remains reachable when pinned + running (candidate: always render the button, overlay status visually via CSS rather than Switch-hide).
Risk notes
- A11y edge case (highest risk): when pinned + running, naive Switch priority hides the pin button from the tree. Need an approach that keeps the button tab-reachable while showing the status indicator.
- Event bubbling: pin click inside
<A> must stopPropagation() (current code already does).
- Combinatorial testing: 4 status states × pinned/unpinned × hover/no-hover = 16 top-level combinations, plus child-level variants.
Out of scope
- Filter / sort button (unchanged).
- Status priority order (unchanged: permission > running > error > unseen per
sidebar-items.tsx:173).
- Child session layout (unchanged).
Manual verification checklist (run bun dev:desktop)
- Idle unpinned row → slot empty, title at unified baseline.
- Hover unpinned row → outline pin in
text-text-weak.
- Pinned row → filled pin in
text-accent-brand.
- Running session (pinned) → spinner visible; pin still focusable via Tab; Enter unpins.
- Running session (unpinned) → spinner visible; hovering does not reveal pin.
- Permission / error / unseen → yellow / red / blue dot respectively.
- Child sessions → no pin at nested levels; status indicator works normally.
- Keyboard-only: Tab order through sidebar remains stable across state transitions.
Related
What task are you trying to do?
Unify the session row left-side region so a single reserved 24px slot displays either the running / permission / error / unseen indicator or the pin icon, instead of two separate reserved slots.
Context
PR #149 (closing #143) stabilized the status slot width and redrew Pin / Filter icons, but kept the two-slot architecture: the pin button is rendered via
SessionItemleadingSlot(before SessionRow) and the status indicator lives insideSessionRow. This creates a visible gap between the pin position and the running spinner position, which does not match the original intent ("pin button position displays as running when running").Deferred from #143 scope during review because the architectural change is medium-risk and deserves its own ticket.
What would a good result look like?
size-6slot with priority:running > permission > error > unseen > pin (if pinned or hover) > empty.tabIndex={isPinned() ? 0 : -1},aria-hiddenhover-to-discover,stopPropagation()on click.Which audience does this matter to most?
Both
Extra context
Implementation sketch
packages/app/src/pages/layout/sidebar-items.tsx:leadingSlot?: JSX.Elementprop onSessionRow.<Match when={props.leadingSlot}>{props.leadingSlot}</Match>to the status<Switch>so the pin button renders only when no status is active.sidebar-items.tsx:229-231.SessionItem, passleadingSlot={!props.level && props.leadingSlot ? props.leadingSlot(props.session) : undefined}toSessionRow.packages/app/src/pages/layout/pawwork-sidebar.tsx:Risk notes
<A>muststopPropagation()(current code already does).Out of scope
sidebar-items.tsx:173).Manual verification checklist (run
bun dev:desktop)text-text-weak.text-accent-brand.Related