fix: add OpenCode slash command discovery and execution support#568
fix: add OpenCode slash command discovery and execution support#568chr1syy wants to merge 5 commits intoRunMaestro:0.16.0-RCfrom
Conversation
OpenCode slash commands (both built-in and custom) were silently ignored because the capability flag was false and discovery was hard-gated to Claude Code only. This adds disk-based discovery for OpenCode commands from .opencode/commands/*.md, ~/.config/opencode/commands/*.md, and opencode.json config files, and makes getSlashCommandDescription() agent-aware to provide correct descriptions for each agent's commands. Closes RunMaestro#552 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Greptile SummaryThis PR adds OpenCode slash command discovery and execution support by enabling disk-based command discovery from three on-disk sources ( Key changes:
Issue found:
Confidence Score: 3/5
Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Session activated\nclaude-code or opencode] --> B{toolType?}
B -->|claude-code| C[fetchCustomCommands\nclaudeCommands/*.md via IPC]
B -->|claude-code| D[discoverAgentCommands\nspawn claude --print /help]
B -->|opencode| E[discoverAgentCommands\ndiscoverOpenCodeSlashCommands]
C --> M[merge into agentCommands]
D --> M
E --> F[Built-ins\ninit,review,undo,redo,share,help,models]
E --> G[.opencode/commands/*.md\nproject-local]
E --> H[XDG_CONFIG_HOME/opencode/commands/*.md\nglobal]
E --> I[opencode.json command property\nproject + global]
F & G & H & I --> J[Promise.all\ncollect into Set]
J --> K{JSON.parse error?}
K -->|SyntaxError — no .code\ncurrent: rethrow ALL commands lost| L[❌ Discovery fails\nno commands returned]
K -->|Valid JSON or ENOENT| M
M --> N[agentCommands stored in session\nautocomplete populated]
O[Claude Code init event\nonSlashCommands] -->|replaces agentCommands| N
Last reviewed commit: c271453 |
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughAdds OpenCode slash-command support: flips opencode capability to support slash commands, implements discovery of built-in and project/global-configured OpenCode commands, integrates discovery into IPC, and makes renderer command descriptions agent-aware. Changes
Sequence DiagramsequenceDiagram
participant Renderer as Renderer (UI)
participant IPC as IPC Handler (agents:discoverSlashCommands)
participant FS as Filesystem (project/global .opencode + opencode.json)
participant Session as Session (agentCommands)
Renderer->>IPC: Request slash commands for agentId "opencode"
IPC->>IPC: Route to OpenCode discovery
IPC->>FS: Read built-in command list
FS-->>IPC: Built-in commands
IPC->>FS: Read project .opencode/commands/*.md
FS-->>IPC: Project command files (names)
IPC->>FS: Read project opencode.json
FS-->>IPC: Project opencode.json (commands)
IPC->>FS: Read global config (~/.opencode/... or base)
FS-->>IPC: Global command files / global opencode.json
IPC->>IPC: Consolidate unique command names
IPC-->>Renderer: Return merged command array
Renderer->>Session: Populate agentCommands with descriptions (agent-aware)
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
📝 Coding Plan
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
The test expected OpenCode to return null from discoverSlashCommands, but it now correctly returns built-in commands. Updated to test Codex (which genuinely has no slash commands) and added a dedicated test for OpenCode built-in command discovery. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
typeof [] === 'object' is true in JS, so an array value for the "command" property in opencode.json would produce bogus /0, /1 slash command entries. Add !Array.isArray() guard to both project-level and global config parsing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
@coderabbitai review |
|
🧠 Learnings used✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/renderer/hooks/wizard/useWizardHandlers.ts (1)
178-268:⚠️ Potential issue | 🟠 MajorOpenCode sessions still never run slash-command discovery here.
Line 183 returns unless the active session is Claude Code, so this effect never calls
agents.discoverSlashCommands('opencode', ...). WithsupportsSlashCommandsnow enabled, OpenCode sessions will still keep an emptyagentCommandslist and won't show the new autocomplete entries.🛠️ Suggested wiring fix
- if (currentSession.toolType !== 'claude-code') return; + if ( + currentSession.toolType !== 'claude-code' && + currentSession.toolType !== 'opencode' + ) { + return; + } if (currentSession.agentCommands && currentSession.agentCommands.length > 0) return; @@ - fetchCustomCommands(); + if (currentSession.toolType === 'claude-code') { + fetchCustomCommands(); + } discoverAgentCommands();🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/hooks/wizard/useWizardHandlers.ts` around lines 178 - 268, The effect currently bails out unless currentSession.toolType === 'claude-code', preventing agents.discoverSlashCommands from running for 'opencode' sessions; update the early-return so it allows any tool that supports slash commands (e.g., replace the strict 'claude-code' check with a call to the supportsSlashCommands predicate or include 'opencode') so discoverAgentCommands (which calls maestro.agents.discoverSlashCommands) runs for those sessions and populates s.agentCommands; adjust the condition around currentSession.toolType in useEffect in useWizardHandlers.ts accordingly.
🧹 Nitpick comments (1)
src/__tests__/main/ipc/handlers/agents.test.ts (1)
1094-1108: This only covers the built-in happy path for OpenCode.The new risk is in
.opencode/commands/*.mdandopencode.jsonparsing, but this assertion only proves the built-in set is seeded. A regression in the disk/config discovery logic—or in theArray.isArrayguard oncommand—would still leave this suite green.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/__tests__/main/ipc/handlers/agents.test.ts` around lines 1094 - 1108, The test currently only verifies built-in OpenCode commands; add assertions and setup to exercise the on-disk/config discovery paths so regressions in parsing .opencode/commands/*.md and opencode.json (and the Array.isArray guard on `command`) are caught: mock the filesystem reads (or the helper that loads .opencode files) to return a mix of valid and invalid command definitions and a sample opencode.json, call the same handler retrieved via handlers.get('agents:discoverSlashCommands') after mocking mockAgentDetector.getAgent to return the opencode agent, and assert the final result includes commands discovered from disk/config and that non-array/invalid `command` entries are ignored (or handled) as expected. Ensure you reference and exercise the command-parsing code path used by agents:discoverSlashCommands so the disk/config logic is validated in the test.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/main/ipc/handlers/agents.ts`:
- Around line 50-116: The catch blocks around reading project/global command
directories and project/global opencode.json currently swallow all errors;
change each catch to only suppress "file not found" (error.code === 'ENOENT')
while rethrowing other errors (including permission errors and JSON
parse/SyntaxError) so they propagate to IPC error handling/Sentry. Locate the
four try/catch blocks that reference projectCommandsDir, globalCommandsDir,
projectConfigPath, and globalConfigPath (they update the commands Set and use
logger and LOG_CONTEXT) and replace the generic catch handlers with logic: if
error?.code === 'ENOENT' then logger.debug(...) as before, else throw error.
In `@src/renderer/constants/app.ts`:
- Around line 120-129: The loop that searches AGENT_BUILTIN_COMMANDS for cmdName
should only run when agentId is not provided; currently if an agentId is passed
but lacks cmdName you still fall back to another agent's command. Modify the
logic around AGENT_BUILTIN_COMMANDS, agentId, and cmdName so that after checking
AGENT_BUILTIN_COMMANDS[agentId]?.[cmdName] you only iterate
Object.values(AGENT_BUILTIN_COMMANDS) when agentId is falsy (i.e., guard the
fallback with if (!agentId) or return undefined when an agentId was supplied and
its map does not contain cmdName).
---
Outside diff comments:
In `@src/renderer/hooks/wizard/useWizardHandlers.ts`:
- Around line 178-268: The effect currently bails out unless
currentSession.toolType === 'claude-code', preventing
agents.discoverSlashCommands from running for 'opencode' sessions; update the
early-return so it allows any tool that supports slash commands (e.g., replace
the strict 'claude-code' check with a call to the supportsSlashCommands
predicate or include 'opencode') so discoverAgentCommands (which calls
maestro.agents.discoverSlashCommands) runs for those sessions and populates
s.agentCommands; adjust the condition around currentSession.toolType in
useEffect in useWizardHandlers.ts accordingly.
---
Nitpick comments:
In `@src/__tests__/main/ipc/handlers/agents.test.ts`:
- Around line 1094-1108: The test currently only verifies built-in OpenCode
commands; add assertions and setup to exercise the on-disk/config discovery
paths so regressions in parsing .opencode/commands/*.md and opencode.json (and
the Array.isArray guard on `command`) are caught: mock the filesystem reads (or
the helper that loads .opencode files) to return a mix of valid and invalid
command definitions and a sample opencode.json, call the same handler retrieved
via handlers.get('agents:discoverSlashCommands') after mocking
mockAgentDetector.getAgent to return the opencode agent, and assert the final
result includes commands discovered from disk/config and that non-array/invalid
`command` entries are ignored (or handled) as expected. Ensure you reference and
exercise the command-parsing code path used by agents:discoverSlashCommands so
the disk/config logic is validated in the test.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 2ed8ee64-8158-4c7f-b0db-2165b17458dc
📒 Files selected for processing (6)
src/__tests__/main/ipc/handlers/agents.test.tssrc/main/agents/capabilities.tssrc/main/ipc/handlers/agents.tssrc/renderer/constants/app.tssrc/renderer/hooks/agent/useAgentListeners.tssrc/renderer/hooks/wizard/useWizardHandlers.ts
- Convert discoverOpenCodeSlashCommands to async using fs.promises - Respect XDG_CONFIG_HOME for global OpenCode config paths - Only suppress ENOENT errors; rethrow permission/parse errors to Sentry - Fix cross-agent description leak in getSlashCommandDescription - Wire OpenCode sessions into slash command discovery in useWizardHandlers - Add tests for disk/config discovery, array guard, and error propagation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
@coderabbitai re-review please |
|
🧠 Learnings used✅ Actions performedFull review triggered. |
| const addCommandsFromConfig = async (configPath: string) => { | ||
| try { | ||
| const content = await fs.promises.readFile(configPath, 'utf-8'); | ||
| const config = JSON.parse(content); | ||
| if (config.command && typeof config.command === 'object' && !Array.isArray(config.command)) { | ||
| for (const name of Object.keys(config.command)) { | ||
| commands.add(name); | ||
| } | ||
| } | ||
| } catch (error: any) { | ||
| if (error?.code === 'ENOENT') { | ||
| logger.debug(`OpenCode config not found: ${configPath}`, LOG_CONTEXT); | ||
| } else { | ||
| throw error; | ||
| } | ||
| } |
There was a problem hiding this comment.
JSON parse errors silently kill all command discovery
JSON.parse(content) throws a SyntaxError for malformed JSON. A SyntaxError has no .code property, so error?.code === 'ENOENT' evaluates to false and the error is re-thrown. Since both addCommandsFromConfig calls run inside Promise.all, a single malformed opencode.json (e.g. a stray trailing comma or an unquoted key) causes the entire discoverOpenCodeSlashCommands to reject — wiping out the built-in commands that were already collected into commands.
The practical impact: a user with any typo in their opencode.json will see zero slash-command autocomplete instead of the seven built-in commands they should always get.
const addCommandsFromConfig = async (configPath: string) => {
try {
const content = await fs.promises.readFile(configPath, 'utf-8');
let config: any;
try {
config = JSON.parse(content);
} catch {
logger.warn(`OpenCode config has invalid JSON, skipping: ${configPath}`, LOG_CONTEXT);
return;
}
if (config.command && typeof config.command === 'object' && !Array.isArray(config.command)) {
for (const name of Object.keys(config.command)) {
commands.add(name);
}
}
} catch (error: any) {
if (error?.code === 'ENOENT') {
logger.debug(`OpenCode config not found: ${configPath}`, LOG_CONTEXT);
} else {
throw error;
}
}
};The same pattern applies to both the project-level and global opencode.json paths read on lines 92–93.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/main/ipc/handlers/agents.ts (1)
32-33: Potential for silent drift with renderer-side command list.
OPENCODE_BUILTIN_COMMANDSis defined here as a string array, whilesrc/renderer/constants/app.ts(line 94-102) definesOPENCODE_BUILTIN_COMMANDSas aRecord<string, string>with descriptions. If commands are added or removed in one location but not the other, the lists will silently diverge.Consider extracting the canonical command list to a shared location (e.g.,
src/shared/constants.ts) and deriving both usages from it.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/ipc/handlers/agents.ts` around lines 32 - 33, OPENCODE_BUILTIN_COMMANDS is duplicated in different shapes which can silently diverge; extract a single canonical source (e.g., a shared constants module exporting a canonicalCommands array) and derive both usages from it: export the canonicalCommands (string[]) and then derive OPENCODE_BUILTIN_COMMANDS (string[]) and the renderer's OPENCODE_BUILTIN_COMMANDS record (mapping to descriptions) by importing and transforming the canonical list; update the references in the handler that uses OPENCODE_BUILTIN_COMMANDS and the renderer constant to import from the shared module and remove the local hardcoded list.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@src/main/ipc/handlers/agents.ts`:
- Around line 32-33: OPENCODE_BUILTIN_COMMANDS is duplicated in different shapes
which can silently diverge; extract a single canonical source (e.g., a shared
constants module exporting a canonicalCommands array) and derive both usages
from it: export the canonicalCommands (string[]) and then derive
OPENCODE_BUILTIN_COMMANDS (string[]) and the renderer's
OPENCODE_BUILTIN_COMMANDS record (mapping to descriptions) by importing and
transforming the canonical list; update the references in the handler that uses
OPENCODE_BUILTIN_COMMANDS and the renderer constant to import from the shared
module and remove the local hardcoded list.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 3cfac1ae-2b88-4a40-9301-f648799f32a3
📒 Files selected for processing (6)
src/__tests__/main/ipc/handlers/agents.test.tssrc/main/agents/capabilities.tssrc/main/ipc/handlers/agents.tssrc/renderer/constants/app.tssrc/renderer/hooks/agent/useAgentListeners.tssrc/renderer/hooks/wizard/useWizardHandlers.ts
…very SyntaxError from JSON.parse has no .code property, so it bypassed the ENOENT check and rejected the entire Promise.all — wiping out built-in commands. Split the try/catch so file-not-found and parse errors are handled independently. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
.opencode/commands/*.md,~/.config/opencode/commands/*.md, andopencode.jsonconfig)supportsSlashCommands: truefor OpenCode agent so commands appear in autocompletegetSlashCommandDescription()agent-aware to provide correct descriptions per agentCloses #552
Changes
src/main/agents/capabilities.tssupportsSlashCommandstotruefor opencodesrc/main/ipc/handlers/agents.tsdiscoverOpenCodeSlashCommands()— disk-based discovery from 3 sources (built-in, project-local, global + config)src/renderer/constants/app.tsOPENCODE_BUILTIN_COMMANDSdescriptions, makegetSlashCommandDescription()agent-awaresrc/renderer/hooks/agent/useAgentListeners.tstoolTypetogetSlashCommandDescription()src/renderer/hooks/wizard/useWizardHandlers.tstoolTypetogetSlashCommandDescription()How it works
Unlike Claude Code (which discovers commands via
system/initJSON event), OpenCode stores commands on disk:init,review,undo,redo,share,help,models.opencode/commands/<name>.mdfiles~/.config/opencode/commands/<name>.mdfilesopencode.json→commandpropertyThe discovery function reads all three sources and returns a unified command list, matching Claude Code's end-to-end pipeline (discover → autocomplete → execute).
Test plan
.opencode/commands/test.mdfile and verify it's discovered🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Improvements
Tests