Skip to content

fix(provider): resolve skills and slash commands against the workspace root - #7882

Open
CurlyPeter wants to merge 1 commit into
pingdotgg:mainfrom
CurlyPeter:workspace-scoped-provider-capabilities
Open

fix(provider): resolve skills and slash commands against the workspace root#7882
CurlyPeter wants to merge 1 commit into
pingdotgg:mainfrom
CurlyPeter:workspace-scoped-provider-capabilities

Conversation

@CurlyPeter

@CurlyPeter CurlyPeter commented Aug 22, 2026

Copy link
Copy Markdown

What changed

Both composer pickers are empty or incomplete in a packaged desktop build. This resolves skills and slash commands against the workspace root instead of the server's single global cwd.

Why

DesktopEnvironment.ts sets the backend cwd to the user's home directory when packaged:

backendCwd: input.isPackaged ? homeDirectory : appRoot,

ServerProvider.skills and ServerProvider.slashCommands are produced once per provider instance against that one cwd — but both are project-scoped:

  • ClaudeSkills scans <configDir>/skills and <cwd>/.claude/skills. With cwd pinned to $HOME, the "project" root resolves to ~/.claude/skills for every project, so a user who keeps no skills there sees none at all and $ reports "No skills found". It reports the mis-scoped scan accurately; it is not a UI bug.
  • the CLI's init handshake reports project-scoped slash commands, so / lists only the built-ins. Measured on one real workspace: the CLI reports 140 commands from the project root versus 48 from a neutral directory.

Running sessions are unaffected — they get the right scope through the CLI's own settings resolution — so this is a discovery gap, not a loss of capability.

Approach

A provider is a machine-level installation in this model, and version/auth/models genuinely are per-machine. So rather than making the snapshot project-aware, this adds a workspace-scoped surface beside it:

  • ProviderInstance gains optional discoverSkillsForCwd / discoverSlashCommandsForCwd. Optional, so the other four drivers are untouched.
  • ProviderRegistry gains matching *ForInstance methods, following the existing getProviderMaintenanceCapabilitiesForInstance delegation pattern, resolving empty when a driver cannot enumerate per directory.
  • new providers.workspaceSkills RPC resolves thread.worktreePath ?? project.workspaceRoot — the same idiom assets.createUrl already uses — mapped to orchestration:read.
  • the composer prefers the workspace result for $ and /, falling back to the snapshot, so an older server behaves exactly as today.

It is keyed on the project with an optional thread, because the composer is frequently on a draft thread with no persisted row, and that is precisely when the pickers matter.

The cache

ClaudeDriver's capabilities cache is now keyed by cwd (capacity 16) rather than holding a single entry probed against the server's cwd.

This completes something already designed for: makeClaudeCapabilitiesCacheKey already includes cwd, and ClaudeHome.test.ts already asserts that two cwds produce two keys — the driver simply never varied it. Binary path and resolved HOME are constant within one instance's scope, so keying on cwd preserves the isolation the composite key provided.

Cost: one CLI probe per workspace per the existing 5-minute TTL. Measured ~1.6 s cold, ~20 ms warm.

Deliberately not done

Skills are still read from the filesystem rather than init.skills. The handshake carries bare names, while ServerProviderSkill requires path and displays description, which only the scan can supply — the rationale in ClaudeSkills.ts's own docstring still holds.

Testing

contracts 258, server 2625, web 2696 — all passing on this branch; typecheck, fmt --check and lint clean. The new RPC is covered by the existing "declares exactly one scope for every RPC in the server group" test.

Verified against a live server: the RPC returns 92 skills (all with descriptions) and 140 slash commands for a workspace where the snapshot returned 0 and 48.

No UI layout changes — the pickers render exactly as before, with populated data.


Happy to split this or move it to an issue-first discussion if you'd prefer; I read CONTRIBUTING and this is larger than a one-liner, though it is a single change with no scope expansion.


Note

Medium Risk
Adds a new read RPC that scans a project workspace and may spawn the Claude CLI, plus cwd-keyed capability caching. Fallback to the machine snapshot keeps older servers working, but probe/cache behavior is now per workspace.

Overview
Fixes empty/incomplete $ and / pickers in packaged desktop builds, where the provider snapshot was scanned against $HOME and missed project .claude/skills and CLI project commands.

