Skip to content
Open
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
10 changes: 10 additions & 0 deletions src/AirExtension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const AIR_EXTENSION_CAPABILITIES_KEY = "capabilities";
export const AIR_DIFF_STATS_KEY = "diffStats";
export const AIR_SESSION_FAILURE_KEY = "sessionFailure";
export const AIR_AGENT_FILE_CHANGE_REPORT_KEY = "agentFileChangeReport";
export const AIR_CUSTOM_INSTRUCTIONS_KEY = "customInstructions";
export const AIR_NATIVE_SUBAGENT_SESSIONS_KEY = "nativeSubagentSessions";
export const AIR_ASYNC_TASKS_KEY = "asyncTasks";
export const AIR_RECOMMENDED_CONFIG_VALUE_KEY = "recommendedValue";
Expand Down Expand Up @@ -60,6 +61,15 @@ export function clientSupportsAirCapability(
&& supported.includes(capability);
}

/** Read custom instructions from `session/new` metadata. */
export function airCustomInstructions(meta: unknown): string | undefined {
const root = asRecord(meta);
const jetbrains = asRecord(root[JETBRAINS_META_KEY]);
const air = asRecord(jetbrains[AIR_META_KEY]);
const instructions = air[AIR_CUSTOM_INSTRUCTIONS_KEY];
return typeof instructions === "string" ? instructions : undefined;
}

function asRecord(value: unknown): Record<string, unknown> {
return value !== null && typeof value === "object" && !Array.isArray(value)
? value as Record<string, unknown>
Expand Down
3 changes: 3 additions & 0 deletions src/CodexAcpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ import {arePathBasenamesEqual, arePathsEqual, isAbsolutePathLike} from "./PathUt
import {CodexSubagentSubscriptions} from "./subagents/CodexSubagentSubscriptions";
import {forkSession as runForkSession} from "./SessionFork";
import type {SessionMetadata, SessionMetadataWithThread} from "./SessionMetadata";
import {airCustomInstructions} from "./AirExtension";
import {isMissingRolloutError, isUnknownThreadError} from "./CodexThreadErrors";
export type {SessionMetadata, SessionMetadataWithThread} from "./SessionMetadata";

Expand Down Expand Up @@ -666,12 +667,14 @@ export class CodexAcpClient {

async newSession(request: acp.NewSessionRequest): Promise<SessionMetadata> {
const additionalDirectories = readAdditionalDirectories(request.cwd, request.additionalDirectories, request._meta);
const developerInstructions = airCustomInstructions(request._meta);
await this.refreshSkills(request.cwd, additionalDirectories);

const response = await this.codexClient.threadStart({
config: await this.createSessionConfig(request.cwd, additionalDirectories, request.mcpServers),
modelProvider: this.getModelProvider(),
cwd: request.cwd,
...(developerInstructions !== undefined && {developerInstructions}),
});

const codexModels = await this.fetchAvailableModels();
Expand Down
2 changes: 2 additions & 0 deletions src/CodexAcpServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ import {TitleGenerator} from "./TitleGenerator";
import {once} from "node:events";
import {
AIR_AGENT_FILE_CHANGE_REPORT_KEY,
AIR_CUSTOM_INSTRUCTIONS_KEY,
AIR_ASYNC_TASKS_KEY,
AIR_NATIVE_SUBAGENT_SESSIONS_KEY,
AIR_RECOMMENDED_CONFIG_VALUE_KEY,
Expand Down Expand Up @@ -422,6 +423,7 @@ export class CodexAcpServer {
[AIR_EXTENSION_CAPABILITIES_KEY]: [
AIR_SESSION_FAILURE_KEY,
AIR_AGENT_FILE_CHANGE_REPORT_KEY,
AIR_CUSTOM_INSTRUCTIONS_KEY,
AIR_NATIVE_SUBAGENT_SESSIONS_KEY,
AIR_ASYNC_TASKS_KEY,
AIR_RECOMMENDED_CONFIG_VALUE_KEY,
Expand Down
58 changes: 58 additions & 0 deletions src/__tests__/CodexACPAgent/CodexAcpClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,64 @@ describe('ACP server test', { timeout: 40_000 }, () => {
expect(listSkillsSpy.mock.invocationCallOrder[0]!).toBeLessThan(threadStartSpy.mock.invocationCallOrder[0]!);
});

it('passes AIR custom instructions to thread start', async () => {
const mockFixture = createCodexMockTestFixture();
const codexAcpClient = mockFixture.getCodexAcpClient();
const codexAppServerClient = mockFixture.getCodexAppServerClient();
vi.spyOn(codexAppServerClient, "listSkills").mockResolvedValue({data: []});
const threadStartSpy = vi.spyOn(codexAppServerClient, "threadStart").mockResolvedValue({
thread: {id: "thread-id"} as any,
model: "gpt-5",
reasoningEffort: "medium",
serviceTier: null,
} as any);
vi.spyOn(codexAppServerClient, "listModels").mockResolvedValue({
data: [createTestModel({id: "gpt-5"})],
nextCursor: null,
});

await codexAcpClient.newSession({
cwd: "/workspace",
mcpServers: [],
_meta: {
jetbrains: {
air: {
customInstructions: "Follow the project rules.",
},
},
},
});

expect(threadStartSpy).toHaveBeenCalledWith(expect.objectContaining({
developerInstructions: "Follow the project rules.",
}));
});

it('ignores malformed AIR custom instructions', async () => {
const mockFixture = createCodexMockTestFixture();
const codexAcpClient = mockFixture.getCodexAcpClient();
const codexAppServerClient = mockFixture.getCodexAppServerClient();
vi.spyOn(codexAppServerClient, "listSkills").mockResolvedValue({data: []});
const threadStartSpy = vi.spyOn(codexAppServerClient, "threadStart").mockResolvedValue({
thread: {id: "thread-id"} as any,
model: "gpt-5",
reasoningEffort: "medium",
serviceTier: null,
} as any);
vi.spyOn(codexAppServerClient, "listModels").mockResolvedValue({
data: [createTestModel({id: "gpt-5"})],
nextCursor: null,
});

await codexAcpClient.newSession({
cwd: "/workspace",
mcpServers: [],
_meta: {jetbrains: {air: {customInstructions: 42}}},
});

expect(threadStartSpy.mock.calls[0]![0].developerInstructions).toBeUndefined();
});

it('prefers ACP additional directories over legacy meta roots for new session skill discovery', async () => {
const mockFixture = createCodexMockTestFixture();
const codexAcpClient = mockFixture.getCodexAcpClient();
Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/CodexACPAgent/initialize.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ describe('CodexACPAgent - initialize', () => {
jetbrains: {
air: {
version: 1,
capabilities: ["sessionFailure", "agentFileChangeReport", "nativeSubagentSessions", "asyncTasks", "recommendedValue"],
capabilities: ["sessionFailure", "agentFileChangeReport", "customInstructions", "nativeSubagentSessions", "asyncTasks", "recommendedValue"],
},
},
},
Expand Down
Loading