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
6 changes: 6 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ export function createProgram() {
.option("-w, --workflow", "Enable graph workflow mode", false)
.option("-t, --theme <name>", "UI theme (dark, light)", "dark")
.option("-m, --model <name>", "Model to use for the chat session")
.option(
"--additional-instructions <text>",
"Append additional instructions to the default chat system prompt",
)
.argument(
"[prompt...]",
"Initial prompt to send (opens interactive session with prompt)",
Expand All @@ -99,6 +103,7 @@ Examples:
$ atomic chat -a opencode Start chat with OpenCode
$ atomic chat -a copilot --workflow Start workflow-enabled chat with Copilot
$ atomic chat --theme light Start chat with light theme
$ atomic chat --additional-instructions "Be concise" "review this patch"
$ atomic chat "fix the typecheck errors" Start chat with an initial prompt
$ atomic chat -a claude "refactor utils" Start chat with agent and prompt

Expand Down Expand Up @@ -162,6 +167,7 @@ Slash Commands (in workflow mode):
theme: localOpts.theme as "dark" | "light",
model: localOpts.model,
initialPrompt: prompt,
additionalInstructions: localOpts.additionalInstructions,
});

process.exit(exitCode);
Expand Down
22 changes: 22 additions & 0 deletions src/commands/chat.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ import {
logActiveProviderDiscoveryPlan,
prepareClaudeRuntimeForChat,
prepareOpenCodeRuntimeConfigForChat,
resolveChatAdditionalInstructions,
shouldAutoInitChat,
} from "./chat.ts";
import { ENHANCED_SYSTEM_PROMPT } from "../sdk/enhanced-system-prompt.ts";

async function withTempDir(run: (dir: string) => Promise<void>): Promise<void> {
const dir = await mkdtemp(join(tmpdir(), "atomic-chat-test-"));
Expand Down Expand Up @@ -375,3 +377,23 @@ test("prepareOpenCodeRuntimeConfigForChat rejects non-opencode discovery plans",
"OpenCode runtime prep requires an OpenCode discovery plan, received claude",
);
});

test("resolveChatAdditionalInstructions defaults to the enhanced system prompt", () => {
expect(resolveChatAdditionalInstructions({})).toBe(ENHANCED_SYSTEM_PROMPT);
});

test("resolveChatAdditionalInstructions appends explicit text to the enhanced system prompt", () => {
expect(
resolveChatAdditionalInstructions({
additionalInstructions: "Use short answers.",
})
).toBe(`${ENHANCED_SYSTEM_PROMPT}\n\nUse short answers.`);
});

test("resolveChatAdditionalInstructions ignores blank appended instructions", () => {
expect(
resolveChatAdditionalInstructions({
additionalInstructions: " ",
})
).toBe(ENHANCED_SYSTEM_PROMPT);
});
17 changes: 17 additions & 0 deletions src/commands/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import type {
} from "../utils/provider-discovery-plan.ts";
import type { PrepareClaudeConfigOptions } from "../utils/claude-config.ts";
import { getModelPreference, getReasoningEffortPreference } from "../utils/settings.ts";
import { ENHANCED_SYSTEM_PROMPT } from "../sdk/enhanced-system-prompt.ts";
import { pathExists } from "../utils/copy.ts";
import { AGENT_CONFIG, type SourceControlType } from "../config.ts";
// initCommand is lazy-loaded only when auto-init is needed
Expand Down Expand Up @@ -61,6 +62,8 @@ export interface ChatCommandOptions {
workflow?: boolean;
/** Initial prompt to send on session start */
initialPrompt?: string;
/** Extra instructions appended to the enhanced system prompt for the session */
additionalInstructions?: string;
}

// ============================================================================
Expand Down Expand Up @@ -526,6 +529,17 @@ function handleThemeCommand(args: string): { newTheme: "dark" | "light"; message
return null;
}

