From ce84ea7fae8d248845b343bdcea2f3c1807b7804 Mon Sep 17 00:00:00 2001 From: Rick Doorakkers Date: Sat, 25 Jul 2026 23:59:03 +0200 Subject: [PATCH] fix: discover project-level Claude skills for the picker in the desktop app Provider snapshot skill discovery runs against the server process cwd, which only matches the user's repository in the npx-t3-inside-a-repo flow. In the desktop app that cwd is unrelated to the project open in the sidebar, so project-level skills (/.claude/skills) never appeared in the $ picker even though they executed fine in threads. Add a server.discoverProviderSkills RPC that resolves skill discovery against the active project's own path (or worktree), exposed as an optional per-instance discoverSkills capability implemented by the Claude driver via the existing discoverClaudeSkills scan. The web app queries it per workspace and falls back to the snapshot skills while loading, on older servers, or for drivers without workspace discovery, so the npx flow and other providers are unchanged. Fixes #2048, fixes #2416. The Codex driver has the same server-cwd assumption (#3040) via its skills/list probe; the new per-instance capability is the seam to fix that separately. Co-Authored-By: Claude Fable 5 --- .../src/provider/Drivers/ClaudeDriver.ts | 10 ++ .../src/provider/Drivers/ClaudeSkills.test.ts | 48 +++++++++ apps/server/src/provider/ProviderDriver.ts | 12 +++ apps/server/src/server.test.ts | 100 +++++++++++++++--- apps/server/src/ws.ts | 18 ++++ apps/web/src/components/ChatView.tsx | 11 +- apps/web/src/components/chat/ChatComposer.tsx | 46 +++++--- apps/web/src/state/queries.ts | 32 ++++++ packages/client-runtime/src/state/server.ts | 6 ++ packages/contracts/src/rpc.ts | 10 ++ packages/contracts/src/server.ts | 15 +++ 11 files changed, 280 insertions(+), 28 deletions(-) diff --git a/apps/server/src/provider/Drivers/ClaudeDriver.ts b/apps/server/src/provider/Drivers/ClaudeDriver.ts index c2fc11311aa..ca025dcba1a 100644 --- a/apps/server/src/provider/Drivers/ClaudeDriver.ts +++ b/apps/server/src/provider/Drivers/ClaudeDriver.ts @@ -54,6 +54,7 @@ import { type ProviderSnapshotSettings, } from "../providerUpdateSettings.ts"; import { makeClaudeCapabilitiesCacheKey, makeClaudeContinuationGroupKey } from "./ClaudeHome.ts"; +import { discoverClaudeSkills } from "./ClaudeSkills.ts"; const decodeClaudeSettings = Schema.decodeSync(ClaudeSettings); const DRIVER_KIND = ProviderDriverKind.make("claudeAgent"); @@ -216,6 +217,15 @@ export const ClaudeDriver: ProviderDriver = { snapshot, adapter, textGeneration, + // Workspace-scoped discovery for the `$` picker: the snapshot scans + // against the server process cwd, which is unrelated to the active + // project in the desktop app. Transports call this with the + // project's own path instead. + discoverSkills: (projectCwd: string) => + discoverClaudeSkills(effectiveConfig, projectCwd, processEnv).pipe( + Effect.provideService(FileSystem.FileSystem, fileSystem), + Effect.provideService(Path.Path, path), + ), } satisfies ProviderInstance; }), }; diff --git a/apps/server/src/provider/Drivers/ClaudeSkills.test.ts b/apps/server/src/provider/Drivers/ClaudeSkills.test.ts index 1ad843d7573..fa2903a1059 100644 --- a/apps/server/src/provider/Drivers/ClaudeSkills.test.ts +++ b/apps/server/src/provider/Drivers/ClaudeSkills.test.ts @@ -188,6 +188,54 @@ it.layer(NodeServices.layer)("discoverClaudeSkills", (it) => { }), ); + it.effect("scopes project skills to the workspace passed in, not the process cwd", () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-claude-skills-" }); + const configDir = path.join(tempDir, "claude-home"); + const projectAlpha = path.join(tempDir, "project-alpha"); + const projectBeta = path.join(tempDir, "project-beta"); + + yield* writeSkill( + path.join(configDir, "skills"), + "shared", + ["---", "name: shared", "description: User-scoped.", "---"].join("\n"), + ); + yield* writeSkill( + path.join(projectAlpha, ".claude", "skills"), + "alpha-deploy", + ["---", "name: alpha-deploy", "---"].join("\n"), + ); + yield* writeSkill( + path.join(projectBeta, ".claude", "skills"), + "beta-release", + ["---", "name: beta-release", "---"].join("\n"), + ); + + // The desktop app runs the server from a cwd unrelated to any project; + // discovery keyed by each project's own path must return that + // project's skills without leaking the other project's set. + const alphaSkills = yield* discoverClaudeSkills({ homePath: configDir }, projectAlpha); + const betaSkills = yield* discoverClaudeSkills({ homePath: configDir }, projectBeta); + + assert.deepEqual( + alphaSkills.map((skill) => [skill.name, skill.scope]), + [ + ["alpha-deploy", "project"], + ["shared", "user"], + ], + ); + assert.deepEqual( + betaSkills.map((skill) => [skill.name, skill.scope]), + [ + ["beta-release", "project"], + ["shared", "user"], + ], + ); + }), + ); + it.effect("returns an empty list when no skill roots exist", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; diff --git a/apps/server/src/provider/ProviderDriver.ts b/apps/server/src/provider/ProviderDriver.ts index c738882c23a..262449cc827 100644 --- a/apps/server/src/provider/ProviderDriver.ts +++ b/apps/server/src/provider/ProviderDriver.ts @@ -25,6 +25,7 @@ import type { ProviderDriverKind, ProviderInstanceEnvironment, ProviderInstanceId, + ServerProviderSkill, } from "@t3tools/contracts"; import type * as Effect from "effect/Effect"; import type * as Schema from "effect/Schema"; @@ -71,6 +72,17 @@ export interface ProviderInstance { readonly snapshot: ServerProviderShape; readonly adapter: ProviderAdapterShape; readonly textGeneration: TextGeneration.TextGeneration["Service"]; + /** + * Best-effort skill discovery resolved against a specific workspace root. + * The snapshot's `skills` are scanned against the server process cwd, + * which only matches the user's repository in the `npx t3`-inside-a-repo + * flow; transports call this with the active project's path so the `$` + * picker sees that project's skills. Absent when the driver has no + * workspace-scoped skill discovery — callers then fall back to the + * snapshot's `skills`. Must never fail; unreadable roots yield fewer + * skills, not errors. + */ + readonly discoverSkills?: (cwd: string) => Effect.Effect>; } export interface ProviderContinuationIdentity { diff --git a/apps/server/src/server.test.ts b/apps/server/src/server.test.ts index 871b79eca90..d64bb7de452 100644 --- a/apps/server/src/server.test.ts +++ b/apps/server/src/server.test.ts @@ -82,6 +82,8 @@ import { OrchestrationListenerCallbackError } from "./orchestration/Errors.ts"; import * as ProjectionSnapshotQuery from "./orchestration/Services/ProjectionSnapshotQuery.ts"; import { SqlitePersistenceMemory } from "./persistence/Layers/Sqlite.ts"; import { PersistenceSqlError } from "./persistence/Errors.ts"; +import type { ProviderInstance } from "./provider/ProviderDriver.ts"; +import * as ProviderInstanceRegistry from "./provider/Services/ProviderInstanceRegistry.ts"; import * as ProviderRegistry from "./provider/Services/ProviderRegistry.ts"; import { makeManualOnlyProviderMaintenanceCapabilities } from "./provider/providerMaintenance.ts"; import * as ServerLifecycleEvents from "./serverLifecycleEvents.ts"; @@ -323,6 +325,9 @@ const buildAppUnderTest = (options?: { layers?: { keybindings?: Partial; providerRegistry?: Partial; + providerInstanceRegistry?: Partial< + ProviderInstanceRegistry.ProviderInstanceRegistry["Service"] + >; serverSettings?: Partial; externalLauncher?: Partial; vcsDriver?: Partial; @@ -544,18 +549,27 @@ const buildAppUnderTest = (options?: { }), ), Layer.provide( - Layer.mock(ProviderRegistry.ProviderRegistry)({ - getProviders: Effect.succeed([]), - refresh: () => Effect.succeed([]), - refreshInstance: () => Effect.succeed([]), - getProviderMaintenanceCapabilitiesForInstance: (_instanceId, provider) => - Effect.succeed( - makeManualOnlyProviderMaintenanceCapabilities({ provider, packageName: null }), - ), - setProviderMaintenanceActionState: () => Effect.succeed([]), - streamChanges: Stream.empty, - ...options?.layers?.providerRegistry, - }), + Layer.mergeAll( + Layer.mock(ProviderRegistry.ProviderRegistry)({ + getProviders: Effect.succeed([]), + refresh: () => Effect.succeed([]), + refreshInstance: () => Effect.succeed([]), + getProviderMaintenanceCapabilitiesForInstance: (_instanceId, provider) => + Effect.succeed( + makeManualOnlyProviderMaintenanceCapabilities({ provider, packageName: null }), + ), + setProviderMaintenanceActionState: () => Effect.succeed([]), + streamChanges: Stream.empty, + ...options?.layers?.providerRegistry, + }), + Layer.mock(ProviderInstanceRegistry.ProviderInstanceRegistry)({ + getInstance: () => Effect.succeed(undefined), + listInstances: Effect.succeed([]), + listUnavailable: Effect.succeed([]), + streamChanges: Stream.empty, + ...options?.layers?.providerInstanceRegistry, + }), + ), ), Layer.provide( Layer.mock(ServerSettings.ServerSettingsService)({ @@ -4456,6 +4470,68 @@ it.layer(NodeServices.layer)("server router seam", (it) => { }).pipe(Effect.provide(NodeHttpServer.layerTest), TestClock.withLive), ); + it.effect( + "routes websocket rpc server.discoverProviderSkills against the requested workspace", + () => + Effect.gen(function* () { + const claudeInstanceId = ProviderInstanceId.make("claudeAgent"); + const requestedCwds: Array = []; + // The handler only reaches for `discoverSkills`; the rest of the + // instance is irrelevant to this route. + const fakeInstance = { + discoverSkills: (cwd: string) => { + requestedCwds.push(cwd); + return Effect.succeed([ + { + name: "deploy", + path: `${cwd}/.claude/skills/deploy/SKILL.md`, + enabled: true, + scope: "project", + }, + ]); + }, + } as unknown as ProviderInstance; + + yield* buildAppUnderTest({ + layers: { + providerInstanceRegistry: { + getInstance: (instanceId) => + Effect.succeed(instanceId === claudeInstanceId ? fakeInstance : undefined), + }, + }, + }); + + const wsUrl = yield* getWsServerUrl("/ws"); + const response = yield* Effect.scoped( + withWsRpcClient(wsUrl, (client) => + Effect.all({ + discovered: client[WS_METHODS.serverDiscoverProviderSkills]({ + instanceId: claudeInstanceId, + cwd: "/projects/alpha", + }), + // Instances without workspace discovery (or unknown ids) answer + // `skills: null` so clients keep the snapshot's skills. + unsupported: client[WS_METHODS.serverDiscoverProviderSkills]({ + instanceId: ProviderInstanceId.make("codex"), + cwd: "/projects/alpha", + }), + }), + ), + ); + + assert.deepEqual(requestedCwds, ["/projects/alpha"]); + assert.deepEqual(response.discovered.skills, [ + { + name: "deploy", + path: "/projects/alpha/.claude/skills/deploy/SKILL.md", + enabled: true, + scope: "project", + }, + ]); + assert.isNull(response.unsupported.skills); + }).pipe(Effect.provide(NodeHttpServer.layerTest), TestClock.withLive), + ); + it.effect("routes websocket rpc projects.searchEntries excludes gitignored files", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; diff --git a/apps/server/src/ws.ts b/apps/server/src/ws.ts index f6f46d1e76e..4928488a2aa 100644 --- a/apps/server/src/ws.ts +++ b/apps/server/src/ws.ts @@ -76,6 +76,7 @@ import { observeRpcStream as instrumentRpcStream, observeRpcStreamEffect as instrumentRpcStreamEffect, } from "./observability/RpcInstrumentation.ts"; +import * as ProviderInstanceRegistry from "./provider/Services/ProviderInstanceRegistry.ts"; import * as ProviderRegistry from "./provider/Services/ProviderRegistry.ts"; import * as ProviderMaintenanceRunner from "./provider/providerMaintenanceRunner.ts"; import * as ServerSelfUpdate from "./cloud/selfUpdate.ts"; @@ -297,6 +298,7 @@ const RPC_REQUIRED_SCOPE = new Map([ [WS_METHODS.serverGetConfig, AuthOrchestrationReadScope], [WS_METHODS.serverRefreshProviders, AuthOrchestrationOperateScope], [WS_METHODS.serverUpdateProvider, AuthOrchestrationOperateScope], + [WS_METHODS.serverDiscoverProviderSkills, AuthOrchestrationReadScope], [WS_METHODS.serverUpdateServer, AuthOrchestrationOperateScope], [WS_METHODS.serverUpsertKeybinding, AuthOrchestrationOperateScope], [WS_METHODS.serverRemoveKeybinding, AuthOrchestrationOperateScope], @@ -419,6 +421,7 @@ const makeWsRpcLayer = ( const previewManager = yield* PreviewManager.PreviewManager; const portDiscovery = yield* PortScanner.PortDiscovery; const providerRegistry = yield* ProviderRegistry.ProviderRegistry; + const providerInstanceRegistry = yield* ProviderInstanceRegistry.ProviderInstanceRegistry; const providerMaintenanceRunner = yield* ProviderMaintenanceRunner.ProviderMaintenanceRunner; const serverSelfUpdate = yield* ServerSelfUpdate.ServerSelfUpdate; const config = yield* ServerConfig.ServerConfig; @@ -1479,6 +1482,21 @@ const makeWsRpcLayer = ( "rpc.aggregate": "server", }, ), + [WS_METHODS.serverDiscoverProviderSkills]: (input) => + observeRpcEffect( + WS_METHODS.serverDiscoverProviderSkills, + Effect.gen(function* () { + const instance = yield* providerInstanceRegistry.getInstance(input.instanceId); + // `skills: null` tells the client this driver has no + // workspace-scoped discovery, so it keeps using the snapshot's + // skills instead of treating the result as "no skills here". + if (!instance?.discoverSkills) { + return { skills: null }; + } + return { skills: yield* instance.discoverSkills(input.cwd) }; + }), + { "rpc.aggregate": "server" }, + ), [WS_METHODS.serverUpdateServer]: (input) => observeRpcEffect(WS_METHODS.serverUpdateServer, serverSelfUpdate.update(input), { "rpc.aggregate": "server", diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index ab1256cddb3..f957e8d9654 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -151,6 +151,7 @@ import { cn, randomHex } from "~/lib/utils"; import { COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS } from "~/workspaceTitlebar"; import { stackedThreadToast, toastManager } from "./ui/toast"; import { decodeProjectScriptKeybindingRule } from "~/lib/projectScriptKeybindings"; +import { useWorkspaceProviderSkills } from "~/state/queries"; import { type NewProjectScriptInput } from "./ProjectScriptsControl"; import { buildProjectScript, @@ -2354,6 +2355,14 @@ function ChatViewContent(props: ChatViewProps) { const defaultInstanceId = defaultInstanceIdForDriver(selectedProvider); return providerStatuses.find((status) => status.instanceId === defaultInstanceId) ?? null; }, [activeProviderInstanceId, providerStatuses, selectedProvider]); + // Skills resolved against the active workspace (project path / worktree) + // rather than the server process cwd baked into the snapshot. + const workspaceProviderSkills = useWorkspaceProviderSkills({ + environmentId, + instanceId: activeProviderStatus?.instanceId ?? null, + cwd: gitCwd, + snapshotSkills: activeProviderStatus?.skills ?? EMPTY_PROVIDER_SKILLS, + }); const providerStatusBannerKey = getProviderStatusBannerKey(activeProviderStatus); const [dismissedProviderStatusBannerKey, setDismissedProviderStatusBannerKey] = useState< string | null @@ -5698,7 +5707,7 @@ function ChatViewContent(props: ChatViewProps) { resolvedTheme={resolvedTheme} timestampFormat={timestampFormat} workspaceRoot={activeWorkspaceRoot} - skills={activeProviderStatus?.skills ?? EMPTY_PROVIDER_SKILLS} + skills={workspaceProviderSkills} anchorMessageId={timelineAnchorMessageId} onAnchorReady={onTimelineAnchorReady} onAnchorSizeChanged={onTimelineAnchorSizeChanged} diff --git a/apps/web/src/components/chat/ChatComposer.tsx b/apps/web/src/components/chat/ChatComposer.tsx index b427037bdf7..411b58a0bb6 100644 --- a/apps/web/src/components/chat/ChatComposer.tsx +++ b/apps/web/src/components/chat/ChatComposer.tsx @@ -9,6 +9,7 @@ import type { RuntimeMode, ScopedThreadRef, ServerProvider, + ServerProviderSkill, ThreadId, TurnId, } from "@t3tools/contracts"; @@ -62,6 +63,7 @@ import { removeInlineTerminalContextPlaceholder, } from "../../lib/terminalContext"; import { useComposerPathSearch } from "../../lib/composerPathSearchState"; +import { useWorkspaceProviderSkills } from "../../state/queries"; import { type ElementContextDraft } from "../../lib/elementContext"; import { ComposerPendingElementContexts } from "./ComposerPendingElementContexts"; import { ComposerPendingReviewComments } from "./ComposerPendingReviewComments"; @@ -93,6 +95,8 @@ import { basenameOfPath } from "../../pierre-icons"; import { cn, randomUUID } from "~/lib/utils"; import { Separator } from "../ui/separator"; +const EMPTY_PROVIDER_SKILLS: ReadonlyArray = []; + function ComposerCommandMenuLayer(props: { anchor: HTMLElement | null; children: ReactNode }) { const [position, setPosition] = useState<{ bottom: number; @@ -864,6 +868,14 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) () => selectedProviderEntry?.models ?? [], [selectedProviderEntry], ); + // Skills resolved against the active workspace (project path / worktree) + // rather than the server process cwd baked into the snapshot. + const workspaceProviderSkills = useWorkspaceProviderSkills({ + environmentId, + instanceId: selectedProviderEntry ? selectedInstanceId : null, + cwd: gitCwd, + snapshotSkills: selectedProviderStatus?.skills ?? EMPTY_PROVIDER_SKILLS, + }); const composerPromptInjectionState = useMemo( () => getComposerPromptInjectionState(prompt), @@ -1071,22 +1083,26 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) return searchSlashCommandItems(slashCommandItems, query); } if (composerTrigger.kind === "skill") { - return searchProviderSkills(selectedProviderStatus?.skills ?? [], composerTrigger.query).map( - (skill) => ({ - id: `skill:${selectedProvider}:${skill.name}`, - type: "skill" as const, - provider: selectedProvider, - skill, - label: formatProviderSkillDisplayName(skill), - description: - skill.shortDescription ?? - skill.description ?? - (skill.scope ? `${skill.scope} skill` : "Run provider skill"), - }), - ); + return searchProviderSkills(workspaceProviderSkills, composerTrigger.query).map((skill) => ({ + id: `skill:${selectedProvider}:${skill.name}`, + type: "skill" as const, + provider: selectedProvider, + skill, + label: formatProviderSkillDisplayName(skill), + description: + skill.shortDescription ?? + skill.description ?? + (skill.scope ? `${skill.scope} skill` : "Run provider skill"), + })); } return []; - }, [composerTrigger, selectedProvider, selectedProviderStatus, workspaceEntries.entries]); + }, [ + composerTrigger, + selectedProvider, + selectedProviderStatus, + workspaceEntries.entries, + workspaceProviderSkills, + ]); const composerMenuOpen = Boolean(composerTrigger); const composerMenuSearchKey = composerTrigger @@ -2559,7 +2575,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) ? composerTerminalContexts : [] } - skills={selectedProviderStatus?.skills ?? []} + skills={workspaceProviderSkills} {...(showMobilePendingAnswerActions ? { className: "max-sm:pb-11" } : {})} onRemoveTerminalContext={removeComposerTerminalContextFromDraft} onChange={onPromptChange} diff --git a/apps/web/src/state/queries.ts b/apps/web/src/state/queries.ts index 79737e6109e..28be8be4d0b 100644 --- a/apps/web/src/state/queries.ts +++ b/apps/web/src/state/queries.ts @@ -7,6 +7,8 @@ import { type VcsRefTarget } from "@t3tools/client-runtime/state/vcs"; import type { EnvironmentId, OrchestrationThread, + ProviderInstanceId, + ServerProviderSkill, ThreadId, VcsListRefsResult, VcsRef, @@ -20,6 +22,7 @@ import { appAtomRegistry } from "../rpc/atomRegistry"; import { orchestrationEnvironment } from "./orchestration"; import { projectEnvironment } from "./projects"; import { useEnvironmentQuery } from "./query"; +import { serverEnvironment } from "./server"; import { useEnvironmentThread } from "./threads"; import { vcsEnvironment } from "./vcs"; @@ -214,6 +217,35 @@ export function useComposerPathSearch(target: ComposerPathSearchTarget) { }; } +/** + * Provider skills resolved against the active workspace, for the `$` picker + * and inline skill rendering. + * + * The provider snapshot's `skills` are discovered against the server process + * cwd, which only matches the user's repository in the `npx t3`-inside-a-repo + * flow — in the desktop app that cwd is unrelated to the project open in the + * sidebar, so project-scoped skills never showed up there. This asks the + * server to re-run discovery against the workspace's own path and falls back + * to the snapshot list while loading, on error (older servers without the + * RPC), or when the driver has no workspace-scoped discovery (`skills: null`). + */ +export function useWorkspaceProviderSkills(input: { + readonly environmentId: EnvironmentId; + readonly instanceId: ProviderInstanceId | null; + readonly cwd: string | null; + readonly snapshotSkills: ReadonlyArray; +}): ReadonlyArray { + const result = useEnvironmentQuery( + input.instanceId !== null && input.cwd !== null + ? serverEnvironment.discoverProviderSkills({ + environmentId: input.environmentId, + input: { instanceId: input.instanceId, cwd: input.cwd }, + }) + : null, + ); + return result.data?.skills ?? input.snapshotSkills; +} + export function useCheckpointDiff( target: CheckpointDiffTarget, options?: { readonly enabled?: boolean }, diff --git a/packages/client-runtime/src/state/server.ts b/packages/client-runtime/src/state/server.ts index ea7f5fb6d75..294d892d02c 100644 --- a/packages/client-runtime/src/state/server.ts +++ b/packages/client-runtime/src/state/server.ts @@ -307,6 +307,12 @@ export function createServerEnvironmentAtoms( Stream.mapAccum(Option.none, projectServerWelcome), ), }), + discoverProviderSkills: createEnvironmentRpcQueryAtomFamily(runtime, { + label: "environment-data:server:discover-provider-skills", + tag: WS_METHODS.serverDiscoverProviderSkills, + staleTimeMs: 15_000, + idleTtlMs: 5 * 60_000, + }), refreshProviders: createEnvironmentRpcCommand(runtime, { label: "environment-data:server:refresh-providers", tag: WS_METHODS.serverRefreshProviders, diff --git a/packages/contracts/src/rpc.ts b/packages/contracts/src/rpc.ts index fa2d23b8ef2..c7f0344296e 100644 --- a/packages/contracts/src/rpc.ts +++ b/packages/contracts/src/rpc.ts @@ -116,6 +116,8 @@ import { import { ServerConfigStreamEvent, ServerConfig, + ServerProviderDiscoverSkillsInput, + ServerProviderDiscoverSkillsResult, ServerProviderUpdateError, ServerProviderUpdateInput, ServerLifecycleStreamEvent, @@ -208,6 +210,7 @@ export const WS_METHODS = { serverGetConfig: "server.getConfig", serverRefreshProviders: "server.refreshProviders", serverUpdateProvider: "server.updateProvider", + serverDiscoverProviderSkills: "server.discoverProviderSkills", serverUpdateServer: "server.updateServer", serverUpsertKeybinding: "server.upsertKeybinding", serverRemoveKeybinding: "server.removeKeybinding", @@ -283,6 +286,12 @@ export const WsServerUpdateProviderRpc = Rpc.make(WS_METHODS.serverUpdateProvide error: Schema.Union([ServerProviderUpdateError, EnvironmentAuthorizationError]), }); +export const WsServerDiscoverProviderSkillsRpc = Rpc.make(WS_METHODS.serverDiscoverProviderSkills, { + payload: ServerProviderDiscoverSkillsInput, + success: ServerProviderDiscoverSkillsResult, + error: EnvironmentAuthorizationError, +}); + export const WsServerUpdateServerRpc = Rpc.make(WS_METHODS.serverUpdateServer, { payload: ServerSelfUpdateInput, success: ServerSelfUpdateResult, @@ -703,6 +712,7 @@ export const WsRpcGroup = RpcGroup.make( WsServerGetConfigRpc, WsServerRefreshProvidersRpc, WsServerUpdateProviderRpc, + WsServerDiscoverProviderSkillsRpc, WsServerUpdateServerRpc, WsServerUpsertKeybindingRpc, WsServerRemoveKeybindingRpc, diff --git a/packages/contracts/src/server.ts b/packages/contracts/src/server.ts index 69699c7a839..a7a1c0160bf 100644 --- a/packages/contracts/src/server.ts +++ b/packages/contracts/src/server.ts @@ -575,6 +575,21 @@ export class ServerProviderUpdateError extends Schema.TaggedErrorClass