Adds providers.workspaceSkills (orchestration read). It resolves thread.worktreePath ?? project.workspaceRoot so draft threads still work, then returns workspace skills plus slash commands. The composer prefers that result and falls back to the machine-scoped snapshot.

Claude-only optional discoverSkillsForCwd / discoverSlashCommandsForCwd on the instance; other drivers omit them. Skills stay a cheap filesystem scan. Slash commands reuse the capabilities probe cache, now keyed by cwd (capacity 16, 5-minute TTL) instead of a single server-cwd entry.

Reviewed by Cursor Bugbot for commit 8e69659. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Resolve skills and slash commands against workspace root via new providers.workspaceSkills RPC

  • Adds providers.workspaceSkills RPC that derives the workspace root from a thread's worktree path or the project root, then returns project-scoped skills and slash commands for a given provider instance
  • ClaudeDriver now probes and caches capabilities per working directory (cwd) with a capacity-16 LRU, and exposes discoverSkillsForCwd / discoverSlashCommandsForCwd on the ProviderInstance interface
  • ProviderRegistry forwards discovery calls to the live instance, returning empty arrays when unsupported or not live
  • The chat UI (ChatView, ChatComposer) prefers workspace-scoped results for the $ and / menus, falling back to the machine-scoped snapshot when unavailable
  • Risk: capabilities cache in ClaudeDriver.create is now keyed by cwd instead of a single global key; any code that relied on the old makeClaudeCapabilitiesCacheKey singleton cache will no longer share state across workspaces
📊 Macroscope summarized 8e69659. 12 files reviewed, 4 issues evaluated, 1 issue filtered, 3 comments posted

🗂️ Filtered Issues

apps/web/src/components/chat/ChatComposer.tsx — 1 comment posted, 2 evaluated, 1 filtered
  • line 917: composerSkills treats an explicitly returned empty workspaceSkills array as unavailable and falls back to selectedProviderStatus.skills. When a workspace legitimately has no skills, the $ picker and editor therefore show the machine-cwd skills instead of no workspace skills, reintroducing the incorrect-scope behavior this change is meant to fix. Use nullish availability rather than length > 0 so only an absent result falls back. [ Already posted ]

…e root

Both composer pickers are empty or incomplete in a packaged desktop build.

`DesktopEnvironment` sets the server's cwd to the user's home directory when
packaged (`isPackaged ? homeDirectory : appRoot`). `ServerProvider.skills` and
`ServerProvider.slashCommands` are produced once per provider instance against
that single cwd, but both are project-scoped:

- `ClaudeSkills` scans `<configDir>/skills` and `<cwd>/.claude/skills`, so the
  "project" root resolves to `~/.claude/skills` for every project. A user who
  keeps no skills there sees none at all, and `$` reports "No skills found".
- the CLI's init handshake reports project-scoped slash commands, so `/` lists
  only the built-ins. Measured on one real workspace: the CLI reports 140
  commands from the project root versus 48 from a neutral directory.

A provider is a machine-level installation in this model — version, auth and
models genuinely are per-machine — so rather than making the snapshot
project-aware this adds a workspace-scoped surface beside it:

- `ProviderInstance` gains optional `discoverSkillsForCwd` and
  `discoverSlashCommandsForCwd`. Optional, so the other drivers are untouched.
- `ProviderRegistry` gains matching `*ForInstance` methods that follow the
  existing `getProviderMaintenanceCapabilitiesForInstance` delegation pattern
  and resolve empty when a driver cannot enumerate per directory.
- new `providers.workspaceSkills` RPC resolves
  `thread.worktreePath ?? project.workspaceRoot` — the same idiom
  `assets.createUrl` already uses — and is mapped to orchestration:read.
- the composer prefers the workspace result for `$` and `/`, falling back to
  the snapshot, so an older server behaves exactly as before.

The RPC is keyed on the project with an optional thread, because the composer
is frequently on a draft thread that has no persisted row yet, and that is
precisely when the pickers matter.

`ClaudeDriver`'s capabilities cache is now keyed by cwd (capacity 16) instead
of holding a single entry probed against the server's cwd.
`makeClaudeCapabilitiesCacheKey` already included cwd and `ClaudeHome.test.ts`
already asserted that two cwds produce two keys, so this completes what the key
was designed for. Binary path and resolved HOME are constant within one
instance's scope, so keying on cwd preserves the isolation the composite key
provided. Cost is one CLI probe per workspace per existing 5-minute TTL:
measured ~1.6s cold, ~20ms warm.