export function resolveChatAdditionalInstructions(
options: Pick<ChatCommandOptions, "additionalInstructions">
): string {
const trimmedAdditionalInstructions = options.additionalInstructions?.trim();
if (!trimmedAdditionalInstructions) {
return ENHANCED_SYSTEM_PROMPT;
}

return `${ENHANCED_SYSTEM_PROMPT}\n\n${trimmedAdditionalInstructions}`;
}

// ============================================================================
// Chat Command Implementation
// ============================================================================
Expand All @@ -543,6 +557,7 @@ export async function chatCommand(options: ChatCommandOptions = {}): Promise<num
model,
workflow = false,
initialPrompt,
additionalInstructions,
} = options;

if (!agentType) {
Expand Down Expand Up @@ -589,6 +604,7 @@ export async function chatCommand(options: ChatCommandOptions = {}): Promise<num
});
logActiveProviderDiscoveryPlan(providerDiscoveryPlan, { projectRoot });
const selectedScm = await getSelectedScm(projectRoot);
const resolvedAdditionalInstructions = resolveChatAdditionalInstructions({ additionalInstructions });

// Parallelize independent config preparation steps
const configPrepTasks: Promise<void>[] = [];
Expand Down Expand Up @@ -694,6 +710,7 @@ export async function chatCommand(options: ChatCommandOptions = {}): Promise<num
model: effectiveModel,
reasoningEffort: resolvedReasoningEffort,
mcpServers,
additionalInstructions: resolvedAdditionalInstructions,
},
theme: await getTheme(theme),
title: `Chat - ${agentName}`,
Expand Down
2 changes: 1 addition & 1 deletion src/sdk/clients/claude.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1203,7 +1203,7 @@ describe("ClaudeAgentClient permissions and options", () => {
expect(options.systemPrompt).toEqual({ type: "preset", preset: "claude_code" });

const withPrompt = privateClient.buildSdkOptions(
{ systemPrompt: "Extra system guidance" },
{ additionalInstructions: "Extra system guidance" },
"session-v1",
);
expect(withPrompt.systemPrompt).toEqual({
Expand Down
4 changes: 2 additions & 2 deletions src/sdk/clients/claude.ts
Original file line number Diff line number Diff line change
Expand Up @@ -583,11 +583,11 @@ export class ClaudeAgentClient implements CodingAgentClient {
hooks: this.buildNativeHooks(),
includePartialMessages: true,
// Use Claude Code's built-in system prompt, appending custom instructions if provided
systemPrompt: config.systemPrompt
systemPrompt: config.additionalInstructions
? {
type: "preset",
preset: "claude_code",
append: config.systemPrompt,
append: config.additionalInstructions,
}
: { type: "preset", preset: "claude_code" },
// Explicitly set the path to Claude Code executable to prevent it from
Expand Down
46 changes: 46 additions & 0 deletions src/sdk/clients/copilot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,52 @@ describe("CopilotClient.getModelDisplayInfo", () => {
});

describe("CopilotClient abort support", () => {
test("creates sessions with append-mode additional instructions", async () => {
const mockSdkSession = {
sessionId: "test-session",
on: mock(() => () => {}),
send: mock(() => Promise.resolve()),
sendAndWait: mock(() => Promise.resolve({ data: { content: "test" } })),
destroy: mock(() => Promise.resolve()),
abort: mock(() => Promise.resolve()),
};

const mockCreateSession = mock(() => Promise.resolve(mockSdkSession));
const mockSdkClient = {
start: mock(() => Promise.resolve()),
stop: mock(() => Promise.resolve()),
createSession: mockCreateSession,
listModels: mock(() => Promise.resolve([
{
id: "test-model",
capabilities: {
limits: { max_context_window_tokens: 128000 },
supports: {},
},
},
])),
};

const client = new CopilotClient({});
(client as any).sdkClient = mockSdkClient;
(client as any).isRunning = true;

await client.createSession({
sessionId: "test-session",
additionalInstructions: "Follow repository conventions.",
});

expect(mockCreateSession).toHaveBeenCalledWith(
expect.objectContaining({
sessionId: "test-session",
systemMessage: {
mode: "append",
content: "Follow repository conventions.",
},
}),
);
});

test("exposes abort method on wrapped session", async () => {
// Create a mock SDK session with abort method
const mockSdkSession = {
Expand Down
4 changes: 2 additions & 2 deletions src/sdk/clients/copilot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1215,8 +1215,8 @@ export class CopilotClient implements CodingAgentClient {
...(modelSupportsReasoning && config.reasoningEffort
? { reasoningEffort: config.reasoningEffort }
: {}),
systemMessage: config.systemPrompt
? { mode: "append", content: config.systemPrompt }
systemMessage: config.additionalInstructions
? { mode: "append", content: config.additionalInstructions }
: undefined,
availableTools: config.tools,
streaming: true,
Expand Down
112 changes: 112 additions & 0 deletions src/sdk/clients/opencode.events.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,118 @@ describe("resolveOpenCodeConfigDirEnv", () => {
});
});

describe("OpenCode additional instruction routing", () => {
test("injects additional instructions into non-agent prompt parts without using system override", async () => {
const client = new OpenCodeClient();
const sessionId = "ses_prompt_additional_instructions";
let capturedParams: Record<string, unknown> | undefined;

(client as unknown as {
resolveModelContextWindow: (modelHint?: string) => Promise<number>;
}).resolveModelContextWindow = async () => 200_000;

(client as unknown as {
sdkClient: {
session: {
prompt: (params: Record<string, unknown>) => Promise<{
data?: {
info?: { tokens?: { input?: number; output?: number } };
parts?: Array<Record<string, unknown>>;
};
}>;
};
};
}).sdkClient = {
session: {
prompt: async (params) => {
capturedParams = params;
return {
data: {
info: {
tokens: { input: 1, output: 1 },
},
parts: [{ type: "text", text: "ok" }],
},
};
},
},
};

const session = await (client as unknown as {
wrapSession: (sid: string, config: Record<string, unknown>) => Promise<{
send: (message: string) => Promise<unknown>;
}>;
}).wrapSession(sessionId, {
additionalInstructions: "Follow repo conventions.",
});

await session.send("Fix the failing tests");

expect(capturedParams?.system).toBeUndefined();
expect(capturedParams?.parts).toEqual([
{
type: "text",
text: [
"<additional_instructions>",
"Follow repo conventions.",
"</additional_instructions>",
"",
"Fix the failing tests",
].join("\n"),
},
]);
});

test("does not inject additional instructions into agent-dispatch prompt parts", async () => {
const client = new OpenCodeClient();
const sessionId = "ses_agent_prompt_no_additional_instructions";
let capturedParams: Record<string, unknown> | undefined;

(client as unknown as {
resolveModelContextWindow: (modelHint?: string) => Promise<number>;
}).resolveModelContextWindow = async () => 200_000;

(client as unknown as {
sdkClient: {
session: {
promptAsync: (params: Record<string, unknown>) => Promise<void>;
};
};
}).sdkClient = {
session: {
promptAsync: async (params) => {
capturedParams = params;
},
},
};

const session = await (client as unknown as {
wrapSession: (sid: string, config: Record<string, unknown>) => Promise<{
sendAsync: (
message: string,
options?: { agent?: string; abortSignal?: AbortSignal },
) => Promise<void>;
}>;
}).wrapSession(sessionId, {
additionalInstructions: "Follow repo conventions.",
});

await session.sendAsync("Investigate the auth flow", { agent: "worker" });

expect(capturedParams?.system).toBeUndefined();
expect(capturedParams?.parts).toEqual([
{
type: "text",
text: "Investigate the auth flow",
},
{
type: "agent",
name: "worker",
},
]);
});
});

describe("transitionOpenCodeCompactionControl", () => {
test("applies bounded transitions through success path", () => {
const started = transitionOpenCodeCompactionControl(
Expand Down
Loading
Loading