Skip to content

feat(skills): add Open skills folder action to the Skills surface - #1482

Merged
Astro-Han merged 6 commits into
devfrom
claude/skills-folder-button
Jun 29, 2026
Merged

feat(skills): add Open skills folder action to the Skills surface#1482
Astro-Han merged 6 commits into
devfrom
claude/skills-folder-button

Conversation

@Astro-Han

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

Copy link
Copy Markdown
Owner

Summary

Adds a desktop-only Open skills folder button to the Skills surface that opens ~/.agents/skills, the update-safe home for user-added skills, creating it on demand.

  • /path API gains a skills field plus an ensureSkills query flag (mirrors ensureConfig); Skill.userSkillsDir() single-sources the path next to builtinRoots. SDK regenerated.
  • New OpenSkillsFolderButton (extracted for isolated testing) gated on canOpenLocalPath, so the web build hides it.
  • Skills header relaid out: title and actions share one vertically-centered row, with the description on its own line below.

Why

Custom skills dropped into the install bundle's resources/skills are wiped on every update, because the Windows NSIS updater replaces the whole install directory (RMDir /r $INSTDIR). There was no in-app way to add a skill and no signposted safe location, so users found the only visible skills folder, which happens to be the volatile one, and lost their work silently and irreversibly on update.

~/.agents/skills lives under the home dir (outside the install bundle) and is already scanned by skill discovery, so it survives updates. This change makes that location discoverable from the app.

Related Issue

#1478

Human Review Status

Pending

Review Focus

  • The /path contract change and the regenerated SDK diff (skills is a required field now; the fixed default-path stores were updated to carry it).
  • The desktop gate: the button must never appear in the web build (no local filesystem to open).

Risk Notes

  • Scope is deliberately the minimal fix: a discoverable, update-safe place to add skills. It does not migrate existing resources/skills content or warn when a custom skill is still found there; both are tracked as follow-ups. Residual risk: a user who keeps using resources/skills directly still loses content on update, but with a signposted button the path of least resistance now leads away from the trap.
  • Platform: the action is desktop-only (platform.openPath); the path resolves to ~/.agents/skills on both macOS and Windows. No packaging/updater/signing code is touched.
  • A user-facing release note would be worth adding when this ships; not included here.

How To Verify

opencode typecheck (tsgo --noEmit): pass
app typecheck (tsgo -b): pass
sdk/js typecheck (tsgo --noEmit): pass
eslint (changed app files): 0 errors
unit  src/pages/skills/open-skills-folder.test.ts: 2 pass (ensures+opens path; no-op when path absent)
component  src/pages/skills/skills-folder-button.test.ts: pass (button shown only when openPath present; click -> path.get({ensureSkills:true}) -> openPath)
e2e  e2e/skills/skills-panel.spec.ts: 4 pass (incl. new assertion that the web surface hides the button)
SDK regen: openapi.json + v2 gen carry `skills` (response) and `ensureSkills` (query); unrelated models-snapshot regen reverted

Visual: walked the Skills route in bun run dev:desktop (zh locale) confirming the button renders and the header reads calmly; bun run snap skills-surface confirms the web header layout and that the gate hides the button.

Screenshots or Recordings

image

