Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions apps/mobile/src/features/threads/use-composer-command-menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,10 @@ import {
import {
dedupeProviderSkillsByName,
getProviderSkillsForSlashMenu,
getProviderSlashCommandsForSlashMenu,
isProviderSkillUserInvocable,
resolveProviderSkillsForCwd,
resolveProviderSlashCommandsForCwd,
} from "@t3tools/client-runtime/providerSkills";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";

Expand Down Expand Up @@ -329,17 +331,26 @@ export function useComposerCommandMenu({

if (trigger.kind === "slash-command") {
const q = trigger.query.toLowerCase();
const visibleSkills = getProviderSkillsForSlashMenu(skills, true);
const commandItems = buildComposerSlashCommandItems({
query: q,
atMessageStart: trigger.rangeStart === 0,
hasThread,
hasCompactableConversation,
offersUsageLimits,
allowInteractionMode: onUpdateInteractionMode !== undefined,
selectedProviderStatus,
selectedProviderStatus: selectedProviderStatus
? {
...selectedProviderStatus,
slashCommands: getProviderSlashCommandsForSlashMenu(
resolveProviderSlashCommandsForCwd(selectedProviderStatus, projectCwd),
visibleSkills,
),
}
: null,
});

const skillItems = getProviderSkillsForSlashMenu(skills, true)
const skillItems = visibleSkills
.filter((skill) => matchesSlashSkillQuery(skill, q))
.map((skill) => ({
id: `skill:${skill.name}`,
Expand Down Expand Up @@ -456,6 +467,7 @@ export function useComposerCommandMenu({
onUpdateInteractionMode,
pathSearch.entries,
pullRequestSearch.entries,
projectCwd,
selectedProviderStatus,
skills,
trigger,
Expand Down
18 changes: 10 additions & 8 deletions apps/server/scripts/acp-mock-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -784,7 +784,7 @@ const program = Effect.gen(function* () {
sessionId: requestedSessionId,
update: {
sessionUpdate: "agent_message_chunk",
content: { type: "text", text: "hello from " },
content: { type: "text", text: "hello from" },
},
});

Expand Down Expand Up @@ -826,13 +826,15 @@ const program = Effect.gen(function* () {
});
}

writeJsonRpcNotification("session/update", {
sessionId: requestedSessionId,
update: {
sessionUpdate: "agent_message_chunk",
content: { type: "text", text: "mock" },
},
});
for (const text of [" ", "mo", "ck"]) {
writeJsonRpcNotification("session/update", {
sessionId: requestedSessionId,
update: {
sessionUpdate: "agent_message_chunk",
content: { type: "text", text },
},
});
}

return yield* Effect.never;
}
Expand Down
54 changes: 32 additions & 22 deletions apps/server/src/provider/Drivers/CursorDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
checkCursorProviderStatus,
makeCursorModelDiscovery,
enrichCursorSnapshot,
makeCursorCommandCatalog,
} from "../Layers/CursorProvider.ts";
import { ProviderEventLoggers } from "../Layers/ProviderEventLoggers.ts";
import { makeManagedServerProvider } from "../makeManagedServerProvider.ts";
Expand All @@ -53,7 +54,7 @@ import {
makeProviderSnapshotSettingsSource,
type ProviderSnapshotSettings,
} from "../providerUpdateSettings.ts";
import { probeCursorSkills } from "./CursorSkills.ts";
import { discoverCursorSkills, probeCursorSkills } from "./CursorSkills.ts";
const decodeCursorSettings = Schema.decodeSync(CursorSettings);

const DRIVER_KIND = ProviderDriverKind.make("cursor");
Expand Down Expand Up @@ -130,11 +131,6 @@ export const CursorDriver: ProviderDriver<CursorSettings, CursorDriverEnv> = {
),
);

const adapter = yield* makeCursorAdapter(effectiveConfig, {
environment: processEnv,
...(eventLoggers.native ? { nativeEventLogger: eventLoggers.native } : {}),
instanceId,
});
const textGeneration = yield* makeCursorTextGeneration(effectiveConfig, processEnv);

const discoverModels = yield* makeCursorModelDiscovery(effectiveConfig, processEnv);
Expand All @@ -151,7 +147,9 @@ export const CursorDriver: ProviderDriver<CursorSettings, CursorDriverEnv> = {
);

const snapshotSettings = makeProviderSnapshotSettingsSource(effectiveConfig, serverSettings);
const snapshot = yield* makeManagedServerProvider<ProviderSnapshotSettings<CursorSettings>>({
const managedSnapshot = yield* makeManagedServerProvider<
ProviderSnapshotSettings<CursorSettings>
>({
resolveMaintenance,
getSettings: snapshotSettings.getSettings,
streamSettings: snapshotSettings.streamSettings,
Expand Down Expand Up @@ -188,6 +186,20 @@ export const CursorDriver: ProviderDriver<CursorSettings, CursorDriverEnv> = {
),
);

const { snapshot, onAvailableCommands, snapshotForCwd } =
yield* makeCursorCommandCatalog(managedSnapshot);
const adapter = yield* makeCursorAdapter(effectiveConfig, {
environment: processEnv,
...(eventLoggers.native ? { nativeEventLogger: eventLoggers.native } : {}),
instanceId,
onAvailableCommands: (commands, cwd) =>
discoverCursorSkills(cwd, processEnv).pipe(
Effect.provideService(FileSystem.FileSystem, fileSystem),
Effect.provideService(Path.Path, path),
Effect.flatMap((skills) => onAvailableCommands(commands, cwd, skills)),
),
});

return {
instanceId,
driverKind: DRIVER_KIND,
Expand All @@ -199,22 +211,20 @@ export const CursorDriver: ProviderDriver<CursorSettings, CursorDriverEnv> = {
snapshotForCwd: (cwd) =>
!effectiveConfig.enabled
? snapshot.getSnapshot
: Effect.all([
snapshot.getSnapshot,
probeCursorSkills(cwd, processEnv).pipe(
Effect.provideService(FileSystem.FileSystem, fileSystem),
Effect.provideService(Path.Path, path),
Effect.mapError(
(cause) =>
new ProviderDriverError({
driver: DRIVER_KIND,
instanceId,
detail: `Failed to discover Cursor skills for '${cwd}'`,
cause,
}),
),
: probeCursorSkills(cwd, processEnv).pipe(
Effect.provideService(FileSystem.FileSystem, fileSystem),
Effect.provideService(Path.Path, path),
Effect.mapError(
(cause) =>
new ProviderDriverError({
driver: DRIVER_KIND,
instanceId,
detail: `Failed to discover Cursor skills for '${cwd}'`,
cause,
}),
),
]).pipe(Effect.map(([machineSnapshot, skills]) => ({ ...machineSnapshot, skills }))),
Effect.flatMap((skills) => snapshotForCwd(cwd, skills)),
),
adapter,
textGeneration,
} satisfies ProviderInstance;
Expand Down
27 changes: 20 additions & 7 deletions apps/server/src/provider/Drivers/OpenCodeDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,11 @@ import {
checkOpenCodeProviderStatus,
makePendingOpenCodeProvider,
openCodeSkillsToServerProviderSkills,
openCodeCommandsToServerProviderSlashCommands,
} from "../Layers/OpenCodeProvider.ts";
import { ProviderEventLoggers } from "../Layers/ProviderEventLoggers.ts";
import { makeManagedServerProvider } from "../makeManagedServerProvider.ts";
import { OpenCodeRuntime } from "../opencodeRuntime.ts";
import { OpenCodeRuntime, loadOpenCodeCommands } from "../opencodeRuntime.ts";
import * as OpenCodeServerOwner from "../OpenCodeServerOwner.ts";
import {
defaultProviderContinuationIdentity,
Expand Down Expand Up @@ -164,7 +165,18 @@ export const OpenCodeDriver: ProviderDriver<OpenCodeSettings, OpenCodeDriverEnv>
// empty skill list and poisons the workspace snapshot the `$` picker
// reads. The SDK `app.skills` endpoint honors the per-request directory
// and returns complete results regardless of size.
const loadSkillsForCwd = (cwd: string) =>
const loadWorkspaceInventory = (client: Parameters<typeof loadOpenCodeCommands>[0]) =>
Effect.all(
{
skills: openCodeRuntime.loadOpenCodeSkills(client),
commands: loadOpenCodeCommands(client).pipe(
Effect.timeout("10 seconds"),
Effect.orElseSucceed(() => []),
),
},
{ concurrency: "unbounded" },
);
const loadWorkspaceForCwd = (cwd: string) =>
effectiveConfig.serverUrl.trim().length > 0
? Effect.scoped(
Effect.gen(function* () {
Expand All @@ -184,11 +196,11 @@ export const OpenCodeDriver: ProviderDriver<OpenCodeSettings, OpenCodeDriverEnv>
? { serverPassword: effectiveConfig.serverPassword }
: {}),
});
return yield* openCodeRuntime.loadOpenCodeSkills(client);
return yield* loadWorkspaceInventory(client);
}),
)
: serverOwner.withServer((server) =>
openCodeRuntime.loadOpenCodeSkills(
loadWorkspaceInventory(
openCodeRuntime.createOpenCodeSdkClient({
baseUrl: server.url,
directory: cwd,
Expand Down Expand Up @@ -247,18 +259,19 @@ export const OpenCodeDriver: ProviderDriver<OpenCodeSettings, OpenCodeDriverEnv>
? snapshot.getSnapshot
: Effect.all([
snapshot.getSnapshot,
loadSkillsForCwd(cwd).pipe(Effect.timeout("20 seconds")),
loadWorkspaceForCwd(cwd).pipe(Effect.timeout("20 seconds")),
]).pipe(
Effect.map(([machineSnapshot, skills]) => ({
Effect.map(([machineSnapshot, { skills, commands }]) => ({
...machineSnapshot,
skills: openCodeSkillsToServerProviderSkills(skills),
slashCommands: openCodeCommandsToServerProviderSlashCommands(commands),
})),
Effect.mapError(
(cause) =>
new ProviderDriverError({
driver: DRIVER_KIND,
instanceId,
detail: `Failed to probe OpenCode skills for '${cwd}'`,
detail: `Failed to probe OpenCode commands and skills for '${cwd}'`,
cause,
}),
),
Expand Down
4 changes: 3 additions & 1 deletion apps/server/src/provider/Layers/CursorAdapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ cursorAdapterTestLayer("CursorAdapterLive", (it) => {
}),
);

it.effect("sends selected project skills in Cursor's native slash form", () =>
it.effect("sends skills in Cursor's native form and preserves exact slash command input", () =>
Effect.gen(function* () {
const adapter = yield* CursorAdapter;
const settings = yield* ServerSettingsService;
Expand Down Expand Up @@ -347,6 +347,7 @@ cursorAdapterTestLayer("CursorAdapterLive", (it) => {
],
],
);
yield* adapter.sendTurn({ threadId, input: "/copy-request-id" });
yield* adapter.stopSession(threadId);

const requests = yield* Effect.promise(() => readJsonLines(requestLogPath));
Expand All @@ -360,6 +361,7 @@ cursorAdapterTestLayer("CursorAdapterLive", (it) => {
{ type: "text", text: "please /review this" },
{ type: "text", text: buildRuntimeInstructions({ harness: "Cursor" }) },
],
[{ type: "text", text: "/copy-request-id" }],
],
);
}),
Expand Down
28 changes: 20 additions & 8 deletions apps/server/src/provider/Layers/CursorAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,10 @@ export interface CursorAdapterLiveOptions {
* the latest snapshot so the closure isn't stale.
*/
readonly resolveSettings?: Effect.Effect<CursorSettings>;
readonly onAvailableCommands?: (
commands: ReadonlyArray<EffectAcpSchema.AvailableCommand>,
cwd: string,
) => Effect.Effect<void>;
}

interface PendingApproval {
Expand Down Expand Up @@ -810,6 +814,11 @@ export function makeCursorAdapter(
return;
case "ModeChanged":
return;
case "AvailableCommandsUpdated":
yield* (
options?.onAvailableCommands?.(event.availableCommands, cwd) ?? Effect.void
);
return;
case "AssistantItemStarted":
ctx.assistantReply = new CursorTransportFailure();
yield* offerRuntimeEvent(
Expand Down Expand Up @@ -1057,16 +1066,19 @@ export function makeCursorAdapter(
});
}

// ACP has no system-message field; keep runtime context separate from the user's text.
// ACP commands parse the complete text. Extra context can turn an exact
// command into an ordinary model prompt or change its arguments.
const result = yield* ctx.acp
.prompt({
prompt: [
...promptParts,
{
type: "text",
text: buildRuntimeInstructions({ harness: "Cursor", model: resolvedModel }),
},
],
prompt: /^\/[^\s/]+(?:\s|$)/.test(rawPrompt)
? promptParts
: [
...promptParts,
{
type: "text",
text: buildRuntimeInstructions({ harness: "Cursor", model: resolvedModel }),
},
],
})
.pipe(
Effect.mapError((error) =>
Expand Down
Loading
Loading