diff --git a/packages/app/src/components/agent-input-area.tsx b/packages/app/src/components/agent-input-area.tsx
index 2f35633e3..bd35b4211 100644
--- a/packages/app/src/components/agent-input-area.tsx
+++ b/packages/app/src/components/agent-input-area.tsx
@@ -26,7 +26,10 @@ import {
} from "./message-input";
import { Theme } from "@/styles/theme";
import { CommandAutocomplete } from "./command-autocomplete";
-import { useAgentCommandsQuery } from "@/hooks/use-agent-commands-query";
+import {
+ useAgentCommandsQuery,
+ type DraftCommandConfig,
+} from "@/hooks/use-agent-commands-query";
import { encodeImages } from "@/utils/encode-images";
import { useKeyboardShortcutsStore } from "@/stores/keyboard-shortcuts-store";
import { focusWithRetries } from "@/utils/web-focus";
@@ -55,6 +58,8 @@ interface AgentInputAreaProps {
autoFocus?: boolean;
/** Callback to expose the addImages function to parent components */
onAddImages?: (addImages: (images: ImageAttachment[]) => void) => void;
+ /** Optional draft context for listing commands before an agent exists. */
+ commandDraftConfig?: DraftCommandConfig;
}
const EMPTY_ARRAY: readonly QueuedMessage[] = [];
@@ -69,6 +74,7 @@ export function AgentInputArea({
onChangeText,
autoFocus = false,
onAddImages,
+ commandDraftConfig,
}: AgentInputAreaProps) {
const { theme } = useUnistyles();
const insets = useSafeAreaInsets();
@@ -117,13 +123,39 @@ export function AgentInputArea({
// Command autocomplete logic
const showCommandAutocomplete = userInput.startsWith("/") && !userInput.includes(" ");
const commandFilter = showCommandAutocomplete ? userInput.slice(1) : "";
+ const normalizedCommandDraftConfig = useMemo(() => {
+ if (!commandDraftConfig) {
+ return undefined;
+ }
- // Prefetch commands when agent is available (not on new agent screen)
+ const cwd = commandDraftConfig.cwd.trim();
+ if (!cwd) {
+ return undefined;
+ }
+
+ const modeId = commandDraftConfig.modeId?.trim() ?? "";
+ const model = commandDraftConfig.model?.trim() ?? "";
+ const thinkingOptionId = commandDraftConfig.thinkingOptionId?.trim() ?? "";
+ return {
+ provider: commandDraftConfig.provider,
+ cwd,
+ ...(modeId ? { modeId } : {}),
+ ...(model ? { model } : {}),
+ ...(thinkingOptionId ? { thinkingOptionId } : {}),
+ };
+ }, [commandDraftConfig]);
+
+ // Prefetch commands for real agents and for draft screens when context is available.
const isRealAgent = agentId && !agentId.startsWith("__");
+ const commandQueryDraftConfig = isRealAgent
+ ? undefined
+ : normalizedCommandDraftConfig;
+ const canLoadCommands = Boolean(serverId) && (isRealAgent || !!commandQueryDraftConfig);
const { commands } = useAgentCommandsQuery({
serverId,
agentId,
- enabled: !!isRealAgent && !!serverId,
+ enabled: canLoadCommands,
+ draftConfig: commandQueryDraftConfig,
});
// Filter commands for keyboard navigation
@@ -742,13 +774,14 @@ export function AgentInputArea({
)}
{/* Command autocomplete dropdown */}
- {showCommandAutocomplete && isRealAgent && (
+ {showCommandAutocomplete && canLoadCommands && (
)}
diff --git a/packages/app/src/components/command-autocomplete.tsx b/packages/app/src/components/command-autocomplete.tsx
index fb83afafe..29e03f5e4 100644
--- a/packages/app/src/components/command-autocomplete.tsx
+++ b/packages/app/src/components/command-autocomplete.tsx
@@ -1,6 +1,9 @@
import { View, Text, Pressable, ScrollView } from "react-native";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
-import { useAgentCommandsQuery } from "@/hooks/use-agent-commands-query";
+import {
+ useAgentCommandsQuery,
+ type DraftCommandConfig,
+} from "@/hooks/use-agent-commands-query";
import { Fonts } from "@/constants/theme";
import { Theme } from "@/styles/theme";
@@ -16,6 +19,7 @@ interface CommandAutocompleteProps {
filter: string;
selectedIndex: number;
onSelect: (command: AgentSlashCommand) => void;
+ draftConfig?: DraftCommandConfig;
}
export function CommandAutocomplete({
@@ -24,12 +28,14 @@ export function CommandAutocomplete({
filter,
selectedIndex,
onSelect,
+ draftConfig,
}: CommandAutocompleteProps) {
const { theme } = useUnistyles();
const { commands, isLoading, isError, error } = useAgentCommandsQuery({
serverId,
agentId,
enabled: true,
+ draftConfig,
});
// Filter commands based on input after /
diff --git a/packages/app/src/hooks/use-agent-commands-query.ts b/packages/app/src/hooks/use-agent-commands-query.ts
index 3180ae15a..1d7e05708 100644
--- a/packages/app/src/hooks/use-agent-commands-query.ts
+++ b/packages/app/src/hooks/use-agent-commands-query.ts
@@ -1,5 +1,6 @@
import { useQuery } from "@tanstack/react-query";
import { useSessionStore } from "@/stores/session-store";
+import type { AgentProvider } from "@server/server/agent/agent-sdk-types";
const COMMANDS_STALE_TIME = 60_000; // Commands rarely change, cache for 1 minute
@@ -9,20 +10,43 @@ interface AgentSlashCommand {
argumentHint: string;
}
-function commandsQueryKey(serverId: string, agentId: string) {
- return ["agentCommands", serverId, agentId] as const;
+export interface DraftCommandConfig {
+ provider: AgentProvider;
+ cwd: string;
+ modeId?: string;
+ model?: string;
+ thinkingOptionId?: string;
+}
+
+function commandsQueryKey(
+ serverId: string,
+ agentId: string,
+ draftConfig?: DraftCommandConfig
+) {
+ return [
+ "agentCommands",
+ serverId,
+ agentId,
+ draftConfig?.provider ?? null,
+ draftConfig?.cwd ?? null,
+ draftConfig?.modeId ?? null,
+ draftConfig?.model ?? null,
+ draftConfig?.thinkingOptionId ?? null,
+ ] as const;
}
interface UseAgentCommandsQueryOptions {
serverId: string;
agentId: string;
enabled?: boolean;
+ draftConfig?: DraftCommandConfig;
}
export function useAgentCommandsQuery({
serverId,
agentId,
enabled = true,
+ draftConfig,
}: UseAgentCommandsQueryOptions) {
const client = useSessionStore(
(state) => state.sessions[serverId]?.client ?? null
@@ -32,12 +56,12 @@ export function useAgentCommandsQuery({
);
const query = useQuery({
- queryKey: commandsQueryKey(serverId, agentId),
+ queryKey: commandsQueryKey(serverId, agentId, draftConfig),
queryFn: async () => {
if (!client) {
throw new Error("Daemon client not available");
}
- const response = await client.listCommands(agentId);
+ const response = await client.listCommands(agentId, { draftConfig });
return response.commands as AgentSlashCommand[];
},
enabled: enabled && !!client && isConnected && !!agentId,
diff --git a/packages/app/src/screens/agent/draft-agent-screen.tsx b/packages/app/src/screens/agent/draft-agent-screen.tsx
index ca98a5700..01e9b7909 100644
--- a/packages/app/src/screens/agent/draft-agent-screen.tsx
+++ b/packages/app/src/screens/agent/draft-agent-screen.tsx
@@ -23,6 +23,7 @@ import { Combobox } from "@/components/ui/combobox";
import { FileDropZone } from "@/components/file-drop-zone";
import { useQuery } from "@tanstack/react-query";
import { useAgentFormState, type CreateAgentInitialValues } from "@/hooks/use-agent-form-state";
+import type { DraftCommandConfig } from "@/hooks/use-agent-commands-query";
import {
CHECKOUT_STATUS_STALE_TIME,
checkoutStatusQueryKey,
@@ -788,6 +789,33 @@ export function DraftAgentScreen({
const createAgentClient = useSessionStore((state) =>
selectedServerId ? state.sessions[selectedServerId]?.client ?? null : null
);
+ const draftCommandConfig = useMemo(() => {
+ const cwd = (
+ isAttachWorktree && selectedWorktreePath ? selectedWorktreePath : workingDir
+ ).trim();
+ if (!cwd) {
+ return undefined;
+ }
+
+ return {
+ provider: selectedProvider,
+ cwd,
+ ...(modeOptions.length > 0 && selectedMode !== "" ? { modeId: selectedMode } : {}),
+ ...(selectedModel.trim() ? { model: selectedModel.trim() } : {}),
+ ...(selectedThinkingOptionId.trim()
+ ? { thinkingOptionId: selectedThinkingOptionId.trim() }
+ : {}),
+ };
+ }, [
+ isAttachWorktree,
+ modeOptions.length,
+ selectedMode,
+ selectedModel,
+ selectedProvider,
+ selectedThinkingOptionId,
+ selectedWorktreePath,
+ workingDir,
+ ]);
const promptValue = machine.tag === "draft" ? machine.promptText : "";
const formErrorMessage = machine.tag === "draft" ? machine.errorMessage : "";
@@ -1240,6 +1268,7 @@ export function DraftAgentScreen({
onChangeText={(next) => dispatch({ type: "DRAFT_SET_PROMPT", text: next })}
autoFocus={machine.tag === "draft"}
onAddImages={handleAddImagesCallback}
+ commandDraftConfig={draftCommandConfig}
/>
diff --git a/packages/server/src/client/daemon-client.test.ts b/packages/server/src/client/daemon-client.test.ts
index 4df5f8cb7..6c934683d 100644
--- a/packages/server/src/client/daemon-client.test.ts
+++ b/packages/server/src/client/daemon-client.test.ts
@@ -769,6 +769,139 @@ describe("DaemonClient", () => {
});
});
+ test("lists commands with draft config via RPC", async () => {
+ const logger = createMockLogger();
+ const mock = createMockTransport();
+
+ const client = new DaemonClient({
+ url: "ws://test",
+ logger,
+ reconnect: { enabled: false },
+ transportFactory: () => mock.transport,
+ });
+ clients.push(client);
+
+ const connectPromise = client.connect();
+ mock.triggerOpen();
+ await connectPromise;
+
+ const promise = client.listCommands("__new_agent__", {
+ draftConfig: {
+ provider: "codex",
+ cwd: "/tmp/project",
+ modeId: "bypassPermissions",
+ model: "gpt-5",
+ thinkingOptionId: "off",
+ },
+ });
+ expect(mock.sent).toHaveLength(1);
+
+ const request = JSON.parse(mock.sent[0]) as {
+ type: "session";
+ message: {
+ type: "list_commands_request";
+ agentId: string;
+ draftConfig?: {
+ provider: string;
+ cwd: string;
+ modeId?: string;
+ model?: string;
+ thinkingOptionId?: string;
+ };
+ requestId: string;
+ };
+ };
+ expect(request.message.type).toBe("list_commands_request");
+ expect(request.message.agentId).toBe("__new_agent__");
+ expect(request.message.draftConfig).toEqual({
+ provider: "codex",
+ cwd: "/tmp/project",
+ modeId: "bypassPermissions",
+ model: "gpt-5",
+ thinkingOptionId: "off",
+ });
+
+ mock.triggerMessage(
+ JSON.stringify({
+ type: "session",
+ message: {
+ type: "list_commands_response",
+ payload: {
+ agentId: "__new_agent__",
+ commands: [
+ { name: "help", description: "Show help", argumentHint: "" },
+ ],
+ error: null,
+ requestId: request.message.requestId,
+ },
+ },
+ })
+ );
+
+ await expect(promise).resolves.toEqual({
+ agentId: "__new_agent__",
+ commands: [{ name: "help", description: "Show help", argumentHint: "" }],
+ error: null,
+ requestId: request.message.requestId,
+ });
+ });
+
+ test("lists commands with legacy requestId signature via RPC", async () => {
+ const logger = createMockLogger();
+ const mock = createMockTransport();
+
+ const client = new DaemonClient({
+ url: "ws://test",
+ logger,
+ reconnect: { enabled: false },
+ transportFactory: () => mock.transport,
+ });
+ clients.push(client);
+
+ const connectPromise = client.connect();
+ mock.triggerOpen();
+ await connectPromise;
+
+ const promise = client.listCommands("agent-1", "req-legacy");
+ expect(mock.sent).toHaveLength(1);
+
+ const request = JSON.parse(mock.sent[0]) as {
+ type: "session";
+ message: {
+ type: "list_commands_request";
+ agentId: string;
+ draftConfig?: unknown;
+ requestId: string;
+ };
+ };
+ expect(request.message.type).toBe("list_commands_request");
+ expect(request.message.agentId).toBe("agent-1");
+ expect(request.message.requestId).toBe("req-legacy");
+ expect(request.message.draftConfig).toBeUndefined();
+
+ mock.triggerMessage(
+ JSON.stringify({
+ type: "session",
+ message: {
+ type: "list_commands_response",
+ payload: {
+ agentId: "agent-1",
+ commands: [],
+ error: null,
+ requestId: "req-legacy",
+ },
+ },
+ })
+ );
+
+ await expect(promise).resolves.toEqual({
+ agentId: "agent-1",
+ commands: [],
+ error: null,
+ requestId: "req-legacy",
+ });
+ });
+
test("auto-acks terminal stream chunks after delivery", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
diff --git a/packages/server/src/client/daemon-client.ts b/packages/server/src/client/daemon-client.ts
index 0355ae193..d4c41001a 100644
--- a/packages/server/src/client/daemon-client.ts
+++ b/packages/server/src/client/daemon-client.ts
@@ -209,6 +209,14 @@ type SpeechModelsListPayload = SpeechModelsListResponse["payload"];
type SpeechModelsDownloadPayload = SpeechModelsDownloadResponse["payload"];
type ListCommandsPayload = ListCommandsResponse["payload"];
type ExecuteCommandPayload = ExecuteCommandResponse["payload"];
+type ListCommandsDraftConfig = Pick<
+ AgentSessionConfig,
+ "provider" | "cwd" | "modeId" | "model" | "thinkingOptionId"
+>;
+type ListCommandsOptions = {
+ requestId?: string;
+ draftConfig?: ListCommandsDraftConfig;
+};
type SetVoiceModePayload = Extract<
SessionOutboundMessage,
{ type: "set_voice_mode_response" }
@@ -2171,15 +2179,30 @@ export class DaemonClient {
});
}
+ async listCommands(agentId: string, requestId?: string): Promise;
async listCommands(
agentId: string,
- requestId?: string
+ options?: ListCommandsOptions
+ ): Promise;
+ async listCommands(
+ agentId: string,
+ requestIdOrOptions?: string | ListCommandsOptions
): Promise {
+ const requestId =
+ typeof requestIdOrOptions === "string"
+ ? requestIdOrOptions
+ : requestIdOrOptions?.requestId;
+ const draftConfig =
+ typeof requestIdOrOptions === "string"
+ ? undefined
+ : requestIdOrOptions?.draftConfig;
+
return this.sendCorrelatedSessionRequest({
requestId,
message: {
type: "list_commands_request",
agentId,
+ ...(draftConfig ? { draftConfig } : {}),
},
responseType: "list_commands_response",
timeout: 30000,
diff --git a/packages/server/src/server/session.ts b/packages/server/src/server/session.ts
index f82c3379a..2b048f591 100644
--- a/packages/server/src/server/session.ts
+++ b/packages/server/src/server/session.ts
@@ -1524,7 +1524,7 @@ export class Session {
}
case "list_commands_request":
- await this.handleListCommandsRequest(msg.agentId, msg.requestId);
+ await this.handleListCommandsRequest(msg);
break;
case "execute_command_request":
@@ -3473,9 +3473,12 @@ export class Session {
/**
* Handle list commands request for an agent
*/
- private async handleListCommandsRequest(agentId: string, requestId: string): Promise {
+ private async handleListCommandsRequest(
+ msg: Extract
+ ): Promise {
+ const { agentId, requestId, draftConfig } = msg;
this.sessionLogger.debug(
- { agentId },
+ { agentId, draftConfig },
`Handling list commands request for agent ${agentId}`
);
@@ -3483,47 +3486,100 @@ export class Session {
const agents = this.agentManager.listAgents();
const agent = agents.find((a) => a.id === agentId);
- if (!agent) {
+ if (agent?.session?.listCommands) {
+ const commands = await agent.session.listCommands();
this.emit({
type: "list_commands_response",
payload: {
agentId,
- commands: [],
- error: `Agent not found: ${agentId}`,
+ commands,
+ error: null,
requestId,
},
});
return;
}
- const session = agent.session;
- if (!session || !session.listCommands) {
- this.emit({
- type: "list_commands_response",
- payload: {
- agentId,
- commands: [],
- error: `Agent does not support listing commands`,
- requestId,
- },
- });
+ if (!agent && draftConfig) {
+ const provider = draftConfig.provider;
+ const client = this.providerRegistry[provider].createClient(this.sessionLogger);
+ const available = await client.isAvailable();
+ if (!available) {
+ this.emit({
+ type: "list_commands_response",
+ payload: {
+ agentId,
+ commands: [],
+ error: `Provider '${provider}' is not available`,
+ requestId,
+ },
+ });
+ return;
+ }
+
+ const sessionConfig: AgentSessionConfig = {
+ provider,
+ cwd: expandTilde(draftConfig.cwd),
+ ...(draftConfig.modeId ? { modeId: draftConfig.modeId } : {}),
+ ...(draftConfig.model ? { model: draftConfig.model } : {}),
+ ...(draftConfig.thinkingOptionId
+ ? { thinkingOptionId: draftConfig.thinkingOptionId }
+ : {}),
+ };
+
+ const ephemeralSession = await client.createSession(sessionConfig);
+ try {
+ if (!ephemeralSession.listCommands) {
+ this.emit({
+ type: "list_commands_response",
+ payload: {
+ agentId,
+ commands: [],
+ error: `Provider '${provider}' does not support listing commands`,
+ requestId,
+ },
+ });
+ return;
+ }
+
+ const commands = await ephemeralSession.listCommands();
+ this.emit({
+ type: "list_commands_response",
+ payload: {
+ agentId,
+ commands,
+ error: null,
+ requestId,
+ },
+ });
+ } finally {
+ try {
+ await ephemeralSession.close();
+ } catch (closeError) {
+ this.sessionLogger.warn(
+ { err: closeError, provider, agentId },
+ "Failed to close ephemeral command listing session"
+ );
+ }
+ }
return;
}
- const commands = await session.listCommands();
-
this.emit({
type: "list_commands_response",
payload: {
agentId,
- commands,
- error: null,
+ commands: [],
+ error: agent
+ ? `Agent does not support listing commands`
+ : `Agent not found: ${agentId}`,
requestId,
},
});
+
} catch (error: any) {
this.sessionLogger.error(
- { err: error, agentId },
+ { err: error, agentId, draftConfig },
"Failed to list commands"
);
this.emit({
diff --git a/packages/server/src/shared/messages.list-commands.test.ts b/packages/server/src/shared/messages.list-commands.test.ts
new file mode 100644
index 000000000..42a610d66
--- /dev/null
+++ b/packages/server/src/shared/messages.list-commands.test.ts
@@ -0,0 +1,46 @@
+import { describe, expect, test } from "vitest";
+import { SessionInboundMessageSchema } from "./messages.js";
+
+describe("list_commands_request schema", () => {
+ test("accepts legacy agent-only payload", () => {
+ const parsed = SessionInboundMessageSchema.parse({
+ type: "list_commands_request",
+ agentId: "agent-123",
+ requestId: "req-123",
+ });
+
+ expect(parsed.type).toBe("list_commands_request");
+ if (parsed.type !== "list_commands_request") {
+ throw new Error("Expected list_commands_request message");
+ }
+ expect(parsed.agentId).toBe("agent-123");
+ expect(parsed.draftConfig).toBeUndefined();
+ });
+
+ test("accepts draft command context payload", () => {
+ const parsed = SessionInboundMessageSchema.parse({
+ type: "list_commands_request",
+ agentId: "__new_agent__",
+ draftConfig: {
+ provider: "codex",
+ cwd: "/tmp/project",
+ modeId: "bypassPermissions",
+ model: "gpt-5",
+ thinkingOptionId: "off",
+ },
+ requestId: "req-456",
+ });
+
+ expect(parsed.type).toBe("list_commands_request");
+ if (parsed.type !== "list_commands_request") {
+ throw new Error("Expected list_commands_request message");
+ }
+ expect(parsed.draftConfig).toEqual({
+ provider: "codex",
+ cwd: "/tmp/project",
+ modeId: "bypassPermissions",
+ model: "gpt-5",
+ thinkingOptionId: "off",
+ });
+ });
+});
diff --git a/packages/server/src/shared/messages.ts b/packages/server/src/shared/messages.ts
index b4c0a5b5b..37870c1db 100644
--- a/packages/server/src/shared/messages.ts
+++ b/packages/server/src/shared/messages.ts
@@ -926,9 +926,18 @@ export const PingMessageSchema = z.object({
clientSentAt: z.number().int().optional(),
});
+const ListCommandsDraftConfigSchema = z.object({
+ provider: AgentProviderSchema,
+ cwd: z.string(),
+ modeId: z.string().optional(),
+ model: z.string().optional(),
+ thinkingOptionId: z.string().optional(),
+});
+
export const ListCommandsRequestSchema = z.object({
type: z.literal("list_commands_request"),
agentId: z.string(),
+ draftConfig: ListCommandsDraftConfigSchema.optional(),
requestId: z.string(),
});