Skills are deliberately still read from the filesystem rather than the init
handshake: `init.skills` carries bare names, while `ServerProviderSkill`
requires `path` and displays `description`, which only the scan can supply.
@coderabbitai

coderabbitai Bot commented Aug 22, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: cff0cc3d-bb17-4c6e-b8e3-4196638855a0

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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 vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Aug 22, 2026

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

One finding: the composer's workspaceSkills prop receives an already-fallback-resolved value, which can make the $ picker show skills for a different provider instance than the one the composer selected. Details inline.

Posted via Macroscope — UI Consistency

interactionMode={interactionMode}
lockedProvider={lockedProvider}
providerStatuses={providerStatuses as ServerProvider[]}
workspaceSkills={activeSkills}

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.

activeSkills already falls back to activeProviderStatus?.skills, so what arrives as workspaceSkills here is often the machine-scoped snapshot of ChatView's resolved instance — and since composerSkills prefers any non-empty workspaceSkills over selectedProviderStatus?.skills, it overrides the composer's own selection.

The two resolutions can disagree: ChatView matches providerStatuses by id only, while ChatComposer additionally filters on enabled && isAvailable plus locked driver/continuation group. With a persisted-but-disabled selection, the workspace RPC returns [] (instance not live) and the $ picker then lists the disabled instance's snapshot skills instead of the instance that will run the turn.

Suggest passing the raw query result and letting ChatComposer own the fallback, the same way workspaceSlashCommands is passed (activeSkills can stay as-is for MessagesTimeline).

Suggested change
workspaceSkills={activeSkills}
workspaceSkills={workspaceCapabilitiesQuery.data?.skills}

Posted via Macroscope — UI Consistency

Comment thread apps/server/src/ws.ts
// An unknown thread id is not an error: a draft thread has no row.
const worktreePath = input.threadId
? yield* projectionSnapshotQuery.getThreadShellById(input.threadId).pipe(
Effect.map((thread) =>

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.

🟡 Medium src/ws.ts:1964

A request can name project A with a thread belonging to project B, and this handler then uses B's worktreePath for the project-scoped skill and slash-command discovery result. The threadId lookup must verify thread.value.projectId === input.projectId and ignore or reject mismatches.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/ws.ts around line 1964:

A request can name project A with a thread belonging to project B, and this handler then uses B's `worktreePath` for the project-scoped skill and slash-command discovery result. The `threadId` lookup must verify `thread.value.projectId === input.projectId` and ignore or reject mismatches.

Comment thread apps/server/src/ws.ts
Effect.map((thread) =>
Option.isSome(thread) ? thread.value.worktreePath : null,
),
Effect.orElseSucceed(() => null),

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.

🟡 Medium src/ws.ts:1967

A projection failure for an existing thread is converted to null, so providersWorkspaceSkills silently scans project.value.workspaceRoot and returns incorrect or incomplete results instead of reporting the RPC error. Remove Effect.orElseSucceed(() => null) so only an actual Option.none() selects the project-root fallback.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/ws.ts around line 1967:

A projection failure for an existing thread is converted to `null`, so `providersWorkspaceSkills` silently scans `project.value.workspaceRoot` and returns incorrect or incomplete results instead of reporting the RPC error. Remove `Effect.orElseSucceed(() => null)` so only an actual `Option.none()` selects the project-root fallback.

description: command.description ?? command.input?.hint ?? "Run provider command",
}));
const query = composerTrigger.query.trim().toLowerCase();
const skillItems = (selectedProviderStatus?.skills ?? [])

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.

🟡 Medium chat/ChatComposer.tsx:1152

The / slash-command menu builds skillItems from selectedProviderStatus.skills, so workspace-only skills available through composerSkills are missing there and stale machine-cwd skills can be shown instead. Use composerSkills for this list, as the $ trigger already does.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/chat/ChatComposer.tsx around line 1152:

The `/` slash-command menu builds `skillItems` from `selectedProviderStatus.skills`, so workspace-only skills available through `composerSkills` are missing there and stale machine-cwd skills can be shown instead. Use `composerSkills` for this list, as the `$` trigger already does.

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