Checklist

  • Type label — this PR carries exactly one of bug, enhancement, task, documentation. Type labels are author-added; the labeler bot does NOT assign them. Add the label in the GitHub UI, then tick this.
  • Routing labels — this PR carries at least one of app, ui, platform, harness, ci. The labeler bot assigned app (packages/app), ui (**/*.tsx per labeler.yml), and harness (packages/opencode + packages/sdk); all correct for the changed paths.
  • Priority label — this PR carries exactly one of P0, P1, P2, P3. The triage bot assigned P2; confirmed (high-severity but low-frequency, and this is the discoverability fix rather than the wipe itself).
  • Human Review Status above is set to Pending, Approved by @<reviewer>, or Not required: <reason> (default is Pending; "not required" is restricted to bot-authored low-risk PRs).
  • 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. Leave unticked only if no visible UI or copy changed.
  • (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. Leave unticked only if none of those surfaces was touched.
  • 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 an “Open folder” action in the Skills area for supported desktop apps.
    • Skills data now includes a dedicated folder path, enabling the app to open the correct location directly.
    • Added localized wording for the new Skills folder action and error message.
  • Bug Fixes

    • Improved handling so the Skills folder is only shown/opened when available, with clearer fallback behavior if it can’t be opened.

The /path response gains a `skills` field: the update-safe home for user-added skills (~/.agents/skills), already scanned by skill discovery's external .agents root. An `ensureSkills` query flag creates it on demand, mirroring ensureConfig, so the renderer never has to resolve or mkdir the path itself.

Skill.userSkillsDir() single-sources the path next to builtinRoots so discovery and the API can't drift. control-openapi exposes ensureSkills as a boolean; the SDK is regenerated. The fixed default-path stores in global-sync carry the new required field.

Groundwork for a discoverable, update-safe place to add custom skills (#1478).
Custom skills dropped into the install bundle's resources/skills are wiped on every update, since the Windows updater replaces the whole install directory (#1478). There was no in-app way to add a skill and no signposted safe location, so users found the only visible skills folder, which is the volatile one.

Add a desktop-only "Open skills folder" button that opens ~/.agents/skills (created on demand via the /path ensureSkills flag), a discoverable and update-safe home that skill discovery already scans. The action is gated on canOpenLocalPath, so the web build hides it.

Lay the header out so the title and actions share one vertically-centered row with the description on its own line below, instead of the actions floating against a taller title block. OpenSkillsFolderButton is extracted so the gate and click wiring can be rendered in isolation; covered by a handler unit test, a component render test (both gate directions + the open flow), and an e2e assertion that the web surface hides the button.

Localized en/zh. Does not migrate existing resources/skills content or warn on it; tracked as follow-up.
@Astro-Han Astro-Han added the enhancement New feature or request label Jun 29, 2026
@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds an "Open Skills Folder" button to the Skills surface. The server’s GET /path endpoint gains an ensureSkills flag that creates and returns the user skills directory. The app adds a createOpenSkillsFolder helper, an OpenSkillsFolderButton, updated GlobalStore path state, new i18n keys, and tests.

Changes

Open Skills Folder feature

Layer / File(s) Summary
Server path contract and resolution
packages/opencode/src/skill/index.ts, packages/opencode/src/server/control-openapi.ts, packages/opencode/src/server/routes/instance/httpapi/groups/root.ts, packages/opencode/src/server/routes/instance/httpapi/handlers/root.ts, packages/opencode/src/server/production-httpapi.ts, packages/sdk/openapi.json
Adds userSkillsDir(), extends GET /path with ensureSkills and skills, updates the handler to create the skills directory when requested, and updates the API wiring and schema metadata.
GlobalStore path shape
packages/app/src/context/global-sync.tsx, packages/app/src/context/global-sync/child-store.ts, packages/app/src/context/global-sync.test.ts, packages/app/src/context/global-sync/bootstrap.test.ts
Adds skills to the global path object and updates the store fixtures and bootstrap tests to match.
Open folder helper and button
packages/app/src/pages/skills/open-skills-folder.ts, packages/app/src/pages/skills/skills-folder-button.tsx, packages/app/src/i18n/en.ts, packages/app/src/i18n/zh.ts, packages/app/src/pages/skills/open-skills-folder.test.ts, packages/app/src/pages/skills/skills-folder-button.test.ts
Adds the helper that requests the skills path and opens it, the conditional button component, localized labels/toast text, and tests for both behaviors.
Skills surface header integration
packages/app/src/pages/skills/skills-surface.tsx, packages/app/e2e/skills/skills-panel.spec.ts
Renders the new button in the Skills header, adjusts layout spacing, and adds an e2e assertion that the action is absent in the seeded panel flow.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

platform

🐇 I sniffed a path beneath the sky,
A skills folder waiting nearby.
Click, hop, and open wide the door,
New labels hum and toasts implore.
thump thump 🗂️

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.27% 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 is concise and accurately summarizes the main change: adding an Open skills folder action to the Skills surface.
Description check ✅ Passed The description matches the required template and fills all major sections, including summary, why, issue, risks, verification, screenshots, and checklist.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ 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/skills-folder-button

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration.


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.

@github-actions github-actions Bot added app Application behavior and product flows ui Design system and user interface harness Model harness, prompts, tool descriptions, and session mechanics P2 Medium priority labels Jun 29, 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/global-sync.test.ts, packages/app/src/context/global-sync.tsx, packages/app/src/context/global-sync/bootstrap.test.ts, packages/app/src/context/global-sync/child-store.ts, packages/app/src/i18n/en.ts, packages/app/src/i18n/zh.ts, packages/app/src/pages/skills/open-skills-folder.test.ts, packages/app/src/pages/skills/open-skills-folder.ts, packages/app/src/pages/skills/skills-folder-button.test.ts, packages/app/src/pages/skills/skills-folder-button.tsx, 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.

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
packages/app/src/pages/skills/skills-folder-button.test.ts (1)

69-70: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Replace the fixed sleep with a deterministic completion signal.

Waiting 20 ms here makes this check timing-dependent. Since the mocks already control openPath, you can await a promise resolved by that mock instead of sleeping.

Proposed fix
 const pathGetCalls = []
 const openPathCalls = []
+let resolveOpened
+const opened = new Promise((resolve) => {
+  resolveOpened = resolve
+})
 const globalSDK = {
   client: {
     path: {
@@
   const platform = {
     openPath: (path) => {
       openPathCalls.push(path)
+      resolveOpened()
       return Promise.resolve()
     },
   }
@@
   assert(button, "desktop host should render the open-folder action")
   assert(button.textContent === "skills.openFolder", "button should use the i18n label key")
   button.click()
-  await new Promise((resolve) => setTimeout(resolve, 20))
+  await opened
🤖 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/skills/skills-folder-button.test.ts` around lines 69 -
70, Replace the fixed 20 ms sleep in the skills-folder-button test with an
awaited signal from the mocked openPath flow so the assertion is deterministic.
Update the test around button.click() to wait for the mock-controlled promise to
resolve, using the existing openPath mock setup in skills-folder-button.test.ts
rather than relying on setTimeout.
packages/opencode/src/server/routes/instance/httpapi/handlers/root.ts (1)

81-82: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the Effect file-system service for the new skills-directory mkdir.

This new fs.mkdir(...) call adds another raw fs/promises write inside Effect-based handler code. Please thread the repo’s file-system service into this path instead so the handler stays consistent with the runtime/test abstractions. As per coding guidelines, "Prefer FileSystem.FileSystem instead of raw fs/promises for effectful file I/O in Effect services."

🤖 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/opencode/src/server/routes/instance/httpapi/handlers/root.ts` around
lines 81 - 82, The new skills-directory creation in the root handler is using a
raw fs/promises mkdir call instead of the Effect file-system abstraction. Update
the logic in the root route handler around ensureSkills to use the repo’s
FileSystem.FileSystem service for mkdir so it stays consistent with the rest of
the Effect-based code and test/runtime abstractions.

Source: Coding guidelines

🤖 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/src/pages/skills/skills-surface.tsx`:
- Around line 133-141: The search textbox in skills-surface lacks an accessible
name because the surrounding label is icon-only; update the input in
skills-surface to include an explicit aria-label or add hidden label text so
assistive technologies can announce it properly. Use the existing search input
markup and keep the current query/setQuery behavior unchanged while adding the
accessible name near the input element.

---

Nitpick comments:
In `@packages/app/src/pages/skills/skills-folder-button.test.ts`:
- Around line 69-70: Replace the fixed 20 ms sleep in the skills-folder-button
test with an awaited signal from the mocked openPath flow so the assertion is
deterministic. Update the test around button.click() to wait for the
mock-controlled promise to resolve, using the existing openPath mock setup in
skills-folder-button.test.ts rather than relying on setTimeout.

In `@packages/opencode/src/server/routes/instance/httpapi/handlers/root.ts`:
- Around line 81-82: The new skills-directory creation in the root handler is
using a raw fs/promises mkdir call instead of the Effect file-system
abstraction. Update the logic in the root route handler around ensureSkills to
use the repo’s FileSystem.FileSystem service for mkdir so it stays consistent
with the rest of the Effect-based code and test/runtime abstractions.
🪄 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: 78feebb4-dbf9-4c77-a27a-cc2374fede61

📥 Commits

Reviewing files that changed from the base of the PR and between dbbebea and 5a170e6.

⛔ Files ignored due to path filters (2)
  • packages/sdk/js/src/v2/gen/sdk.gen.ts is excluded by !**/gen/**
  • packages/sdk/js/src/v2/gen/types.gen.ts is excluded by !**/gen/**
📒 Files selected for processing (17)
  • packages/app/e2e/skills/skills-panel.spec.ts
  • packages/app/src/context/global-sync.test.ts
  • packages/app/src/context/global-sync.tsx
  • packages/app/src/context/global-sync/bootstrap.test.ts
  • packages/app/src/context/global-sync/child-store.ts
  • packages/app/src/i18n/en.ts
  • packages/app/src/i18n/zh.ts
  • packages/app/src/pages/skills/open-skills-folder.test.ts
  • packages/app/src/pages/skills/open-skills-folder.ts
  • packages/app/src/pages/skills/skills-folder-button.test.ts
  • packages/app/src/pages/skills/skills-folder-button.tsx
  • packages/app/src/pages/skills/skills-surface.tsx
  • packages/opencode/src/server/control-openapi.ts
  • packages/opencode/src/server/routes/instance/httpapi/groups/root.ts
  • packages/opencode/src/server/routes/instance/httpapi/handlers/root.ts
  • packages/opencode/src/skill/index.ts
  • packages/sdk/openapi.json

Comment thread packages/app/src/pages/skills/skills-surface.tsx
The search box sat inside an icon-only label, so assistive tech had no
usable name for the textbox. Add an aria-label reusing the placeholder
copy. Flagged in review.
Replace the fixed 20ms sleep after the click with a promise the openPath
mock resolves, so the assertion no longer races a timer. Flagged in review.
getPaths used raw fs.mkdir inside the Effect handler. Switch both the
config and skills mkdirs to AppFileSystem.Service.makeDirectory (orDie to
preserve the existing fail-fast behavior) and provide AppFileSystem to the
production router runtime, matching the house FS abstraction. Flagged in review.
@Astro-Han

Copy link
Copy Markdown
Owner Author

Addressed all three CodeRabbit findings:

  1. Search field a11y (skills-surface.tsx) — added aria-label reusing the placeholder copy so the icon-only label no longer leaves the textbox unnamed. (5191af3)
  2. Test determinism (skills-folder-button.test.ts) — replaced the fixed 20ms sleep with a promise the openPath mock resolves, so the assertion waits on the real signal instead of a timer. (135d67f)
  3. Raw fs.mkdir in the Effect handler (handlers/root.ts) — converted both the config and skills mkdirs in getPaths to AppFileSystem.Service.makeDirectory and provided AppFileSystem.defaultLayer to the production router runtime. Note: the codebase's FS abstraction is the AppFileSystem wrapper (@opencode-ai/core/filesystem), not the bare FileSystem.FileSystem, which is why this also adds the layer; Effect.orDie keeps the prior fail-fast behavior. (4d7876a)

Verification: opencode + app typecheck pass; packages/app skills tests (3) pass; test/server/instance-root-routes.test.ts (6) pass — exercises /path through the real route runtime including the newly provided AppFileSystem layer; eslint 0 errors on the changed app source.

The as-Path fixtures enumerated every path field except the newly required
skills, compiling only because the cast suppressed the missing-property
check. Add skills so the fixtures' typed shape matches the real PathInfo.

@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 (1)
packages/app/src/context/global-sync/bootstrap.test.ts (1)

140-167: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use a non-empty sentinel for skills in at least one bootstrap fixture.

Every new skills field here is "", so these tests still pass if bootstrap forgets to reconcile path.skills at all. Give one mocked path/expected snapshot a distinct value and assert it survives the merge path.

Example test hardening
-      path: { get: async () => ({ data: { state: "", config: "", skills: "", worktree: "", directory, home: "" } as Path }) },
+      path: { get: async () => ({ data: { state: "", config: "", skills: "/tmp/test-skills", worktree: "", directory, home: "" } as Path }) },

-          path: { state: "", config: "", skills: "", worktree: "", directory: "", home: "" } as Path,
+          path: { state: "", config: "", skills: "/tmp/test-skills", worktree: "", directory: "", home: "" } as Path,

Also applies to: 201-228, 271-325, 358-380, 431-473, 505-526, 562-583, 612-638, 672-698, 746-773

🤖 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/context/global-sync/bootstrap.test.ts` around lines 140 -
167, The bootstrapDirectory tests are not asserting that path.skills is actually
preserved because both the mocked input and expected snapshot use an empty
string. Update at least one bootstrapDirectory fixture to give Path.skills a
non-empty sentinel value and assert that the merged global.path.skills still
contains that value after bootstrap, so the test fails if skills reconciliation
is skipped.
🤖 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/src/context/global-sync/bootstrap.test.ts`:
- Around line 140-167: The bootstrapDirectory tests are not asserting that
path.skills is actually preserved because both the mocked input and expected
snapshot use an empty string. Update at least one bootstrapDirectory fixture to
give Path.skills a non-empty sentinel value and assert that the merged
global.path.skills still contains that value after bootstrap, so the test fails
if skills reconciliation is skipped.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 6141abdf-30f7-4b7b-aab4-a5f1689041fc

📥 Commits

Reviewing files that changed from the base of the PR and between 5a170e6 and 560ec45.

📒 Files selected for processing (5)
  • packages/app/src/context/global-sync/bootstrap.test.ts
  • packages/app/src/pages/skills/skills-folder-button.test.ts
  • packages/app/src/pages/skills/skills-surface.tsx
  • packages/opencode/src/server/production-httpapi.ts
  • packages/opencode/src/server/routes/instance/httpapi/handlers/root.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/opencode/src/server/routes/instance/httpapi/handlers/root.ts
  • packages/app/src/pages/skills/skills-folder-button.test.ts
  • packages/app/src/pages/skills/skills-surface.tsx

@Astro-Han
Astro-Han merged commit 669fca3 into dev Jun 29, 2026
44 checks passed
@Astro-Han
Astro-Han deleted the claude/skills-folder-button branch June 29, 2026 14:35
Astro-Han added a commit that referenced this pull request Jun 29, 2026
Bump desktop release metadata to 2026.6.13 to ship two user-facing fixes already merged to dev.

Goal:
- Cut a stable release carrying the sidebar drag fix (#1481) and the Skills folder entry (#1482).

Change boundary:
- packages/desktop-electron/package.json: 2026.6.12 -> 2026.6.13.
- bun.lock: sync the matching workspace package version (only the version line changed).

Verification:
- bun install --frozen-lockfile + --lockfile-only in the release worktree; git diff is exactly the two version lines; git diff --check passed.
- PR #1483 CI green (one flaky e2e connecting-indicator test re-run to pass; unrelated to a version bump).

Residual risk: none for the bump itself. Release build/publish/mirror/verification follow .github/RELEASE_CHECKLIST.md.
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 harness Model harness, prompts, tool descriptions, and session mechanics P2 Medium priority ui Design system and user interface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant