Skip to content

feat(app): add a skills capability gallery#1176

Merged
Astro-Han merged 13 commits into
devfrom
claude/skill-gallery
Jun 5, 2026
Merged

feat(app): add a skills capability gallery#1176
Astro-Han merged 13 commits into
devfrom
claude/skill-gallery

Conversation

@Astro-Han

@Astro-Han Astro-Han commented Jun 5, 2026

Copy link
Copy Markdown
Owner

Summary

Adds a top-level Skills surface to the PawWork sidebar (between Search and Automations) that shows what the agent can do, and a deterministic way to start using a skill.

  • Read-only capability gallery. Reads the real installed skills from the existing GET /skill route (client.app.skills) and renders grouped two-column borderless icon rows. Each row humanizes the skill name into a readable title and shows the description clamped to one line. A search box filters across title, name, and description.
  • Minimal detail modal. Brand glyph, humanized title, the description verbatim, and the full SKILL.md body via the shared @opencode-ai/ui/markdown renderer (code blocks with copy). No prev/next nav.
  • "Use in chat" activation. A primary action in the detail opens a fresh session in the current project and seeds the composer with the same structured skill chip the slash picker inserts ({ type: "skill", name, source: "skill" }), via a new ?skill= route param. The picked skill is the skill that loads.

Generic, format-driven rendering with no per-skill curation: skills are unbounded (a user may author hundreds), so nothing hand-matches per skill. skillSummary() is the single isolated seam where a future schema-free summary derivation can land without touching skills or the data model.

Why

For a non-technical everyday-work user, the dominant unmet need is discovery ("what can this thing actually do for me"), not toggling skills on and off. v1 makes capability visible; management (enable/disable) and a marketplace are deferred layers. The full design direction, alternatives considered, and rejected options are recorded on #220.