Reviewed the new provider capability service surface (ProviderInstance.discoverSkillsForCwd / discoverSlashCommandsForCwd, ProviderRegistry.discoverSkillsForInstance / discoverSlashCommandsForInstance, the providers.workspaceSkills RPC and the client atom family) against the Effect service conventions.

Imports, Effect.fn usage, dependency acquisition (yield* FileSystem.FileSystem / Path.Path inside the driver scope), error modelling at the RPC boundary and the atom family reusing the shared connectionAtomRuntime all look consistent with the existing patterns — no service-injection, hidden-runtime, or error-shape violations found.

Two findings on change discipline, noted inline.

Posted via Macroscope — Effect Service Conventions

type ProviderSnapshotSettings,
} from "../providerUpdateSettings.ts";
import { makeClaudeCapabilitiesCacheKey, makeClaudeContinuationGroupKey } from "./ClaudeHome.ts";
import { makeClaudeContinuationGroupKey } from "./ClaudeHome.ts";

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.

Dropping this import leaves makeClaudeCapabilitiesCacheKey in ClaudeHome.ts with no production caller — the only remaining references are in ClaudeHome.test.ts. Suggest deleting the helper and its now-obsolete test cases along with the cache-key change so no dead pre-refactor path is retained.

Posted via Macroscope — Effect Service Conventions

Comment thread apps/server/src/ws.ts
),
{ "rpc.aggregate": "workspace" },
),
[WS_METHODS.providersWorkspaceSkills]: (input) =>

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.

This is new backend behavior (workspace-root resolution where a persisted thread's worktree overrides the project root, an unknown project failing as OrchestrationGetSnapshotError, an unknown thread tolerated, and the two registry discovery calls fanned out against the resolved root) and it ships without focused tests — only existing registry mocks were extended.

Consider adding a focused case for the handler in apps/server/src/server.test.ts (the Layer.mock(ProviderRegistry.ProviderRegistry) seam there already supports overriding discoverSkillsForInstance / discoverSlashCommandsForInstance) covering draft-thread vs. worktree root resolution and the unknown-project error, plus a case in apps/server/src/provider/Layers/ProviderRegistry.test.ts for the empty-array fallback when the instance is not live or its driver omits discoverSkillsForCwd.

Posted via Macroscope — Effect Service Conventions

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

Cursor Bugbot has reviewed your changes using high effort and found 3 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want fixes drafted automatically? Bugbot Autofix can create code changes for findings. A team admin can enable Autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 8e69659. Configure here.

skill.description ??
(skill.scope ? `${skill.scope} skill` : "Run provider skill"),
}),
);

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.

Slash menu skills still machine-scoped

High Severity

The / picker still builds skillItems from selectedProviderStatus?.skills (machine-scoped snapshot) while the $ path correctly uses composerSkills. In packaged builds that leaves project skills missing from / search even though workspace discovery succeeded, so the two triggers disagree after this fix.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8e69659. Configure here.

const activeSkills =
workspaceCapabilitiesQuery.data?.skills ??
activeProviderStatus?.skills ??
EMPTY_PROVIDER_SKILLS;

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.

Empty skills skip snapshot fallback

Medium Severity

activeSkills uses nullish coalescing on data?.skills, so a successful empty array from drivers without discoverSkillsForCwd replaces the snapshot. That contradicts the registry contract and the slash-command path, which only adopts workspace data when length > 0. Codex snapshot skills are cleared for the timeline once the RPC returns.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8e69659. Configure here.

// and the project root is the right answer for it anyway.
...(activeServerThread ? { threadId: activeServerThread.id } : {}),
},
})

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.

Workspace query uses wrong instance

Medium Severity

The new workspace RPC is keyed on activeProviderInstanceId, which does not apply the composer’s enabled/available and locked-provider filters used by selectedInstanceId. When those diverge, the pickers can show another instance’s skills or slash commands while labeling them for the instance that will actually send the turn.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8e69659. Configure here.

@macroscopeapp

macroscopeapp Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Skipped

Macroscope did not run approvability analysis for this PR. Macroscope could not determine whether this PR modifies its approvability configuration, so the PR was not approved automatically. A PR that may change the rules that govern approval is never approved automatically.

Not approved because:

  • 3 blocking correctness issues found at or above your repo's Minimum Blocking Severity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L 100-499 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant