Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
244f2e4
fix: restore Cmd+/- font size shortcuts lost when custom menu was added
pedramamini Mar 4, 2026
5272ae1
feat: add Create Worktree command to command palette with auto-focus
pedramamini Mar 4, 2026
f6e9ebb
fix: format symphony-registry.json and register font size shortcuts
pedramamini Mar 4, 2026
05a4bfb
fix: resolve duplicate entries in file tree by using tree-structured …
pedramamini Mar 5, 2026
38a741c
feat: add --read-only flag to maestro-cli send command
pedramamini Mar 5, 2026
05f3cf8
fix: allow session name pill to shrink so date doesn't collide with t…
pedramamini Mar 5, 2026
8eaed7e
feat: add unread agents filter toggle with Cmd+Shift+U shortcut
pedramamini Mar 5, 2026
ce5cf67
fix: use textMain for session names to prevent visual dimming
pedramamini Mar 6, 2026
8d2b003
fix: count only agents with entries in lookback window for Director's…
pedramamini Mar 6, 2026
926f978
fix: preserve draft input when replaying a previous message
pedramamini Mar 6, 2026
7409805
fix: match unread agent indicator dot position to tab unread pattern
pedramamini Mar 6, 2026
bca4bed
feat: show View history link on files tab during batch run
pedramamini Mar 6, 2026
4d0e212
fix: remove hardcoded max-width on header session name
pedramamini Mar 6, 2026
564d85f
fix: skip directory collision warning when agents are on different hosts
pedramamini Mar 6, 2026
e47cb59
fix: check sessionSshRemoteConfig as primary SSH remote ID source
pedramamini Mar 6, 2026
16322c8
fix: use dark text colors for context warning sash in light mode
pedramamini Mar 6, 2026
d507d10
fix: dropdown clipping on hamburger menu and live overlay, rename Rem…
pedramamini Mar 6, 2026
96190ec
fix: always show .maestro folder in file tree regardless of ignore pa…
pedramamini Mar 6, 2026
cecb0cb
fix: improve light theme contrast for syntax highlighting and colors
pedramamini Mar 7, 2026
734d773
fix: include busy agents in unread agents filter
pedramamini Mar 7, 2026
44be9a7
fix: suppress empty groups and New Group button in unread agents filter
pedramamini Mar 7, 2026
c6734e5
feat: add empty state for unread agents filter with centered Bot icon
pedramamini Mar 7, 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
34 changes: 30 additions & 4 deletions src/__tests__/cli/commands/send.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@ describe('send command', () => {
'claude-code',
'/path/to/project',
'Hello world',
undefined
undefined,
{ readOnlyMode: undefined }
);
expect(consoleSpy).toHaveBeenCalledTimes(1);

Expand Down Expand Up @@ -128,7 +129,8 @@ describe('send command', () => {
'claude-code',
'/path/to/project',
'Continue from before',
'session-xyz-789'
'session-xyz-789',
{ readOnlyMode: undefined }
);

const output = JSON.parse(consoleSpy.mock.calls[0][0]);
Expand All @@ -153,7 +155,8 @@ describe('send command', () => {
'claude-code',
'/custom/project/path',
'Do something',
undefined
undefined,
{ readOnlyMode: undefined }
);
});

Expand All @@ -173,7 +176,30 @@ describe('send command', () => {

expect(detectCodex).toHaveBeenCalled();
expect(detectClaude).not.toHaveBeenCalled();
expect(spawnAgent).toHaveBeenCalledWith('codex', expect.any(String), 'Use codex', undefined);
expect(spawnAgent).toHaveBeenCalledWith('codex', expect.any(String), 'Use codex', undefined, {
readOnlyMode: undefined,
});
});

it('should pass readOnlyMode when --read-only flag is set', async () => {
vi.mocked(resolveAgentId).mockReturnValue('agent-abc-123');
vi.mocked(getSessionById).mockReturnValue(mockAgent());
vi.mocked(detectClaude).mockResolvedValue({ available: true, path: '/usr/bin/claude' });
vi.mocked(spawnAgent).mockResolvedValue({
success: true,
response: 'Read-only response',
agentSessionId: 'session-ro',
});

await send('agent-abc', 'Analyze this code', { readOnly: true });

expect(spawnAgent).toHaveBeenCalledWith(
'claude-code',
'/path/to/project',
'Analyze this code',
undefined,
{ readOnlyMode: true }
);
});

it('should exit with error when agent ID is not found', async () => {
Expand Down
36 changes: 36 additions & 0 deletions src/__tests__/cli/services/agent-spawner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1075,6 +1075,42 @@ Some text with [x] in it that's not a checkbox
}
});