Activation reuses the existing inline slash-skill chip (#1156) rather than a primed natural-language prompt: a sentence would fall back to the model matching the skill description and could load nothing, the wrong skill, or a different one than the user clicked. Listing (GET /skill) and activation (the skill chip) stay orthogonal and add no new mechanism.

Related Issue

Closes #220.

Human Review Status

Pending

Review Focus

  • The Automations-mirrored shell wiring: activeSurface signal, LayoutShellFrame skills slot, sidebar entry, ShellSurface context. The session sidebar stays live behind the surface; Escape closes the open detail first, then the surface.
  • The ?skill= bootstrap reuses the existing generic route-prompt bootstrap hook (useSessionRoutePromptBootstrap) rather than adding a parallel seam — confirm that reuse reads cleanly.
  • Verbatim machine-facing descriptions: the known readability tradeoff is captured behind the skillSummary() seam (identity passthrough in v1). See [Feature] Skill management GUI in the desktop app #220.

Risk Notes

  • v1 intentionally does not touch permission.skill or introduce a disabled-skills field; no enable/disable. The gallery currently lists Skill.all() via GET /skill; switching the source to agent-available skills is noted as a follow-up on [Feature] Skill management GUI in the desktop app #220.
  • The detail renders the full SKILL.md body, which can expose setup commands and agent-facing rules to end users. Accepted for v1; revisit if it reads poorly.
  • No platform/packaging/updater surface touched.

How To Verify

typecheck (packages/app, tsgo -b): clean
eslint (changed src files): 0 errors
E2E e2e/skills/skills-panel.spec.ts (bun script/e2e-local.ts): 2 passed
  - sidebar entry opens the gallery; Escape closes detail then surface
  - "Use in chat" opens a new session with the summarize skill chip inserted (bare label, no leading slash)
Snap target skills-surface (bun run snap skills-surface): 1 passed
  - reviewed gallery, detail modal (with "Use in chat" footer), and filtered grid

Screenshots or Recordings

Visual check via the added skills-surface snap target (real backend skills + seeded project skills). Reviewed grid (gallery / detail modal / filtered search) at docs/design/preview/screenshots/skills-surface.png. Surfaces checked: the capability gallery, the detail modal with the "Use in chat" footer, and the search-filtered grid.

Checklist

  • Type label — this PR carries exactly one of bug, enhancement, task, documentation.
  • Routing labels — this PR carries at least one of app, ui, platform, harness, ci.
  • Priority label — this PR carries exactly one of P0, P1, P2, P3.
  • Human Review Status above is set to Pending, Approved by @<reviewer>, or Not required: <reason>.
  • I linked the related issue, or stated in Summary why there is no issue.
  • I described the review focus and any meaningful risks.
  • I replaced the example block in How To Verify with the real verification steps and the key result for each.
  • I did not introduce unrelated refactors, dependencies, generated files, or file changes beyond the stated scope.
  • (conditional) I manually checked visible UI or copy changes when needed, with screenshots or recordings.
  • (conditional) I considered macOS and Windows impact for platform, packaging, updater, signing, paths, shell, or permissions changes. Leave unticked only if no platform/packaging surface was touched.
  • (conditional) I called out docs, release notes, dependencies, permissions, credentials, deletion behavior, generated content, or local file changes when relevant.
  • I reviewed the final diff for unrelated changes and suspicious dependency changes.
  • I am targeting dev, and my PR title and commit messages use Conventional Commits in English.

Summary by CodeRabbit

  • New Features

    • Added a new "Skills" section to the sidebar for browsing and searching available skills.
    • Users can view individual skill details and descriptions in a dedicated panel.
    • Added "Use in chat" functionality to insert selected skills into chat sessions.
    • Skills are searchable and filterable by name and description.
    • Proper keyboard navigation support (Escape key) for closing skill panels.
  • Tests

    • Added end-to-end tests for skills UI interactions including panel opening/closing and skill insertion into chat.
    • Added snapshot tests for skills gallery and detail views.

Astro-Han added 2 commits June 5, 2026 12:04
Add a top-level "Skills" surface that shows what the agent can do, sitting
between Search and Automations in the PawWork sidebar. The dominant unmet
need for a non-technical user is discovery ("what can this thing do"), not
toggling, so v1 is a read-only capability gallery; management and a
marketplace are deferred layers.

Rendering is generic and format-driven, with no per-skill curation: the
skill name is humanized into a readable title and the description is shown
verbatim. skillSummary() is the single isolated seam where a future
schema-free summary derivation can land without touching skills or the data
model. The gallery reads the real installed skills from the existing
GET /skill route (client.app.skills) and renders grouped two-column
borderless icon rows; a search box filters across title, name, and
description. Opening a row shows a minimal detail modal: brand glyph, title,
verbatim description, and the full SKILL.md body via the shared
@opencode-ai/ui/markdown renderer.

The surface mirrors the Automations shell-takeover wiring (activeSurface
signal, LayoutShellFrame slot, sidebar entry, ShellSurface context) and
keeps the session sidebar live behind it. Escape closes the open detail
first, then the surface.

Visual check: added the skills-surface snap target (real backend skills,
seeded project skills) and reviewed the gallery, detail modal, and filtered
grid in docs/design/preview/screenshots/skills-surface.png.

Design direction recorded on #220.
Add a primary "Use in chat" action to the skill detail. Clicking it leaves
the Skills surface, opens a fresh session in the current project, and seeds
the composer with the same structured skill chip the slash picker inserts
(`{ type: "skill", name, source: "skill" }`), routed via a new ?skill=
search param that the session bootstrap consumes.

Activation is therefore deterministic: the picked skill is the skill that
loads. This deliberately avoids a primed natural-language prompt, which would
fall back to the model matching the skill description and could load nothing,
the wrong skill, or a different one than the user clicked. Listing
(GET /skill) and activation (the skill chip) stay orthogonal: the gallery
reads from one, "Use in chat" writes through the other, adding no new
activation mechanism.

The ?skill= bootstrap reuses the existing generic route-prompt bootstrap
hook, so no new seam is introduced.

E2E (e2e/skills/skills-panel.spec.ts): the sidebar entry opens the gallery,
Escape closes the detail then the surface, and "Use in chat" opens a new
session with the summarize skill chip inserted (bare label, no leading
slash). Detail footer button reviewed in the refreshed skills-surface snap.
@Astro-Han Astro-Han added the enhancement New feature or request label Jun 5, 2026
@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@Astro-Han, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 53 minutes and 11 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 8ebc0af1-de55-4887-808e-6e518f9f262d

📥 Commits

Reviewing files that changed from the base of the PR and between 8cc3eb5 and 9cbce3e.

📒 Files selected for processing (16)
  • packages/app/e2e/session/titlebar-right-rail-contract.spec.ts
  • packages/app/e2e/skills/skills-panel.spec.ts
  • packages/app/e2e/snap/skills-surface.snap.ts
  • packages/app/src/components/session/session-header.tsx
  • packages/app/src/components/titlebar.tsx
  • packages/app/src/context/shell-surface.tsx
  • packages/app/src/i18n/en.ts
  • packages/app/src/i18n/zh-branding.test.ts
  • packages/app/src/i18n/zh.ts
  • packages/app/src/pages/layout.tsx
  • packages/app/src/pages/layout/sidebar-items.tsx
  • packages/app/src/pages/session.tsx
  • packages/app/src/pages/session/right-panel-tab-strip.tsx
  • packages/app/src/pages/skills/skill-detail.tsx
  • packages/app/src/pages/skills/skill-presentation.ts
  • packages/app/src/pages/skills/skills-surface.tsx
📝 Walkthrough

Walkthrough

This PR implements a complete Skills UI surface for browsing and selecting skills from the desktop app. Users can open a searchable gallery from the sidebar, view skill details in a modal, and initiate a chat session with a selected skill already seeded into the prompt composer via a query parameter.

Changes

Skills UI and In-Chat Integration

Layer / File(s) Summary
Skills Data Model and Presentation Helpers
packages/app/src/pages/skills/skill-presentation.ts
Defines SkillInfo interface and three presentation utilities: skillTitle() converts raw names to title case by splitting on -/_; skillSummary() trims descriptions; skillMatches() performs case-insensitive filtering across name and description.
Gallery and Detail UI Components
packages/app/src/pages/skills/skill-detail.tsx, packages/app/src/pages/skills/skills-surface.tsx
SkillDetail renders a modal dialog with skill metadata and markdown body. SkillsSurface fetches skills from the SDK, renders a searchable grid of SkillRow tiles, filters by query, and shows the selected skill's detail panel with a "Use in chat" button. Escape key closes detail first, then surface.
Shell Surface State and Layout Orchestration
packages/app/src/context/shell-surface.tsx, packages/app/src/pages/layout.tsx, packages/app/src/pages/layout/layout-shell-frame.tsx
Context extends with skillsOpen, openSkills, closeSkills. Layout manages activeSurface state, implements toggleSkills() and useSkillInChat(name) handlers, and wires a skills panel to LayoutShellFrame. LayoutShellFrame treats skills as a takeover surface alongside settings/automations, conditionally mounting SkillsSurface and managing title/overlay display.
Sidebar Navigation Entry
packages/app/src/pages/layout/pawwork-sidebar-top.tsx, packages/app/src/pages/layout/pawwork-sidebar.tsx
PawworkSidebarTop and PawworkSidebar extend props to accept skillsActive, skillsLabel, onOpenSkills. Sidebar renders a new skills button with icon, active state styling, and tooltip. Layout wires the state and handler into sidebar props.
Session Integration and Skill Prompt Seeding
packages/app/src/pages/session.tsx
Page reads ?skill=<name> query parameter and seeds the prompt with a skill chip entry (type: "skill", source: "skill", content: /<name>) when present. Clears query param after bootstrapping so the skill does not re-seed on refresh.
Internationalization Strings
packages/app/src/i18n/en.ts, packages/app/src/i18n/zh.ts
Adds localization entries for Skills sidebar label, page title/subtitle, search placeholder, empty-state title, detail suffix, and "Use in chat" button in English and Chinese.
End-to-End and Snapshot Tests
packages/app/e2e/skills/skills-panel.spec.ts, packages/app/e2e/snap/skills-surface.snap.ts
Functional E2E tests verify sidebar opens skills, skill selection opens detail, Escape closes detail then surface, and "Use in chat" initiates a new session with the skill chip inserted. Snapshot test seeds test skills, captures gallery/detail/search views, and composes them into a visual regression snapshot.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • Astro-Han/pawwork#1156: Implements the inline SkillPart chip mechanics in the prompt composer that this PR's "Use in chat" flow depends on for rendering the skill chip in the session.

Suggested labels

enhancement, P2, app, ui, harness


🐰 Skills bloom in the app with graceful care,
Gallery tiles where knowledge lives and flows,
Search and select, then seed them in the air—
One /skill chip, and conversation grows.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 47.06% 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
Title check ✅ Passed The title clearly and concisely describes the main change: adding a skills capability gallery to the app, which matches the primary objective of this PR.
Description check ✅ Passed The PR description includes all required template sections: Summary (what changed), Why (problem/context), Related Issue (closes #220), Human Review Status (Pending), Review Focus, Risk Notes, How To Verify (with real results), and a complete checklist with most items ticked.
Linked Issues check ✅ Passed The PR implements core v1 functionality for issue #220: a read-only skills gallery with search, detail modal, and 'Use in chat' activation via ?skill= parameter. However, it explicitly defers Layer 1+ features (enable/disable, settings, permissions, registry manifest).
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing v1 skills gallery functionality: UI components, routes, context, i18n, and E2E tests. No unrelated refactors, dependency changes, or file alterations beyond the stated scope.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/skill-gallery

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.

@github-actions github-actions Bot added app Application behavior and product flows ui Design system and user interface P2 Medium priority labels Jun 5, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested priority: P2 (includes user-path files (packages/app/src/context/shell-surface.tsx, packages/app/src/i18n/en.ts, packages/app/src/i18n/zh.ts, packages/app/src/pages/layout.tsx, packages/app/src/pages/layout/layout-shell-frame.tsx, packages/app/src/pages/layout/pawwork-sidebar-top.tsx, packages/app/src/pages/layout/pawwork-sidebar.tsx, packages/app/src/pages/session.tsx, packages/app/src/pages/skills/skill-detail.tsx, packages/app/src/pages/skills/skill-presentation.ts, packages/app/src/pages/skills/skills-surface.tsx)).

P1/P0 are reserved for maintainer confirmation. Please relabel manually if this is a release blocker, security issue, data-loss risk, or updater/runtime failure.

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

Copy link
Copy Markdown

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 introduces a new "Skills" gallery and detail view, allowing users to browse, search, and activate project-scoped skills directly in chat sessions. It adds the SkillsSurface and SkillDetail components, integrates them into the main shell layout and sidebar, supports a ?skill query parameter to seed the chat composer, and includes comprehensive E2E and snapshot tests. The reviewer suggested adding a loading guard to the skills list to prevent the empty state UI from showing prematurely while the data is still loading.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread packages/app/src/pages/skills/skills-surface.tsx Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
packages/app/src/i18n/en.ts (1)

800-800: ⚡ Quick win

Clarify the subtitle for non-technical users.

The phrase "when it fires" uses technical jargon that may not be clear to the non-technical users mentioned in the PR objectives (#220). Additionally, "Open one" has a vague antecedent.

Consider rewording for clarity:

  • "What PawWork can do. Open a skill to see when it activates and how it works."
  • "What PawWork can do. Select a skill to see its trigger conditions and how it works."
📝 Suggested rewording
-  "skills.subtitle": "What PawWork can do. Open one to see when it fires and how.",
+  "skills.subtitle": "What PawWork can do. Open a skill to see when it activates and how it works.",
🤖 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/i18n/en.ts` at line 800, Update the "skills.subtitle" string
in the i18n key to remove technical jargon and clarify the antecedent; replace
"What PawWork can do. Open one to see when it fires and how." with a clearer
phrasing such as "What PawWork can do. Select a skill to see when it activates
and how it works." (modify the value for the "skills.subtitle" key in
packages/app/src/i18n/en.ts accordingly).
packages/app/e2e/snap/skills-surface.snap.ts (2)

95-97: ⚡ Quick win

Prefer locator assertion over page.waitForFunction() for element absence.

Checking element absence can be more idiomatically expressed using Playwright's built-in locator count assertion instead of raw DOM queries.

♻️ Recommended refactor using locator assertion
-  await page.waitForFunction(
-    () => document.querySelectorAll('[data-action="skill-open"][data-skill="web-research"]').length === 0,
-  )
+  await expect(page.locator('[data-action="skill-open"][data-skill="web-research"]')).toHaveCount(0)

As per coding guidelines: "Wait for observable state with locator assertions, expect.poll(...), or existing helpers in E2E tests rather than using arbitrary timeouts".

🤖 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/e2e/snap/skills-surface.snap.ts` around lines 95 - 97, Replace
the raw DOM check using page.waitForFunction with Playwright locator assertion:
target the selector '[data-action="skill-open"][data-skill="web-research"]' via
page.locator(...) and await expect(...).toHaveCount(0) so the test waits for
absence using Playwright's observable locator APIs instead of
page.waitForFunction.

79-79: ⚡ Quick win

Prefer expect.poll() over page.waitForFunction() for count assertions.

The coding guidelines recommend using expect.poll(...) for waiting on observable state. This assertion can be more clearly expressed with Playwright's polling mechanism.

♻️ Recommended refactor using expect.poll()
-  await page.waitForFunction(() => document.querySelectorAll('[data-action="skill-open"]').length >= 6)
+  await expect.poll(() => rows.count()).toBeGreaterThanOrEqual(6)

As per coding guidelines: "Wait for observable state with locator assertions, expect.poll(...), or existing helpers in E2E tests rather than using arbitrary timeouts".

🤖 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/e2e/snap/skills-surface.snap.ts` at line 79, Replace the direct
use of page.waitForFunction for the skill count check with Playwright's
expect.poll-based polling: locate the selector used in the wait
(document.querySelectorAll('[data-action="skill-open"]')) and use expect.poll to
repeatedly evaluate its length/count and assert it is >= 6; update the test to
call expect.poll(() => /* count retrieval */).toBeGreaterThanOrEqual(6) (or use
locator.count() inside the poll) so the assertion uses Playwright's polling
mechanism instead of page.waitForFunction.
🤖 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.

Nitpick comments:
In `@packages/app/e2e/snap/skills-surface.snap.ts`:
- Around line 95-97: Replace the raw DOM check using page.waitForFunction with
Playwright locator assertion: target the selector
'[data-action="skill-open"][data-skill="web-research"]' via page.locator(...)
and await expect(...).toHaveCount(0) so the test waits for absence using
Playwright's observable locator APIs instead of page.waitForFunction.
- Line 79: Replace the direct use of page.waitForFunction for the skill count
check with Playwright's expect.poll-based polling: locate the selector used in
the wait (document.querySelectorAll('[data-action="skill-open"]')) and use
expect.poll to repeatedly evaluate its length/count and assert it is >= 6;
update the test to call expect.poll(() => /* count retrieval
*/).toBeGreaterThanOrEqual(6) (or use locator.count() inside the poll) so the
assertion uses Playwright's polling mechanism instead of page.waitForFunction.

In `@packages/app/src/i18n/en.ts`:
- Line 800: Update the "skills.subtitle" string in the i18n key to remove
technical jargon and clarify the antecedent; replace "What PawWork can do. Open
one to see when it fires and how." with a clearer phrasing such as "What PawWork
can do. Select a skill to see when it activates and how it works." (modify the
value for the "skills.subtitle" key in packages/app/src/i18n/en.ts accordingly).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 2cbba17a-6a7d-45e9-b49f-d102d84e7208

📥 Commits

Reviewing files that changed from the base of the PR and between b6d4f80 and 8cc3eb5.

📒 Files selected for processing (13)
  • packages/app/e2e/skills/skills-panel.spec.ts
  • packages/app/e2e/snap/skills-surface.snap.ts
  • packages/app/src/context/shell-surface.tsx
  • packages/app/src/i18n/en.ts
  • packages/app/src/i18n/zh.ts
  • packages/app/src/pages/layout.tsx
  • packages/app/src/pages/layout/layout-shell-frame.tsx
  • packages/app/src/pages/layout/pawwork-sidebar-top.tsx
  • packages/app/src/pages/layout/pawwork-sidebar.tsx
  • packages/app/src/pages/session.tsx
  • packages/app/src/pages/skills/skill-detail.tsx
  • packages/app/src/pages/skills/skill-presentation.ts
  • packages/app/src/pages/skills/skills-surface.tsx

Astro-Han added 11 commits June 5, 2026 12:44
The skills surface is a main-region takeover that keeps the session sidebar
live, exactly like automations. But the titlebar/sidebar chrome that those
portals and route-active highlights render only suppressed themselves for
settings and automations, not skills, so opening Skills left the titlebar's
"New session" label, the right-utility toggle, the right-panel tab strip, and
the sidebar route-active highlight showing over the surface.

Add skillsOpen() alongside the existing automationsOpen() guards so the
skills takeover presents the same clean chrome as automations:
- session-header.tsx: left + right titlebar portals
- right-panel-tab-strip.tsx: the right-panel tab strip portal
- sidebar-items.tsx: route-active suppression on the session/new-session rows

Verified by an A/B snap of both surfaces open on the same route + sidebar
state: the skills titlebar now matches automations (center title only).
Opening a main-takeover surface (settings / automations / skills) from a
/session route that has the right utility panel open leaked the panel's
titlebar chrome over the surface: a 1px border-l divider, ~380px of reserved
rail width, and — under settings — the still-portalled tab strip ("Status").

Root cause: the right panel's titlebar chrome derived its visibility from
scattered, inconsistent conditions, none of which knew a surface was
covering the still-mounted session and its panel. titlebar's tabsRailActive
only checked `/session route && rightPanel.opened()`; right-panel-tab-strip
gated automations/skills but not settings. The prior fix only patched
session-header's two portals, so it missed the rail's own width/border-l and
the settings tab-strip leg — three separate chrome traces, one patched.

Fix: add a single source of truth `mainSurfaceOpen` to the shell-surface
context (derived from `activeSurface`), and have every piece of session
chrome a takeover covers read it: titlebar tabsRailActive, right-panel-tab-
strip, session-header's left/right portals (collapsed from the three-flag
form), and sidebar route-active suppression. Future surfaces only update the
one derivation.

`--right-panel-width` is left untouched on purpose: it is the panel's real
geometry, not a chrome-visibility flag. Zeroing it would animate the rail
0→380px on surface close against a body already at 380px (seam mismatch).
Only the rail's active state retracts.

Verified: new case in titlebar-right-rail-contract.spec.ts opens the right
panel then each of settings/automations/skills and asserts
#pawwork-titlebar-tabs has width 0, borderLeft 0px, 0 children — red before
(width 380), green after. Visually confirmed the divider and the settings
status-tab remnant are gone.
The skills gallery and "Use in chat" navigation resolved against the
owner project root (currentProject().worktree), mirroring the Automations
surface. But skills are directory-resolved capabilities, not project
entities: the composer's slash picker queries them against the active
route directory (the SDK directory). In a sandbox/workspace sub-route the
two diverge, so the gallery could list a different skill set than the
composer offers, and "Use in chat" dropped the user from their active
workspace into a root session.

Resolve skills against the active currentDir() like the composer does,
falling back to the project root only when no directory is active.
The gallery rendered the empty-state copy ("No skills match your search")
whenever filtered() was empty. While the resource is still resolving its
first batch, skills() is undefined and filtered() is empty, so every normal
load flashed "no skills" before the list appeared, and a load failure was
mislabeled as "no skills" with no way to tell the two apart.

Key the body off the resource state: a height-reserving placeholder while
loading (no flicker, no "Loading…" label since local skills resolve fast),
a dedicated message on failure, and the empty copy only once a real
filtered-to-zero result is in hand.
…session

Two independent route bootstraps seed the composer from ?skill= and ?prompt=.
The in-app entry points never set both ("Use in chat" sets only skill,
"Create via chat" only prompt), but a hand-built deep link could carry both,
and which one won was left to bootstrap definition order. Make the precedence
explicit — skill wins — so a combined link deterministically inserts the skill
chip instead of a text prompt.
The skill detail reader was a hand-rolled overlay: its own scrim, click-outside
close, stopPropagation, and role/aria-modal, plus a keydown listener on the
surface that sniffed the DOM for overlay components to decide whether Escape
should close the detail or the surface.

Mount it through useDialog().show instead, so the dialog base owns the modal
shell — overlay, focus trap, initial focus, focus restore, background inert,
Escape — and the detail only composes its content. The surface's Escape handler
now defers to a detailOpen() signal driven by the dialog's onClose (which fires
synchronously on close, unlike dialog.active, which lingers through the exit
animation), replacing the brittle DOM sniff. This also gives the reader the
correct full-window modal semantics its aria-modal already claimed.

Trade-off: the reader now covers the whole window (standard modal) rather than
only the surface region; accepted for the accessibility and consistency win.
…trap

The prior "skill wins" guard only stopped the prompt bootstrap from seeding
while ?skill= was still present. But the skill bootstrap cleared only the skill
param, which flipped that guard back off — the surviving ?prompt= then became
visible to the prompt accessor and re-seeded the composer a beat later,
overwriting the skill chip with plain text.

Clear both params when the skill is consumed so a combined deep link
(?skill=summarize&prompt=hello) deterministically lands the skill chip and the
stray prompt text never resurfaces.
…tail

The skills surface's Escape handler tracked only its own detail reader
(detailOpen), so Escape would wrongly close the surface when another modal was
up. That happens in practice: the sidebar stays live behind the surface, and its
search calls command.show() directly — bypassing the keybind gate that
suppresses ⌘K palettes behind a surface — so a command palette can sit on top of
the gallery.

Defer to dialog.active instead: the single source of truth for "a shared
dialog-stack modal owns Escape", covering both the detail reader and a
sidebar-opened palette, and replacing the old DOM overlay sniff. Trade-off:
dialog.active lingers through useDialog's ~100ms exit grace, so an Escape fired
within that window after a modal closes is absorbed rather than closing the
surface — negligible for a real user, and the e2e waits the grace out.

Adds regression tests: a palette opened over the gallery consumes Escape before
the surface, and the combined ?skill=&prompt= deep link seeds only the chip.
Two-up list rows now pair the humanized title with a two-line clamped
description, replacing the cramped single-line subtitle that packed the
band. The detail reader moves the frontmatter description into a bordered
cream summary card set apart from the SKILL.md body so the machine-facing
blurb no longer blurs into it, and drops the raw slug from the header —
it only echoed the humanized title and still shows in the footer path.
Removes the now-unused skillSummary passthrough.
- zh subtitle: use the 爪印 brand name (the established Chinese product
  name throughout zh.ts) and drop the "触发" jargon, per a DeepSeek-v4-pro
  copy review — was "PawWork 能做的事。…何时触发、如何工作。"
- en subtitle: name the antecedent ("Open a skill") and replace the
  "fires" jargon with "when it helps and how it works" (CodeRabbit)
- snap e2e: wait on observable state via expect.poll(...).toBeGreaterThan
  Equal and expect(locator).toHaveCount(0) instead of page.waitForFunction,
  matching the repo's E2E guideline (CodeRabbit)
The integrations settings description still read "PawWork" while every
other Chinese string uses 爪印. Also widen the branding test from a
five-key spot-check to a full scan of the dict, so a stray "PawWork"
can't slip back into any Chinese string unnoticed.
@Astro-Han Astro-Han merged commit 0b42f59 into dev Jun 5, 2026
34 checks passed
@Astro-Han Astro-Han deleted the claude/skill-gallery branch June 5, 2026 10:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app Application behavior and product flows enhancement New feature or request P2 Medium priority ui Design system and user interface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] Skill management GUI in the desktop app

1 participant