Found expired OAuth token, attempting refresh... Token refresh successful I'd be happy to help you create a clean update summary for a GitHub project! However, I don't see any input provided after "INPUT:" in your message. Could you please share the changelog, commit history, or release notes that you'd like me to summarize? Once you provide that information, I'll create an exciting CHANGES section with 10-word bullets and relevant emojis as requested.
21 KiB
Agent Support Architecture
This document describes the architecture for supporting multiple AI coding agents in Maestro, the refactoring needed to move from Claude-specific code to a generic agent abstraction, and how to add new agents.
Vernacular
Use these terms consistently throughout the codebase:
| Term | Definition |
|---|---|
| Maestro Agent | A configured AI assistant in Maestro (e.g., "My Claude Assistant") |
| Provider | The underlying AI service (Claude Code, OpenCode, Codex, Gemini CLI) |
| Provider Session | A conversation session managed by the provider (e.g., Claude's session_id) |
| Tab | A Maestro UI tab that maps 1:1 to a Provider Session |
Hierarchy: Maestro Agent → Provider → Provider Sessions → Tabs
Table of Contents
- Vernacular
- Overview
- Agent Capability Model
- Message Display Classification
- Current State: Claude-Specific Code
- Target State: Generic Agent Architecture
- Refactoring Plan
- Test Impact
- Adding a New Agent
- Agent-Specific Implementations
Overview
Maestro currently supports Claude Code as its primary AI agent. To support additional agents (OpenCode, Gemini CLI, Codex, Qwen3 Coder, etc.), we need to:
- Abstract Claude-specific code into a generic agent interface
- Define agent capabilities that control UI feature availability
- Create agent-specific adapters for session storage, output parsing, and CLI arguments
- Rename Claude-specific identifiers to generic "agent" terminology
Agent Capability Model
Each agent declares its capabilities, which determine which UI features are available when that agent is active.
Capability Interface
// src/main/agent-capabilities.ts
interface AgentCapabilities {
// Core features
supportsResume: boolean; // Can resume previous sessions (--resume, --session, etc.)
supportsReadOnlyMode: boolean; // Has a plan/read-only mode
supportsJsonOutput: boolean; // Emits structured JSON for parsing
supportsSessionId: boolean; // Emits session ID for tracking
// Advanced features
supportsImageInput: boolean; // Can receive images in prompts
supportsSlashCommands: boolean; // Has discoverable slash commands
supportsSessionStorage: boolean; // Persists sessions we can browse
supportsCostTracking: boolean; // Reports token costs (API-based agents)
supportsUsageStats: boolean; // Reports token counts
// Streaming behavior
supportsBatchMode: boolean; // Runs per-message (vs persistent process)
supportsStreaming: boolean; // Streams output incrementally
// Message classification
supportsResultMessages: boolean; // Distinguishes final result from intermediary messages
}
Capability-to-UI Feature Mapping
| Capability | UI Feature | Component |
|---|---|---|
supportsReadOnlyMode |
Read-only toggle in input area | InputArea.tsx |
supportsSessionStorage |
Agent Sessions browser tab | RightPanel.tsx, AgentSessionsBrowser.tsx |
supportsResume |
Resume button in session browser | AgentSessionsBrowser.tsx |
supportsCostTracking |
Cost widget display | MainPanel.tsx |
supportsUsageStats |
Token usage display | MainPanel.tsx, TabBar.tsx |
supportsImageInput |
Image attachment button | InputArea.tsx |
supportsSlashCommands |
Slash command autocomplete | InputArea.tsx, autocomplete |
supportsSessionId |
Session ID pill in header | MainPanel.tsx |
supportsResultMessages |
Show only final result in AI Terminal | LogViewer.tsx |
Per-Agent Capability Definitions
// src/main/agent-capabilities.ts
const AGENT_CAPABILITIES: Record<string, AgentCapabilities> = {
'claude-code': {
supportsResume: true,
supportsReadOnlyMode: true, // --permission-mode plan
supportsJsonOutput: true, // --output-format stream-json
supportsSessionId: true,
supportsImageInput: true, // --input-format stream-json
supportsSlashCommands: true, // Emits in init message
supportsSessionStorage: true, // ~/.claude/projects/
supportsCostTracking: true, // API-based
supportsUsageStats: true,
supportsBatchMode: true,
supportsStreaming: true,
supportsResultMessages: true, // type: "result" vs type: "assistant"
},
'opencode': {
supportsResume: true, // --session <id>
supportsReadOnlyMode: true, // --agent plan
supportsJsonOutput: true, // --format json
supportsSessionId: true, // sessionID in events
supportsImageInput: true, // -f flag
supportsSlashCommands: false, // TBD - needs investigation
supportsSessionStorage: true, // TBD - needs investigation
supportsCostTracking: false, // Local models = free
supportsUsageStats: true, // tokens in step_finish
supportsBatchMode: true,
supportsStreaming: true,
supportsResultMessages: false, // TBD - needs investigation
},
'gemini-cli': {
supportsResume: false, // TBD
supportsReadOnlyMode: false, // TBD
supportsJsonOutput: false, // TBD
supportsSessionId: false,
supportsImageInput: false,
supportsSlashCommands: false,
supportsSessionStorage: false,
supportsCostTracking: true, // API-based
supportsUsageStats: false,
supportsBatchMode: false, // TBD
supportsStreaming: true,
supportsResultMessages: false, // TBD
},
// Template for new agents - start with all false
'_template': {
supportsResume: false,
supportsReadOnlyMode: false,
supportsJsonOutput: false,
supportsSessionId: false,
supportsImageInput: false,
supportsSlashCommands: false,
supportsSessionStorage: false,
supportsCostTracking: false,
supportsUsageStats: false,
supportsBatchMode: false,
supportsStreaming: false,
supportsResultMessages: false,
},
};
Message Display Classification
Providers emit both intermediary messages (streaming content, tool calls, thinking) and result messages (final response). The AI Terminal should display result messages prominently while suppressing or collapsing intermediary messages.
Result vs Intermediary Messages
| Provider | Result Message | Intermediary Messages | Display Behavior |
|---|---|---|---|
| Claude Code | type: "result" → msg.result |
type: "assistant" (streaming content) |
Show result only, suppress intermediary |
| OpenCode | type: "step_finish" (TBD) |
type: "text", type: "tool_use" |
TBD - needs investigation |
| Gemini CLI | TBD | TBD | TBD |
| Codex | TBD | TBD | TBD |
Implementation Notes
For providers with supportsResultMessages: true:
- Parse streaming output for message type
- Buffer intermediary messages (may show in expandable section)
- Display result message as the primary content in AI Terminal
For providers with supportsResultMessages: false:
- Show all messages as they stream
- No distinction between intermediary and final content
Claude Code Message Types
// Intermediary - suppress in AI Terminal
{ type: "assistant", message: { content: [...] } }
// Result - show in AI Terminal
{ type: "result", result: "Final response text", session_id: "...", modelUsage: {...} }
// System/Init - metadata only
{ type: "system", subtype: "init", session_id: "...", slash_commands: [...] }
Current State: Claude-Specific Code
The codebase has ~200+ references to "claude" that need refactoring. They fall into these categories:
Category 1: Generic Session Identifiers (RENAME)
These represent "the agent's conversation session ID" - universal to all agents:
| Current Name | New Name | Files |
|---|---|---|
claudeSessionId |
agentSessionId |
20+ files |
activeClaudeSessionId |
activeAgentSessionId |
App.tsx, MainPanel.tsx |
claudeCommands |
agentCommands |
Session interface |
ClaudeSession interface |
AgentSession |
AgentSessionsBrowser.tsx |
ClaudeSessionOrigin |
AgentSessionOrigin |
index.ts |
Key locations:
src/renderer/types/index.ts:238,307,327- AITab and Session interfacessrc/shared/types.ts:44- HistoryEntry interfacesrc/renderer/App.tsx- 60+ occurrencessrc/main/index.ts- 20+ occurrences
Category 2: Generic Functions (RENAME)
| Current Name | New Name |
|---|---|
startNewClaudeSession |
startNewAgentSession |
handleJumpToClaudeSession |
handleJumpToAgentSession |
onResumeClaudeSession |
onResumeAgentSession |
onNewClaudeSession |
onNewAgentSession |
spawnAgentForSession |
(already generic) |
Category 3: IPC API (REDESIGN)
Current: window.maestro.claude.*
New: window.maestro.agentSessions.* with agent ID parameter
// Before
window.maestro.claude.listSessions(projectPath)
window.maestro.claude.readSessionMessages(projectPath, sessionId)
// After
window.maestro.agentSessions.list(agentId, projectPath)
window.maestro.agentSessions.read(agentId, projectPath, sessionId)
Category 4: Session Storage (ABSTRACT)
Each agent stores sessions differently:
- Claude Code:
~/.claude/projects/<encoded-path>/<session-id>.jsonl - OpenCode: TBD (server-managed sessions)
Create AgentSessionStorage interface with per-agent implementations.
Category 5: Output Parsing (ABSTRACT)
Each agent has different JSON schemas:
| Agent | Session ID Field | Text Content | Token Stats |
|---|---|---|---|
| Claude Code | session_id |
msg.result |
msg.modelUsage |
| OpenCode | sessionID |
msg.part.text |
msg.part.tokens |
Create AgentOutputParser interface with per-agent implementations.
Category 6: CLI Arguments (CONFIGURE)
| Feature | Claude Code | OpenCode |
|---|---|---|
| Resume | --resume <id> |
--session <id> |
| Read-only | --permission-mode plan |
--agent plan |
| JSON output | --output-format stream-json |
--format json |
| Batch mode | --print |
run subcommand |
Add to AgentConfig:
interface AgentConfig {
// ... existing fields
resumeArgs?: (sessionId: string) => string[];
readOnlyArgs?: string[];
batchModeArgs?: string[];
jsonOutputArgs?: string[];
}
Category 7: KEEP AS AGENT-SPECIFIC
These should NOT be renamed - they are legitimately Claude-only:
id: 'claude-code'in AGENT_DEFINITIONSbinaryName: 'claude'~/.claude/localpath detection- Claude-specific CLI args in the agent definition
- Comments explaining Claude-specific behavior
Target State: Generic Agent Architecture
New Files to Create
src/main/
├── agent-capabilities.ts # Capability definitions per agent
├── agent-session-storage.ts # Abstract session storage interface
│ ├── ClaudeSessionStorage # Claude implementation
│ └── OpenCodeSessionStorage # OpenCode implementation
├── agent-output-parser.ts # Abstract output parser interface
│ ├── ClaudeOutputParser # Claude implementation
│ └── OpenCodeOutputParser # OpenCode implementation
└── agent-pricing.ts # Cost calculation per agent
Extended AgentConfig
// src/main/agent-detector.ts
interface AgentConfig {
// Identification
id: string;
name: string;
binaryName: string;
command: string;
// Base arguments
args: string[];
// Capability-driven arguments
resumeArgs?: (sessionId: string) => string[]; // e.g., ['--resume', id] or ['--session', id]
readOnlyArgs?: string[]; // e.g., ['--permission-mode', 'plan']
jsonOutputArgs?: string[]; // e.g., ['--format', 'json']
batchModePrefix?: string[]; // e.g., ['run'] for opencode
// Runtime info
available: boolean;
path?: string;
customPath?: string;
requiresPty?: boolean;
hidden?: boolean;
// Capabilities (reference to AGENT_CAPABILITIES)
capabilities: AgentCapabilities;
// Pricing (for cost tracking)
pricing?: {
inputPerMillion: number;
outputPerMillion: number;
cacheReadPerMillion?: number;
cacheCreationPerMillion?: number;
};
// Session storage configuration
sessionStoragePath?: (projectPath: string) => string; // e.g., ~/.claude/projects/...
// Default context window size
defaultContextWindow?: number; // e.g., 200000 for Claude
}
UI Capability Checks
// src/renderer/hooks/useAgentCapabilities.ts
function useAgentCapabilities(agentId: string): AgentCapabilities {
const [capabilities, setCapabilities] = useState<AgentCapabilities | null>(null);
useEffect(() => {
window.maestro.agents.getCapabilities(agentId).then(setCapabilities);
}, [agentId]);
return capabilities ?? DEFAULT_CAPABILITIES;
}
// Usage in components:
function InputArea({ session }) {
const capabilities = useAgentCapabilities(session.toolType);
return (
<div>
{/* Only show read-only toggle if agent supports it */}
{capabilities.supportsReadOnlyMode && (
<ReadOnlyToggle />
)}
{/* Only show image button if agent supports it */}
{capabilities.supportsImageInput && (
<ImageAttachButton />
)}
</div>
);
}
Refactoring Plan
Phase 1: Foundation (Types & Capabilities)
Effort: 2-3 hours
- Create
src/main/agent-capabilities.tswith capability interface and definitions - Add
capabilitiesfield toAgentConfig - Expose capabilities via IPC:
window.maestro.agents.getCapabilities(agentId) - Create
useAgentCapabilitieshook
Phase 2: Identifier Renames
Effort: 3-4 hours
-
Rename in type definitions:
claudeSessionId→agentSessionIdclaudeCommands→agentCommandsClaudeSession→AgentSession
-
Rename in components and hooks (find-and-replace with review)
-
Rename state variables and functions in App.tsx
Phase 3: Abstract Session Storage
Effort: 4-5 hours
- Create
AgentSessionStorageinterface - Extract Claude session logic from
index.tsintoClaudeSessionStorage - Create factory function
getSessionStorage(agentId) - Update IPC handlers to use abstraction
Phase 4: Abstract Output Parsing
Effort: 3-4 hours
- Create
AgentOutputParserinterface - Extract Claude parsing from
process-manager.tsintoClaudeOutputParser - Create
OpenCodeOutputParser - Update
ProcessManagerto use factory
Phase 5: IPC API Refactor
Effort: 2-3 hours
- Add new generic API:
window.maestro.agentSessions.* - Deprecate old API:
window.maestro.claude.*(keep working, log warning) - Update all call sites
Phase 6: UI Capability Gates
Effort: 2-3 hours
- Add capability checks to
InputArea(read-only toggle, image button) - Add capability checks to
RightPanel(session browser availability) - Add capability checks to
MainPanel(cost widget, session ID pill) - Add capability checks to
AgentSessionsBrowser
Phase 7: Add OpenCode Support
Effort: 3-4 hours
- Add OpenCode to
AGENT_DEFINITIONSwith full config - Implement
OpenCodeOutputParser - Implement
OpenCodeSessionStorage(or mark as unsupported) - Test end-to-end: new session, resume, read-only mode
Total Estimated Effort: 20-26 hours
Test Impact
The test suite contains 147 test files, of which 55 files (37%) contain Claude-specific references that will need updates during refactoring.
Test Files Requiring Updates
| Category | Files | Changes Needed |
|---|---|---|
| Setup/Mocks | setup.ts |
Rename window.maestro.claude mock to agentSessions |
| Type Tests | templateVariables.test.ts |
Update claudeSessionId in test data |
| Hook Tests | useSessionManager.test.ts, useBatchProcessor.test.ts |
Update session mock properties |
| Component Tests | TabBar.test.tsx, SessionList.test.tsx, MainPanel.test.tsx, HistoryPanel.test.tsx, ProcessMonitor.test.tsx, +8 more |
Update claudeSessionId in mock data |
| CLI Tests | batch-processor.test.ts, storage.test.ts |
Update mock session objects |
| Agent Tests | agent-detector.test.ts |
Keep as-is (tests Claude-specific detection) |
Tests That Should NOT Change
Some tests are legitimately Claude-specific and should remain unchanged:
agent-detector.test.ts- Tests thatclaude-codeagent is properly detected- Storage tests with
claude-codeconfig paths - Tests Claude-specific settings - Any test verifying Claude CLI argument construction
Refactoring Strategy with Tests
Recommended approach: Update tests incrementally alongside code changes.
For each refactoring phase:
- Make the code change (e.g., rename
claudeSessionId→agentSessionId) - Run tests - They will fail due to type/name mismatches
- Update failing tests with new names
- Run tests again - They should pass
- Commit both code + test changes together
This approach ensures:
- Tests catch any missed renames (TypeScript will also help)
- Each commit is self-contained and all tests pass
- The test suite remains a safety net throughout refactoring
Test Mock Updates Per Phase
Phase 2 (Identifier Renames):
// Before (in test files)
createTestSession({ claudeSessionId: 'test-123' })
// After
createTestSession({ agentSessionId: 'test-123' })
Phase 5 (IPC API Refactor):
// Before (in setup.ts)
claude: {
listSessions: vi.fn(),
readSessionMessages: vi.fn(),
}
// After
agentSessions: {
list: vi.fn(), // Now takes agentId parameter
read: vi.fn(),
}
Estimated Test Update Effort
| Phase | Test Files Affected | Effort |
|---|---|---|
| Phase 1: Foundation | 0 (new code) | 0 |
| Phase 2: Identifier Renames | ~45 files | 1-2 hours |
| Phase 3: Session Storage | ~5 files | 30 min |
| Phase 4: Output Parsing | ~3 files | 30 min |
| Phase 5: IPC API | ~10 files | 30 min |
| Phase 6: UI Capability Gates | ~10 files | 30 min |
| Phase 7: OpenCode Support | 0 (new tests) | Write new tests |
| Total | 55 files | ~3-4 hours |
Creating Test Factories
To simplify future refactoring, consider creating test factories:
// src/__tests__/factories/session.ts
export function createMockSession(overrides?: Partial<Session>): Session {
return {
id: 'test-session-1',
name: 'Test Session',
toolType: 'claude-code',
agentSessionId: null, // Generic name
agentCommands: [], // Generic name
// ... other fields
...overrides,
};
}
export function createMockAITab(overrides?: Partial<AITab>): AITab {
return {
id: 'tab-1',
agentSessionId: null, // Generic name
// ... other fields
...overrides,
};
}
Using factories means future renames only require updating the factory, not every test file.
Adding a New Agent
See CONTRIBUTING.md for the step-by-step guide and capability checklist.
Agent-Specific Implementations
Claude Code
CLI Reference:
claude --print --verbose --output-format stream-json --dangerously-skip-permissions "prompt"
claude --print --resume <session-id> "prompt"
claude --print --permission-mode plan "prompt" # Read-only
Session Storage: ~/.claude/projects/<encoded-path>/<session-id>.jsonl
JSON Output Schema:
{"type": "system", "subtype": "init", "session_id": "...", "slash_commands": [...]}
{"type": "assistant", "message": {...}}
{"type": "result", "result": "response text", "session_id": "...", "modelUsage": {...}}
Session ID Field: session_id (snake_case)
OpenCode
CLI Reference:
opencode run --format json "prompt"
opencode run --session <session-id> --format json "prompt"
opencode run --agent plan --format json "prompt" # Read-only
opencode run --model ollama/qwen3:4b --format json "prompt" # Custom model
Session Storage: Server-managed (TBD)
JSON Output Schema:
{"type": "step_start", "sessionID": "...", "part": {...}}
{"type": "text", "sessionID": "...", "part": {"text": "response"}}
{"type": "tool_use", "sessionID": "...", "part": {"tool": "write", "state": {...}}}
{"type": "step_finish", "sessionID": "...", "part": {"tokens": {"input": N, "output": N}}}
Session ID Field: sessionID (camelCase)
Gemini CLI (Placeholder)
Status: Not yet implemented
CLI Reference: TBD
Known Info:
- API-based (has cost tracking)
- May support streaming
Codex (Placeholder)
Status: Not yet implemented
CLI Reference: TBD
Qwen3 Coder (Placeholder)
Status: Not yet implemented
CLI Reference: TBD