it('should include read-only args for Claude when readOnlyMode is true', async () => {
const resultPromise = spawnAgent('claude-code', '/project', 'prompt', undefined, {
readOnlyMode: true,
});

await new Promise((resolve) => setTimeout(resolve, 0));

const [, args] = mockSpawn.mock.calls[0];
// Should include Claude's read-only args from centralized definitions
expect(args).toContain('--permission-mode');
expect(args).toContain('plan');
// Should still have base args
expect(args).toContain('--print');
expect(args).toContain('--dangerously-skip-permissions');

mockStdout.emit('data', Buffer.from('{"type":"result","result":"Done"}\n'));
mockChild.emit('close', 0);
await resultPromise;
});

it('should not include read-only args when readOnlyMode is false', async () => {
const resultPromise = spawnAgent('claude-code', '/project', 'prompt', undefined, {
readOnlyMode: false,
});

await new Promise((resolve) => setTimeout(resolve, 0));

const [, args] = mockSpawn.mock.calls[0];
expect(args).not.toContain('--permission-mode');
expect(args).not.toContain('plan');

mockStdout.emit('data', Buffer.from('{"type":"result","result":"Done"}\n'));
mockChild.emit('close', 0);
await resultPromise;
});

it('should generate unique session-id for each spawn', async () => {
// First spawn
const promise1 = spawnAgent('claude-code', '/project', 'prompt1');
Expand Down
75 changes: 75 additions & 0 deletions src/__tests__/renderer/components/QuickActionsModal.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1615,4 +1615,79 @@ describe('QuickActionsModal', () => {
expect(screen.queryByText('Context: Send to Agent')).not.toBeInTheDocument();
});
});

describe('Create Worktree action', () => {
it('shows Create Worktree action for git repo sessions with callback', () => {
const onQuickCreateWorktree = vi.fn();
const props = createDefaultProps({
sessions: [createMockSession({ isGitRepo: true })],
onQuickCreateWorktree,
});
render(<QuickActionsModal {...props} />);

expect(screen.getByText('Create Worktree')).toBeInTheDocument();
});

it('calls onQuickCreateWorktree with active session and closes modal', () => {
const onQuickCreateWorktree = vi.fn();
const session = createMockSession({ isGitRepo: true });
const props = createDefaultProps({
sessions: [session],
onQuickCreateWorktree,
});
render(<QuickActionsModal {...props} />);

fireEvent.click(screen.getByText('Create Worktree'));

expect(onQuickCreateWorktree).toHaveBeenCalledWith(session);
expect(props.setQuickActionOpen).toHaveBeenCalledWith(false);
});

it('resolves to parent session when active session is a worktree child', () => {
const onQuickCreateWorktree = vi.fn();
const parentSession = createMockSession({
id: 'parent-1',
name: 'Parent',
isGitRepo: true,
});
const childSession = createMockSession({
id: 'child-1',
name: 'Child',
isGitRepo: true,
parentSessionId: 'parent-1',
worktreeBranch: 'feature-1',
});
const props = createDefaultProps({
sessions: [parentSession, childSession],
activeSessionId: 'child-1',
onQuickCreateWorktree,
});
render(<QuickActionsModal {...props} />);

fireEvent.click(screen.getByText('Create Worktree'));

// Should resolve to parent, not the child
expect(onQuickCreateWorktree).toHaveBeenCalledWith(parentSession);
});

it('does not show Create Worktree when session is not a git repo', () => {
const onQuickCreateWorktree = vi.fn();
const props = createDefaultProps({
sessions: [createMockSession({ isGitRepo: false })],
onQuickCreateWorktree,
});
render(<QuickActionsModal {...props} />);

expect(screen.queryByText('Create Worktree')).not.toBeInTheDocument();
});

it('does not show Create Worktree when callback is not provided', () => {
const props = createDefaultProps({
sessions: [createMockSession({ isGitRepo: true })],
});
render(<QuickActionsModal {...props} />);

expect(screen.queryByText('Create Worktree')).not.toBeInTheDocument();
});
});
});
2 changes: 2 additions & 0 deletions src/__tests__/renderer/hooks/useFileExplorerEffects.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,7 @@ describe('useFileExplorerEffects', () => {
renderHook(() => useFileExplorerEffects(deps));

expect(flattenTree).toHaveBeenCalledWith(tree, new Set(['src']));
expect(useFileExplorerStore.getState().filteredFileTree).toEqual(tree);
expect(useFileExplorerStore.getState().flatFileList).toEqual(flatResult);
});

Expand All @@ -437,6 +438,7 @@ describe('useFileExplorerEffects', () => {
const deps = createDeps();
renderHook(() => useFileExplorerEffects(deps));

expect(useFileExplorerStore.getState().filteredFileTree).toEqual([]);
expect(useFileExplorerStore.getState().flatFileList).toEqual([]);
});

Expand Down
Loading