feat: add in-chat code mode toggle with claude/codex swap

This commit is contained in:
Gagancreates 2026-05-19 04:08:33 +05:30
parent 949ab4c243
commit d5dff0554c
11 changed files with 320 additions and 99 deletions

View file

@ -525,7 +525,7 @@ export function setupIpcHandlers() {
return runsCore.createRun(args);
},
'runs:createMessage': async (_event, args) => {
return { messageId: await runsCore.createMessage(args.runId, args.message, args.voiceInput, args.voiceOutput, args.searchEnabled, args.middlePaneContext) };
return { messageId: await runsCore.createMessage(args.runId, args.message, args.voiceInput, args.voiceOutput, args.searchEnabled, args.middlePaneContext, args.codeMode) };
},
'runs:authorizePermission': async (_event, args) => {
await runsCore.authorizePermission(args.runId, args.authorization);

View file

@ -954,7 +954,7 @@ function App() {
voice.start()
}, [voice])
const handlePromptSubmitRef = useRef<((message: PromptInputMessage, mentions?: FileMention[], stagedAttachments?: StagedAttachment[], searchEnabled?: boolean) => Promise<void>) | null>(null)
const handlePromptSubmitRef = useRef<((message: PromptInputMessage, mentions?: FileMention[], stagedAttachments?: StagedAttachment[], searchEnabled?: boolean, codeMode?: 'claude' | 'codex') => Promise<void>) | null>(null)
const pendingVoiceInputRef = useRef(false)
// Palette: per-tab editor handles for capturing cursor context on Cmd+K, and pending payload
@ -2136,6 +2136,19 @@ function App() {
status: 'running',
timestamp: Date.now(),
}])
// Detect acpx-driven coding-agent runs so the composer can retroactively
// flip code mode on with the right agent (when the user reached the skill
// via plain prompt rather than the explicit toggle).
if (llmEvent.toolName === 'executeCommand') {
const input = llmEvent.input as { command?: unknown } | undefined
const cmd = typeof input?.command === 'string' ? input.command : ''
const match = cmd.match(/\bacpx\b[\s\S]*?\b(claude|codex)\b/)
if (match) {
window.dispatchEvent(new CustomEvent('code-mode-detected', {
detail: { runId: event.runId, agent: match[1] as 'claude' | 'codex' },
}))
}
}
} else if (llmEvent.type === 'finish-step') {
const nextUsage = normalizeUsage(llmEvent.usage)
if (nextUsage) {
@ -2428,6 +2441,7 @@ function App() {
mentions?: FileMention[],
stagedAttachments: StagedAttachment[] = [],
searchEnabled?: boolean,
codeMode?: 'claude' | 'codex',
) => {
if (isProcessing) return
@ -2535,6 +2549,7 @@ function App() {
voiceInput: pendingVoiceInputRef.current || undefined,
voiceOutput: ttsEnabledRef.current ? ttsModeRef.current : undefined,
searchEnabled: searchEnabled || undefined,
codeMode: codeMode || undefined,
middlePaneContext,
})
analytics.chatMessageSent({
@ -2550,6 +2565,7 @@ function App() {
voiceInput: pendingVoiceInputRef.current || undefined,
voiceOutput: ttsEnabledRef.current ? ttsModeRef.current : undefined,
searchEnabled: searchEnabled || undefined,
codeMode: codeMode || undefined,
middlePaneContext,
})
analytics.chatMessageSent({
@ -5736,6 +5752,7 @@ function App() {
<AskHumanRequest
key={request.toolCallId}
query={request.query}
options={request.options}
onResponse={(response) => handleAskHumanResponse(request.toolCallId, request.subflow, response)}
isProcessing={isActive && isProcessing}
/>

View file

@ -9,6 +9,7 @@ import { useState, useRef, useEffect } from "react";
export type AskHumanRequestProps = ComponentProps<"div"> & {
query: string;
options?: string[];
onResponse: (response: string) => void;
isProcessing?: boolean;
};
@ -16,17 +17,21 @@ export type AskHumanRequestProps = ComponentProps<"div"> & {
export const AskHumanRequest = ({
className,
query,
options,
onResponse,
isProcessing = false,
...props
}: AskHumanRequestProps) => {
const [response, setResponse] = useState("");
const textareaRef = useRef<HTMLTextAreaElement>(null);
const hasOptions = Array.isArray(options) && options.length > 0;
useEffect(() => {
// Auto-focus the textarea when component mounts
textareaRef.current?.focus();
}, []);
// Auto-focus the textarea when in free-text mode; nothing to focus for buttons.
if (!hasOptions) {
textareaRef.current?.focus();
}
}, [hasOptions]);
const handleSubmit = () => {
const trimmed = response.trim();
@ -36,6 +41,11 @@ export const AskHumanRequest = ({
}
};
const handleOptionClick = (option: string) => {
if (isProcessing) return;
onResponse(option);
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
@ -65,30 +75,47 @@ export const AskHumanRequest = ({
{query}
</p>
</div>
<div className="space-y-2">
<Textarea
ref={textareaRef}
value={response}
onChange={(e) => setResponse(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Type your response..."
disabled={isProcessing}
rows={3}
className="resize-none"
/>
<div className="flex justify-end">
<Button
variant="default"
size="sm"
onClick={handleSubmit}
disabled={!canSubmit}
className="gap-2"
>
<ArrowUpIcon className="size-4" />
Send Response
</Button>
{hasOptions ? (
<div className="flex flex-wrap gap-2">
{options!.map((option) => (
<Button
key={option}
variant="outline"
size="sm"
onClick={() => handleOptionClick(option)}
disabled={isProcessing}
className="bg-background"
>
{option}
</Button>
))}
</div>
</div>
) : (
<div className="space-y-2">
<Textarea
ref={textareaRef}
value={response}
onChange={(e) => setResponse(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Type your response..."
disabled={isProcessing}
rows={3}
className="resize-none"
/>
<div className="flex justify-end">
<Button
variant="default"
size="sm"
onClick={handleSubmit}
disabled={!canSubmit}
className="gap-2"
>
<ArrowUpIcon className="size-4" />
Send Response
</Button>
</div>
</div>
)}
</div>
</div>
</div>

View file

@ -18,6 +18,7 @@ import {
Mic,
Plus,
Square,
Terminal,
X,
} from 'lucide-react'
@ -109,7 +110,7 @@ function getAttachmentIcon(kind: AttachmentIconKind) {
}
interface ChatInputInnerProps {
onSubmit: (message: PromptInputMessage, mentions?: FileMention[], attachments?: StagedAttachment[], searchEnabled?: boolean) => void
onSubmit: (message: PromptInputMessage, mentions?: FileMention[], attachments?: StagedAttachment[], searchEnabled?: boolean, codeMode?: 'claude' | 'codex') => void
onStop?: () => void
isProcessing: boolean
isStopping?: boolean
@ -174,6 +175,8 @@ function ChatInputInner({
const [searchAvailable, setSearchAvailable] = useState(false)
const [isRowboatConnected, setIsRowboatConnected] = useState(false)
const [workDir, setWorkDir] = useState<string | null>(null)
const [codingAgent, setCodingAgent] = useState<'claude' | 'codex'>('claude')
const [codeModeEnabled, setCodeModeEnabled] = useState(false)
// When a run exists, freeze the dropdown to the run's resolved model+provider.
useEffect(() => {
@ -256,17 +259,73 @@ function ChatInputInner({
return () => window.removeEventListener('models-config-changed', handler)
}, [loadModelConfig])
// Load currently configured work directory
// Listen for coding-agent runs that were triggered without the explicit code-mode
// toggle. App.tsx dispatches this when it sees an acpx executeCommand fire. We
// flip the pill on with the detected agent so the UI reflects what's happening.
useEffect(() => {
const handler = (ev: Event) => {
const detail = (ev as CustomEvent<{ runId?: string; agent?: 'claude' | 'codex' }>).detail
if (!detail || !detail.agent) return
if (runId && detail.runId && detail.runId !== runId) return
setCodeModeEnabled(true)
setCodingAgent(detail.agent)
}
window.addEventListener('code-mode-detected', handler)
return () => window.removeEventListener('code-mode-detected', handler)
}, [runId])
// Cross-platform basename — handles both / and \ separators.
const basename = useCallback((p: string): string => {
const trimmed = p.replace(/[\\/]+$/, '')
const idx = Math.max(trimmed.lastIndexOf('/'), trimmed.lastIndexOf('\\'))
return idx >= 0 ? trimmed.slice(idx + 1) : trimmed
}, [])
// Load coding-agent preference for a given workdir.
// Storage: config/coding-agents.json — { [workDirPath]: 'claude' | 'codex' }
const loadCodingAgentFor = useCallback(async (dir: string | null): Promise<'claude' | 'codex'> => {
if (!dir) return 'claude'
try {
const result = await window.ipc.invoke('workspace:readFile', { path: 'config/coding-agents.json' })
const parsed = JSON.parse(result.data) as Record<string, unknown>
const value = parsed?.[dir]
if (value === 'codex' || value === 'claude') return value
} catch {
/* file missing or invalid — fall through to default */
}
return 'claude'
}, [])
const persistCodingAgent = useCallback(async (dir: string, agent: 'claude' | 'codex') => {
let existing: Record<string, 'claude' | 'codex'> = {}
try {
const result = await window.ipc.invoke('workspace:readFile', { path: 'config/coding-agents.json' })
const parsed = JSON.parse(result.data) as Record<string, unknown>
for (const [k, v] of Object.entries(parsed ?? {})) {
if (v === 'claude' || v === 'codex') existing[k] = v
}
} catch { /* start fresh */ }
existing[dir] = agent
await window.ipc.invoke('workspace:writeFile', {
path: 'config/coding-agents.json',
data: JSON.stringify(existing, null, 2),
})
}, [])
// Load currently configured work directory (and its agent preference)
const loadWorkDir = useCallback(async () => {
let dir: string | null = null
try {
const result = await window.ipc.invoke('workspace:readFile', { path: 'config/workdir.json' })
const parsed = JSON.parse(result.data)
const value = typeof parsed?.path === 'string' ? parsed.path.trim() : ''
setWorkDir(value || null)
dir = value || null
} catch {
setWorkDir(null)
dir = null
}
}, [])
setWorkDir(dir)
setCodingAgent(await loadCodingAgentFor(dir))
}, [loadCodingAgentFor])
useEffect(() => {
loadWorkDir()
@ -296,12 +355,13 @@ function ChatInputInner({
data: JSON.stringify({ path: chosen }, null, 2),
})
setWorkDir(chosen)
setCodingAgent(await loadCodingAgentFor(chosen))
toast.success(`Work directory set: ${chosen}`)
} catch (err) {
console.error('Failed to set work directory', err)
toast.error('Failed to set work directory')
}
}, [workDir])
}, [workDir, loadCodingAgentFor])
const handleClearWorkDir = useCallback(async () => {
try {
@ -310,6 +370,7 @@ function ChatInputInner({
data: JSON.stringify({}, null, 2),
})
setWorkDir(null)
setCodingAgent('claude')
toast.success('Work directory cleared')
} catch (err) {
console.error('Failed to clear work directory', err)
@ -317,6 +378,21 @@ function ChatInputInner({
}
}, [])
const handleToggleCodingAgent = useCallback(async () => {
const next: 'claude' | 'codex' = codingAgent === 'claude' ? 'codex' : 'claude'
setCodingAgent(next)
// Persist only when scoped to a workdir; without one there's nothing to key on.
if (!workDir) return
try {
await persistCodingAgent(workDir, next)
} catch (err) {
console.error('Failed to save coding agent', err)
toast.error('Failed to save coding agent')
// revert on failure
setCodingAgent(codingAgent)
}
}, [workDir, codingAgent, persistCodingAgent])
// Check search tool availability (exa or signed-in via gateway)
useEffect(() => {
const checkSearch = async () => {
@ -401,12 +477,14 @@ function ChatInputInner({
const handleSubmit = useCallback(() => {
if (!canSubmit) return
onSubmit({ text: message.trim(), files: [] }, controller.mentions.mentions, attachments, searchEnabled || undefined)
// codeMode is sticky per conversation — don't reset after send.
const effectiveCodeMode = codeModeEnabled ? codingAgent : undefined
onSubmit({ text: message.trim(), files: [] }, controller.mentions.mentions, attachments, searchEnabled || undefined, effectiveCodeMode)
controller.textInput.clear()
controller.mentions.clearMentions()
setAttachments([])
setSearchEnabled(false)
}, [attachments, canSubmit, controller, message, onSubmit, searchEnabled])
}, [attachments, canSubmit, controller, message, onSubmit, searchEnabled, codeModeEnabled, codingAgent, workDir])
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
@ -589,7 +667,7 @@ function ChatInputInner({
className="flex h-7 max-w-[180px] shrink-0 items-center gap-1.5 rounded-full border border-border bg-muted/40 px-2.5 text-xs text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
>
<FolderCog className="h-3.5 w-3.5" />
<span className="truncate">{workDir.split('/').pop() || workDir}</span>
<span className="truncate">{basename(workDir) || workDir}</span>
</button>
</TooltipTrigger>
<TooltipContent side="top">
@ -619,6 +697,52 @@ function ChatInputInner({
</button>
)
)}
{codeModeEnabled ? (
<div className="flex h-7 shrink-0 items-center rounded-full bg-secondary text-xs font-medium text-foreground">
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={() => setCodeModeEnabled(false)}
className="flex h-full items-center gap-1.5 rounded-l-full pl-2.5 pr-2 transition-colors hover:bg-secondary/70"
>
<Terminal className="h-3.5 w-3.5" />
<span>Code</span>
</button>
</TooltipTrigger>
<TooltipContent side="top">Code mode on click to disable</TooltipContent>
</Tooltip>
<span className="text-foreground/30">·</span>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={handleToggleCodingAgent}
className="flex h-full items-center rounded-r-full pl-2 pr-2.5 transition-colors hover:bg-secondary/70"
>
<span>{codingAgent === 'claude' ? 'Claude' : 'Codex'}</span>
</button>
</TooltipTrigger>
<TooltipContent side="top">
Coding agent: {codingAgent === 'claude' ? 'Claude Code' : 'Codex'} click to swap
</TooltipContent>
</Tooltip>
</div>
) : (
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={() => setCodeModeEnabled(true)}
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
aria-label="Code mode"
>
<Terminal className="h-4 w-4" />
</button>
</TooltipTrigger>
<TooltipContent side="top">Use a coding agent (Claude Code or Codex)</TooltipContent>
</Tooltip>
)}
<div className="flex-1" />
{lockedModel ? (
<span
@ -779,7 +903,7 @@ export interface ChatInputWithMentionsProps {
knowledgeFiles: string[]
recentFiles: string[]
visibleFiles: string[]
onSubmit: (message: PromptInputMessage, mentions?: FileMention[], attachments?: StagedAttachment[]) => void
onSubmit: (message: PromptInputMessage, mentions?: FileMention[], attachments?: StagedAttachment[], searchEnabled?: boolean, codeMode?: 'claude' | 'codex') => void
onStop?: () => void
isProcessing: boolean
isStopping?: boolean

View file

@ -386,9 +386,10 @@ export async function mapAgentTool(t: z.infer<typeof ToolAttachment>): Promise<T
case "builtin": {
if (t.name === "ask-human") {
return tool({
description: "Ask a human before proceeding",
description: "Ask a human before proceeding. Optionally pass `options` (an array of short button labels) to render the question as a one-click choice; the user's response will be the chosen label verbatim.",
inputSchema: z.object({
question: z.string().describe("The question to ask the human"),
options: z.array(z.string()).optional().describe("Optional short button labels (2-4 recommended). If provided, the user picks one with a single click instead of typing. The response you receive will be the chosen label."),
}),
});
}
@ -1059,6 +1060,7 @@ export async function* streamAgent({
let voiceInput = false;
let voiceOutput: 'summary' | 'full' | null = null;
let searchEnabled = false;
let codeMode: 'claude' | 'codex' | null = null;
let middlePaneContext:
| { kind: 'note'; path: string; content: string }
| { kind: 'browser'; url: string; title: string }
@ -1207,6 +1209,9 @@ export async function* streamAgent({
if (msg.searchEnabled) {
searchEnabled = true;
}
// Code mode is per-message: latest message decides whether the assistant
// should route coding work through the code-with-agents skill / chosen agent.
codeMode = msg.codeMode ?? null;
if (msg.voiceOutput) {
voiceOutput = msg.voiceOutput;
}
@ -1310,6 +1315,28 @@ Do not announce the work directory unless it's relevant. Just use it.`;
loopLogger.log('search enabled, injecting search prompt');
instructionsWithDateTime += `\n\n# Search\nThe user has requested a search. Use the web-search tool to answer their query.`;
}
if (codeMode) {
loopLogger.log('code mode enabled, injecting coding-agent context', codeMode);
const agentDisplay = codeMode === 'claude' ? 'Claude Code' : 'Codex';
const otherAgent = codeMode === 'claude' ? 'codex' : 'claude';
const otherDisplay = codeMode === 'claude' ? 'Codex' : 'Claude Code';
instructionsWithDateTime += `\n\n# Code Mode (Active) — Default agent: ${agentDisplay}
The user has turned on **code mode** and the composer chip is set to **${agentDisplay}** (\`${codeMode}\`). Use this as the **default** agent for coding tasks in this turn.
**The user can override the agent at any time, two ways:**
1. By toggling the chip in the composer (preferred).
2. By asking you directly in chat ("use codex", "switch to claude", "do this with ${otherDisplay}", etc.). When the user explicitly asks to use a different agent in the current message, honor that use \`${otherAgent}\` instead of \`${codeMode}\` for this turn, and briefly mention they can also toggle it via the chip for stickiness.
When you call \`executeCommand\` to run the coding agent, the command shape is:
\`\`\`
npx acpx@latest --approve-all --cwd <workdir> <agent> exec "<prompt>"
\`\`\`
Where \`<agent>\` is either \`claude\` or \`codex\` — pick based on (in priority order): an explicit in-chat override → the chip setting (\`${codeMode}\`).
If the user's message is clearly NOT a coding request (small talk, an unrelated question), answer directly without invoking the coding agent. Code mode signals readiness, not that every message must route through the agent.`;
}
let streamError: string | null = null;
for await (const event of streamLlm(
model,
@ -1365,11 +1392,16 @@ Do not announce the work directory unless it's relevant. Just use it.`;
const underlyingTool = agent.tools![part.toolName];
if (underlyingTool.type === "builtin" && underlyingTool.name === "ask-human") {
loopLogger.log('emitting ask-human-request, toolCallId:', part.toolCallId);
const rawOptions = (part.arguments as { options?: unknown }).options;
const options = Array.isArray(rawOptions)
? rawOptions.filter((o): o is string => typeof o === 'string' && o.trim().length > 0)
: undefined;
yield* processEvent({
runId,
type: "ask-human-request",
toolCallId: part.toolCallId,
query: part.arguments.question,
...(options && options.length > 0 ? { options } : {}),
subflow: [],
});
}

View file

@ -80,7 +80,7 @@ ${thirdPartyBlock}**Meeting Prep:** When users ask you to prepare for a meeting,
**Document Collaboration:** When users ask you to work on a document, collaborate on writing, create a new document, edit/refine existing notes, or say things like "let's work on [X]", "help me write [X]", "create a doc for [X]", or "let's draft [X]", you MUST load the \`doc-collab\` skill first. **This applies even for small one-off edits** — the skill carries the canonical *terse-and-scannable* writing style for the knowledge base, and that style applies whether you're authoring a fresh note or fixing a single section. Load it before writing anything into a note.
**Code with Agents:** When users ask you to write code, build a project, create a script, fix a bug, or do any software development task, load the \`code-with-agents\` skill first. It provides guidance for delegating coding work to Claude Code or Codex via acpx.
**Code with Agents:** When users ask you to write code, build a project, create a script, fix a bug, or do any software development task **including simple things like "create a .c file" or "write a hello-world in Python"** your FIRST action MUST be \`loadSkill('code-with-agents')\`. Do NOT reach for \`executeCommand\` (PowerShell / bash / shell) or any workspace file tool to do code work yourself before loading this skill. The skill decides whether to delegate to Claude Code / Codex (via acpx) or hand control back to you, and it presents the user a one-click choice when needed. Paths outside the Rowboat workspace root (e.g. \`G:/...\`, \`~/projects/...\`) are NORMAL for coding tasks — do NOT raise "outside workspace" concerns or fall back to your own tools.
**App Control:** When users ask you to open notes, show the bases or graph view, filter or search notes, or manage saved views, load the \`app-navigation\` skill first. It provides structured guidance for navigating the app UI and controlling the knowledge base view.

View file

@ -1,90 +1,105 @@
export const skill = String.raw`
# Code with Agents Skill
Use this skill when the user asks you to write code, build a project, create scripts, fix bugs, or do any software development task that should be delegated to a coding agent (Claude Code or Codex).
Use this skill whenever the user asks you to write code, build a project, create scripts, fix bugs, read/explain code, or do any software development task even simple file creations like "make a .c file".
## Important: delegate ALL coding work
Coding agents operate on **arbitrary file paths** (including paths outside the Rowboat workspace root, like \`G:/4th sem/CN\` or \`~/projects/foo\`). Do NOT raise "outside workspace" concerns, and do NOT fall back to your own \`executeCommand\` (PowerShell / bash) or workspace file tools to do code work yourself.
Once the user has chosen to use Claude Code or Codex, you MUST delegate ALL code-related tasks to the coding agent. This includes:
- Writing, editing, or refactoring code
- Reading, summarizing, or explaining code
- Debugging and fixing bugs
- Running tests or build commands
- Exploring project structure
- Any other task that involves interacting with a codebase
---
Do NOT attempt to do any of these yourself no reading files, no running commands, no writing code. You are the coordinator; the coding agent does the work. Your job is to translate the user's request into a clear prompt and pass it to the agent.
## STEP 1 MANDATORY FIRST ACTION
## Prerequisites
Look in your **system context** for a section titled **"# Code Mode (Active)"**.
The user must have one of the following installed on their machine:
- **Claude Code** https://claude.ai/code
- **Codex** https://codex.openai.com
### Case A "# Code Mode (Active)" IS present
These are external tools that you cannot install for the user.
Code mode is on and the user has selected an agent. Skip directly to Step 2. Do NOT call ask-human.
## Workflow
### Case B "# Code Mode (Active)" is NOT present
### Step 1: Gather requirements
Your **very next tool call MUST be \`ask-human\`** with options. Do not write any explanation text first. Do not describe a plan. Do not check the workspace boundary. Just call:
Before running anything, confirm the following with the user:
\`\`\`
ask-human({
question: "How should I handle this coding request?",
options: [
"Use code mode (Claude Code)",
"Use code mode (Codex)",
"Continue with default Rowboat"
]
})
\`\`\`
1. **Working directory** Ask which folder the code should be written in, unless the user has already specified it. Example: "Which folder should I work in?"
2. **Agent choice** Ask whether to use **Claude Code** or **Codex**. Mention that the chosen agent must already be installed on their machine.
This is non-negotiable. The user gets clickable buttons. Free-text "which agent?" questions are forbidden here.
### Step 2: Confirm execution plan
**Branch on the response:**
- "Use code mode (Claude Code)" proceed to Step 2 with agent = \`claude\`.
- "Use code mode (Codex)" proceed to Step 2 with agent = \`codex\`.
- "Continue with default Rowboat" ABANDON this skill. Handle the request yourself using your own tools (workspace file tools, \`executeCommand\` shell, etc.). The rest of this skill does not apply for this turn.
Once you know the folder and agent, tell the user:
---
> I'll use [Claude Code / Codex] to [description of the task] in \`[folder]\`. Permission requests from the coding agent itself (file writes, command execution, etc.) will be automatically approved once it starts. Wait for the user's confirmation before you execute anything.
## STEP 2 Resolve workdir, confirm, execute
### Step 3: Execute with acpx
**Resolve the workdir** (in this priority order):
1. A path the user named in their original message (e.g. \`G:/4th sem/CN\`).
2. The path from a "# User Work Directory" block in your context.
3. Ask once in plain text: "Which folder should I work in?"
Use the \`executeCommand\` tool to run the coding agent via acpx. The command format is:
**Confirm briefly** with the user (one short line):
**For Claude Code:**
` + "`" + `
npx acpx@latest --approve-all --cwd <folder> claude exec "<prompt>"
` + "`" + `
> I'll use [Claude Code / Codex] to [task description] in \`[folder]\`. Permission requests from the coding agent will be auto-approved. Reply "yes" to proceed.
**For Codex:**
` + "`" + `
npx acpx@latest --approve-all --cwd <folder> codex exec "<prompt>"
` + "`" + `
**Execute** with the chosen agent. Call \`executeCommand\` with this exact shape:
\`\`\`
npx acpx@latest --approve-all --cwd <folder> <agent> exec "<prompt>"
\`\`\`
Where \`<agent>\` is \`claude\` or \`codex\`, picked by (in priority order):
- An explicit in-chat override from the user this turn ("use codex", "switch to claude") honor it.
- The agent chosen in Step 1 / the "# Code Mode (Active)" block.
Concrete examples:
\`\`\`
npx acpx@latest --approve-all --cwd ~/projects/myapp claude exec "fix the off-by-one bug in foo.ts"
npx acpx@latest --approve-all --cwd "G:/4th sem/CN" codex exec "create a C program that divides two numbers with divide-by-zero handling"
\`\`\`
### Critical: flag order
The \`--approve-all\` and \`--cwd\` flags are global flags and MUST come before the agent name (\`claude\` or \`codex\`). This is the correct order:
\`--approve-all\` and \`--cwd\` are GLOBAL flags and MUST appear BEFORE the agent name:
` + "`" + `
npx acpx@latest [global flags] <agent> exec "<prompt>"
` + "`" + `
- Correct: \`npx acpx@latest --approve-all --cwd <folder> <agent> exec "<prompt>"\`
- Wrong: \`npx acpx@latest <agent> --approve-all exec "..."\` (will fail)
**Correct:**
` + "`" + `
npx acpx@latest --approve-all --cwd ~/projects/myapp claude exec "fix the bug"
` + "`" + `
### Writing good prompts for the agent
**Wrong (will fail):**
` + "`" + `
npx acpx@latest claude --approve-all exec "fix the bug"
` + "`" + `
- Be specific: file names, function signatures, expected behavior.
- Mention constraints (language, framework, style).
- Expand short user requests into clear, actionable prompts.
### Writing good prompts
---
When constructing the prompt for the coding agent:
- Be specific and detailed about what to build or fix
- Include file names, function signatures, and expected behavior
- Mention any constraints (language, framework, style)
- If the user gave you a short request, expand it into a clear, actionable prompt for the agent
## STEP 3 Report results
### Step 4: Report results
After the command finishes:
- Pass through the coding agent's summary as-is. Do not rewrite.
- Refer to file paths as plain text. Do NOT use \`\`\`file:path\`\`\` reference blocks.
- Only add your own explanation if the command failed (non-zero exit). If exit code is 5, permissions were denied (shouldn't happen with \`--approve-all\` — flag this).
After the command finishes, look for the summary that the coding agent produced at the end of its output and pass that along to the user as-is. Do not rewrite or add to it. Only add your own explanation if the command failed or the exit code is non-zero.
---
Do NOT use file reference blocks (e.g. \`\`\`file:path/to/file\`\`\`) when mentioning code files — they may not open correctly. Just refer to file paths as plain text.
## Once delegating: delegate fully
- If the exit code is 5, it means permissions were denied this should not happen with \`--approve-all\`, but if it does, let the user know
After Step 2 fires, delegate ALL related coding tasks for this turn to the coding agent writing, editing, reading, debugging, exploring structure, running tests. You are the coordinator; the agent does the work.
## Prerequisites (informational)
The user must have one of these installed locally these are external tools you cannot install:
- Claude Code https://claude.ai/code
- Codex https://codex.openai.com
`;
export default skill;

View file

@ -8,17 +8,20 @@ export type MiddlePaneContext =
| { kind: 'note'; path: string; content: string }
| { kind: 'browser'; url: string; title: string };
export type CodeMode = 'claude' | 'codex';
type EnqueuedMessage = {
messageId: string;
message: UserMessageContentType;
voiceInput?: boolean;
voiceOutput?: VoiceOutputMode;
searchEnabled?: boolean;
codeMode?: CodeMode;
middlePaneContext?: MiddlePaneContext;
};
export interface IMessageQueue {
enqueue(runId: string, message: UserMessageContentType, voiceInput?: boolean, voiceOutput?: VoiceOutputMode, searchEnabled?: boolean, middlePaneContext?: MiddlePaneContext): Promise<string>;
enqueue(runId: string, message: UserMessageContentType, voiceInput?: boolean, voiceOutput?: VoiceOutputMode, searchEnabled?: boolean, middlePaneContext?: MiddlePaneContext, codeMode?: CodeMode): Promise<string>;
dequeue(runId: string): Promise<EnqueuedMessage | null>;
}
@ -34,7 +37,7 @@ export class InMemoryMessageQueue implements IMessageQueue {
this.idGenerator = idGenerator;
}
async enqueue(runId: string, message: UserMessageContentType, voiceInput?: boolean, voiceOutput?: VoiceOutputMode, searchEnabled?: boolean, middlePaneContext?: MiddlePaneContext): Promise<string> {
async enqueue(runId: string, message: UserMessageContentType, voiceInput?: boolean, voiceOutput?: VoiceOutputMode, searchEnabled?: boolean, middlePaneContext?: MiddlePaneContext, codeMode?: CodeMode): Promise<string> {
if (!this.store[runId]) {
this.store[runId] = [];
}
@ -45,6 +48,7 @@ export class InMemoryMessageQueue implements IMessageQueue {
voiceInput,
voiceOutput,
searchEnabled,
codeMode,
middlePaneContext,
});
return id;

View file

@ -39,9 +39,9 @@ export async function createRun(opts: z.infer<typeof CreateRunOptions>): Promise
return run;
}
export async function createMessage(runId: string, message: UserMessageContentType, voiceInput?: boolean, voiceOutput?: VoiceOutputMode, searchEnabled?: boolean, middlePaneContext?: MiddlePaneContext): Promise<string> {
export async function createMessage(runId: string, message: UserMessageContentType, voiceInput?: boolean, voiceOutput?: VoiceOutputMode, searchEnabled?: boolean, middlePaneContext?: MiddlePaneContext, codeMode?: 'claude' | 'codex'): Promise<string> {
const queue = container.resolve<IMessageQueue>('messageQueue');
const id = await queue.enqueue(runId, message, voiceInput, voiceOutput, searchEnabled, middlePaneContext);
const id = await queue.enqueue(runId, message, voiceInput, voiceOutput, searchEnabled, middlePaneContext, codeMode);
const runtime = container.resolve<IAgentRuntime>('agentRuntime');
runtime.trigger(runId);
return id;

View file

@ -228,6 +228,7 @@ const ipcSchemas = {
voiceInput: z.boolean().optional(),
voiceOutput: z.enum(['summary', 'full']).optional(),
searchEnabled: z.boolean().optional(),
codeMode: z.enum(['claude', 'codex']).optional(),
middlePaneContext: z.discriminatedUnion('kind', [
z.object({
kind: z.literal('note'),

View file

@ -75,6 +75,7 @@ export const AskHumanRequestEvent = BaseRunEvent.extend({
type: z.literal("ask-human-request"),
toolCallId: z.string(),
query: z.string(),
options: z.array(z.string()).optional(),
});
export const AskHumanResponseEvent = BaseRunEvent.extend({