| author | |
| committer | |
| log | 921cb3c8bc772309ccab4c422b091d0a91497a49 |
| tree | 022833348398bed1a93702256eee2351b004910f |
| parent | 7ab800a43c770b2c48a2d9306c89b6177f61ad4a |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>10 files changed, 222 insertions(+), 28 deletions(-)
apps/server/src/provider/Layers/AntigravityProvider.test.ts+12| ... | ... | @@ -620,6 +620,18 @@ it.layer(testLayer)("Antigravity provider snapshots", (it) => { |
| 620 | 620 | after.workspaceSnapshots?.find((entry) => entry.cwd === "/workspace")?.skills, |
| 621 | 621 | ).toEqual(skills); |
| 622 | 622 | expect((yield* harness.provider.snapshotForCwd("/workspace")).skills).toEqual(skills); |
| 623 | ||
| 624 | const rescanned = [ | |
| 625 | ...skills, | |
| 626 | { name: "review", path: "/workspace/.agent/skills/review", enabled: true }, | |
| 627 | ]; | |
| 628 | yield* harness.provider.snapshotForCwd("/workspace", rescanned); | |
| 629 | yield* harness.provider.onSessionStarted(started, "/workspace"); | |
| 630 | expect( | |
| 631 | (yield* harness.provider.snapshot.getSnapshot).workspaceSnapshots?.find( | |
| 632 | (entry) => entry.cwd === "/workspace", | |
| 633 | )?.skills, | |
| 634 | ).toEqual(rescanned); | |
| 623 | 635 | }), |
| 624 | 636 | ), |
| 625 | 637 | ); |
apps/server/src/provider/Layers/AntigravityProvider.ts+18-1| ... | ... | @@ -377,7 +377,24 @@ export const makeAntigravityProvider = Effect.fn("makeAntigravityProvider")(func |
| 377 | 377 | cwd: string, |
| 378 | 378 | skills?: ServerProvider["skills"], |
| 379 | 379 | ) { |
| 380 | if (skills) discoveredSkills.set(cwd, skills); | |
| 380 | if (skills) { | |
| 381 | discoveredSkills.set(cwd, skills); | |
| 382 | // A rescan replaces the stored entry's skills. Session callbacks and | |
| 383 | // health checks republish that entry, so it must not keep old ones. | |
| 384 | yield* SubscriptionRef.update(metadata, (state) => | |
| 385 | state.draft.workspaceSnapshots?.some((entry) => entry.cwd === cwd) | |
| 386 | ? { | |
| 387 | ...state, | |
| 388 | draft: { | |
| 389 | ...state.draft, | |
| 390 | workspaceSnapshots: state.draft.workspaceSnapshots.map((entry) => | |
| 391 | entry.cwd === cwd ? { ...entry, skills } : entry, | |
| 392 | ), | |
| 393 | }, | |
| 394 | } | |
| 395 | : state, | |
| 396 | ); | |
| 397 | } | |
| 381 | 398 | const snapshot = yield* getSnapshot; |
| 382 | 399 | const workspace = snapshot.workspaceSnapshots?.find((entry) => entry.cwd === cwd); |
| 383 | 400 | const resolvedSkills = skills ?? workspace?.skills ?? discoveredSkills.get(cwd) ?? []; |
apps/server/src/provider/Layers/ProviderRegistry.test.ts+57-1| ... | ... | @@ -1479,6 +1479,12 @@ it.layer(Layer.mergeAll(TestNodeServices, ServerSettingsModule.layerTest(), Test |
| 1479 | 1479 | slashCommands: [], |
| 1480 | 1480 | } as const satisfies ServerProvider; |
| 1481 | 1481 | const snapshotCalls = yield* Ref.make(0); |
| 1482 | const scopedResult = yield* Ref.make<ServerProvider>(scopedProvider); | |
| 1483 | const cacheInvalidations = yield* Ref.make(0); | |
| 1484 | const scanGate = yield* Ref.make<{ | |
| 1485 | readonly started: Deferred.Deferred<void>; | |
| 1486 | readonly release: Deferred.Deferred<void>; | |
| 1487 | } | null>(null); | |
| 1482 | 1488 | const returnPendingSnapshot = yield* Ref.make(true); |
| 1483 | 1489 | const probeStarted = yield* Deferred.make<void>(); |
| 1484 | 1490 | const releaseProbe = yield* Deferred.make<void>(); |
| ... | ... | @@ -1508,6 +1514,7 @@ it.layer(Layer.mergeAll(TestNodeServices, ServerSettingsModule.layerTest(), Test |
| 1508 | 1514 | applyUsageLimits: () => Effect.void, |
| 1509 | 1515 | }, |
| 1510 | 1516 | snapshotForCwd, |
| 1517 | invalidateCaches: Ref.update(cacheInvalidations, (count) => count + 1), | |
| 1511 | 1518 | adapter: {} as ProviderInstance["adapter"], |
| 1512 | 1519 | textGeneration: {} as ProviderInstance["textGeneration"], |
| 1513 | 1520 | }); |
| ... | ... | @@ -1517,7 +1524,13 @@ it.layer(Layer.mergeAll(TestNodeServices, ServerSettingsModule.layerTest(), Test |
| 1517 | 1524 | if (yield* Ref.get(returnPendingSnapshot)) return pendingScopedProvider; |
| 1518 | 1525 | yield* Deferred.succeed(probeStarted, undefined); |
| 1519 | 1526 | yield* Deferred.await(releaseProbe); |
| 1520 | return scopedProvider; | |
| 1527 | const result = yield* Ref.get(scopedResult); | |
| 1528 | const gate = yield* Ref.getAndSet(scanGate, null); | |
| 1529 | if (gate) { | |
| 1530 | yield* Deferred.succeed(gate.started, undefined); | |
| 1531 | yield* Deferred.await(gate.release); | |
| 1532 | } | |
| 1533 | return result; | |
| 1521 | 1534 | }), |
| 1522 | 1535 | ); |
| 1523 | 1536 | const rebuiltProvider = { |
| ... | ... | @@ -1593,6 +1606,49 @@ it.layer(Layer.mergeAll(TestNodeServices, ServerSettingsModule.layerTest(), Test |
| 1593 | 1606 | ); |
| 1594 | 1607 | yield* registry.refreshWorkspaceSnapshot({ instanceId, cwd: "/workspace" }); |
| 1595 | 1608 | assert.strictEqual(yield* Ref.get(snapshotCalls), 2); |
| 1609 | const newSkills = [ | |
| 1610 | ...scopedProvider.skills, | |
| 1611 | { name: "added", path: "/workspace/added/SKILL.md", enabled: true }, | |
| 1612 | ]; | |
| 1613 | yield* Ref.set(scopedResult, { ...scopedProvider, skills: newSkills }); | |
| 1614 | yield* registry.refreshWorkspaceSnapshot({ | |
| 1615 | instanceId, | |
| 1616 | cwd: "/workspace", | |
| 1617 | fresh: true, | |
| 1618 | }); | |
| 1619 | assert.strictEqual(yield* Ref.get(snapshotCalls), 3); | |
| 1620 | assert.strictEqual(yield* Ref.get(cacheInvalidations), 1); | |
| 1621 | assert.deepStrictEqual( | |
| 1622 | (yield* registry.getProviders)[0]?.workspaceSnapshots?.map((s) => s.skills), | |
| 1623 | [newSkills], | |
| 1624 | ); | |
| 1625 | ||
| 1626 | // A slow fresh scan that read older files must not overwrite a | |
| 1627 | // newer scan that finished first. | |
| 1628 | const slowStarted = yield* Deferred.make<void>(); | |
| 1629 | const releaseSlow = yield* Deferred.make<void>(); | |
| 1630 | yield* Ref.set(scanGate, { started: slowStarted, release: releaseSlow }); | |
| 1631 | yield* Ref.set(scopedResult, scopedProvider); | |
| 1632 | const slowScan = yield* registry | |
| 1633 | .refreshWorkspaceSnapshot({ instanceId, cwd: "/workspace", fresh: true }) | |
| 1634 | .pipe(Effect.forkChild); | |
| 1635 | yield* Deferred.await(slowStarted); | |
| 1636 | const latestSkills = [ | |
| 1637 | ...newSkills, | |
| 1638 | { name: "latest", path: "/workspace/latest/SKILL.md", enabled: true }, | |
| 1639 | ]; | |
| 1640 | yield* Ref.set(scopedResult, { ...scopedProvider, skills: latestSkills }); | |
| 1641 | yield* registry.refreshWorkspaceSnapshot({ | |
| 1642 | instanceId, | |
| 1643 | cwd: "/workspace", | |
| 1644 | fresh: true, | |
| 1645 | }); | |
| 1646 | yield* Deferred.succeed(releaseSlow, undefined); | |
| 1647 | yield* Fiber.join(slowScan); | |
| 1648 | assert.deepStrictEqual( | |
| 1649 | (yield* registry.getProviders)[0]?.workspaceSnapshots?.map((s) => s.skills), | |
| 1650 | [latestSkills], | |
| 1651 | ); | |
| 1596 | 1652 | |
| 1597 | 1653 | yield* Ref.set(instancesRef, [rebuiltInstance]); |
| 1598 | 1654 | yield* PubSub.publish(registryChanges, undefined); |
apps/server/src/provider/Layers/ProviderRegistry.ts+67-26| ... | ... | @@ -82,6 +82,15 @@ const hasModelCapabilities = (model: ServerProvider["models"][number]): boolean |
| 82 | 82 | |
| 83 | 83 | const MAX_WORKSPACE_SNAPSHOTS_PER_PROVIDER = 16; |
| 84 | 84 | |
| 85 | function dropProviderWorkspaceSnapshot(provider: ServerProvider, cwd: string): ServerProvider { | |
| 86 | return provider.workspaceSnapshots?.some((snapshot) => snapshot.cwd === cwd) | |
| 87 | ? { | |
| 88 | ...provider, | |
| 89 | workspaceSnapshots: provider.workspaceSnapshots.filter((snapshot) => snapshot.cwd !== cwd), | |
| 90 | } | |
| 91 | : provider; | |
| 92 | } | |
| 93 | ||
| 85 | 94 | export function upsertProviderWorkspaceSnapshot( |
| 86 | 95 | provider: ServerProvider, |
| 87 | 96 | cwd: string, |
| ... | ... | @@ -835,17 +844,44 @@ export const ProviderRegistryLive = Layer.effect( |
| 835 | 844 | return yield* Ref.get(providersRef); |
| 836 | 845 | }); |
| 837 | 846 | |
| 847 | const updateProviders = ( | |
| 848 | update: (providers: ReadonlyArray<ServerProvider>) => ReadonlyArray<ServerProvider>, | |
| 849 | ) => | |
| 850 | Ref.modify(providersRef, (currentProviders) => { | |
| 851 | const nextProviders = update(currentProviders); | |
| 852 | return [[currentProviders, nextProviders] as const, nextProviders]; | |
| 853 | }).pipe( | |
| 854 | Effect.tap(([previousProviders, nextProviders]) => | |
| 855 | haveProvidersChanged(previousProviders, nextProviders) | |
| 856 | ? PubSub.publish(changesPubSub, nextProviders) | |
| 857 | : Effect.void, | |
| 858 | ), | |
| 859 | Effect.map(([, nextProviders]) => nextProviders), | |
| 860 | ); | |
| 861 | ||
| 838 | 862 | const refreshWorkspaceSnapshot = Effect.fn("refreshWorkspaceSnapshot")(function* (input: { |
| 839 | 863 | readonly instanceId: ProviderInstanceId; |
| 840 | 864 | readonly cwd: string; |
| 865 | readonly fresh?: boolean; | |
| 841 | 866 | }) { |
| 867 | // Fresh scans drop other instances' snapshots for this cwd first, so a | |
| 868 | // composer on one of them scans again on next use, even when this | |
| 869 | // instance is gone or cannot be scanned. | |
| 870 | if (input.fresh) { | |
| 871 | yield* updateProviders((providers) => | |
| 872 | providers.map((candidate) => | |
| 873 | candidate.instanceId === input.instanceId | |
| 874 | ? candidate | |
| 875 | : dropProviderWorkspaceSnapshot(candidate, input.cwd), | |
| 876 | ), | |
| 877 | ); | |
| 878 | } | |
| 842 | 879 | const providers = yield* Ref.get(providersRef); |
| 843 | 880 | const provider = providers.find((candidate) => candidate.instanceId === input.instanceId); |
| 844 | if ( | |
| 845 | !provider || | |
| 846 | !provider.enabled || | |
| 847 | provider.workspaceSnapshots?.some((s) => s.cwd === input.cwd) | |
| 848 | ) { | |
| 881 | const workspaceSnapshotOf = (candidate: ServerProvider | undefined) => | |
| 882 | candidate?.workspaceSnapshots?.find((s) => s.cwd === input.cwd); | |
| 883 | const scannedFrom = workspaceSnapshotOf(provider); | |
| 884 | if (!provider || !provider.enabled || (!input.fresh && scannedFrom)) { | |
| 849 | 885 | return providers; |
| 850 | 886 | } |
| 851 | 887 | const instance = yield* instanceRegistry.getInstance(input.instanceId); |
| ... | ... | @@ -857,42 +893,47 @@ export const ProviderRegistryLive = Layer.effect( |
| 857 | 893 | next.set(instance, new Set(current).add(input.cwd)); |
| 858 | 894 | return [true, next] as const; |
| 859 | 895 | }); |
| 860 | if (!claimed) return yield* Ref.get(providersRef); | |
| 861 | return yield* instance.snapshotForCwd(input.cwd).pipe( | |
| 896 | // A fresh scan never joins a running one, which may predate the change. | |
| 897 | if (!claimed && !input.fresh) return yield* Ref.get(providersRef); | |
| 898 | // Fresh scans also re-read the machine snapshot: Claude's plugin | |
| 899 | // commands come from it, not from the cwd scan. | |
| 900 | const refreshMachineSnapshot = input.fresh | |
| 901 | ? (instance.invalidateCaches ?? Effect.void).pipe( | |
| 902 | Effect.andThen(refreshInstance(input.instanceId)), | |
| 903 | ) | |
| 904 | : Effect.void; | |
| 905 | return yield* refreshMachineSnapshot.pipe( | |
| 906 | Effect.andThen(instance.snapshotForCwd(input.cwd)), | |
| 862 | 907 | Effect.flatMap((scopedSnapshot) => |
| 863 | 908 | scopedSnapshot.status === "error" |
| 864 | 909 | ? Ref.get(providersRef) |
| 865 | 910 | : instanceRegistry.getInstance(input.instanceId).pipe( |
| 866 | 911 | Effect.flatMap((currentInstance) => { |
| 867 | 912 | if (currentInstance !== instance) return Ref.get(providersRef); |
| 868 | return Ref.modify(providersRef, (currentProviders) => { | |
| 869 | const nextProviders = currentProviders.map((candidate) => | |
| 913 | // Write only if the cwd's snapshot did not change during the | |
| 914 | // scan. A session event or another scan that landed first is newer. | |
| 915 | return updateProviders((currentProviders) => | |
| 916 | currentProviders.map((candidate) => | |
| 870 | 917 | candidate.instanceId === input.instanceId && |
| 871 | !candidate.workspaceSnapshots?.some((s) => s.cwd === input.cwd) | |
| 918 | Equal.equals(workspaceSnapshotOf(candidate), scannedFrom) | |
| 872 | 919 | ? upsertProviderWorkspaceSnapshot(candidate, input.cwd, scopedSnapshot) |
| 873 | 920 | : candidate, |
| 874 | ); | |
| 875 | return [[currentProviders, nextProviders] as const, nextProviders]; | |
| 876 | }).pipe( | |
| 877 | Effect.tap(([previousProviders, nextProviders]) => | |
| 878 | haveProvidersChanged(previousProviders, nextProviders) | |
| 879 | ? PubSub.publish(changesPubSub, nextProviders) | |
| 880 | : Effect.void, | |
| 881 | 921 | ), |
| 882 | Effect.map(([, nextProviders]) => nextProviders), | |
| 883 | 922 | ); |
| 884 | 923 | }), |
| 885 | 924 | ), |
| 886 | 925 | ), |
| 887 | 926 | Effect.ensuring( |
| 888 | Ref.update(workspaceRefreshesRef, (refreshes) => { | |
| 889 | const next = new Map(refreshes); | |
| 890 | const current = new Set(next.get(instance)); | |
| 891 | current.delete(input.cwd); | |
| 892 | if (current.size) next.set(instance, current); | |
| 893 | else next.delete(instance); | |
| 894 | return next; | |
| 895 | }), | |
| 927 | claimed | |
| 928 | ? Ref.update(workspaceRefreshesRef, (refreshes) => { | |
| 929 | const next = new Map(refreshes); | |
| 930 | const current = new Set(next.get(instance)); | |
| 931 | current.delete(input.cwd); | |
| 932 | if (current.size) next.set(instance, current); | |
| 933 | else next.delete(instance); | |
| 934 | return next; | |
| 935 | }) | |
| 936 | : Effect.void, | |
| 896 | 937 | ), |
| 897 | 938 | ); |
| 898 | 939 | }); |
apps/server/src/provider/Services/ProviderRegistry.ts+7| ... | ... | @@ -48,9 +48,16 @@ export interface ProviderRegistryShape { |
| 48 | 48 | instanceId: ProviderInstanceId, |
| 49 | 49 | ) => Effect.Effect<ReadonlyArray<ServerProvider>>; |
| 50 | 50 | |
| 51 | /** | |
| 52 | * Fill the skills and slash commands snapshot for one cwd. A cwd that | |
| 53 | * already has a snapshot is left alone unless `fresh` is set. A fresh scan | |
| 54 | * also refreshes the instance's machine snapshot and drops other | |
| 55 | * instances' snapshots for the cwd. | |
| 56 | */ | |
| 51 | 57 | readonly refreshWorkspaceSnapshot: (input: { |
| 52 | 58 | readonly instanceId: ProviderInstanceId; |
| 53 | 59 | readonly cwd: string; |
| 60 | readonly fresh?: boolean; | |
| 54 | 61 | }) => Effect.Effect<ReadonlyArray<ServerProvider>>; |
| 55 | 62 | |
| 56 | 63 | /** |
apps/server/src/ws.ts+1| ... | ... | @@ -2612,6 +2612,7 @@ const makeWsRpcLayer = ( |
| 2612 | 2612 | ? providerRegistry.refreshWorkspaceSnapshot({ |
| 2613 | 2613 | instanceId: input.instanceId, |
| 2614 | 2614 | cwd: input.cwd, |
| 2615 | fresh: input.fresh === true, | |
| 2615 | 2616 | }) |
| 2616 | 2617 | : input.instanceId !== undefined |
| 2617 | 2618 | ? providerRegistry.refreshInstance(input.instanceId) |
apps/web/src/components/CommandPalette.tsx+52| ... | ... | @@ -53,6 +53,7 @@ import { |
| 53 | 53 | MonitorIcon, |
| 54 | 54 | MoonIcon, |
| 55 | 55 | PaletteIcon, |
| 56 | RotateCcwIcon, | |
| 56 | 57 | SettingsIcon, |
| 57 | 58 | SquarePenIcon, |
| 58 | 59 | SunIcon, |
| ... | ... | @@ -93,6 +94,8 @@ import { desktopLocalBackendId } from "../connection/desktopLocal"; |
| 93 | 94 | import { filesystemEnvironment } from "../state/filesystem"; |
| 94 | 95 | import { projectEnvironment } from "../state/projects"; |
| 95 | 96 | import { useEnvironmentQuery } from "../state/query"; |
| 97 | import { serverEnvironment } from "../state/server"; | |
| 98 | import { threadEnvironment } from "../state/threads"; | |
| 96 | 99 | import { sourceControlEnvironment } from "../state/sourceControl"; |
| 97 | 100 | import { useAtomCommand } from "../state/use-atom-command"; |
| 98 | 101 | import { useAtomQueryRunner } from "../state/use-atom-query-runner"; |
| ... | ... | @@ -725,6 +728,12 @@ function OpenCommandPaletteDialog(props: { |
| 725 | 728 | const startProjectClone = useAtomCommand(sourceControlEnvironment.startProjectClone, { |
| 726 | 729 | reportFailure: false, |
| 727 | 730 | }); |
| 731 | const stopThreadSession = useAtomCommand(threadEnvironment.stopSession, { | |
| 732 | reportFailure: false, | |
| 733 | }); | |
| 734 | const refreshProviders = useAtomCommand(serverEnvironment.refreshProviders, { | |
| 735 | reportFailure: false, | |
| 736 | }); | |
| 728 | 737 | const { environments } = useEnvironments(); |
| 729 | 738 | const desktopLocalBootstraps = useDesktopLocalBootstraps(); |
| 730 | 739 | const primaryEnvironmentId = usePrimaryEnvironmentId(); |
| ... | ... | @@ -1871,6 +1880,49 @@ function OpenCommandPaletteDialog(props: { |
| 1871 | 1880 | } |
| 1872 | 1881 | } |
| 1873 | 1882 | |
| 1883 | if (activeThread !== null) { | |
| 1884 | const thread = activeThread; | |
| 1885 | actionItems.push({ | |
| 1886 | kind: "action", | |
| 1887 | value: "action:restart-agent-session", | |
| 1888 | searchTerms: ["restart", "reset", "reload", "agent", "session", "skills", "plugins", "mcp"], | |
| 1889 | title: "Restart agent session", | |
| 1890 | icon: <RotateCcwIcon className={ITEM_ICON_CLASS} />, | |
| 1891 | // Stopping the provider process keeps the conversation: the next message | |
| 1892 | // spawns a fresh one that resumes it and reloads skills, plugins, and MCP | |
| 1893 | // servers. The fresh workspace scan updates the composer's slash menu. | |
| 1894 | // Failures throw into executeItem's error toast. | |
| 1895 | run: async () => { | |
| 1896 | const { environmentId } = thread; | |
| 1897 | if (thread.session && thread.session.status !== "stopped") { | |
| 1898 | const stopped = await stopThreadSession({ | |
| 1899 | environmentId, | |
| 1900 | input: { threadId: thread.id }, | |
| 1901 | }); | |
| 1902 | if (stopped._tag === "Failure") throw squashAtomCommandFailure(stopped); | |
| 1903 | } | |
| 1904 | // The server stops the process after accepting the command. A failed | |
| 1905 | // stop shows in the thread. | |
| 1906 | toastManager.add({ | |
| 1907 | type: "success", | |
| 1908 | title: "Agent session will restart", | |
| 1909 | description: "Your next message starts a fresh session.", | |
| 1910 | }); | |
| 1911 | const project = projectByKey.get(`${environmentId}:${thread.projectId}`); | |
| 1912 | if (!project) return; | |
| 1913 | const refreshed = await refreshProviders({ | |
| 1914 | environmentId, | |
| 1915 | input: { | |
| 1916 | instanceId: thread.session?.providerInstanceId ?? thread.modelSelection.instanceId, | |
| 1917 | cwd: thread.worktreePath ?? project.workspaceRoot, | |
| 1918 | fresh: true, | |
| 1919 | }, | |
| 1920 | }); | |
| 1921 | if (refreshed._tag === "Failure") throw squashAtomCommandFailure(refreshed); | |
| 1922 | }, | |
| 1923 | }); | |
| 1924 | } | |
| 1925 | ||
| 1874 | 1926 | actionItems.push({ |
| 1875 | 1927 | kind: "action", |
| 1876 | 1928 | value: "action:open-file-picker", |
docs/user/composer.md+4| ... | ... | @@ -152,6 +152,10 @@ provider. On mobile, both are also available before starting a thread on |
| 152 | 152 | The slash menu also includes skills unless you turn off **Settings → General → |
| 153 | 153 | Show skills in slash menu**. Only skills enabled for the provider are listed. |
| 154 | 154 | |
| 155 | After you add or change skills, plugins, or MCP servers, use **Restart agent | |
| 156 | session** in the command palette on web and desktop. The conversation continues, | |
| 157 | and your next message starts the agent again with the new setup. | |
| 158 | ||
| 155 | 159 | Provider commands must start the message to run. T3 Code commands such as |
| 156 | 160 | `/model` and `/plan`, and skill mentions, work on any line. |
| 157 | 161 |
packages/client-runtime/src/state/server.ts+1| ... | ... | @@ -1103,6 +1103,7 @@ export function createServerEnvironmentAtoms<R, E>( |
| 1103 | 1103 | environmentId, |
| 1104 | 1104 | input.instanceId ?? null, |
| 1105 | 1105 | input.cwd ?? null, |
| 1106 | input.fresh ?? false, | |
| 1106 | 1107 | input.refreshModels ?? false, |
| 1107 | 1108 | ]), |
| 1108 | 1109 | }, |
packages/contracts/src/rpc.ts+3| ... | ... | @@ -494,6 +494,9 @@ const WsServerRefreshProvidersRpc = Rpc.make(WS_METHODS.serverRefreshProviders, |
| 494 | 494 | */ |
| 495 | 495 | instanceId: Schema.optional(ProviderInstanceId), |
| 496 | 496 | cwd: Schema.optional(TrimmedNonEmptyString), |
| 497 | /** With `instanceId` and `cwd`: rescan the workspace's skills and slash | |
| 498 | * commands even when a snapshot for that cwd already exists. */ | |
| 499 | fresh: Schema.optional(Schema.Boolean), | |
| 497 | 500 | /** Explicit user request: bypass T3-owned caches and rediscover models. |
| 498 | 501 | * Background status refreshes must not open agent sessions. */ |
| 499 | 502 | refreshModels: Schema.optional(Schema.Boolean), |