mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-06-03 19:25:19 +02:00
Code Mode: in-chat toggle, settings tab, and permission/command UX (#572)
* feat: add in-chat code mode toggle with claude/codex swap * feat: show agent and add swap-and-retry on acpx permission card * style: reorder permission card buttons (approve, deny, swap) * feat: add tooltips to composer plus and web search buttons * feat: add code mode settings tab with agent install/auth checks * feat: show sign-in command when agent installed but signed out * style: refine code-mode permission and command block UX - Render permission block before the command block - Collapse permission details after a response; click header to expand - Drop status icons/badge; use minimal green / bold red blocks - Auto-collapse the running command block once it completes * feat: rotating progress labels for code-mode commands; darker tool borders - Code-mode (acpx) command block shows status-aware labels: rotating 'Working on the task…' phrases (5s each, holding on the last) while running, then 'Completed the task' / "Couldn't complete the task" - Darken outer border on all tool blocks in light and dark modes * fix: detect Claude Code sign-in via macOS Keychain On macOS, Claude Code stores OAuth credentials in the login Keychain (service 'Claude Code-credentials'), not in ~/.claude/.credentials.json. Read the Keychain as a fallback so signed-in Mac users are detected. * feat: persistent per-chat sessions for code-mode coding agents - Use a named acpx session (rowboat-<runId>) per chat so follow-up coding requests resume the same agent and keep context - Create the session once at chat start (sessions new --name), then prompt with -s <name>; reuse on follow-ups (no re-create) - Drop the redundant in-chat 'reply yes' confirmation (the executeCommand permission card is the confirmation) - Code-mode output uses plain-text paths (overrides global filepath rule) - On not-installed/auth errors, point user to Settings -> Code Mode * fix: code-mode session creation uses idempotent ensure, run sequentially - Use 'sessions ensure --name' instead of 'sessions new' so reopening a chat resumes the existing session instead of erroring on a name clash - Create the session and run the prompt as separate sequential calls so the permission/command blocks render one at a time (not all at once) * fix: reliable Claude Code session resume on Windows (avoid claude.cmd EINVAL) Resuming a code-mode chat after restarting the app spawns a fresh ACP agent. On Windows + Node >=20.12 the bridge spawning claude.cmd throws EINVAL, so the session queue owner fails to start. Rowboat injects CLAUDE_CODE_EXECUTABLE=claude.exe to dodge this, but the override didn't reliably reach the spawn. Windows-only; no-op on macOS/Linux. - executeCommand now accepts an env override and the non-abortable fallback path passes it through (was silently dropped) - resolveClaudeExeOnWindows also scans known npm/pnpm/volta global bin dirs, not just PATH (Electron's runtime PATH can omit them) - add --timeout 600 to acpx prompt commands so a genuine stall fails cleanly instead of hanging on 'Running' forever
This commit is contained in:
parent
b89b91258e
commit
537b6f66bb
22 changed files with 1084 additions and 171 deletions
|
|
@ -392,9 +392,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."),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
|
@ -1065,6 +1066,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 }
|
||||
|
|
@ -1213,6 +1215,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;
|
||||
}
|
||||
|
|
@ -1316,6 +1321,50 @@ 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';
|
||||
// Deterministic, per-chat session name so the coding agent keeps
|
||||
// context across the user's requests within this chat. Reusing the
|
||||
// same -s <name> resumes the session; the first call creates it.
|
||||
const sessionName = `rowboat-${runId}`;
|
||||
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.
|
||||
|
||||
**Persistent session for this chat — session name: \`${sessionName}\`.** This chat uses one named agent session so the agent keeps context across your requests. The session must exist before it can be prompted (\`-s\` only resumes; it does not create).
|
||||
|
||||
**1. First coding action in this chat — ensure the session exists:**
|
||||
|
||||
\`\`\`
|
||||
npx acpx@latest --approve-all --cwd <workdir> <agent> sessions ensure --name ${sessionName}
|
||||
\`\`\`
|
||||
|
||||
(\`ensure\` creates the session if missing and reuses it if it already exists — safe to call when reopening this chat later.)
|
||||
|
||||
**2. Then run the prompt:**
|
||||
|
||||
\`\`\`
|
||||
npx acpx@latest --approve-all --timeout 600 --cwd <workdir> <agent> -s ${sessionName} "<prompt>"
|
||||
\`\`\`
|
||||
|
||||
**3. Every follow-up coding request in this chat — reuse the same session (do NOT create again):**
|
||||
|
||||
\`\`\`
|
||||
npx acpx@latest --approve-all --timeout 600 --cwd <workdir> <agent> -s ${sessionName} "<prompt>"
|
||||
\`\`\`
|
||||
|
||||
Run these as **separate, sequential** \`executeCommand\` calls — issue the \`sessions ensure\` call first and WAIT for it to finish, then issue the prompt call. Do NOT fire both in the same turn / batch.
|
||||
|
||||
Where \`<agent>\` is either \`claude\` or \`codex\` — pick based on (in priority order): an explicit in-chat override → the chip setting (\`${codeMode}\`). Use \`${sessionName}\` exactly — do NOT invent a different name, and do NOT use \`exec\` (it is one-shot and forgets).
|
||||
|
||||
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,
|
||||
|
|
@ -1371,11 +1420,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: [],
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import { getRuntimeContext, getRuntimeContextPrompt } from "./runtime-context.js
|
|||
import { composioAccountsRepo } from "../../composio/repo.js";
|
||||
import { isConfigured as isComposioConfigured } from "../../composio/client.js";
|
||||
import { CURATED_TOOLKITS } from "@x/shared/dist/composio.js";
|
||||
import container from "../../di/container.js";
|
||||
import type { ICodeModeConfigRepo } from "../../code-mode/repo.js";
|
||||
|
||||
const runtimeContextPrompt = getRuntimeContextPrompt(getRuntimeContext());
|
||||
|
||||
|
|
@ -29,7 +31,7 @@ Load the \`composio-integration\` skill when the user asks to interact with any
|
|||
`;
|
||||
}
|
||||
|
||||
function buildStaticInstructions(composioEnabled: boolean, catalog: string): string {
|
||||
function buildStaticInstructions(composioEnabled: boolean, catalog: string, codeModeEnabled: boolean = true): string {
|
||||
// Conditionally include Composio-related instruction sections
|
||||
const emailDraftSuffix = composioEnabled
|
||||
? ` Do NOT load this skill for reading, fetching, or checking emails — use the \`composio-integration\` skill for that instead.`
|
||||
|
|
@ -80,7 +82,9 @@ ${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.
|
||||
${codeModeEnabled
|
||||
? `**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.`
|
||||
: `**Code with Agents (disabled):** Code mode is currently OFF in the user's settings. Do NOT load \`code-with-agents\` and do NOT call acpx. Handle coding requests yourself with your normal tools if you can. After answering, add a final line letting the user know they can delegate coding to Claude Code or Codex by enabling Code Mode in Settings → Code Mode.`}
|
||||
|
||||
**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.
|
||||
|
||||
|
|
@ -312,30 +316,29 @@ Never output raw file paths in plain text when they could be wrapped in a filepa
|
|||
/** Keep backward-compatible export for any external consumers */
|
||||
export const CopilotInstructions = buildStaticInstructions(true, skillCatalog);
|
||||
|
||||
/**
|
||||
* Cached Composio instructions. Invalidated by calling invalidateCopilotInstructionsCache().
|
||||
*/
|
||||
let cachedInstructions: string | null = null;
|
||||
|
||||
/**
|
||||
* Invalidate the cached instructions so the next buildCopilotInstructions() call
|
||||
* regenerates the Composio section. Call this after connecting/disconnecting a toolkit.
|
||||
*/
|
||||
export function invalidateCopilotInstructionsCache(): void {
|
||||
cachedInstructions = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build full copilot instructions with dynamic Composio tools section.
|
||||
* Results are cached and reused until invalidated via invalidateCopilotInstructionsCache().
|
||||
*/
|
||||
export async function buildCopilotInstructions(): Promise<string> {
|
||||
if (cachedInstructions !== null) return cachedInstructions;
|
||||
const composioEnabled = await isComposioConfigured();
|
||||
const catalog = composioEnabled
|
||||
? skillCatalog
|
||||
: buildSkillCatalog({ excludeIds: ['composio-integration'] });
|
||||
const baseInstructions = buildStaticInstructions(composioEnabled, catalog);
|
||||
let codeModeEnabled = false;
|
||||
try {
|
||||
const repo = container.resolve<ICodeModeConfigRepo>('codeModeConfigRepo');
|
||||
codeModeEnabled = (await repo.getConfig()).enabled;
|
||||
} catch {
|
||||
// repo unavailable — default to disabled
|
||||
}
|
||||
const excludeIds: string[] = [];
|
||||
if (!composioEnabled) excludeIds.push('composio-integration');
|
||||
if (!codeModeEnabled) excludeIds.push('code-with-agents');
|
||||
const catalog = excludeIds.length > 0
|
||||
? buildSkillCatalog({ excludeIds })
|
||||
: skillCatalog;
|
||||
const baseInstructions = buildStaticInstructions(composioEnabled, catalog, codeModeEnabled);
|
||||
const composioPrompt = await getComposioToolsPrompt();
|
||||
cachedInstructions = composioPrompt
|
||||
? baseInstructions + '\n' + composioPrompt
|
||||
|
|
|
|||
|
|
@ -1,90 +1,140 @@
|
|||
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:
|
||||
**State your intent in one line, then execute immediately — do NOT wait for a "yes".** The \`executeCommand\` call surfaces a permission card that is itself the user's confirmation, so an extra in-chat "reply yes to proceed" is redundant friction. Say something like:
|
||||
|
||||
**For Claude Code:**
|
||||
` + "`" + `
|
||||
npx acpx@latest --approve-all --cwd <folder> claude exec "<prompt>"
|
||||
` + "`" + `
|
||||
> Using [Claude Code / Codex] to [task description] in \`[folder]\`.
|
||||
|
||||
**For Codex:**
|
||||
` + "`" + `
|
||||
npx acpx@latest --approve-all --cwd <folder> codex exec "<prompt>"
|
||||
` + "`" + `
|
||||
…and then immediately make the \`executeCommand\` call in the same turn.
|
||||
|
||||
**Execute** with the chosen agent using a **persistent named session** so follow-up coding requests in this chat resume the same agent and keep context.
|
||||
|
||||
Pick \`<agent>\` (\`claude\` or \`codex\`) 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.
|
||||
|
||||
Pick \`<session-name>\` — **stable for this whole chat**:
|
||||
- If the "# Code Mode (Active)" block gives a session name (e.g. \`rowboat-<runId>\`), use that exact name.
|
||||
- Otherwise pick one short, kebab-case name and **reuse it for every coding call this turn and in follow-ups** — never a new name each time.
|
||||
|
||||
**\`-s\` resumes an existing session; it does NOT create one.** So ensure the session exists once at the start, then prompt:
|
||||
|
||||
**1. First coding action in this chat — ensure the session exists:**
|
||||
|
||||
\`\`\`
|
||||
npx acpx@latest --approve-all --cwd <folder> <agent> sessions ensure --name <session-name>
|
||||
\`\`\`
|
||||
|
||||
(\`ensure\` creates the session if missing and reuses it if it already exists — so reopening this chat later just resumes the same session instead of erroring.)
|
||||
|
||||
**2. Then run the prompt:**
|
||||
|
||||
\`\`\`
|
||||
npx acpx@latest --approve-all --timeout 600 --cwd <folder> <agent> -s <session-name> "<prompt>"
|
||||
\`\`\`
|
||||
|
||||
**3. Every follow-up coding request in this chat — reuse the same session (do NOT create again):**
|
||||
|
||||
\`\`\`
|
||||
npx acpx@latest --approve-all --timeout 600 --cwd <folder> <agent> -s <session-name> "<prompt>"
|
||||
\`\`\`
|
||||
|
||||
**Run steps 1 and 2 as separate, sequential \`executeCommand\` calls.** Issue the \`sessions ensure\` call FIRST, wait for it to finish, and only THEN issue the prompt call. Do NOT fire both in the same turn / batch — each must surface and complete its own permission + command block before the next begins.
|
||||
|
||||
Do NOT use \`exec\` — it is one-shot and forgets everything.
|
||||
|
||||
Concrete example:
|
||||
|
||||
\`\`\`
|
||||
# First coding message in the chat — ensure the session, then prompt:
|
||||
npx acpx@latest --approve-all --cwd "G:\\Blogging\\myblog" claude sessions ensure --name diskspace-check
|
||||
npx acpx@latest --approve-all --timeout 600 --cwd "G:\\Blogging\\myblog" claude -s diskspace-check "Check the system disk space and report total, used, and free space."
|
||||
|
||||
# Follow-up in the same chat — reuse the session, no create:
|
||||
npx acpx@latest --approve-all --timeout 600 --cwd "G:\\Blogging\\myblog" claude -s diskspace-check "Summarize what we did and the final findings."
|
||||
\`\`\`
|
||||
|
||||
### 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\`, \`--timeout\`, and \`--cwd\` are GLOBAL flags and MUST appear BEFORE the agent name. \`sessions ensure --name <name>\` and \`-s <session-name>\` come AFTER the agent name:
|
||||
|
||||
` + "`" + `
|
||||
npx acpx@latest [global flags] <agent> exec "<prompt>"
|
||||
` + "`" + `
|
||||
- ✓ Correct: \`npx acpx@latest --approve-all --timeout 600 --cwd <folder> <agent> -s <session-name> "<prompt>"\`
|
||||
- ✗ Wrong: \`npx acpx@latest <agent> --approve-all -s <name> "..."\` (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. (This overrides the global "always wrap paths in filepath blocks" rule — for code-mode output, plain text.)
|
||||
- Only add your own explanation if the command failed (non-zero exit):
|
||||
- Exit code 5 — permissions were denied (shouldn't happen with \`--approve-all\`; flag it).
|
||||
- Exit code 4 / "No acpx session found" — the \`-s <session-name>\` session doesn't exist yet. Create it once with \`npx acpx@latest --approve-all --cwd <folder> <agent> sessions ensure --name <session-name>\`, then retry the prompt. (\`-s\` only resumes; it never creates.)
|
||||
- "command not found" / agent not installed, or an auth/sign-in error — the agent isn't set up. Tell the user to install or sign in to the agent via **Settings → Code Mode**, where Rowboat shows the install and sign-in status.
|
||||
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -95,21 +95,47 @@ const LLMPARSE_MIME_TYPES: Record<string, string> = {
|
|||
// When the LLM invokes acpx via executeCommand, pre-resolve claude's real .exe
|
||||
// from the npm-shim layout and inject it via env so the bridge can spawn it.
|
||||
function resolveClaudeExeOnWindows(): string | undefined {
|
||||
const pathDirs = (process.env.PATH ?? '').split(';');
|
||||
for (const dir of pathDirs) {
|
||||
const trimmed = dir.trim();
|
||||
if (!trimmed) continue;
|
||||
const cmdPath = path.join(trimmed, 'claude.cmd');
|
||||
if (!existsSync(cmdPath)) continue;
|
||||
const exeFromLayout = path.join(trimmed, 'node_modules', '@anthropic-ai', 'claude-code', 'bin', 'claude.exe');
|
||||
// Candidate dirs = everything on PATH, plus well-known npm/pnpm/volta global
|
||||
// bin dirs. Electron's runtime PATH can omit these even when the user's shell
|
||||
// includes them, which would otherwise leave us unable to find claude.exe and
|
||||
// force a fallback to claude.cmd (which Node refuses to spawn — EINVAL).
|
||||
const home = process.env.USERPROFILE ?? '';
|
||||
const appData = process.env.APPDATA || (home && path.join(home, 'AppData', 'Roaming'));
|
||||
const localAppData = process.env.LOCALAPPDATA || (home && path.join(home, 'AppData', 'Local'));
|
||||
const programFiles = process.env.ProgramFiles || 'C:\\Program Files';
|
||||
const knownDirs = [
|
||||
appData && path.join(appData, 'npm'),
|
||||
localAppData && path.join(localAppData, 'npm'),
|
||||
appData && path.join(appData, 'pnpm'),
|
||||
localAppData && path.join(localAppData, 'pnpm'),
|
||||
home && path.join(home, '.volta', 'bin'),
|
||||
path.join(programFiles, 'nodejs'),
|
||||
].filter(Boolean) as string[];
|
||||
|
||||
const pathDirs = (process.env.PATH ?? '').split(';').map((d) => d.trim()).filter(Boolean);
|
||||
const seen = new Set<string>();
|
||||
const candidates = [...pathDirs, ...knownDirs].filter((d) => {
|
||||
const key = d.toLowerCase();
|
||||
if (seen.has(key)) return false;
|
||||
seen.add(key);
|
||||
return true;
|
||||
});
|
||||
|
||||
for (const dir of candidates) {
|
||||
// Direct npm-shim layout: <dir>\node_modules\@anthropic-ai\claude-code\bin\claude.exe
|
||||
const exeFromLayout = path.join(dir, 'node_modules', '@anthropic-ai', 'claude-code', 'bin', 'claude.exe');
|
||||
if (existsSync(exeFromLayout)) return exeFromLayout;
|
||||
|
||||
// Otherwise parse the claude.cmd shim for the real exe path.
|
||||
const cmdPath = path.join(dir, 'claude.cmd');
|
||||
if (!existsSync(cmdPath)) continue;
|
||||
try {
|
||||
const content = readFileSync(cmdPath, 'utf-8');
|
||||
const absMatch = content.match(/[A-Z]:[\\/][^\s"]*claude\.exe/i);
|
||||
if (absMatch && existsSync(absMatch[0])) return absMatch[0];
|
||||
const relMatch = content.match(/%~dp0[\\/]?([^\s"%]+claude\.exe)/i);
|
||||
if (relMatch) {
|
||||
const resolved = path.join(trimmed, relMatch[1]);
|
||||
const resolved = path.join(dir, relMatch[1]);
|
||||
if (existsSync(resolved)) return resolved;
|
||||
}
|
||||
} catch {
|
||||
|
|
@ -825,7 +851,7 @@ export const BuiltinTools: z.infer<typeof BuiltinToolsSchema> = {
|
|||
}
|
||||
|
||||
// Fallback to original for backward compatibility
|
||||
const result = await executeCommand(command, { cwd: workingDir });
|
||||
const result = await executeCommand(command, { cwd: workingDir, env: envOverride });
|
||||
|
||||
return {
|
||||
success: result.exitCode === 0,
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@ export async function executeCommand(
|
|||
cwd?: string;
|
||||
timeout?: number; // timeout in milliseconds
|
||||
maxBuffer?: number; // max buffer size in bytes
|
||||
env?: NodeJS.ProcessEnv; // override environment (e.g. CLAUDE_CODE_EXECUTABLE for acpx)
|
||||
}
|
||||
): Promise<CommandResult> {
|
||||
try {
|
||||
|
|
@ -89,6 +90,7 @@ export async function executeCommand(
|
|||
timeout: options?.timeout,
|
||||
maxBuffer: options?.maxBuffer || 1024 * 1024, // default 1MB
|
||||
shell,
|
||||
env: options?.env,
|
||||
});
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
3
apps/x/packages/core/src/code-mode/index.ts
Normal file
3
apps/x/packages/core/src/code-mode/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export { CodeModeConfig, CodeModeAgentStatus, AgentStatus } from './types.js';
|
||||
export { FSCodeModeConfigRepo, type ICodeModeConfigRepo } from './repo.js';
|
||||
export { checkCodeModeAgentStatus } from './status.js';
|
||||
42
apps/x/packages/core/src/code-mode/repo.ts
Normal file
42
apps/x/packages/core/src/code-mode/repo.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import { WorkDir } from '../config/config.js';
|
||||
import { CodeModeConfig } from './types.js';
|
||||
|
||||
export interface ICodeModeConfigRepo {
|
||||
getConfig(): Promise<CodeModeConfig>;
|
||||
setConfig(config: CodeModeConfig): Promise<void>;
|
||||
}
|
||||
|
||||
export class FSCodeModeConfigRepo implements ICodeModeConfigRepo {
|
||||
private readonly configPath = path.join(WorkDir, 'config', 'code-mode.json');
|
||||
private readonly defaultConfig: CodeModeConfig = { enabled: false };
|
||||
|
||||
constructor() {
|
||||
this.ensureConfigFile();
|
||||
}
|
||||
|
||||
private async ensureConfigFile(): Promise<void> {
|
||||
try {
|
||||
await fs.access(this.configPath);
|
||||
} catch {
|
||||
await fs.mkdir(path.dirname(this.configPath), { recursive: true });
|
||||
await fs.writeFile(this.configPath, JSON.stringify(this.defaultConfig, null, 2));
|
||||
}
|
||||
}
|
||||
|
||||
async getConfig(): Promise<CodeModeConfig> {
|
||||
try {
|
||||
const content = await fs.readFile(this.configPath, 'utf8');
|
||||
return CodeModeConfig.parse(JSON.parse(content));
|
||||
} catch {
|
||||
return this.defaultConfig;
|
||||
}
|
||||
}
|
||||
|
||||
async setConfig(config: CodeModeConfig): Promise<void> {
|
||||
const validated = CodeModeConfig.parse(config);
|
||||
await fs.mkdir(path.dirname(this.configPath), { recursive: true });
|
||||
await fs.writeFile(this.configPath, JSON.stringify(validated, null, 2));
|
||||
}
|
||||
}
|
||||
199
apps/x/packages/core/src/code-mode/status.ts
Normal file
199
apps/x/packages/core/src/code-mode/status.ts
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import fs from 'fs/promises';
|
||||
import { existsSync } from 'fs';
|
||||
import { CodeModeAgentStatus } from './types.js';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
// Where claude.cmd / codex.cmd typically live when installed via npm/pnpm/yarn.
|
||||
// We scan these directly because Electron's spawned shell sometimes doesn't
|
||||
// inherit the user's full PATH (especially on macOS GUI launches, and even on
|
||||
// Windows when global npm prefix isn't propagated to system PATH).
|
||||
function commonInstallPaths(binary: string): string[] {
|
||||
const home = os.homedir();
|
||||
if (process.platform === 'win32') {
|
||||
const appData = process.env.APPDATA || path.join(home, 'AppData', 'Roaming');
|
||||
const localAppData = process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
|
||||
const programFiles = process.env.ProgramFiles || 'C:\\Program Files';
|
||||
return [
|
||||
path.join(appData, 'npm', `${binary}.cmd`),
|
||||
path.join(appData, 'npm', `${binary}.exe`),
|
||||
path.join(localAppData, 'npm', `${binary}.cmd`),
|
||||
path.join(localAppData, 'pnpm', `${binary}.cmd`),
|
||||
path.join(home, 'AppData', 'Roaming', 'pnpm', `${binary}.cmd`),
|
||||
path.join(programFiles, 'nodejs', `${binary}.cmd`),
|
||||
path.join(home, '.volta', 'bin', `${binary}.cmd`),
|
||||
];
|
||||
}
|
||||
return [
|
||||
'/usr/local/bin',
|
||||
'/opt/homebrew/bin', // Apple Silicon Homebrew
|
||||
'/usr/bin',
|
||||
path.join(home, '.npm-global', 'bin'),
|
||||
path.join(home, '.local', 'bin'),
|
||||
path.join(home, '.volta', 'bin'),
|
||||
path.join(home, '.nvm', 'versions', 'node'), // partial; nvm has versioned subdirs
|
||||
path.join(home, 'bin'),
|
||||
].map(dir => path.join(dir, binary));
|
||||
}
|
||||
|
||||
async function probeShell(binary: string): Promise<boolean> {
|
||||
try {
|
||||
if (process.platform === 'win32') {
|
||||
const { stdout } = await execAsync(`where ${binary}`, { timeout: 5000 });
|
||||
return stdout.trim().length > 0;
|
||||
}
|
||||
// Login shell so ~/.zprofile / ~/.bashrc PATH additions are visible —
|
||||
// essential for Homebrew, nvm, asdf, volta installs on macOS GUI launches.
|
||||
const { stdout } = await execAsync(`/bin/sh -lc 'command -v ${binary}'`, { timeout: 5000 });
|
||||
return stdout.trim().length > 0;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function isInstalled(binary: string): Promise<boolean> {
|
||||
if (await probeShell(binary)) return true;
|
||||
// Fallback: scan well-known install locations directly.
|
||||
for (const candidate of commonInstallPaths(binary)) {
|
||||
if (existsSync(candidate)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function decodeJwtPayload(token: string): Record<string, unknown> | null {
|
||||
try {
|
||||
const parts = token.split('.');
|
||||
if (parts.length < 2) return null;
|
||||
const padded = parts[1].replace(/-/g, '+').replace(/_/g, '/');
|
||||
const pad = padded.length % 4 === 0 ? '' : '='.repeat(4 - (padded.length % 4));
|
||||
const json = Buffer.from(padded + pad, 'base64').toString('utf-8');
|
||||
const parsed = JSON.parse(json);
|
||||
return typeof parsed === 'object' && parsed !== null ? parsed as Record<string, unknown> : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Given the raw credentials JSON (from a file or the macOS Keychain), decide
|
||||
// whether it represents a usable signed-in state: a valid API key, an unexpired
|
||||
// access token, or a refresh token (which can mint a new access token).
|
||||
function isClaudeCredentialSignedIn(raw: string): boolean {
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as Record<string, unknown>;
|
||||
|
||||
const oauth = parsed.claudeAiOauth as Record<string, unknown> | undefined;
|
||||
if (oauth) {
|
||||
const access = typeof oauth.accessToken === 'string' ? oauth.accessToken : '';
|
||||
const refresh = typeof oauth.refreshToken === 'string' ? oauth.refreshToken : '';
|
||||
if (refresh.length > 0) return true;
|
||||
if (access.length > 0) {
|
||||
if (typeof oauth.expiresAt === 'number' && oauth.expiresAt > 0 && oauth.expiresAt < Date.now()) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof parsed.apiKey === 'string' && parsed.apiKey.length > 10) return true;
|
||||
if (typeof parsed.accessToken === 'string' && parsed.accessToken.length > 10) return true;
|
||||
} catch {
|
||||
// malformed JSON
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Reads Claude Code's credentials from the macOS login Keychain, where the
|
||||
// CLI stores them on macOS (service "Claude Code-credentials"). On Linux/Windows
|
||||
// it uses the ~/.claude/.credentials.json file instead, so this is a no-op there.
|
||||
//
|
||||
// Caveats:
|
||||
// - The first read by this app (a different binary than the `claude` CLI that
|
||||
// created the item) triggers a one-time macOS authorization dialog; the user
|
||||
// must "Always Allow". Headless/SSH sessions can't show it and will fail.
|
||||
// - If CLAUDE_CONFIG_DIR is set, Claude appends a SHA-256 suffix to the service
|
||||
// name, which this lookup won't match — such setups usually keep the file too.
|
||||
async function readClaudeKeychainCredential(): Promise<string | null> {
|
||||
if (process.platform !== 'darwin') return null;
|
||||
try {
|
||||
const { stdout } = await execAsync(
|
||||
`security find-generic-password -s "Claude Code-credentials" -w`,
|
||||
{ timeout: 5000 },
|
||||
);
|
||||
const out = stdout.trim();
|
||||
return out.length > 0 ? out : null;
|
||||
} catch {
|
||||
// not present in keychain
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Validates Claude Code auth. On macOS the credentials live in the login
|
||||
// Keychain; on Linux/Windows in ~/.claude/.credentials.json (or ~/.config
|
||||
// fallback). We check both so detection works across platforms.
|
||||
async function checkClaudeSignedIn(): Promise<boolean> {
|
||||
const home = os.homedir();
|
||||
const candidates = [
|
||||
path.join(home, '.claude', '.credentials.json'),
|
||||
path.join(home, '.config', 'claude', '.credentials.json'),
|
||||
];
|
||||
for (const full of candidates) {
|
||||
try {
|
||||
const raw = await fs.readFile(full, 'utf-8');
|
||||
if (isClaudeCredentialSignedIn(raw)) return true;
|
||||
} catch {
|
||||
// try next candidate
|
||||
}
|
||||
}
|
||||
|
||||
// macOS: credentials are stored in the Keychain rather than on disk.
|
||||
const keychainRaw = await readClaudeKeychainCredential();
|
||||
if (keychainRaw && isClaudeCredentialSignedIn(keychainRaw)) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validates Codex auth at ~/.codex/auth.json on all platforms.
|
||||
// Considered signed in if API key set, or a refresh_token / access_token
|
||||
// exists. id_token expiry is intentionally NOT used as a rejection signal —
|
||||
// id_tokens are short-lived (~1h) but refresh_tokens persist for weeks.
|
||||
async function checkCodexSignedIn(): Promise<boolean> {
|
||||
const home = os.homedir();
|
||||
const full = path.join(home, '.codex', 'auth.json');
|
||||
try {
|
||||
const raw = await fs.readFile(full, 'utf-8');
|
||||
const parsed = JSON.parse(raw) as Record<string, unknown>;
|
||||
|
||||
if (typeof parsed.OPENAI_API_KEY === 'string' && parsed.OPENAI_API_KEY.length > 10) return true;
|
||||
|
||||
const tokens = parsed.tokens as Record<string, unknown> | undefined;
|
||||
if (tokens) {
|
||||
const refresh = typeof tokens.refresh_token === 'string' ? tokens.refresh_token : '';
|
||||
const access = typeof tokens.access_token === 'string' ? tokens.access_token : '';
|
||||
const id = typeof tokens.id_token === 'string' ? tokens.id_token : '';
|
||||
if (refresh.length > 0 || access.length > 0 || id.length > 0) return true;
|
||||
}
|
||||
} catch {
|
||||
// file missing or unreadable
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Exported for diagnostics — silenced unused-var warning by re-export only.
|
||||
export { decodeJwtPayload };
|
||||
|
||||
export async function checkCodeModeAgentStatus(): Promise<CodeModeAgentStatus> {
|
||||
const [claudeInstalled, codexInstalled, claudeSignedIn, codexSignedIn] = await Promise.all([
|
||||
isInstalled('claude'),
|
||||
isInstalled('codex'),
|
||||
checkClaudeSignedIn(),
|
||||
checkCodexSignedIn(),
|
||||
]);
|
||||
return {
|
||||
claude: { installed: claudeInstalled, signedIn: claudeSignedIn },
|
||||
codex: { installed: codexInstalled, signedIn: codexSignedIn },
|
||||
};
|
||||
}
|
||||
18
apps/x/packages/core/src/code-mode/types.ts
Normal file
18
apps/x/packages/core/src/code-mode/types.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import z from "zod";
|
||||
|
||||
export const CodeModeConfig = z.object({
|
||||
enabled: z.boolean(),
|
||||
});
|
||||
export type CodeModeConfig = z.infer<typeof CodeModeConfig>;
|
||||
|
||||
export const AgentStatus = z.object({
|
||||
installed: z.boolean(),
|
||||
signedIn: z.boolean(),
|
||||
});
|
||||
export type AgentStatus = z.infer<typeof AgentStatus>;
|
||||
|
||||
export const CodeModeAgentStatus = z.object({
|
||||
claude: AgentStatus,
|
||||
codex: AgentStatus,
|
||||
});
|
||||
export type CodeModeAgentStatus = z.infer<typeof CodeModeAgentStatus>;
|
||||
|
|
@ -11,6 +11,7 @@ import { IAgentRuntime, AgentRuntime } from "../agents/runtime.js";
|
|||
import { FSOAuthRepo, IOAuthRepo } from "../auth/repo.js";
|
||||
import { FSClientRegistrationRepo, IClientRegistrationRepo } from "../auth/client-repo.js";
|
||||
import { FSGranolaConfigRepo, IGranolaConfigRepo } from "../knowledge/granola/repo.js";
|
||||
import { FSCodeModeConfigRepo, ICodeModeConfigRepo } from "../code-mode/repo.js";
|
||||
import { IAbortRegistry, InMemoryAbortRegistry } from "../runs/abort-registry.js";
|
||||
import { FSAgentScheduleRepo, IAgentScheduleRepo } from "../agent-schedule/repo.js";
|
||||
import { FSAgentScheduleStateRepo, IAgentScheduleStateRepo } from "../agent-schedule/state-repo.js";
|
||||
|
|
@ -38,6 +39,7 @@ container.register({
|
|||
oauthRepo: asClass<IOAuthRepo>(FSOAuthRepo).singleton(),
|
||||
clientRegistrationRepo: asClass<IClientRegistrationRepo>(FSClientRegistrationRepo).singleton(),
|
||||
granolaConfigRepo: asClass<IGranolaConfigRepo>(FSGranolaConfigRepo).singleton(),
|
||||
codeModeConfigRepo: asClass<ICodeModeConfigRepo>(FSCodeModeConfigRepo).singleton(),
|
||||
agentScheduleRepo: asClass<IAgentScheduleRepo>(FSAgentScheduleRepo).singleton(),
|
||||
agentScheduleStateRepo: asClass<IAgentScheduleStateRepo>(FSAgentScheduleStateRepo).singleton(),
|
||||
slackConfigRepo: asClass<ISlackConfigRepo>(FSSlackConfigRepo).singleton(),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
@ -424,6 +425,27 @@ const ipcSchemas = {
|
|||
enabled: z.boolean(),
|
||||
}),
|
||||
},
|
||||
'codeMode:getConfig': {
|
||||
req: z.null(),
|
||||
res: z.object({
|
||||
enabled: z.boolean(),
|
||||
}),
|
||||
},
|
||||
'codeMode:setConfig': {
|
||||
req: z.object({
|
||||
enabled: z.boolean(),
|
||||
}),
|
||||
res: z.object({
|
||||
success: z.literal(true),
|
||||
}),
|
||||
},
|
||||
'codeMode:checkAgentStatus': {
|
||||
req: z.null(),
|
||||
res: z.object({
|
||||
claude: z.object({ installed: z.boolean(), signedIn: z.boolean() }),
|
||||
codex: z.object({ installed: z.boolean(), signedIn: z.boolean() }),
|
||||
}),
|
||||
},
|
||||
'granola:setConfig': {
|
||||
req: z.object({
|
||||
enabled: z.boolean(),
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue