-
Notifications
You must be signed in to change notification settings - Fork 4.8k
fix(provider): resolve skills and slash commands against the workspace root #7882
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1934,6 +1934,57 @@ const makeWsRpcLayer = ( | |
| ), | ||
| { "rpc.aggregate": "workspace" }, | ||
| ), | ||
| [WS_METHODS.providersWorkspaceSkills]: (input) => | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 Consider adding a focused case for the handler in Posted via Macroscope — Effect Service Conventions |
||
| observeRpcEffect( | ||
| WS_METHODS.providersWorkspaceSkills, | ||
| Effect.gen(function* () { | ||
| // The project root is the base; a persisted thread's worktree | ||
| // overrides it. Keyed on the project rather than the thread so a | ||
| // DRAFT thread still resolves — it has no server-side row yet, | ||
| // and that is exactly when the skill picker matters most. | ||
| const project = yield* projectionSnapshotQuery | ||
| .getProjectShellById(input.projectId) | ||
| .pipe( | ||
| Effect.mapError( | ||
| (cause) => | ||
| new OrchestrationGetSnapshotError({ | ||
| message: "Failed to load project for skill discovery", | ||
| cause, | ||
| }), | ||
| ), | ||
| ); | ||
| if (Option.isNone(project)) { | ||
| return yield* new OrchestrationGetSnapshotError({ | ||
| message: `Unknown project ${input.projectId}`, | ||
| }); | ||
| } | ||
| // 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) => | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Medium A request can name project A with a thread belonging to project B, and this handler then uses B's 🤖 Copy this AI Prompt to have your agent fix this: |
||
| Option.isSome(thread) ? thread.value.worktreePath : null, | ||
| ), | ||
| Effect.orElseSucceed(() => null), | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Medium A projection failure for an existing thread is converted to 🤖 Copy this AI Prompt to have your agent fix this: |
||
| ) | ||
| : null; | ||
| const workspaceRoot = worktreePath ?? project.value.workspaceRoot; | ||
| // Run both against the same resolved root. The skills scan is | ||
| // filesystem-only; the slash-command probe may spawn the CLI but | ||
| // is cached per directory by the driver. | ||
| const [skills, slashCommands] = yield* Effect.all( | ||
| [ | ||
| providerRegistry.discoverSkillsForInstance(input.instanceId, workspaceRoot), | ||
| providerRegistry.discoverSlashCommandsForInstance( | ||
| input.instanceId, | ||
| workspaceRoot, | ||
| ), | ||
| ], | ||
| { concurrency: 2 }, | ||
| ); | ||
| return { workspaceRoot, skills, slashCommands }; | ||
| }), | ||
| { "rpc.aggregate": "provider" }, | ||
| ), | ||
| [WS_METHODS.assetsCreateUrl]: (input) => | ||
| observeRpcEffect( | ||
| WS_METHODS.assetsCreateUrl, | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -239,6 +239,7 @@ import { environmentCatalog } from "../connection/catalog"; | |||||
| import { selectThreadTerminalUiState, useTerminalUiStateStore } from "../terminalUiStateStore"; | ||||||
| import { useKnownTerminalSessions, useThreadRunningTerminalIds } from "../state/terminalSessions"; | ||||||
| import { projectEnvironment } from "../state/projects"; | ||||||
| import { providerSkillsEnvironment } from "../state/providerSkills"; | ||||||
| import { useEnvironmentQuery } from "../state/query"; | ||||||
| import { | ||||||
| primaryServerAvailableEditorsAtom, | ||||||
|
|
@@ -2720,6 +2721,38 @@ function ChatViewContent(props: ChatViewProps) { | |||||
| const defaultInstanceId = defaultInstanceIdForDriver(selectedProvider); | ||||||
| return providerStatuses.find((status) => status.instanceId === defaultInstanceId) ?? null; | ||||||
| }, [activeProviderInstanceId, providerStatuses, selectedProvider]); | ||||||
| // Skills for the `$` picker, resolved against this thread's workspace root. | ||||||
| // `activeProviderStatus.skills` is machine-scoped: the server scans it once | ||||||
| // per provider instance against its own cwd, which a packaged desktop build | ||||||
| // sets to the user's home directory, so it reports user-scope skills only and | ||||||
| // is empty on a machine that keeps none there. Falling back to it keeps the | ||||||
| // picker working against a server that predates this RPC. | ||||||
| const workspaceCapabilitiesQuery = useEnvironmentQuery( | ||||||
| activeThread && activeProject && activeProviderInstanceId | ||||||
| ? providerSkillsEnvironment.workspaceSkills({ | ||||||
| environmentId: activeThread.environmentId, | ||||||
| input: { | ||||||
| projectId: activeProject.id, | ||||||
| instanceId: activeProviderInstanceId, | ||||||
| // Only a persisted thread has a server-side row; a draft has none, | ||||||
| // and the project root is the right answer for it anyway. | ||||||
| ...(activeServerThread ? { threadId: activeServerThread.id } : {}), | ||||||
| }, | ||||||
| }) | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Workspace query uses wrong instanceMedium Severity The new workspace RPC is keyed on Additional Locations (1)Reviewed by Cursor Bugbot for commit 8e69659. Configure here. |
||||||
| : null, | ||||||
| ); | ||||||
| const activeSkills = | ||||||
| workspaceCapabilitiesQuery.data?.skills ?? | ||||||
| activeProviderStatus?.skills ?? | ||||||
| EMPTY_PROVIDER_SKILLS; | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Empty skills skip snapshot fallbackMedium Severity
Additional Locations (1)Reviewed by Cursor Bugbot for commit 8e69659. Configure here. |
||||||
| // Same story for `/`: the snapshot's slashCommands come from a probe run | ||||||
| // against the server's own cwd, so they are the CLI's built-ins only. The | ||||||
| // workspace-scoped result adds the project's own commands. | ||||||
| const activeSlashCommands = | ||||||
| workspaceCapabilitiesQuery.data?.slashCommands && | ||||||
| workspaceCapabilitiesQuery.data.slashCommands.length > 0 | ||||||
| ? workspaceCapabilitiesQuery.data.slashCommands | ||||||
| : undefined; | ||||||
| const providerStatusBannerKey = getProviderStatusBannerKey(activeProviderStatus); | ||||||
| const [dismissedProviderStatusBannerKey, setDismissedProviderStatusBannerKey] = useState< | ||||||
| string | null | ||||||
|
|
@@ -6534,7 +6567,7 @@ function ChatViewContent(props: ChatViewProps) { | |||||
| resolvedTheme={resolvedTheme} | ||||||
| timestampFormat={timestampFormat} | ||||||
| workspaceRoot={activeWorkspaceRoot} | ||||||
| skills={activeProviderStatus?.skills ?? EMPTY_PROVIDER_SKILLS} | ||||||
| skills={activeSkills} | ||||||
| anchorMessageId={timelineAnchorMessageId} | ||||||
| onAnchorReady={onTimelineAnchorReady} | ||||||
| contentInsetEndAdjustment={composerOverlayHeight} | ||||||
|
|
@@ -6661,6 +6694,8 @@ function ChatViewContent(props: ChatViewProps) { | |||||
| interactionMode={interactionMode} | ||||||
| lockedProvider={lockedProvider} | ||||||
| providerStatuses={providerStatuses as ServerProvider[]} | ||||||
| workspaceSkills={activeSkills} | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The two resolutions can disagree: ChatView matches Suggest passing the raw query result and letting ChatComposer own the fallback, the same way
Suggested change
Posted via Macroscope — UI Consistency |
||||||
| workspaceSlashCommands={activeSlashCommands} | ||||||
| activeProjectDefaultModelSelection={ | ||||||
| activeProject?.defaultModelSelection | ||||||
| } | ||||||
|
|
||||||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Dropping this import leaves
makeClaudeCapabilitiesCacheKeyinClaudeHome.tswith no production caller — the only remaining references are inClaudeHome.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