Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
9032eff
MAESTRO: feat: add gemini-cli to ToolType, agent definitions, and cap…
openasocket Feb 20, 2026
0aeefd3
MAESTRO: feat: add gemini-cli to AGENT_ARTIFACTS in contextGroomer.ts
openasocket Feb 20, 2026
3d58ff4
MAESTRO: feat: add gemini-cli to AGENT_TARGET_NOTES and getAgentDispl…
openasocket Feb 20, 2026
a1edf62
MAESTRO: feat: enable gemini-cli as supported agent in wizard AgentSe…
openasocket Feb 20, 2026
f944ba0
MAESTRO: feat: add gemini-cli display name to sessionValidation and N…
openasocket Feb 20, 2026
649bdb4
MAESTRO: feat: add gemini-cli to all Record<ToolType> maps, error pat…
openasocket Feb 20, 2026
6799215
MAESTRO: feat: add Gemini CLI output parser for stream-json NDJSON ev…
openasocket Feb 20, 2026
a795221
MAESTRO: Add Gemini session storage
openasocket Feb 20, 2026
967234f
MAESTRO: Add Gemini CLI support to CLI stack
openasocket Feb 20, 2026
d76a4d7
MAESTRO: ensure gemini surfaces across UI
openasocket Feb 20, 2026
fdb840e
MAESTRO: wire gemini storage + stats
openasocket Feb 20, 2026
11c9b62
MAESTRO: align Gemini parser tests with spec
openasocket Feb 20, 2026
8d8b319
MAESTRO: cover gemini error patterns
openasocket Feb 20, 2026
09ca7dd
MAESTRO: fix Gemini CLI streaming display + harden integration
openasocket Feb 21, 2026
7f073df
MAESTRO: add Gemini workspace approval detection + IPC bridge
openasocket Feb 21, 2026
e69d6e0
MAESTRO: wire Gemini workspace dirs into session + spawn pipeline
openasocket Feb 21, 2026
8c6022c
MAESTRO: add WorkspaceApprovalModal component for Gemini workspace ac…
openasocket Feb 21, 2026
a0a8631
MAESTRO: wire WorkspaceApprovalModal into store, listeners, and rende…
openasocket Feb 21, 2026
4612bfc
MAESTRO: emit thinking-chunk for non-partial text events in StdoutHan…
openasocket Feb 21, 2026
dac5527
MAESTRO: add Gemini session stats store for persisting live token usage
openasocket Feb 21, 2026
0fab07c
MAESTRO: capture per-turn Gemini token usage via gemini-session-stats…
openasocket Feb 21, 2026
220ccac
MAESTRO: merge persisted Gemini token stats into parseGeminiSessionCo…
openasocket Feb 21, 2026
4e641c0
MAESTRO: add sessionId extraction regex tests for Gemini stats lookup
openasocket Feb 21, 2026
748367a
MAESTRO: implement deleteMessagePair for Gemini session editing
openasocket Feb 21, 2026
8d41494
MAESTRO: add session metadata support (naming, starring, named sessio…
openasocket Feb 21, 2026
19aa811
MAESTRO: fix Gemini text routing and context usage calculation
openasocket Feb 21, 2026
59bf5e1
MAESTRO: add Gemini stats store schema/defaults tests to agentSession…
openasocket Feb 21, 2026
8168f37
MAESTRO: add getAllNamedSessions tests for agent exclusion, starred p…
openasocket Feb 21, 2026
f995f9e
MAESTRO: add StdoutHandler Gemini text routing tests for result fallb…
openasocket Feb 21, 2026
8690da8
MAESTRO: add Gemini context usage percentage and display tests
openasocket Feb 21, 2026
5caedf7
MAESTRO: improve Gemini error detection with model-specific capacity …
openasocket Feb 21, 2026
5351b62
MAESTRO: fix Gemini group chat sandbox and batch/readOnly arg conflicts
openasocket Feb 21, 2026
7710c25
MAESTRO: fix SSH arg passthrough losing Gemini --include-directories …
openasocket Feb 28, 2026
0e6b224
MAESTRO: fix SSH path normalization for Gemini --include-directories …
openasocket Feb 28, 2026
5cd54fb
MAESTRO: report silent JSON parse failure via Sentry in Gemini sessio…
openasocket Feb 28, 2026
3fe7935
MAESTRO: add Windows path patterns to extractDeniedPath for cross-pla…
openasocket Feb 28, 2026
f2d8798
MAESTRO: replace console.warn with Sentry reporting for workspace app…
openasocket Feb 28, 2026
ace796a
MAESTRO: fix SSH sshRemoteId fallback in WorkspaceApprovalModal
openasocket Feb 28, 2026
883ebd5
MAESTRO: fix list-sessions CLI to route Gemini CLI agents to dedicate…
openasocket Feb 28, 2026
39fd499
MAESTRO: verify and harden wizard Gemini NDJSON conversation handling
openasocket Feb 28, 2026
09f9cfb
MAESTRO: verify and update Gemini CLI capability flags against v0.29.…
openasocket Feb 28, 2026
958f817
MAESTRO: fix orphaned intermediate messages in Gemini deleteMessagePair
openasocket Feb 28, 2026
84d70d2
MAESTRO: fix misleading plan (read-only) wording in context groomer
openasocket Feb 28, 2026
50f6367
MAESTRO: update inline wizard error message to include all supported …
openasocket Feb 28, 2026
194abe1
MAESTRO: fix comment/code mismatch in AgentSelectionScreen AGENT_TILES
openasocket Feb 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 79 additions & 1 deletion src/__tests__/cli/commands/list-sessions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,12 @@ vi.mock('../../../cli/services/storage', () => ({
// Mock agent-sessions
vi.mock('../../../cli/services/agent-sessions', () => ({
listClaudeSessions: vi.fn(),
listGeminiSessions: vi.fn(),
}));

import { listSessions } from '../../../cli/commands/list-sessions';
import { resolveAgentId, getSessionById } from '../../../cli/services/storage';
import { listClaudeSessions } from '../../../cli/services/agent-sessions';
import { listClaudeSessions, listGeminiSessions } from '../../../cli/services/agent-sessions';

describe('list sessions command', () => {
let consoleSpy: MockInstance;
Expand Down Expand Up @@ -255,4 +256,81 @@ describe('list sessions command', () => {
expect(consoleSpy).toHaveBeenCalledTimes(1);
expect(processExitSpy).not.toHaveBeenCalled();
});

it('should route gemini-cli agents to listGeminiSessions', () => {
vi.mocked(resolveAgentId).mockReturnValue('agent-gemini-1');
vi.mocked(getSessionById).mockReturnValue(
mockAgent({ id: 'agent-gemini-1', name: 'Gemini Agent', toolType: 'gemini-cli' })
);
vi.mocked(listGeminiSessions).mockReturnValue({
sessions: [
{
sessionId: 'gemini-session-1',
sessionName: 'Gemini Session',
projectPath: '/path/to/project',
timestamp: '2026-02-08T10:00:00.000Z',
modifiedAt: '2026-02-08T10:05:00.000Z',
firstMessage: 'Help with Gemini',
messageCount: 4,
sizeBytes: 3000,
costUsd: 0,
inputTokens: 0,
outputTokens: 0,
cacheReadTokens: 0,
cacheCreationTokens: 0,
durationSeconds: 300,
},
],
totalCount: 1,
filteredCount: 1,
});

listSessions('agent-gemini', {});

expect(listGeminiSessions).toHaveBeenCalledWith('/path/to/project', {
limit: 25,
skip: 0,
search: undefined,
});
expect(listClaudeSessions).not.toHaveBeenCalled();
expect(consoleSpy).toHaveBeenCalledTimes(1);
expect(consoleSpy.mock.calls[0][0]).toContain('Gemini Agent');
});

it('should route gemini-cli agents to listGeminiSessions in JSON mode', () => {
vi.mocked(resolveAgentId).mockReturnValue('agent-gemini-1');
vi.mocked(getSessionById).mockReturnValue(
mockAgent({ id: 'agent-gemini-1', name: 'Gemini Agent', toolType: 'gemini-cli' })
);
vi.mocked(listGeminiSessions).mockReturnValue({
sessions: [],
totalCount: 0,
filteredCount: 0,
});

listSessions('agent-gemini', { json: true });

expect(listGeminiSessions).toHaveBeenCalled();
expect(listClaudeSessions).not.toHaveBeenCalled();
const output = JSON.parse(consoleSpy.mock.calls[0][0]);
expect(output.success).toBe(true);
expect(output.agentName).toBe('Gemini Agent');
});

it('should route claude-code agents to listClaudeSessions (not listGeminiSessions)', () => {
vi.mocked(resolveAgentId).mockReturnValue('agent-claude-1');
vi.mocked(getSessionById).mockReturnValue(
mockAgent({ id: 'agent-claude-1', name: 'Claude Agent', toolType: 'claude-code' })
);
vi.mocked(listClaudeSessions).mockReturnValue({
sessions: [],
totalCount: 0,
filteredCount: 0,
});

listSessions('agent-claude', {});

expect(listClaudeSessions).toHaveBeenCalled();
expect(listGeminiSessions).not.toHaveBeenCalled();
});
});
41 changes: 39 additions & 2 deletions src/__tests__/cli/commands/run-playbook.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ vi.mock('../../../cli/services/batch-processor', () => ({
// Mock the agent-spawner service
vi.mock('../../../cli/services/agent-spawner', () => ({
detectClaude: vi.fn(),
detectCodex: vi.fn(),
detectGemini: vi.fn(),
}));

// Mock the jsonl output
Expand Down Expand Up @@ -74,7 +76,7 @@ import { runPlaybook } from '../../../cli/commands/run-playbook';
import { getSessionById } from '../../../cli/services/storage';
import { findPlaybookById } from '../../../cli/services/playbooks';
import { runPlaybook as executePlaybook } from '../../../cli/services/batch-processor';
import { detectClaude } from '../../../cli/services/agent-spawner';
import { detectClaude, detectCodex, detectGemini } from '../../../cli/services/agent-spawner';
import { emitError } from '../../../cli/output/jsonl';
import {
formatRunEvent,
Expand Down Expand Up @@ -124,12 +126,14 @@ describe('run-playbook command', () => {
throw new Error(`process.exit(${code})`);
});

// Default: Claude is available
// Default: CLI agents are available
vi.mocked(detectClaude).mockResolvedValue({
available: true,
version: '1.0.0',
path: '/usr/local/bin/claude',
});
vi.mocked(detectCodex).mockResolvedValue({ available: true, path: '/usr/local/bin/codex' });
vi.mocked(detectGemini).mockResolvedValue({ available: true, path: '/usr/local/bin/gemini' });

// Default: agent is not busy
vi.mocked(isSessionBusyWithCli).mockReturnValue(false);
Expand Down Expand Up @@ -341,6 +345,39 @@ describe('run-playbook command', () => {
});
});

describe('Gemini CLI not found', () => {
beforeEach(() => {
vi.mocked(findPlaybookById).mockReturnValue({
playbook: mockPlaybook(),
agentId: 'agent-gem',
});
vi.mocked(getSessionById).mockReturnValue(
mockSession({ id: 'agent-gem', toolType: 'gemini-cli' })
);
});

it('should error when Gemini CLI is not available (human-readable)', async () => {
vi.mocked(detectGemini).mockResolvedValue({ available: false });

await expect(runPlaybook('pb-123', {})).rejects.toThrow('process.exit(1)');

expect(formatError).toHaveBeenCalledWith(
'Gemini CLI not found. Please install @google/gemini-cli.'
);
});

it('should error when Gemini CLI is not available (JSON)', async () => {
vi.mocked(detectGemini).mockResolvedValue({ available: false });

await expect(runPlaybook('pb-123', { json: true })).rejects.toThrow('process.exit(1)');

expect(emitError).toHaveBeenCalledWith(
'Gemini CLI not found. Please install @google/gemini-cli.',
'GEMINI_NOT_FOUND'
);
});
});

describe('playbook not found', () => {
it('should error when playbook is not found (human-readable)', async () => {
vi.mocked(findPlaybookById).mockImplementation(() => {
Expand Down
44 changes: 43 additions & 1 deletion src/__tests__/cli/commands/send.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ vi.mock('../../../cli/services/agent-spawner', () => ({
spawnAgent: vi.fn(),
detectClaude: vi.fn(),
detectCodex: vi.fn(),
detectGemini: vi.fn(),
}));

// Mock storage
Expand All @@ -32,7 +33,12 @@ vi.mock('../../../main/parsers/usage-aggregator', () => ({
}));

import { send } from '../../../cli/commands/send';
import { spawnAgent, detectClaude, detectCodex } from '../../../cli/services/agent-spawner';
import {
spawnAgent,
detectClaude,
detectCodex,
detectGemini,
} from '../../../cli/services/agent-spawner';
import { resolveAgentId, getSessionById } from '../../../cli/services/storage';
import { estimateContextUsage } from '../../../main/parsers/usage-aggregator';

Expand Down Expand Up @@ -164,6 +170,27 @@ describe('send command', () => {
expect(spawnAgent).toHaveBeenCalledWith('codex', expect.any(String), 'Use codex', undefined);
});

it('should work with gemini-cli agent type', async () => {
vi.mocked(resolveAgentId).mockReturnValue('agent-gem-1');
vi.mocked(getSessionById).mockReturnValue(
mockAgent({ id: 'agent-gem-1', toolType: 'gemini-cli' })
);
vi.mocked(detectGemini).mockResolvedValue({
available: true,
path: '/usr/local/bin/gemini',
});
vi.mocked(spawnAgent).mockResolvedValue({
success: true,
response: 'Gemini response',
agentSessionId: 'gem-session',
});

await send('agent-gem', 'Use gemini', {});

expect(detectGemini).toHaveBeenCalled();
expect(spawnAgent).toHaveBeenCalledWith('gemini-cli', expect.any(String), 'Use gemini', undefined);
});

it('should exit with error when agent ID is not found', async () => {
vi.mocked(resolveAgentId).mockImplementation(() => {
throw new Error('Agent not found: bad-id');
Expand Down Expand Up @@ -204,6 +231,21 @@ describe('send command', () => {
expect(processExitSpy).toHaveBeenCalledWith(1);
});

it('should exit with error when Gemini CLI is not found', async () => {
vi.mocked(resolveAgentId).mockReturnValue('agent-gem-1');
vi.mocked(getSessionById).mockReturnValue(
mockAgent({ id: 'agent-gem-1', toolType: 'gemini-cli' })
);
vi.mocked(detectGemini).mockResolvedValue({ available: false });

await send('agent-gem', 'Hello', {});

const output = JSON.parse(consoleSpy.mock.calls[0][0]);
expect(output.success).toBe(false);
expect(output.code).toBe('GEMINI_NOT_FOUND');
expect(processExitSpy).toHaveBeenCalledWith(1);
});

it('should handle agent failure with error in response', async () => {
vi.mocked(resolveAgentId).mockReturnValue('agent-abc-123');
vi.mocked(getSessionById).mockReturnValue(mockAgent());
Expand Down
Loading