Skip to content

chore: v1.1 polish — skills, panel fixes, README SEO#1

Merged
Astro-Han merged 7 commits intodevfrom
claude/festive-mcnulty
Apr 15, 2026
Merged

chore: v1.1 polish — skills, panel fixes, README SEO#1
Astro-Han merged 7 commits intodevfrom
claude/festive-mcnulty

Conversation

@Astro-Han
Copy link
Copy Markdown
Owner

Summary

  • Rewrite 3 built-in skills with CSO-optimized structure (description uses "Use when..." triggers, unified sections, ~30% fewer words)
  • Fix right panel auto-open — switch data source from message summary (turnDiffs) to Snapshot (sync.data.session_diff), the authoritative source
  • Fix file-tree button highlight — button now stays active when side panel is open on any tab (not just Files), since the standalone diff icon was removed earlier
  • Rewrite dual-language READMEs — concrete use cases, badges, "Who This Is For" section, fix license label from MIT to Apache-2.0

Test plan

  • Open a session, trigger file changes — right panel should auto-expand
  • With panel open on Changes tab, verify file-tree button in header is highlighted
  • Click file-tree button to toggle panel open/closed on both tabs
  • Verify skill cards still work (document-processing, data-analysis, writing-assistant)
  • Check README renders correctly on GitHub

- Description fields now use "Use when..." trigger format
- Unified section structure: Clarify Before Acting / Tool Selection / Workflow / Guardrails
- Removed redundant Error Handling + Output Requirements sections
- ~30% fewer words while preserving all functional guidance
…mary

The auto-open effect watched turnDiffs (from lastUserMessage.summary.diffs)
which could be empty or stale during streaming. Switch to diffs() which
reads from sync.data.session_diff (the Snapshot mechanism), the
authoritative data source that the Files tab already uses.
The standalone diff icon was removed earlier, making the file-tree
button the only toggle for the side panel. Update highlight condition
from opened() && tab() === "files" to just opened() so the button
stays active on the Changes tab too.
- Add concrete use cases (documents, data analysis, writing, PDF)
- Add badges (license, platform)
- Add "Who This Is For" section with search-friendly keywords
- Fix license from MIT to Apache-2.0
- Screenshot placeholder for future asset
- Task-focused language instead of generic "AI workstation"
Copy link
Copy Markdown
Owner Author

@Astro-Han Astro-Han left a comment

Choose a reason for hiding this comment

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

Posting the requested review finding.

