Skip to content

[Feature] Stabilize session row left-side visuals (status slot width + Pin/Filter icons) #143

@Astro-Han

Description

@Astro-Han

What task are you trying to do?

Keep the session name at a stable horizontal baseline in the left sidebar as a session toggles between idle / running / waiting-on-permission / errored / has-unseen-messages. At the same time, make the Pin and Sort (filter) icons visually match the rest of the sidebar icon set instead of rendering noticeably bolder.

What do you do today?

Two independent issues on the left side of each session row.

1. Status slot width is conditional. packages/app/src/pages/layout/sidebar-items.tsx:116-136 wraps the entire status indicator (size-6) in <Show when={props.isWorking() || props.hasPermissions() || props.hasError() || props.unseenCount() > 0}>. When a session has none of those four states the whole size-6 block is removed from layout, so the session title starts roughly one slot-width plus the gap-2 further left (≈32px) than an adjacent row that does have one of those states. As states come and go (a run starts, a permission prompt clears, an error is acknowledged) the title shifts horizontally.

Note that the Pin button slot to the left of the status slot is not the source of the shift. Its base class at packages/app/src/pages/layout/pawwork-sidebar.tsx:146 ("inline-flex size-6 ...": true) is unconditional; Pin toggles opacity and pointer-events but always reserves size-6.

2. Pin and Filter SVGs are off-family. packages/app/src/pages/layout/pawwork-sidebar.tsx ships two hand-drawn SVGs at viewBox="0 0 12 12": FilterIcon (L20-27) uses stroke-width="1.2" + stroke-linecap="round", and PinIcon (L29-41) uses stroke-width="1.1" + stroke-linejoin="round". The majority of sidebar icons in packages/ui/src/components/icon.tsx use viewBox="0 0 20 20" with default stroke width and per-glyph linecap, so the Pin and Filter icons render noticeably bolder and less uniform than their neighbours.

What would a good result look like?

  1. Every session row reserves a stable size-6 status slot whether or not any status is currently active; the session title starts at the same horizontal position on every row at the same nesting level. The existing priority switch inside the slot is unchanged, including its tint from messageAgentColor(...) and its behaviour on child sessions. (Effective priority is permission → running → error → unseenhasPermissions() forces isWorking() to false at sidebar-items.tsx:173, so permission wins over running even though the <Switch> lists isWorking first.)
  2. Pin and Filter icons sit in the same visual weight family as the rest of the sidebar: 20×20 grid, default stroke width, per-glyph linecap chosen to match neighbouring glyphs.

Which audience does this matter to most?

Both

Extra context

Scope — ship as one PR with two commits, one per section below.

Commit 1 — fix(app): reserve sidebar status slot width to keep session title baseline stable

  • packages/app/src/pages/layout/sidebar-items.tsx:116-136 — change the outer <Show when={...}> into an unconditional size-6 container. The inner <Switch> (running / permission / error / unseen) stays conditional, so when no state is active the container renders empty but still occupies size-6 of width.
  • No behaviour change for active states: spinner keeps its tint, dot colours unchanged, priority order unchanged.
  • Child sessions are automatically covered for the status slot because SessionRow is the same component for every session regardless of level. The Pin slot remains top-level only by design — SessionItem gates the Pin leadingSlot on !props.level at sidebar-items.tsx:231, and this issue does not change that. Child rows therefore still have a narrower leading offset than top-level rows; that is intentional, not a regression.

Commit 2 — refactor(app): redraw sidebar Pin/Filter icons on the 20×20 grid

  • packages/app/src/pages/layout/pawwork-sidebar.tsx:20-27 (FilterIcon) and :29-41 (PinIcon) — change viewBox to "0 0 20 20" and rewrite the path coordinates to the 20-unit grid (changing only the viewBox attribute would render the glyph at ~60% of its visual size). Drop the explicit stroke-width. Pick stroke-linecap per glyph to match the visual weight of the neighbouring icons it sits beside (new-session, dot-grid, archive); upstream icon.tsx uses both square and round on different glyphs, so there is no single "correct" value to paste.
  • The icons stay file-local in pawwork-sidebar.tsx. Do not add them to packages/ui/src/components/icon.tsx, which follows upstream.

Relationship to #77

Not in scope

  • Pin slot. Already size-6 unconditional, no change needed.
  • Pin button a11y contract (tabIndex={isPinned ? 0 : -1} + aria-hidden={isPinned ? undefined : "true"}, hover-to-discover on unpinned rows) — unchanged.
  • Status slot priority switch internals — unchanged.
  • Accent-brand colour on pinned / filter-active states — unchanged (see [Feature] UI polish batch: cohesion, affordance, hierarchy #77 P1-5).
  • Top running progress bar — unchanged.
  • Sort button interaction — redraw only.
  • Promoting Pin / Filter into packages/ui/src/components/icon.tsx.

Manual UI verification (run bun dev:desktop)

After Commit 1:

  1. Open a project with a mix of idle, running, pinned, and unpinned sessions. Scroll through and hover each row. Titles do not shift horizontally — every row at the same nesting level starts at the same x-position regardless of status slot activity.
  2. Start a session. Spinner appears in the status slot (in agent tint); title does not move.
  3. A session reporting a permission request, an error, or unseen messages shows the yellow / red / blue dot respectively in the status slot; title does not move.
  4. Expand a parent session that has child sessions. Child rows obey the same stable baseline; their status indicators (when any) appear in the reserved slot.

After Commit 2:
5. Place the sidebar next to rows that use the new-session glyph and the dot-grid action menu. Pin and Filter icons read as the same visual weight — no bolder stroke, no tighter curves.
6. Pinned, no hover → filled pin in text-accent-brand on a 20×20 grid.
7. Unpinned, hover → outline pin in text-text-weak on the 20×20 grid.
8. Sort button active (sort=project) → filter icon in text-accent-brand on the 20×20 grid.
9. Keyboard-only: Tab through a pinned session row → focus lands on pin button, Enter toggles unpin. Tab through an unpinned row → pin button is not in the tab order (unchanged behaviour, just re-verify the redraw did not regress the contract).

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Medium priorityenhancementNew feature or requestuiDesign system and user interface

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions