Skip to content

feat(ui,app): sidebar L35 row rewrite (slice 09, issue #440)#495

Merged
Astro-Han merged 49 commits intodevfrom
claude/slice-09-sidebar
May 8, 2026
Merged

feat(ui,app): sidebar L35 row rewrite (slice 09, issue #440)#495
Astro-Han merged 49 commits intodevfrom
claude/slice-09-sidebar

Conversation

@Astro-Han
Copy link
Copy Markdown
Owner

@Astro-Han Astro-Han commented May 7, 2026

Summary

Slice 09 of the #440 visual rewrite. Closes #303.

  • Row outer shell rebuilt to L35: 32px tall, 10px horizontal padding, radius-sm, flex-centered (no leading-[1.4]). Active title now picks up font-medium.
  • Right-slot status reduced to the 4-state model: asking (comment icon, brand orange) → busy (spinner) → error (circle-x, red) → time (sans 12, fg-weaker). Old 6px colored dots and the pinned-icon state are gone — pin status is signaled by the row's presence in the Pinned section.
  • Action overlay three-dot iconbtn scoped to --radius-md and --row-active-overlay (0.06) hover so it reads as its own target on top of the 0.04 row hover. The IconButton base contract stays at --radius-sm + --hover-overlay; the slice 09 override is scoped to [data-component="pawwork-session-row"] [data-action="session-row-menu"] only. Status fades to opacity 0 (no display:none).
  • 4-item menu: Pin / Rename / Export… / (sep) / Delete, each with a leading icon. Keyboard shortcut affordances (the / / hint glyphs and a real <kbd> element) are deferred to slice 14 (L46) — slice 09 ships the icon-leading layout only.
  • Sort: replaced the icon-only filter toggle with a Kobalte DropdownMenu trigger (still icon-only IconButton icon="sort" to fit the 32px row beside the All label) offering 按时间 / 按项目 (By time / By project), with active-mode check icon and data-mode for testability.
  • Layout shape: existing segments now carry pawwork-side-top / pawwork-side-scroll / pawwork-side-foot markers so L37's shell shape is testable. The traffic / collapse-control segment (pawwork-side-traffic) is intentionally deferred to slice 17 — the OS chrome already renders the macOS traffic-lights today, and adding a 32px placeholder now would double the empty space at the top. Slice 17 hides the OS chrome and moves the controls into the sidebar; that's when the placeholder lands.
  • Footer: settings button keeps its TooltipKeybind wrapper for the keybind affordance; the visible row matches the L35 nav cluster (32 high, 10 horizontal padding) alongside the other two sidebar nav buttons.
  • Removed dead ProjectIcon export from sidebar-items.tsx (no callers).
  • Sidebar geometry uses literal pixels (h-[32px], size-[20px], size-[16px]) because PawWork's 13px root font size scales Tailwind rem-based sizing — h-8 would render at 26px, not 32.
  • Project group collapse (fix(scripts): point dev:desktop at packages/desktop-electron #50): project headers in the project sort mode are now toggle buttons with a folder icon and a hover-revealed chevron. Collapsed state animates via grid-template-rows 0fr↔1fr (motion-reduce friendly), is keyed by project label, and persists across reloads via pawworkProjectCollapsed on layout-page-store. Items stay mounted while collapsed (focus / scroll preserved) and inert removes them from the tab order.
  • Settings overlay close-on-nav: clicking a sidebar session (or "new session") while the settings overlay is open closes the overlay before navigating, wired through closeSettingsSurface in createShellNavigation. The global setNavigate hook is also wrapped so notification clicks and other non-shell deep-link routes close the overlay before routing.
  • Spinner: the shared Spinner now accepts an optional aria-label prop (default "Loading") so localized callers can pass language.t("common.loading"); the keyframe is gated behind prefers-reduced-motion: reduce.
  • Command palette redesign (search header geometry, fixed body height, item rules) — full content / feature redesign tracked as follow-up Redesign command palette content & default view #501.
  • Project rename / delete actions tracked as follow-up Split project rename / delete into its own PR #502 (out of slice 09 scope).

Boundary check (no out-of-scope paths)

Touched only:

  • packages/app/src/pages/layout/{sidebar-items.tsx, pawwork-sidebar.tsx, sidebar.css, layout.tsx, layout-page-store.ts, shell-navigation.ts, session-menu-actions.{ts,test.ts}, sidebar-status-kind.{ts,test.ts}}
  • packages/app/src/i18n/{zh.ts, en.ts} (sort + settings keys; app.name.desktop shortened to PawWork)
  • packages/app/src/components/{prompt-input.tsx, prompt-input/workspace-chip.tsx, dialog-select-file.tsx} (font-weight unification + magnifying-glass restoration on the file dialog)
  • packages/ui/src/components/{command-palette.css, menu.css, icon-button.css, spinner.{tsx,css}} (palette geometry, unified context+dropdown menu styling, row-overlay scoping, shared spinner)
  • packages/app/e2e/sidebar/{sidebar-slice-09.spec.ts (new), sidebar-project-collapse.spec.ts (new), sidebar-session-organization.spec.ts (selector), sidebar-leading-slot.spec.ts (drop obsolete pin-button assertion)}
  • packages/app/e2e/settings/settings-close-on-nav.spec.ts (new)

No changes to sidebar-workspace.tsx, titlebar, composer, message-flow, settings page content, or packages/opencode.

Departures

  • Sub-session indentation kept at padding-left = 10 + level * 16. The L35 anatomy locks the flat row; nested sessions need visual hierarchy. Annotated inline in sidebar-items.tsx.
  • Right-slot error state is wired to the existing notification.session.unseenHasError signal (unread tool-output errors). Without a session-level error field in the data layer today, unseen-error is the available proxy.
  • Row left padding base 8 → 10 to match L35 row padding 0 10 (was 8 + level*16, now 10 + level*16).
  • NewSessionItem leading icon goes 14 → 16 (the standard chrome size after slice 08). The previous 14px override in sidebar.css was a deliberate departure from slice 08 with no L35/L37 backing — dropped.
  • Sort trigger stays icon-only. L35 row geometry only fits a 20px icon button beside the "All" label; a text + chevron trigger would force the All header to two lines or push the action out of the row. The DropdownMenu still exposes the two options with check + data-mode, and the pawwork-sort-trigger selector is stable for E2E.
  • pawwork-side-traffic placeholder deferred to slice 17. The OS already paints macOS traffic-lights on the window chrome; reserving a 32px band on top of that would render 64px of empty space at the sidebar head. Slice 17 hides the OS chrome and moves the cluster into the sidebar — that's when the placeholder lands without doubling the void. The deferral is annotated inline in pawwork-sidebar.tsx.
  • Menu shortcut hints deferred to slice 14. An earlier draft of this slice rendered placeholder span hints next to each menu label; that was rolled back in favor of leading icons (commit bf2f1d6). Real <kbd> and the canonical hint affordance ship with slice 14 (L46).
  • Settings footer keeps TooltipKeybind. The inline ⌘ glyph belongs with the broader keybind hint affordance in slice 14; until then, the tooltip surface remains the source of truth for the shortcut.

Test plan

  • bun turbo typecheck --filter=@opencode-ai/app --filter=@opencode-ai/ui (5/5 packages)
  • bun test --preload ./happydom.ts src/pages/layout (101 pass, 0 fail)
  • bun test:e2e -- sidebar/sidebar-slice-09.spec.ts (3/3 pass)
  • bun test:e2e -- sidebar/sidebar-project-collapse.spec.ts settings/settings-close-on-nav.spec.ts (3/3 pass)
  • bun test:e2e -- sidebar/sidebar-session-organization.spec.ts sidebar/sidebar-leading-slot.spec.ts (3/3 pass)
  • Manual bun run dev:desktop Electron walkthrough — verified sidebar rows (hover/active/focus), three-dot menu and right-click menu parity, Pin/Rename/Export/Delete, sort popover toggle, status states (asking/busy/error/time), project collapse persistence across reload, settings close-on-nav for sidebar sessions, command palette geometry, light + dark themes.

Crosscheck

Plan was reviewed by Claude (opus) + Codex prior to implementation. P0 candidates were investigated and dropped (comment icon exists; STANDARDS.md is a real local-only spec source). All P1/P2 findings were folded back into the plan and applied in commits.

Summary by CodeRabbit

  • New Features

    • Added sidebar sort functionality with time and project options
    • Added project group collapse with persisted state across reloads
    • Added keyboard shortcut indicators in UI elements
  • UI/UX Improvements

    • Standardized component heights across the interface (28px → 32px)
    • Redesigned spinner animation using CSS-based approach
    • Enhanced dropdown menu styling and hover behavior (unified with right-click menu)
    • Added icons to session menu actions
    • Settings overlay now closes when navigating away via the sidebar
  • Localization

    • Added English translations for release notes UI and sorting controls
    • Added Chinese translations for sidebar sorting features

Astro-Han added 8 commits May 7, 2026 19:55
Drop the dead ProjectIcon export (no callers) and rewrite the row outer
shell + right-slot status system to the L35 lock:

- Row: fixed h-8, padding 0 10, radius-sm, flex-centered (no leading-[1.4])
- Active title: text-fg-strong + font-medium
- Status: 4 states asking | busy | error | time, exclusive, opacity-faded
  on hover/focus/menu-open (never display:none)
- Drop the unseen / pinned-icon visual states; pin signal is now carried
  by the Pinned section position, unread signal is dropped (L35 lock has
  no slot for it)
- Drop SessionItemProps.pinned and SessionItemProps.dense; menu state
  reads pinnedIDs() directly in pawwork-sidebar
- Sub-session indentation kept at level*16 as a deliberate departure
…ayout

- Replace icon-only sort toggle with text+chev DropdownMenu (排序 / Sort) exposing two options 按时间 / 按项目 (By time / By project) per L35.
- Add data-component "pawwork-side-traffic" 32-high placeholder per L37 so slice 17 can drop in macOS traffic-lights and the collapse button without reflowing geometry.
- Add data-component "pawwork-side-top|side-scroll|side-foot" markers on existing segments so the L37 four-segment shape is testable.
- Inline ⌘, hint in footer (drops TooltipKeybind wrapper); kbd container styling lands in slice 14.
- Render Rename ↵ / Delete ⌫ shortcut hints in dropdown + context menus.
…deeper)

Slice 08 collapsed Icon size=small to 16px globally, so the leading-slot
14px override is no longer load-bearing — drop it and let NewSessionItem
render at the standard chrome size.

Add a row-scoped rule that bumps the action-overlay iconbtn radius to md
and its hover to --row-active-overlay (0.06 alpha, one step deeper than
the row's 0.04) so it reads as its own target on a hovered row.
Add sidebar-slice-09.spec.ts covering:
- 4-state right-slot status + 4-item menu with Rename ↵ / Delete ⌫ hints
- Sort trigger as text+chev popover with two options (按时间/按项目)
- L37 four-segment shape (side-traffic 32 above side-top, side-foot)

Patch sidebar-session-organization.spec.ts to use the new
pawwork-sort-trigger selector and pick the project option from
the popover instead of toggling the old ghost icon button.
PawWork sets root font size to 13px (dense desktop), so Tailwind h-N
sizing scales rem-based and h-8 ends up 26px instead of L35's 32. Pin
the row outer (h-32), status slot (20×20), Spinner (16×16), and
side-traffic placeholder (h-32) to literal pixels.

Drop the now-stale pin-button-in-leading-slot assertion in
sidebar-leading-slot.spec.ts: per L35 the row no longer renders a pin
glyph, so pin status is signaled by the row's presence in the Pinned
section (still asserted).
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 7, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

This PR consolidates sidebar session indicators into a four-state status model (asking/busy/error/time), adds icon fields to session menu actions, standardizes UI component heights to 32px, restructures the sidebar into three layout sections with keybind-aware controls, refactors the spinner to CSS animation, and adds supporting tests and translations.

Changes

Sidebar Status Model, Menu Icons, and Layout Restructuring

Layer / File(s) Summary
Type Contracts and Utility Functions
packages/app/src/pages/layout/sidebar-status-kind.ts, packages/ui/src/components/icon.tsx, packages/app/src/pages/layout/session-menu-actions.ts
New SidebarStatusKind type (asking|busy|error|time) and SidebarStatusInput interface establish status contract. sidebarStatusKind() function maps three boolean flags to priority-ordered status. IconName type exported from icon component. SessionMenuAction interface adds icon: IconName field.
Session Menu Actions Icon Population
packages/app/src/pages/layout/session-menu-actions.ts, packages/app/src/pages/layout/session-menu-actions.test.ts
buildSessionMenuActions assigns icons: pin, pencil-line, download, trash. Tests verify icon assignments in correct order.
UI Component Sizing Standardization
packages/ui/src/components/button.css, packages/ui/src/components/picker.css, packages/ui/src/components/text-field.css, packages/ui/src/components/line-comment-styles.ts, packages/ui/src/components/icon-button.css
Button, picker trigger, text-field, line-comment action components updated to 32px height/min-height. Text-field textarea padding increased to 8px vertical.
Spinner Component and CSS Animation
packages/ui/src/components/spinner.tsx, packages/ui/src/components/spinner.css
Spinner refactored from SVG multi-rect to minimal <div> with role="status". CSS adds circular border animation via pw-spin keyframes (360° over 1200ms infinite).
Dropdown and Icon Button Styling
packages/ui/src/components/dropdown-menu.css, packages/ui/src/components/icon-button.css
Dropdown menu items enforce 32px min-height with 0 8px padding. Highlighted item uses --row-hover-overlay token. Icon-button hover and selected states use --row-active-overlay token.
Component Styling Tests
packages/ui/test/icon-button-states.test.ts
Icon-button test verifies hover and selected state use --row-active-overlay token.
Session Item Status Computation
packages/app/src/pages/layout/sidebar-items.tsx
SessionItem derives isAsking memo from permission auto-response and session permission/question requests. statusKind memo produces four-state status. statusContent() renders icon or time text per state.
Session Row Rendering and Overlays
packages/app/src/pages/layout/sidebar-items.tsx
SessionRow redesigned with focus/hover styling. Row container uses inline padding (10 + level×16px). Right-side split into two overlays: default status (20px, fades) and action overlay (absolute, appears on hover/focus).
Sidebar Structure and Keybinds
packages/app/src/pages/layout/pawwork-sidebar.tsx, packages/app/src/pages/layout.tsx
PawworkSidebar props add newSessionKeybind and searchKeybind accessors. Layout restructured into three sections: pawwork-side-top (TooltipKeybind controls), pawwork-side-scroll (pinned/all sessions, sort dropdown), pawwork-side-foot (settings). Sort mode replaced with dropdown selecting time or project.
Sidebar Menu and Title Styling
packages/app/src/pages/layout/pawwork-sidebar.tsx, packages/app/src/pages/layout/sidebar-items.tsx
Menu actions render Icon with text-icon-weak class. Session title updated with active-state styling. Menu logic refactored to direct pin check.
Sidebar CSS Transitions
packages/app/src/pages/layout/sidebar.css
Session row background-color transitions use --duration-fast. Status element opacity transitions use --duration-base.
Tests and E2E Validation
packages/app/src/pages/layout/sidebar-status-kind.test.ts, packages/app/e2e/sidebar/sidebar-leading-slot.spec.ts, packages/app/e2e/sidebar/sidebar-session-organization.spec.ts, packages/app/e2e/sidebar/sidebar-slice-09.spec.ts
Unit tests verify status precedence logic. E2E tests remove pin button geometry checks, update sort interaction, verify menu icons visible and layout containers stack correctly (top/scroll/foot).
Internationalization
packages/app/src/i18n/en.ts, packages/app/src/i18n/zh.ts
English and Chinese dictionaries add release notes UI entries and sidebar sort labels (sort, "By time", "By project").

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related issues

  • Astro-Han/pawwork#303: This PR implements the shape-differentiated session status indicators (asking/busy/error/time with icons) requested in the issue, replacing the color-only dot pattern with distinct icon/glyph rendering per state.

Possibly related PRs

  • Astro-Han/pawwork#183: Both PRs refactor the four-state sidebar session status model in sidebar-items.tsx and add related tests/structure.
  • Astro-Han/pawwork#359: Both PRs modify packages/ui/src/components/icon.tsx—this PR exports IconName type, while the retrieved PR changes the icons registry keys that affect that type.
  • Astro-Han/pawwork#461: Both PRs modify sidebar implementation and related e2e specs (pawwork-sidebar.tsx, sidebar-session-organization.spec.ts).

Suggested labels

enhancement, P2, app, ui

Poem

🐰 Four states dance in gentle light,
From spinner's spin to time's soft sight,
Icons bloom where colors once held sway,
A sidebar springs in fresh new way,
With keybinds singing all the day! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR addresses #303 by replacing 6px color-only dots with shape-differentiated icons: spinner (busy), comment icon (asking), circle-x (error), and time display (idle), fully meeting the accessibility and clarity objectives.
Out of Scope Changes check ✅ Passed All changes are scoped to sidebar layout, i18n keys, e2e tests, and CSS primitives as documented. No unrelated refactors, dependencies, or out-of-scope path modifications are present.
Title check ✅ Passed The title accurately summarizes the main change: a complete sidebar L35 row rewrite for slice 09, addressing issue #440.
Description check ✅ Passed The description is comprehensive and well-structured, covering summary, rationale, boundary checks, departures, test plans, and crosschecks. All major template sections are present and filled with substantive detail.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/slice-09-sidebar

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the sidebar geometry and session row status logic to align with updated design specifications. Key changes include removing the pin glyph from individual rows, replacing the sort toggle with a dropdown menu, and introducing a centralized 4-state status indicator (asking, busy, error, or time). Additionally, keyboard shortcut hints have been added to session actions and the settings button, and several components were updated to use fixed pixel heights for consistent layout. Review feedback suggests converting remaining rem-based heights to fixed pixel values and aligning the settings button styling with the new 32px row specification.

Comment thread packages/app/src/pages/layout/pawwork-sidebar.tsx Outdated
Comment thread packages/app/src/pages/layout/pawwork-sidebar.tsx Outdated
Astro-Han added 13 commits May 7, 2026 20:46
Slice 08 (#440, 6889e01) collapsed the Icon multi-size API to a fixed
16x16 contract; `size` is no longer in IconProps. Strip the leftover
`size="small"` from 7 sidebar callsites that survived our pre-merge
state. Functionally a no-op since the rendered size was already 16x16,
but the prop is now a TS error.
The L37 spec reserved a 32px top segment so slice 17 (which moves the
macOS traffic-lights into the sidebar) wouldn't reflow geometry. While
the OS still paints traffic-lights on its window chrome, the placeholder
just doubles the empty space at the top. Drop it; slice 17 will reintroduce
the segment when it actually fills it.

Updates the four-segment e2e to the three-segment shape it reflects.
The slice 09 footer button rendered an inline `⌘,` next to its label,
while the new-session and search buttons above it had no shortcut hint
at all. Three buttons in the same column with three different shortcut
treatments looks unintentional. Wrap all three with TooltipKeybind so
the keybind reveals on hover with the same affordance everywhere; thread
`newSessionKeybind` and `searchKeybind` props from layout.tsx, mirroring
`settingsKeybind`. Pure visual consolidation; behavior unchanged.
The right-slot status box was forced to a 20×20 square (`size-[20px]`),
which fits icons (16px) but truncated time text like "刚刚" into a
vertical character stack on narrow sidebars. Switch the inner box to
`h-full min-w-[20px]` so it stays icon-sized when the slot holds an
Icon/Spinner but flexes horizontally for text content. Add
`whitespace-nowrap` on the time `<span>` as belt + suspenders.
The new-session, search, and settings buttons rendered at ~30px (`py-1.5`
+ content), while the L35 session row was 32px. The 2-4px gap looked
unintentional in a single sidebar column.

Treat the whole sidebar as one nav system: lock all three top/foot
buttons + the session row to 32px, matching the L30/L31 list-row family.
This is a deliberate sidebar-scoped deviation from L24 buttons 28 (which
still applies to dialog/composer/form Buttons elsewhere — those never
sit next to list rows so the visual conflict doesn't arise). Same shape
Codex.app uses with its single `--height-token-nav-row` token.
L35 locks row hover/active transition at `var(--duration-fast)` (80ms)
ease-out. The Tailwind `transition-colors` shorthand defaulted to 150ms
cubic-bezier — close, not exact. Add a CSS rule scoped to the row so
hover/active state changes feel as snappy as the spec asks.
L35 specifies section labels ("已置顶", "全部会话", project group headers)
and the sort trigger as `--type-caption` (= 400 13px/130%, 500 weight on
the trigger). The implementation used `text-12-regular` / `text-12-medium`
(12px), shaving 1px off the font and breaking visual rhythm against the
13px row title.

Lock all four section headers to `text-13-regular` and the sort trigger
to `text-13-medium`, plus pin the trigger height to a literal `h-[24px]`
to avoid drift if base font-size ever shifts.
The section header bands ("已置顶", "全部会话" + sort trigger, project
group titles in project-mode) used `mt-3 + pt-3 + pb-2` ad-hoc spacing
that summed to ~44px — out of step with the 32px nav-row rhythm we just
unified across the rest of the sidebar.

Lock all three to `h-[32px] flex items-center px-2` so every visual unit
in the sidebar lives on the same 32-grid. Section headers still read as
non-clickable separators (no hover background, fg-weak text). The sort
trigger remains h24 nested inside, centered with breathing room above
and below.
Locking section headers to 32 left them visually equidistant from the
section above and the rows inside, which made the eye read them as
"another row" rather than a separator. Apply the affinity-spacing rule
(Refactoring UI):

- mt-4 (16px) above each section header — clear gap from the previous
  group, signaling "new section"
- 0 below — header sits tight against its own rows so the eye groups
  them as one unit (Gestalt proximity: header belongs to what's below)
- first:mt-0 (or index() === 0 inside <For>) so the first visible
  section doesn't get a stray top gap

Drops nav's `gap-1` since headers now own their breathing room
explicitly. Inner section gap-0.5 (2px between sibling rows) preserved.
Dropping `first:mt-0` revealed why first-child guarding was wrong here:
the user's view had no pinned section, so "全部会话" became the first
nav child, took mt-0, and stuck to the search button above with no
section break.

The side-top button cluster and the first scroll section ARE different
groups — affinity spacing demands the same 16px gap there as between
any two sidebar sections. Always apply mt-4 to top-level section header
containers (pinned, "全部会话"). Project group sections inside the For
keep `index() === 0` guarding because the first group should attach to
its containing "全部会话" header (header-to-content tight).
The 1px `border-t` between scroll content and the settings footer was
doing the same job as a section break — but with a hard line instead of
breathing room. Now that affinity spacing carries every other section
boundary in the sidebar, the divider is redundant noise.

Replace `border-t border-border-weaker px-3 py-2` with `px-3 pt-4 pb-3`
(16 above, 12 below — same 16px rhythm as section headers, 12 mirrors
side-top's pt-3 for top/bottom symmetry to the sidebar edges).
Lift Button, TextField (normal variant), Picker trigger, and line-comment action default heights from 28 to 32 so primitive defaults align with the L35 sidebar rhythm (rows / pinned / settings all at 32) and Picker dropdown items (already at 32). This removes the trigger-to-item height jump and unifies the read / select / act surfaces.

No size API added. The remaining 28 hardcoded usages are all on surfaces scheduled for removal or rework: dialog-select-model and dialog-manage-models move to settings Models pane per design preview, right-side panel tabs and session-new-view skill chips are pending separate redesigns.
@Astro-Han Astro-Han force-pushed the claude/slice-09-sidebar branch from 213d8d4 to 397a678 Compare May 7, 2026 14:43
Astro-Han added 3 commits May 7, 2026 23:05
The L35 right-slot asking state previously only tracked PermissionRequest, which left an agent ask() pause showing as busy in the sidebar while the main region correctly surfaced a question. Mirror the use-session-blockers OR set (permission || question-blocker || question) so the sidebar matches the main region semantics.

Renames hasPermissions to isAsking to make the intent legible at the call site.
The legacy 16-square pulse animation was an opencode-era brand mark and conflicts with the PawWork loading affordance shown in design preview (docs/design/ui_kits/desktop/styles.css L1407). Replace with a rotating ring built on a single div + border-right transparent + 700ms linear rotate.

Keeps the component contract (class / classList / style passthrough, currentColor, 18px default), so all four call sites (sidebar row busy state, sidebar workspace, dialog-connect-provider, message-timeline) pick up the new look without changes.
Three drifts from the just-replaced ring against STANDARDS.md L60/L65/L128:

- Keyframe was spinner-rotate; the L65 lock names pw-spin as the canonical 700ms rotation reused by spinner and the thinking ring.
- Border was 1.4px (carried from the design preview todo spinner at 7px); L60 calls for 1.5px equivalent at 16.
- Sidebar busy slot rendered the spinner at 16; the keyshape circle is Ø18 inside the 20x20 status box per L60/L128.
@Astro-Han Astro-Han force-pushed the claude/slice-09-sidebar branch 3 times, most recently from 8539005 to 91c511f Compare May 7, 2026 15:15
Astro-Han added 9 commits May 7, 2026 23:28
Dropdown items were rendering at ~26px (padding 4 + line-height-large), out of step with the L30 menu / L35 sidebar / picker item 32 row family. Pin min-height to 32 with horizontal padding 0 8, and swap the highlighted background from --surface-raised (solid panel tone) to --row-hover-overlay so menu / picker / list-row hovers all share the same overlay token.
The Rename ↵ / Delete ⌫ hint string was a placeholder — no command.keybind ever wired session.rename or session.delete, so the suffix advertised shortcuts that did nothing. Drop the shortcut field entirely (do not lie to users) and add a leading icon per row instead (pin / pencil-line / download / trash).

Exports IconName from @opencode-ai/ui/icon so the action type can constrain icon to the registry instead of a free-form string.

Updates the unit test to assert the icon registry order, and the e2e spec to assert leading icon-svg slots on each menuitem.
Sort menu was the only place left with naked text labels; align it with the session menu icon convention. Use schedule for By time and folder for By project as leading slots, and swap the active-state ✓ glyph for the check icon so the row carries icons in both leading and trailing slots.
Replace the text + chevron-down trigger with a 24x24 ghost IconButton wrapped in a Tooltip; brings the sort affordance under the L23 secondary-control standard so it sits inside the 32 section header with 4px breathing room above and below, matching the row menu IconButton elsewhere in the sidebar.

Uses sliders as a placeholder icon (chevron-grabber-vertical reads as drag-handle, sliders is the closest fit in the current registry). A dedicated sort icon is queued in pawwork-chrome-icon-imagegen-v4-slice08 Sheet 12; swap once that lands.
Trace cell #1 of imagegen Sheet 12 (decreasing list bars + leading
down arrow, sort-desc semantics). Rescaled to landscape keyshape 18×14
inside the 20×20 viewBox (1-unit margin preserved). Replaces the
sliders placeholder on the sidebar sort trigger.
Two visual mismatches between the sidebar sort trigger and the
session-row dot-grid trigger:

- Hover affordance: row-menu uses radius-md + --row-active-overlay
  while the sort trigger fell back to the IconButton default
  radius-sm + --hover-overlay, reading visibly faded next to it.
  Share the row-menu rules.
- Sort glyph: previous transform was height-bound at 14 inside the
  20×20 viewBox, leaving the width at 17.43 and the icon looking
  smaller than dot-grid (which spans ~16). Re-scale width-bound to
  18 so both axes use the live area; height becomes 14.46, still
  inside the 18×18 live area.
Previous commit pulled the sort trigger onto the row-menu hover
treatment (radius-md + 6% black) and stretched the sort glyph
width-bound past the landscape 14-unit height ceiling. Both moves
violate the standards:

- L23 says IconButton ghost is one variant: radius-sm + 4% black.
  L35 explicitly carves out the row action-overlay (radius-md + 6%)
  with a written reason — the row already has a 0.04 hover layer to
  sit on top of. The sort trigger lives outside the row, so the L23
  default is the correct fit.
- L21 keyshapes are bounding boxes, not minima. Landscape is 18×14.
  The traced glyph aspect (1.245) is slightly squarer than 18×14
  (1.286), so the height-bound scale (17.43×14) is what fits inside
  the keyshape; width-bound (18×14.46) overflows it.

The cosmetic delta versus dot-grid is by design: row-menu and
sort/footer iconbuttons are two different control classes.
Two control classes (default 4% / radius-sm vs sidebar action-overlay
6% / radius-md) collapse into one. Visual differences inside the same
sidebar (sort trigger vs row-menu) violated the elegance rule, and the
written L35 carve-out shifted the judgment call onto every future
contributor adding an IconButton.

Changes:
- icon-button.css: hover token --hover-overlay -> --row-active-overlay
  (4% -> 6%), radius --radius-sm -> --radius-md
- sidebar.css: drop the row-menu override block; the new default
  matches it
- pawwork-sidebar.tsx: drop the now-redundant class="rounded-md" on
  the row-menu trigger
- icon-button-states.test.ts: track the token rename

Token reuse: --row-active-overlay (named for list rows) is shared by
IconButton hover at the same 6% tier — avoids minting a synonym token
like --control-hover-overlay. Button (secondary/danger) hover stays on
--hover-overlay (4%) because dialog/composer/form contexts read fine
at 4% and a heavier overlay would compete with the surrounding
content.

STANDARDS.md L23 + L35 updated locally (untracked spec doc) to reflect
the unified rule.
Session row pads 10px left/right per L35, but every adjacent surface
fell back to a slightly tighter rhythm:

- pinned label / sort header / project group header: px-2 (8px)
- show-more / search-history rows: px-2 (8px)
- new-session / search / settings buttons: pl-2 pr-3 (8/12)

That left a 2px stair-step between section headers and the rows they
introduce, and made the sort trigger sit 2px inboard of the row-menu
trigger directly below it. Snap every container to px-2.5 (10px) so
the entire sidebar shares one inner edge.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (3)
packages/app/src/pages/layout/session-menu-actions.test.ts (1)

30-30: 💤 Low value

LGTM — icon order assertion locks down the 4-state model.

The icons for the exportAvailable: false case (["pin", "pencil-line", "trash"]) are not asserted in the second test; consider adding that assertion there too for full regression coverage.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/app/src/pages/layout/session-menu-actions.test.ts` at line 30, The
second test in session-menu-actions.test.ts doesn't assert the icons when
exportAvailable is false; update that test (the one that sets exportAvailable:
false / exercises the actions variable) to include an assertion that
actions.map(action => action.icon) equals ["pin", "pencil-line", "trash"] so the
3-state icon order is locked down for regression coverage.
packages/ui/test/icon-button-states.test.ts (1)

64-65: ⚡ Quick win

Scope this assertion to the hover rule to avoid false positives.

Right now the test only checks token presence anywhere in CSS, so it can pass even if hover mapping regresses.

Proposed test hardening
-  test("hover applies --row-active-overlay token", () => {
-    expect(CSS).toContain("var(--row-active-overlay)")
-  })
+  test("hover applies --row-active-overlay token", () => {
+    expect(CSS).toMatch(
+      /&:hover:not\(:disabled\)\s*\{[^}]*background-color:\s*var\(--row-active-overlay\)/
+    )
+  })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ui/test/icon-button-states.test.ts` around lines 64 - 65, The test
"hover applies --row-active-overlay token" currently only checks for the token
anywhere in CSS; update it to assert the token appears specifically inside the
hover rule so it fails on regressions. In the test in
icon-button-states.test.ts, change the expectation to look for a hover selector
combined with the token (e.g., match a substring or regex that includes a :hover
(or the component hover selector used in the styles) followed by
var(--row-active-overlay)) so the assertion ensures the hover rule itself
contains the token rather than the token existing elsewhere.
packages/ui/src/components/spinner.css (1)

10-10: ⚡ Quick win

Add a reduced-motion fallback for spinner animation.

The new infinite rotation should respect prefers-reduced-motion.

Proposed accessibility-safe tweak
 [data-component="spinner"] {
@@
   animation: pw-spin 1200ms linear infinite;
 }
+
+@media (prefers-reduced-motion: reduce) {
+  [data-component="spinner"] {
+    animation: none;
+  }
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ui/src/components/spinner.css` at line 10, The spinner's infinite
rotation declared by the animation property using "pw-spin" should respect the
user's prefers-reduced-motion setting; update the spinner.css rule that sets
animation: pw-spin 1200ms linear infinite to include a prefers-reduced-motion
media query that disables or replaces the animation (e.g., set animation: none
or a non-animated fallback and ensure visibility/placement is preserved) so
users who request reduced motion won't see the continuous rotation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/app/e2e/sidebar/sidebar-slice-09.spec.ts`:
- Line 23: Replace the brittle boolean assertion that checks count (the variable
named count in sidebar-slice-09.spec.ts) with a containment assertion so
failures show the actual value; specifically, change the line using expect(count
=== 3 || count === 4).toBe(true) to an assertion that expects the allowed values
array to contain count (e.g., expect([3, 4]).toContain(count)) so test output
reports the actual count on failure.
- Around line 82-86: Split the combined null-guard + ordering assertions into
explicit null checks followed by value comparisons: first assert that topBox,
scrollBox, and footBox (results of boundingBox() calls) are not null (e.g.,
expect(topBox).not.toBeNull()), then dereference their .y values and assert
ordering with comparisons (e.g., expect(topBox.y).toBeLessThan(scrollBox.y) and
expect(scrollBox.y).toBeLessThan(footBox.y)). Update the test around the
boundingBox() results (topBox, scrollBox, footBox) to perform these two-step
checks so failures indicate which element was null versus a positional ordering
failure.

In `@packages/ui/src/components/icon-button.css`:
- Line 8: Revert the global border-radius change in
packages/ui/src/components/icon-button.css (remove the line setting
border-radius: var(--radius-md) on the base icon-button) and instead apply the
radius only to the sidebar row menu trigger by adding a scoped rule that targets
the selector with data-component="pawwork-session-row" and
data-action="session-row-menu" applied to the icon-button (i.e.,
[data-component="pawwork-session-row"]
[data-action="session-row-menu"][data-component="icon-button"]), setting its
border-radius to var(--radius-md).

In `@packages/ui/src/components/spinner.tsx`:
- Around line 3-12: The Spinner component currently hardcodes
aria-label="Loading"; add an optional ariaLabel prop to Spinner's props
(ariaLabel?: string) with a default value "Loading", and use that prop for the
div's aria-label instead of the literal so callers (e.g., sidebar-items.tsx) can
pass language.t(...) translated strings while preserving existing behavior when
no prop is supplied.

---

Nitpick comments:
In `@packages/app/src/pages/layout/session-menu-actions.test.ts`:
- Line 30: The second test in session-menu-actions.test.ts doesn't assert the
icons when exportAvailable is false; update that test (the one that sets
exportAvailable: false / exercises the actions variable) to include an assertion
that actions.map(action => action.icon) equals ["pin", "pencil-line", "trash"]
so the 3-state icon order is locked down for regression coverage.

In `@packages/ui/src/components/spinner.css`:
- Line 10: The spinner's infinite rotation declared by the animation property
using "pw-spin" should respect the user's prefers-reduced-motion setting; update
the spinner.css rule that sets animation: pw-spin 1200ms linear infinite to
include a prefers-reduced-motion media query that disables or replaces the
animation (e.g., set animation: none or a non-animated fallback and ensure
visibility/placement is preserved) so users who request reduced motion won't see
the continuous rotation.

In `@packages/ui/test/icon-button-states.test.ts`:
- Around line 64-65: The test "hover applies --row-active-overlay token"
currently only checks for the token anywhere in CSS; update it to assert the
token appears specifically inside the hover rule so it fails on regressions. In
the test in icon-button-states.test.ts, change the expectation to look for a
hover selector combined with the token (e.g., match a substring or regex that
includes a :hover (or the component hover selector used in the styles) followed
by var(--row-active-overlay)) so the assertion ensures the hover rule itself
contains the token rather than the token existing elsewhere.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 8615c657-b734-4622-9175-6b8fc80fd6e8

📥 Commits

Reviewing files that changed from the base of the PR and between 467f98d and b6e2e1a.

📒 Files selected for processing (23)
  • packages/app/e2e/sidebar/sidebar-leading-slot.spec.ts
  • packages/app/e2e/sidebar/sidebar-session-organization.spec.ts
  • packages/app/e2e/sidebar/sidebar-slice-09.spec.ts
  • packages/app/src/i18n/en.ts
  • packages/app/src/i18n/zh.ts
  • packages/app/src/pages/layout.tsx
  • packages/app/src/pages/layout/pawwork-sidebar.tsx
  • packages/app/src/pages/layout/session-menu-actions.test.ts
  • packages/app/src/pages/layout/session-menu-actions.ts
  • packages/app/src/pages/layout/sidebar-items.tsx
  • packages/app/src/pages/layout/sidebar-status-kind.test.ts
  • packages/app/src/pages/layout/sidebar-status-kind.ts
  • packages/app/src/pages/layout/sidebar.css
  • packages/ui/src/components/button.css
  • packages/ui/src/components/dropdown-menu.css
  • packages/ui/src/components/icon-button.css
  • packages/ui/src/components/icon.tsx
  • packages/ui/src/components/line-comment-styles.ts
  • packages/ui/src/components/picker.css
  • packages/ui/src/components/spinner.css
  • packages/ui/src/components/spinner.tsx
  • packages/ui/src/components/text-field.css
  • packages/ui/test/icon-button-states.test.ts

Comment thread packages/app/e2e/sidebar/sidebar-slice-09.spec.ts Outdated
Comment thread packages/app/e2e/sidebar/sidebar-slice-09.spec.ts Outdated
Comment thread packages/ui/src/components/icon-button.css Outdated
Comment thread packages/ui/src/components/spinner.tsx Outdated
Astro-Han added 7 commits May 8, 2026 00:34
Pinned idle sessions now show a pin glyph in the right slot instead of
falling through to the timestamp. Priority becomes
asking → busy → error → pin → time, matching the L35 mental model:
the three live signals still preempt pin, but pin is meaningful enough
to displace the passive time.

- sidebar-status-kind.ts: add "pin" kind + pinned input; tests cover
  every pairwise priority and the pinned-only case.
- sidebar-items.tsx: SessionItem accepts isPinned, statusKind feeds
  pinned only when no live signal is active, statusContent renders
  Icon name="pin" with text-icon-weak (passive-tier color, lighter
  than the live brand/error tints).
- pawwork-sidebar.tsx: pass isPinned per row by checking
  pinnedIDs().includes(session.id).
The "Pinned" section header at the top of the sidebar already signals
which sessions are pinned — adding a per-row pin icon repeats the same
information. The header is present in both sort modes (time and
project), so location alone is sufficient.

Restores the original 4-state right-slot priority: asking → busy →
error → time.
…nu.css

DropdownMenu (three-dots) and ContextMenu (right-click) were styled by
two separate CSS files that had drifted: different row height, hover
overlay, item radius, font size, content radius, and shadow. The two
menus showed the same actions but didn't look the same.

Collapse both stylesheets into a single menu.css using combined
selectors that cover both Kobalte primitives. Trigger semantics still
differ at the component layer; only the visual surface is unified.

Future tweaks land once and apply everywhere — no sync drift.
…debar

Clicking a session row or "New Session" while the settings overlay was
open did nothing visible. The path-watcher effect intended to close
settings on URL change captured pathname at unreliable moments and
did not fire consistently, so the overlay stayed on top of the
navigation target.

Make the close explicit: shell-navigation now closes the settings
surface as the first step of openSession and openNewSession (including
the project-chooser fallback). Drop the path-watcher effect — direct
caller-side close is the canonical path.
"PawWork Desktop" was wider than the settings sidebar footer slot and
read as redundant — every settings page is the desktop one. Drop the
suffix in EN; ZH stays "爪印".
The palette was tight: search row was a thin 40px placeholder with no
icon, items used picker.css default 0/8 padding (off-spec, L32 wants
0/12), and the modal grew/shrank with result count which made every
keystroke jump the page.

Changes
- Search header is now a 52px ghost strip with leading magnifying
  glass and 16px horizontal padding; input renders type-body without
  the 20px clamp that made the box read as cramped.
- Body locks to 480px (or 100dvh - 32px on small windows) so result
  count changes scroll inside the modal instead of resizing it.
- Items align to L32: height 32, padding 0/12, radius-sm; selected row
  uses surface-interactive-base + brand-primary check icon.
- Section headers get type-h3 family (uppercase, 0.5 tracking) per
  L32; empty state grows to 48/32 padding.
- Drop hideIcon from the file/command picker so the palette shows the
  search icon to match other List consumers' search affordance.
The model picker, variant Select, and workspace chip showed visibly
different font weights in the composer footer. Two were rendered via
the Button component (which sets font-weight: medium by default), and
the existing override class "text-13-regular!" did not apply because
Tailwind v4 only processes the "!" important suffix on its own
utilities — not on custom CSS classes — so the bang was a silent
no-op.

Make all three triggers explicitly font-normal (Tailwind built-in),
and clean up the bogus "!" suffixes (text-13-regular!, justify-start!)
so the class lists describe what actually applies.
Astro-Han added 7 commits May 8, 2026 12:43
When the sidebar is sorted by project, each project group can now be
folded so the user can hide branches they're not actively working on
without losing them.

- Project header is a clickable button (whole row hot, not just the
  chev) with a leading folder icon to distinguish project groups from
  session rows, and a trailing chev that appears on hover and rotates
  -90° when collapsed.
- Default expanded; folded state persists per-project label in the
  page store with a sanitizing migrate so corrupt entries can't bleed
  into runtime.
- Toggle uses reconcile so removing a key actually drops it (default
  setStore on objects merges and would never clear the field).
- Animate height via grid-template-rows 0fr↔1fr, 200ms exponential
  ease-out, motion-reduce friendly. Items stay mounted so scroll and
  focus survive the toggle; inert removes them from the tab order
  while collapsed.
- Tighten inter-section margin from mt-4 to mt-0.5 so collapsed groups
  list at the same rhythm as session rows. The 16px section break in
  L35 is for top-level segments (Pinned / All / Footer), not project
  sub-groups.
The two new sidebar behaviors in slice-09 had no real-user-path
coverage — only the shell-navigation unit test verified the close-on-
navigate call order, and project collapse had no test at all. Per
AGENTS.md, sidebar / dialog / menu interactions need E2E.

- project-collapse.spec.ts: clicks the project group toggle, asserts
  aria-expanded / data-collapsed flips and the wrapper's bounding box
  height collapses to 0 (grid-template-rows 0fr clip). Second case
  reloads and asserts the persisted collapsed state survives.
- settings-close-on-nav.spec.ts: opens the settings overlay, clicks a
  sidebar session, asserts the overlay hides and the URL navigates to
  the clicked session.

Tag the collapse content wrapper with data-component / data-collapsed
so the test can target the visible-hide signal — Playwright's
toBeHidden treats overflow:hidden + 0fr clipping as visible since the
clipped element keeps its layout height.
Sort trigger stayed icon-only after manual UX review (a text+chev
trigger does not fit the L35 row beside the All header); rename the
test to describe the actual behavior. Tighten the three-segment shape
assertions so failures point to a specific bounding box rather than a
boolean roll-up.
The slice 09 change to --radius-md and --row-active-overlay was meant
for the sidebar three-dot trigger that sits on top of the row's own
hover layer; applying it to the global IconButton selector pulled the
heavier overlay and wider radius into titlebar, prompt toolbar, and
every other ghost icon button. Revert the base to --radius-sm and
--hover-overlay (the documented IconButton contract), and add a
sidebar-row-scoped override so the slice 09 visual still ships where
intended. Tighten the contract test to assert the base radius and to
match the hover token inside the &:hover rule rather than anywhere in
the file.
The shared Spinner sat in the UI package with a hardcoded
aria-label="Loading", so screen readers always announced English even
when the rest of the sidebar was localized. Add an optional aria-label
prop (default unchanged) and pass the translated string from the
sidebar busy state. Also gate the infinite rotation behind
prefers-reduced-motion so users with reduced-motion settings don't see
a continuously spinning indicator.
Slice 09 replaced the unreliable path-watcher effect with an explicit
closeSettingsSurface call inside createShellNavigation, which covers
sidebar-driven session opens. Notification clicks (and any other
caller of @/utils/notification-click) route through useNavigate
directly and were leaving the settings overlay on top of the new
route. Wrap the navigate function injected into setNavigate so it
closes the overlay before routing, restoring the same guarantee for
non-shell entry points without bringing back the path-watcher effect.
The full 4-state case already locks down the icon sequence; mirror it
in the exportAvailable: false path so a regression that swaps pin /
rename / delete icons cannot slip through.
@Astro-Han Astro-Han changed the title feat(ui,app): sidebar L35 row + four-segment shell (slice 09, issue #440) feat(ui,app): sidebar L35 row rewrite (slice 09, issue #440) May 8, 2026
@Astro-Han Astro-Han merged commit 35da84a into dev May 8, 2026
22 checks passed
@Astro-Han Astro-Han deleted the claude/slice-09-sidebar branch May 8, 2026 05:42
Astro-Han added a commit that referenced this pull request May 8, 2026
## Summary

Slice 09 of the #440 visual rewrite. Closes #303.

- Row outer shell rebuilt to L35: 32px tall, 10px horizontal padding, radius-sm, flex-centered (no leading-[1.4]). Active title now picks up `font-medium`.
- Right-slot status reduced to the 4-state model: `asking` (comment icon, brand orange) → `busy` (spinner) → `error` (circle-x, red) → `time` (sans 12, fg-weaker). Old 6px colored dots and the pinned-icon state are gone — pin status is signaled by the row's presence in the Pinned section.
- Action overlay three-dot iconbtn scoped to `--radius-md` and `--row-active-overlay` (0.06) hover so it reads as its own target on top of the 0.04 row hover. The IconButton base contract stays at `--radius-sm` + `--hover-overlay`; the slice 09 override is scoped to `[data-component="pawwork-session-row"] [data-action="session-row-menu"]` only. Status fades to opacity 0 (no `display:none`).
- 4-item menu: Pin / Rename / Export… / (sep) / Delete, each with a leading icon. Keyboard shortcut affordances (the `↵` / `⌘` / `⌫` hint glyphs and a real `<kbd>` element) are deferred to slice 14 (L46) — slice 09 ships the icon-leading layout only.
- Sort: replaced the icon-only filter toggle with a Kobalte DropdownMenu trigger (still icon-only `IconButton icon="sort"` to fit the 32px row beside the All label) offering 按时间 / 按项目 (By time / By project), with active-mode check icon and `data-mode` for testability.
- Layout shape: existing segments now carry `pawwork-side-top` / `pawwork-side-scroll` / `pawwork-side-foot` markers so L37's shell shape is testable. The traffic / collapse-control segment (`pawwork-side-traffic`) is intentionally deferred to slice 17 — the OS chrome already renders the macOS traffic-lights today, and adding a 32px placeholder now would double the empty space at the top. Slice 17 hides the OS chrome and moves the controls into the sidebar; that's when the placeholder lands.
- Footer: settings button keeps its `TooltipKeybind` wrapper for the keybind affordance; the visible row matches the L35 nav cluster (32 high, 10 horizontal padding) alongside the other two sidebar nav buttons.
- Removed dead `ProjectIcon` export from `sidebar-items.tsx` (no callers).
- Sidebar geometry uses literal pixels (`h-[32px]`, `size-[20px]`, `size-[16px]`) because PawWork's 13px root font size scales Tailwind rem-based sizing — `h-8` would render at 26px, not 32.
- Project group collapse (#50): project headers in the project sort mode are now toggle buttons with a folder icon and a hover-revealed chevron. Collapsed state animates via grid-template-rows 0fr↔1fr (motion-reduce friendly), is keyed by project label, and persists across reloads via `pawworkProjectCollapsed` on `layout-page-store`. Items stay mounted while collapsed (focus / scroll preserved) and `inert` removes them from the tab order.
- Settings overlay close-on-nav: clicking a sidebar session (or "new session") while the settings overlay is open closes the overlay before navigating, wired through `closeSettingsSurface` in `createShellNavigation`. The global `setNavigate` hook is also wrapped so notification clicks and other non-shell deep-link routes close the overlay before routing.
- Spinner: the shared `Spinner` now accepts an optional `aria-label` prop (default `"Loading"`) so localized callers can pass `language.t("common.loading")`; the keyframe is gated behind `prefers-reduced-motion: reduce`.
- Command palette redesign (search header geometry, fixed body height, item rules) — full content / feature redesign tracked as follow-up #501.
- Project rename / delete actions tracked as follow-up #502 (out of slice 09 scope).

## Boundary check (no out-of-scope paths)

Touched only:
- `packages/app/src/pages/layout/{sidebar-items.tsx, pawwork-sidebar.tsx, sidebar.css, layout.tsx, layout-page-store.ts, shell-navigation.ts, session-menu-actions.{ts,test.ts}, sidebar-status-kind.{ts,test.ts}}`
- `packages/app/src/i18n/{zh.ts, en.ts}` (sort + settings keys; `app.name.desktop` shortened to `PawWork`)
- `packages/app/src/components/{prompt-input.tsx, prompt-input/workspace-chip.tsx, dialog-select-file.tsx}` (font-weight unification + magnifying-glass restoration on the file dialog)
- `packages/ui/src/components/{command-palette.css, menu.css, icon-button.css, spinner.{tsx,css}}` (palette geometry, unified context+dropdown menu styling, row-overlay scoping, shared spinner)
- `packages/app/e2e/sidebar/{sidebar-slice-09.spec.ts (new), sidebar-project-collapse.spec.ts (new), sidebar-session-organization.spec.ts (selector), sidebar-leading-slot.spec.ts (drop obsolete pin-button assertion)}`
- `packages/app/e2e/settings/settings-close-on-nav.spec.ts (new)`

No changes to `sidebar-workspace.tsx`, titlebar, composer, message-flow, settings page content, or `packages/opencode`.

## Departures

- **Sub-session indentation** kept at `padding-left = 10 + level * 16`. The L35 anatomy locks the flat row; nested sessions need visual hierarchy. Annotated inline in `sidebar-items.tsx`.
- **Right-slot `error` state** is wired to the existing `notification.session.unseenHasError` signal (unread tool-output errors). Without a session-level error field in the data layer today, unseen-error is the available proxy.
- **Row left padding base 8 → 10** to match L35 row padding 0 10 (was `8 + level*16`, now `10 + level*16`).
- **`NewSessionItem` leading icon goes 14 → 16** (the standard chrome size after slice 08). The previous 14px override in `sidebar.css` was a deliberate departure from slice 08 with no L35/L37 backing — dropped.
- **Sort trigger stays icon-only.** L35 row geometry only fits a 20px icon button beside the "All" label; a text + chevron trigger would force the All header to two lines or push the action out of the row. The DropdownMenu still exposes the two options with check + `data-mode`, and the `pawwork-sort-trigger` selector is stable for E2E.
- **`pawwork-side-traffic` placeholder deferred to slice 17.** The OS already paints macOS traffic-lights on the window chrome; reserving a 32px band on top of that would render 64px of empty space at the sidebar head. Slice 17 hides the OS chrome and moves the cluster into the sidebar — that's when the placeholder lands without doubling the void. The deferral is annotated inline in `pawwork-sidebar.tsx`.
- **Menu shortcut hints deferred to slice 14.** An earlier draft of this slice rendered placeholder span hints next to each menu label; that was rolled back in favor of leading icons (commit `bf2f1d6`). Real `<kbd>` and the canonical hint affordance ship with slice 14 (L46).
- **Settings footer keeps `TooltipKeybind`.** The inline ⌘ glyph belongs with the broader keybind hint affordance in slice 14; until then, the tooltip surface remains the source of truth for the shortcut.

## Test plan

- [x] `bun turbo typecheck --filter=@opencode-ai/app --filter=@opencode-ai/ui` (5/5 packages)
- [x] `bun test --preload ./happydom.ts src/pages/layout` (101 pass, 0 fail)
- [x] `bun test:e2e -- sidebar/sidebar-slice-09.spec.ts` (3/3 pass)
- [x] `bun test:e2e -- sidebar/sidebar-project-collapse.spec.ts settings/settings-close-on-nav.spec.ts` (3/3 pass)
- [x] `bun test:e2e -- sidebar/sidebar-session-organization.spec.ts sidebar/sidebar-leading-slot.spec.ts` (3/3 pass)
- [x] Manual `bun run dev:desktop` Electron walkthrough — verified sidebar rows (hover/active/focus), three-dot menu and right-click menu parity, Pin/Rename/Export/Delete, sort popover toggle, status states (asking/busy/error/time), project collapse persistence across reload, settings close-on-nav for sidebar sessions, command palette geometry, light + dark themes.

## Crosscheck

Plan was reviewed by Claude (opus) + Codex prior to implementation. P0 candidates were investigated and dropped (`comment` icon exists; `STANDARDS.md` is a real local-only spec source). All P1/P2 findings were folded back into the plan and applied in commits.

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

* **New Features**
  * Added sidebar sort functionality with time and project options
  * Added project group collapse with persisted state across reloads
  * Added keyboard shortcut indicators in UI elements

* **UI/UX Improvements**
  * Standardized component heights across the interface (28px → 32px)
  * Redesigned spinner animation using CSS-based approach
  * Enhanced dropdown menu styling and hover behavior (unified with right-click menu)
  * Added icons to session menu actions
  * Settings overlay now closes when navigating away via the sidebar

* **Localization**
  * Added English translations for release notes UI and sorting controls
  * Added Chinese translations for sidebar sorting features

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] Sidebar session indicator: shape-differentiated icons (not color-only dots)

1 participant