Comment thread packages/app/src/pages/session.tsx Outdated
Snapshot (sync.data.session_diff) is only fetched when wantsReview is
true, so reopened sessions with the panel closed would see empty diffs.
Fall back to turnDiffs (message summary) when Snapshot data is not yet
available, keeping SSE-pushed Snapshot as the primary source for active
sessions.
The button's action (toggleTab("files")) and label ("Toggle file tree")
are Files-tab-specific, so the active/aria-expanded state should match.
Highlighting on the Changes tab would be misleading since the file tree
is not visible.
@Astro-Han Astro-Han merged commit 8b544ba into dev Apr 15, 2026
@Astro-Han Astro-Han deleted the claude/festive-mcnulty branch April 15, 2026 06:52
Astro-Han added a commit that referenced this pull request Apr 15, 2026
- Fix locale source: use oc_locale cookie, not Intl API (P1)
- Add question tool schema details to skill questions (P1)
- Add aria-expanded to diff button spec (P2)
- Narrow-screen changes deferred to separate patch (Design #1)
Astro-Han added a commit that referenced this pull request Apr 23, 2026
Remove centered Mark logo, add subtle local-processing reassurance note
below composer, tune typography and spacing to match the #151 mockup.
Also fix a latent bug where the title used text-24-medium (not defined
in packages/ui utilities); now uses text-20-medium, the largest defined
size utility.

Pure visual change — Skill card onClick behavior and zh/en card copy
unchanged.

Refs #151 (PR #1 commit 1 of 2).
Astro-Han added a commit that referenced this pull request Apr 23, 2026
Replace the three generic developer-oriented icons with mockup-faithful
custom SVGs (folder / bar-chart / pencil) tinted per Skill (warm orange
/ success green / violet) via semantic design tokens plus one arbitrary
hex for violet since no violet token exists in the design system.

Card visual switches from vertical card with description text to inline
pill (icon + title side by side, no description), matching the mockup's
light button style. Neutral chip border at rest, lifted border on hover.
Click behavior unchanged.

Home shell layout refines to match the #151 mockup: shift content down
via pt-[28vh], compact title→subtitle→pills rhythm, restore subtitle
with new copy "PawWork 可以帮你处理文件、分析信息、撰写内容并完成各类任务。"
and its English counterpart.

The upstream Icon component is kept in sidebar-items.tsx for the skill
badge (falls back to folder / status / pencil-line), so badge icons
differ from home for data-analysis during the PR #1 → PR #2 interval;
PR #2 removes the badge entirely.

Refs #151 (PR #1 commit 2 of 2).
Astro-Han added a commit that referenced this pull request Apr 23, 2026
Remove centered Mark logo, add subtle local-processing reassurance note
below composer, tune typography and spacing to match the #151 mockup.
Also fix a latent bug where the title used text-24-medium (not defined
in packages/ui utilities); now uses text-20-medium, the largest defined
size utility.

Pure visual change — Skill card onClick behavior and zh/en card copy
unchanged.

Refs #151 (PR #1 commit 1 of 2).
Astro-Han added a commit that referenced this pull request Apr 23, 2026
Replace the three generic developer-oriented icons with mockup-faithful
custom SVGs (folder / bar-chart / pencil) tinted per Skill (warm orange
/ success green / violet) via semantic design tokens plus one arbitrary
hex for violet since no violet token exists in the design system.

Card visual switches from vertical card with description text to inline
pill (icon + title side by side, no description), matching the mockup's
light button style. Neutral chip border at rest, lifted border on hover.
Click behavior unchanged.

Home shell layout refines to match the #151 mockup: shift content down
via pt-[28vh], compact title→subtitle→pills rhythm, restore subtitle
with new copy "PawWork 可以帮你处理文件、分析信息、撰写内容并完成各类任务。"
and its English counterpart.

The upstream Icon component is kept in sidebar-items.tsx for the skill
badge (falls back to folder / status / pencil-line), so badge icons
differ from home for data-analysis during the PR #1 → PR #2 interval;
PR #2 removes the badge entirely.

Refs #151 (PR #1 commit 2 of 2).
Astro-Han added a commit that referenced this pull request May 5, 2026
…rity test (slice #1, #440)

- add dark overrides for success/warning/error (bg/text/base), diff-add/del,
  icon-disabled, ring-base — light pastels were inheriting into dark mode
- restore 4 tokens dropped during theme.css rewrite:
  font-family-mono--font-feature-settings, shadow-xs-border-base,
  shadow-lg-border-base, surface-base-active (with dark counterparts)
- extend parity test: dark-completeness assertion (light regulated tokens
  must have dark override or be in SAME_IN_DARK), @media mirror ↔
  [data-color-scheme="dark"] exact-match suite (115 pass, was 64)
- remove one-time migration script slice-01-rename.ts
Astro-Han added a commit that referenced this pull request May 5, 2026
…lass sweep (slice #1, #440)

- dark shadow-raised/floating/modal: remove drop shadows, keep inset
  highlight only (L18 bans drop shadows in dark mode)
- surface-sunken light: alias to --bg-cream (#faf9f7), not a third
  neutral tier (L9 specifies 2 tiers only)
- replace 22 invalid Tailwind token classes across 13 component files:
  bg-background-strong → bg-surface-raised,
  text-text-subtle → text-fg-weak,
  text-text-danger-base → text-error,
  ring-interactive-base → ring-brand-primary,
  icon-strong-base → text-icon-strong,
  border-border-subtle → border-border-weak,
  border-t-accent-base → border-t-brand-primary,
  hover:border-border-strong → hover:border-border-base
- fix theme.css comment: dataset.colorScheme is written in context.tsx,
  not loader.ts
Astro-Han added a commit that referenced this pull request May 5, 2026
…ssue #440)

Slice #1 of eleven. Establishes the semantic token foundation for all subsequent
PawWork UI component slices (#2#11). This is a permanent carve-out from upstream
opencode — see AGENTS.md §Upstream Sync and .gitattributes.

## What changed

- theme.css: full rewrite against PawWork STANDARDS (warm neutrals, brand orange
  #ff5910, 13px dense, system-font). Three-selector structure:
    :root (light) → [data-color-scheme="dark"] (authoritative dark) → @media mirror
  Fixes a latent upstream bug: @media-only meant the Settings toggle had no effect.

- pawwork.json: rewritten to STANDARDS values with new token names.

- colors.txt → tailwind/colors.css: 104 tokens (incl. legacy compat aliases);
  regenerated Tailwind --color-* bridge via script/tailwind.ts.

- 50+ token renames across 144 component files in packages/ui/src and packages/app/src.

- HTML entry files (app/index.html, desktop-electron renderer/*.html): updated to
  var(--bg-base) and synced theme-color meta. Now in .gitattributes carve-out.

- UNREGULATED section in theme.css: holds legacy compat aliases (button-brand-*,
  icon-success-base, icon-info-base, icon-on-interactive-base, surface-base-active)
  so existing component consumers stay styled without entering STANDARDS managed set.

- .gitattributes: added merge=pawwork-keep-ours driver entries for packages/ui/**
  and key packages/app paths. Driver must be registered per-clone:
    git config merge.pawwork-keep-ours.driver "true"
  Verify before upstream sync: bash packages/ui/script/verify-merge-driver.sh

## Tests added

- packages/ui/test/theme-parity.test.ts (128 assertions, 0 fail):
  light root ↔ pawwork.json light.overrides, dark block ↔ dark.overrides,
  dark completeness (SAME_IN_DARK set), @media mirror exact-match, and two
  runtime-critical non-regulated token assertions (--text-mix-blend-mode: plus-lighter).

- packages/ui/test/undefined-tokens.test.ts:
  Scans every var(--xxx) in theme.css, Tailwind bridge, and HTML entry files;
  asserts each resolves to a definition.

## Deferred to slice #2

- colors.txt ↔ colors.css generation consistency test (manual regen + review for now)
- TSX/class utility audit (old token utility names, misspelled utilities)
- --surface-base-active classification (state token vs. compat bridge)
- Legacy runtime token deprecation checklist

## Key decisions for future agents

1. [data-color-scheme="dark"] is authoritative. @media block is mirror-only (first-paint).
   Both must stay in sync — theme-parity.test.ts enforces this.
2. UNREGULATED section in theme.css is intentional scope. Tokens there are consumed by
   components but not STANDARDS-managed. Do not promote to regulated without adding to
   pawwork.json and updating the parity test.
3. The merge driver only fires on two-sided conflicts. One-sided upstream changes still
   clean-merge. Always review upstream-sync PR diffs for carve-out paths.
4. colors.css is a generated file (do not hand-edit). Regen: bun run script/tailwind.ts.
Astro-Han added a commit that referenced this pull request May 5, 2026
…ssue #440)

Slice #1 of eleven. Establishes the semantic token foundation for all subsequent
PawWork UI component slices (#2#11). This is a permanent carve-out from upstream
opencode — see AGENTS.md §Upstream Sync and .gitattributes.

## What changed

- theme.css: full rewrite against PawWork STANDARDS (warm neutrals, brand orange
  #ff5910, 13px dense, system-font). Three-selector structure:
    :root (light) → [data-color-scheme="dark"] (authoritative dark) → @media mirror
  Fixes a latent upstream bug: @media-only meant the Settings toggle had no effect.

- pawwork.json: rewritten to STANDARDS values with new token names.

- colors.txt → tailwind/colors.css: 104 tokens (incl. legacy compat aliases);
  regenerated Tailwind --color-* bridge via script/tailwind.ts.

- 50+ token renames across 144 component files in packages/ui/src and packages/app/src.

- HTML entry files (app/index.html, desktop-electron renderer/*.html): updated to
  var(--bg-base) and synced theme-color meta. Now in .gitattributes carve-out.

- UNREGULATED section in theme.css: holds legacy compat aliases (button-brand-*,
  icon-success-base, icon-info-base, icon-on-interactive-base, surface-base-active)
  so existing component consumers stay styled without entering STANDARDS managed set.

- .gitattributes: added merge=pawwork-keep-ours driver entries for packages/ui/**
  and key packages/app paths. Driver must be registered per-clone:
    git config merge.pawwork-keep-ours.driver "true"
  Verify before upstream sync: bash packages/ui/script/verify-merge-driver.sh

## Tests added

- packages/ui/test/theme-parity.test.ts (128 assertions, 0 fail):
  light root ↔ pawwork.json light.overrides, dark block ↔ dark.overrides,
  dark completeness (SAME_IN_DARK set), @media mirror exact-match, and two
  runtime-critical non-regulated token assertions (--text-mix-blend-mode: plus-lighter).

- packages/ui/test/undefined-tokens.test.ts:
  Scans every var(--xxx) in theme.css, Tailwind bridge, and HTML entry files;
  asserts each resolves to a definition.

## Deferred to slice #2

- colors.txt ↔ colors.css generation consistency test (manual regen + review for now)
- TSX/class utility audit (old token utility names, misspelled utilities)
- --surface-base-active classification (state token vs. compat bridge)
- Legacy runtime token deprecation checklist

## Key decisions for future agents

1. [data-color-scheme="dark"] is authoritative. @media block is mirror-only (first-paint).
   Both must stay in sync — theme-parity.test.ts enforces this.
2. UNREGULATED section in theme.css is intentional scope. Tokens there are consumed by
   components but not STANDARDS-managed. Do not promote to regulated without adding to
   pawwork.json and updating the parity test.
3. The merge driver only fires on two-sided conflicts. One-sided upstream changes still
   clean-merge. Always review upstream-sync PR diffs for carve-out paths.
4. colors.css is a generated file (do not hand-edit). Regen: bun run script/tailwind.ts.
Astro-Han added a commit that referenced this pull request May 7, 2026
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.
Astro-Han added a commit that referenced this pull request May 8, 2026
* feat(app): add sidebar sort label and short option keys

* feat(app): extract sidebar 4-state status kind helper

* feat(app): rewrite sidebar session row to L35 (h32, 4-state status)

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

* feat(app): add shortcut hint field to SessionMenuAction

* feat(app): rebuild sidebar shell with sort popover and four-segment layout

- 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.

* style(app): scope sidebar row iconbtn to L35 hover (radius-md + 0.06 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.

* test(app): e2e for slice 09 sidebar shell + align sort selector

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.

* fix(app): use literal px for slice 09 row + status geometry

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).

* fix(app): drop dead Icon size prop after slice 08 collapse

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.

* fix(app): drop side-traffic placeholder until slice 17 owns the chrome

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.

* fix(app): unify sidebar quick-action buttons under TooltipKeybind

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.

* fix(app): keep sidebar status time text on a single line

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.

* fix(app): unify sidebar action buttons to 32px nav-row height

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.

* style(app): apply --duration-fast to sidebar row transition per L35

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.

* fix(app): use 13px caption + h24 sort trigger per L35 typography lock

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.

* fix(app): lock sidebar section header containers to 32px nav-row

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.

* fix(app): apply affinity spacing to sidebar section headers

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.

* fix(app): always apply mt-4 to top-level sidebar section headers

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).

* fix(app): drop sidebar footer divider in favor of affinity spacing

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).

* feat(ui): lift primitive default heights to 32

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.

* fix(app): include question and blocker in sidebar asking signal

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.

* style(ui): replace pulse-block spinner with rotating ring

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.

* style(ui): align spinner with STANDARDS keyshape and pw-spin keyframe

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.

* style(ui): slow spinner from 700ms to 1200ms

Bring rotation rate down to a calmer cadence per design feedback. STANDARDS.md L60 / L65 updated locally so the doc and code agree.

* style(app): lock sidebar status opacity to --duration-base 120ms

Status default and overlay opacity were inheriting Tailwind's default 150ms transition-opacity, which sits outside the L22 three-tier ladder (80 / 120 / 240). Promote both to data-status-default / data-status-overlay attributes so sidebar.css can pin them to --duration-base (120ms) — the fastest tier where opacity fade still reads as smooth, leaving --duration-fast (80ms) for color-only swaps where perceptual budget is lower.

* style(ui): lock dropdown menu items to 32px row family

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.

* refactor(app): swap session menu shortcut hints for leading icons

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.

* refactor(app): give sidebar sort options leading icons + check mark

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.

* refactor(app): switch sidebar sort trigger to icon-only IconButton

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.

* feat(ui): add sort icon and wire it into sidebar trigger

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.

* fix(ui): align sort trigger hover and icon size with row-menu

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.

* revert(ui): restore sort trigger and icon to L21/L23 spec

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.

* feat(ui): unify IconButton hover to radius-md + 6% globally

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.

* fix(app): align all sidebar inner padding to 10px to match row spec

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.

* feat(app): add pin as 4th sidebar status priority (above time)

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).

* revert(app): drop pin icon from sidebar right-slot, restore 4-state

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.

* refactor(ui): merge dropdown-menu and context-menu css into shared menu.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.

* fix(app): close settings overlay when navigating to a session from sidebar

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.

* fix(app): shorten settings footer en label to "PawWork"

"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 "爪印".

* refactor(ui,app): polish command palette per L32 with fixed-height body

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.

* fix(app): unify composer picker trigger font weight to regular

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.

* feat(app): collapsible project groups in sidebar

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.

* test(app): cover project collapse and settings-close-on-nav with e2e

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.

* test(app): rename slice-09 sort spec and tighten shape assertions

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.

* fix(ui): scope IconButton row-overlay tokens to sidebar row trigger

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.

* fix(ui,app): localize Spinner aria-label and honour reduced motion

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.

* fix(app): close settings overlay for non-shell navigation

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.

* test(app): assert icon order in 3-state session menu case

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.
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.

1 participant