Skip to content

0.15.0 RC polish round two#485

Merged
pedramamini merged 22 commits intomainfrom
0.15.0-RC-polish-round-two
Mar 1, 2026
Merged

0.15.0 RC polish round two#485
pedramamini merged 22 commits intomainfrom
0.15.0-RC-polish-round-two

Conversation

@pedramamini
Copy link
Collaborator

@pedramamini pedramamini commented Mar 1, 2026

  • perf: remove streaming debug logs and fix index-based React keys
  • fix: address PR review feedback from CodeRabbit
  • perf: prevent session bloat by stripping file tree and preview history from persistence
  • feat: honor local .gitignore patterns in file tree indexing
  • refactor: clean up file-scale PR with shared gitignore parser, options object, and rescan effect
  • perf: cherry-pick low-risk optimizations from PR Adopt async file IO for history and stats #385
  • fix: make blocked Symphony issues selectable for document exploration
  • feat: add group @mentions in group chat and fix navigation history
  • feat: allow changing agent provider in Edit Agent modal
  • feat: archive state for group chat (feat: archive state for group chat  #377)
  • fix: address PR 0.15.0-RC polish round two #450 review feedback across stats, UI, and accessibility
  • fix: move blocked issue message to footer with greyed-out Start button
  • fix: prevent context menus from rendering off-screen
  • fix: prevent orphaned file preview when tab bar is hidden
  • fix: address PR review — missed onRemoteCommand path and weak test
  • polish: UI refinements for header z-index, status dot opacity, and Director's Notes beta badge
  • polish: Director's Notes keeps old content visible during regeneration
  • fix: update prop extraction hooks for rebased branch compatibility
  • feat: add layout algorithm options to Document Graph (Mind Map, Radial, Force-Directed)
  • polish: add "Load full file" button to truncated file preview banner
  • fix: worktree auto-run spin-out creates duplicate agents and fails to start
  • fix: lint cleanup, auto-scroll empty-tab guard, and worktree batch improvements
  • feat: add "Open in Finder/Explorer" button to Storage Location settings
  • fix: prefix unused destructured vars to satisfy ESLint after rebase
  • feat: add "View history" link to Auto Run batch progress footer
  • feat: show issue counts on Symphony tiles via GitHub Search API
  • fix: show task summary on TodoWrite tool cards in AI terminal
  • fix: remove close button from group chat header, fix agent switching
  • fix: skip archived group chats during keyboard navigation cycling
  • fix: file explorer dedup, worktree timer race, ENOENT handling, type fixes

Summary by CodeRabbit

Release Notes

  • New Features

    • Added keyboard accessibility to header titles (Enter/Space keys to trigger actions)
    • Implemented paginated GitHub issue count retrieval with improved caching and repository slug tracking
  • Bug Fixes

    • Fixed runtime errors from null file reads in image loading across multiple components
    • Added null-safety guards for file operations preventing crashes on unreadable files
    • Improved error handling and cleanup in worktree operations
  • Refactors

    • Strengthened cache validation with repository slug tracking
    • Improved document graph layout type synchronization

@coderabbitai
Copy link

coderabbitai bot commented Mar 1, 2026

📝 Walkthrough

Walkthrough

PR adds null-safety guards for file reading across multiple components, implements paginated GitHub issue fetching with cache validation via repoSlugs tracking, updates test mocks and assertions for shortcuts and console.warn calls, adds keyboard accessibility to GroupChatHeader, and refactors various formatting and state synchronization changes.

Changes

Cohort / File(s) Summary
Test Formatting & Mock Updates
src/__tests__/renderer/components/mindMapLayouts.test.ts, src/__tests__/renderer/components/GroupChatHeader.test.tsx, src/__tests__/renderer/components/TerminalOutput.test.tsx, src/__tests__/renderer/components/auto-scroll.test.tsx, src/__tests__/renderer/hooks/useBatchProcessor.test.ts
Cosmetic line-wrapping and formatting adjustments; mock structure updated for Shortcut to include id/label fields; hoisted mocks via vi.hoisted introduced.
Console.warn Assertions & Symphony Type Tests
src/__tests__/renderer/utils/fileExplorer.test.ts, src/__tests__/shared/symphony-types.test.ts
Added console.warn invocation checks during file deduplication; added repoSlugs field validation in SymphonyCache tests.
Symphony Pagination & Cache Validation
src/shared/symphony-types.ts, src/main/ipc/handlers/symphony.ts
Added repoSlugs field to SymphonyIssueCounts type for cache validation. Implemented paginated GitHub issue fetching (per_page=100, up to 10 pages). Cache now tracks requested repo slugs to validate freshness across requests.
Symphony State Management
src/renderer/hooks/symphony/useSymphony.ts
Clear issueCounts on empty repos list or fetch error.
File Reading Null-Safety Guards
src/renderer/components/AutoRun.tsx, src/renderer/components/FilePreview.tsx, src/renderer/components/DebugWizardModal.tsx, src/renderer/components/ImageDiffViewer.tsx, src/renderer/components/InlineWizard/DocumentGenerationView.tsx, src/renderer/components/MarkdownRenderer.tsx, src/renderer/components/Wizard/.../DocumentEditor.tsx, src/renderer/hooks/batch/useAutoRunImageHandling.ts, src/renderer/hooks/git/useFileExplorerEffects.ts, src/renderer/hooks/tabs/useTabHandlers.ts, src/renderer/hooks/ui/useAppHandlers.ts
Guard readFile results against null/undefined before calling startsWith() or processing content; add early returns when content is null.
Worktree & Error Handling Improvements
src/renderer/hooks/batch/useAutoRunHandlers.ts, src/renderer/hooks/batch/useBatchProcessor.ts, src/renderer/hooks/batch/useWorktreeManager.ts
Wrapped worktree creation in try/catch with dedup marker cleanup; improved error logging in batch processor; explicit baseBranch variable tracking for PR creation flow.
Keyboard Accessibility & Layout Formatting
src/renderer/components/GroupChatHeader.tsx, src/renderer/App.tsx, src/renderer/components/DocumentGraph/DocumentGraphView.tsx, src/renderer/components/DocumentGraph/mindMapLayouts.ts, src/renderer/stores/settingsStore.ts
Added keyboard event handling (Enter/Space) for GroupChatHeader title rename with focusable state; added layoutType state sync via useEffect; new DOCUMENT_GRAPH_LAYOUT_TYPES constant for validation; parameter list reformatting across layout functions.
API Type Update & Minor Styling
src/renderer/global.d.ts, src/renderer/components/SymphonyModal.tsx
Updated readFile signature to return Promise\<string | null\>; minor message text wrapping adjustment.

Sequence Diagram

sequenceDiagram
    participant Client as useSymphony Hook
    participant IPC as symphony.ts Handler
    participant GitHub as GitHub API
    participant Cache as Settings Store

    Client->>IPC: getIssueCounts(repos)
    
    rect rgba(100, 150, 200, 0.5)
    note right of IPC: Cache Validation
    IPC->>IPC: Normalize & sort repos
    IPC->>Cache: Check cached repoSlugs
    alt Slugs match && cache fresh
        IPC-->>Client: Return cached counts
    end
    end
    
    rect rgba(150, 100, 200, 0.5)
    note right of IPC: Paginated Fetch (1-10 pages)
    IPC->>GitHub: Fetch issues page 1<br/>(per_page=100)
    GitHub-->>IPC: Page 1 results
    IPC->>IPC: Aggregate into counts map
    
    loop While page has 100 items
        IPC->>GitHub: Fetch next page
        GitHub-->>IPC: Next page results
        IPC->>IPC: Aggregate counts
    end
    end
    
    rect rgba(200, 150, 100, 0.5)
    note right of Cache: Cache Update
    IPC->>Cache: Store counts, fetchedAt,<br/>repoSlugs
    end
    
    IPC-->>Client: Return fresh counts
    Client->>Client: Update state
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title '0.15.0 RC polish round two' is vague and does not clearly describe the main change or scope of the pull request. Replace the title with a specific description of the primary change, such as 'Add null-safety guards and Symphony issue count pagination for 0.15.0 RC' or similar descriptor that captures the key substantive changes.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 84.78% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 0.15.0-RC-polish-round-two

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (6)
src/renderer/components/GroupChatHeader.tsx (1)

48-55: ⚠️ Potential issue | 🟡 Minor

Add keyboard accessibility to the clickable heading.

The h1 element has onClick but lacks keyboard support. Users navigating with keyboards cannot activate the rename action via this element. Add tabIndex={0} and an onKeyDown handler for Enter/Space.

♿ Proposed fix for keyboard accessibility
 <h1
 	className="text-lg font-semibold cursor-pointer hover:opacity-80"
 	style={{ color: theme.colors.textMain }}
 	onClick={onRename}
+	onKeyDown={(e) => {
+		if (e.key === 'Enter' || e.key === ' ') {
+			e.preventDefault();
+			onRename();
+		}
+	}}
+	tabIndex={0}
 	title="Click to rename"
+	role="button"
 >
 	Group Chat: {name}
 </h1>

As per coding guidelines: "Add tabIndex attribute and focus event handlers when implementing components that need keyboard focus".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/GroupChatHeader.tsx` around lines 48 - 55, The
clickable heading in GroupChatHeader (the h1 that renders "Group Chat: {name}"
and uses onRename) lacks keyboard support; add tabIndex={0} so it is focusable
and implement an onKeyDown handler that calls onRename when Enter or Space is
pressed (checking event.key === 'Enter' || event.key === ' ' / 'Spacebar' as
needed) and optionally preventDefault for Space to avoid page scrolling;
consider adding role="button" and an accessible title/aria-label if not already
present.
src/renderer/hooks/batch/useWorktreeManager.ts (1)

358-365: ⚠️ Potential issue | 🟡 Minor

Propagate targetBranch in the exception path too.

targetBranch is returned for handled outcomes but omitted when execution falls into catch, so callers lose branch context on thrown failures.

💡 Suggested fix
 const createPR = useCallback(
 	async (options: CreatePROptions): Promise<PRCreationResult> => {
 		const { worktreePath, mainRepoCwd, worktree, documents, totalCompletedTasks } = options;
+		let baseBranch: string | undefined = worktree.prTargetBranch;
 
 		console.log('[WorktreeManager] Creating PR from worktree branch', worktree.branchName);
 
 		try {
 			// Use the user-selected target branch, or fall back to default branch detection
-			let baseBranch = worktree.prTargetBranch;
 			if (!baseBranch) {
 				const defaultBranchResult = await window.maestro.git.getDefaultBranch(mainRepoCwd);
 				baseBranch =
 					defaultBranchResult.success && defaultBranchResult.branch
 						? defaultBranchResult.branch
 						: 'main';
 			}
@@
 		} catch (error) {
 			console.error('[WorktreeManager] Error creating PR:', error);
 			return {
 				success: false,
 				error: error instanceof Error ? error.message : 'Unknown error',
+				targetBranch: baseBranch,
 			};
 		}
 	},
 	[generatePRTitle, generatePRBody]
 );

Also applies to: 407-412

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/hooks/batch/useWorktreeManager.ts` around lines 358 - 365, The
code computes baseBranch from worktree.prTargetBranch (falling back to
window.maestro.git.getDefaultBranch(mainRepoCwd) or 'main') but only
returns/attaches targetBranch for handled outcomes; when an exception is thrown
the branch context is lost—update the catch/throw paths in useWorktreeManager
(the blocks around baseBranch / worktree.prTargetBranch and the similar section
at lines ~407-412) to include the resolved baseBranch/targetBranch in the thrown
error or the error payload so callers always receive targetBranch information.
src/renderer/hooks/batch/useBatchProcessor.ts (1)

1-2014: ⚠️ Potential issue | 🟡 Minor

CI blocker: Prettier check is failing for this file.

The pipeline reports formatting failure here; please run formatter on this file before merge.

As per coding guidelines src/**/*.{ts,tsx,js,jsx,json}: Use tabs for indentation, not spaces, throughout the codebase.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/hooks/batch/useBatchProcessor.ts` around lines 1 - 2014,
Prettier/formatting fails due to spaces used for indentation; reformat this file
to use tabs per project rules. Run the project's formatter (prettier or npm/yarn
format script) on the file and ensure tabs are used (not spaces) across
top-level functions/hooks like useBatchProcessor, startBatchRun,
updateBatchStateAndBroadcast, pauseBatchOnError, and other exported functions;
fix any lingering lint/Prettier issues reported and re-run CI to verify the
Prettier check passes before merging.
src/renderer/hooks/batch/useAutoRunHandlers.ts (1)

114-127: ⚠️ Potential issue | 🟡 Minor

Clear the dedup marker when worktree setup throws.

If window.maestro.git.worktreeSetup(...) throws at Line 119, the marker set at Line 116 is never cleared. That can cause transient false dedup behavior until TTL expiry.

Proposed fix
 		// Step 2: Create worktree on disk
-		const result = await window.maestro.git.worktreeSetup(
-			parentSession.cwd,
-			worktreePath,
-			branchName,
-			sshRemoteId
-		);
+		let result;
+		try {
+			result = await window.maestro.git.worktreeSetup(
+				parentSession.cwd,
+				worktreePath,
+				branchName,
+				sshRemoteId
+			);
+		} catch (error) {
+			clearRecentlyCreatedWorktreePath(worktreePath);
+			throw error;
+		}
 		if (!result.success) {
 			clearRecentlyCreatedWorktreePath(worktreePath);
 			notifyToast({
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/hooks/batch/useAutoRunHandlers.ts` around lines 114 - 127, The
marker set by markWorktreePathAsRecentlyCreated(worktreePath) isn't cleared if
window.maestro.git.worktreeSetup(...) throws; wrap the worktreeSetup call in a
try/catch (or try/finally) so that
clearRecentlyCreatedWorktreePath(worktreePath) is always invoked on error
(and/or in finally) before rethrowing or returning an error result, ensuring the
dedup marker is removed even when worktreeSetup throws.
src/renderer/App.tsx (1)

1-3244: ⚠️ Potential issue | 🟡 Minor

Prettier check is failing for this file in CI.

Formatting must be normalized before merge (npx prettier --write src/renderer/App.tsx), otherwise the current pipeline failure remains.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/App.tsx` around lines 1 - 3244, Prettier formatting in
src/renderer/App.tsx is failing CI; run the formatter and commit the changes.
Run npx prettier --write src/renderer/App.tsx (or your repo's prettier command),
verify the file (MaestroConsoleInner / MaestroConsole) is reformatted, review
any changes, then stage and commit the updated file so the CI Prettier check
passes.
src/renderer/components/DocumentGraph/DocumentGraphView.tsx (1)

1-2142: ⚠️ Potential issue | 🟡 Minor

Prettier check is failing for this file in CI.

Please run npx prettier --write src/renderer/components/DocumentGraph/DocumentGraphView.tsx to clear the current pipeline failure.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/DocumentGraph/DocumentGraphView.tsx` around lines 1 -
2142, Prettier formatting is failing for DocumentGraphView.tsx; run the
formatter and commit the updated file to fix CI. Specifically, run `npx prettier
--write src/renderer/components/DocumentGraph/DocumentGraphView.tsx`, review the
changes in the DocumentGraphView component (and helper buildFileTreeFromPaths)
to ensure no logic was altered, then stage and commit the formatted file so the
pipeline passes.
🧹 Nitpick comments (7)
src/renderer/components/FilePreview.tsx (1)

2438-2438: Consider user feedback for large file loading.

When loading full content of very large files, syntax highlighting may cause a brief UI freeze. This is an acceptable trade-off for a user-initiated action, but you could optionally:

  • Add a loading spinner while content renders
  • For files significantly larger than 100KB (e.g., >500KB), show a confirmation or warning

This is low priority since the user explicitly requests full content.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/FilePreview.tsx` at line 2438, The onClick should
guard large-file loads and surface progress: update the FilePreview component's
onClick handler (where setShowFullContent is called) to an async handler that
(1) checks the file size (use the existing file.size or contentLength) and for
sizes > 500*1024 shows a user confirmation/warning before proceeding, (2) sets a
local loading state (e.g., isLoadingFullContent) to true before triggering the
full-content render (setShowFullContent(true)), and (3) clear the loading state
after the render completes; optionally use requestIdleCallback or setTimeout to
defer heavy syntax-highlighting work so the spinner can display. Ensure you add
and use isLoadingFullContent state and render a spinner in FilePreview when
true, and reuse the existing setShowFullContent symbol so behavior remains
consistent.
src/__tests__/renderer/components/GroupChatHeader.test.tsx (1)

43-52: Consider resetting mocks between tests.

The mock functions in defaultProps are defined once at module level. While current tests likely pass, shared mocks can cause flaky tests if call counts accumulate. Adding a beforeEach to clear mocks improves test isolation.

🧪 Proposed fix to reset mocks
 const defaultProps = {
 	theme: mockTheme,
 	name: 'Test Chat',
 	participantCount: 3,
 	onRename: vi.fn(),
 	onShowInfo: vi.fn(),
 	rightPanelOpen: false,
 	onToggleRightPanel: vi.fn(),
 	shortcuts: mockShortcuts,
 };

 describe('GroupChatHeader', () => {
+	beforeEach(() => {
+		vi.clearAllMocks();
+	});
+
 	it('renders group chat name and participant count', () => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/__tests__/renderer/components/GroupChatHeader.test.tsx` around lines 43 -
52, Add test isolation by resetting and reinitializing the module-level mocks
before each test: add a beforeEach that calls vi.clearAllMocks() (or
vi.resetAllMocks()) and reassigns defaultProps with fresh handler mocks
(onRename, onShowInfo, onToggleRightPanel) so call counts don’t accumulate
between tests; reference the existing defaultProps object and the mock handler
names to locate where to recreate the vi.fn() instances.
src/renderer/stores/settingsStore.ts (1)

714-719: Deduplicate allowed layout literals to avoid drift.

The same ['mindmap', 'radial', 'force'] list is duplicated in setter and loader. Centralizing it avoids subtle future mismatches.

♻️ Proposed refactor
 export type DocumentGraphLayoutType = 'mindmap' | 'radial' | 'force';
+const DOCUMENT_GRAPH_LAYOUT_TYPES = ['mindmap', 'radial', 'force'] as const;
 
 ...
 	setDocumentGraphLayoutType: (value) => {
-		const valid: DocumentGraphLayoutType[] = ['mindmap', 'radial', 'force'];
-		const layoutType = valid.includes(value) ? value : 'mindmap';
+		const layoutType = DOCUMENT_GRAPH_LAYOUT_TYPES.includes(value) ? value : 'mindmap';
 		set({ documentGraphLayoutType: layoutType });
 		window.maestro.settings.set('documentGraphLayoutType', layoutType);
 	},
 ...
 		if (allSettings['documentGraphLayoutType'] !== undefined) {
 			const lt = allSettings['documentGraphLayoutType'] as string;
-			if (['mindmap', 'radial', 'force'].includes(lt)) {
+			if (DOCUMENT_GRAPH_LAYOUT_TYPES.includes(lt as DocumentGraphLayoutType)) {
 				patch.documentGraphLayoutType = lt as DocumentGraphLayoutType;
 			}
 		}

Also applies to: 1622-1627

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/stores/settingsStore.ts` around lines 714 - 719, The allowed
layout literals are duplicated; extract the array ['mindmap','radial','force']
into a single constant (e.g., ALLOWED_DOCUMENT_GRAPH_LAYOUTS:
DocumentGraphLayoutType[]) defined near the top of the module and use that
constant in setDocumentGraphLayoutType (replace the local valid variable) and
the corresponding loader code (the other occurrence around lines 1622-1627) so
both places use ALLOWED_DOCUMENT_GRAPH_LAYOUTS.includes(value) and default to
'mindmap' if invalid.
src/__tests__/renderer/utils/fileExplorer.test.ts (1)

460-495: Assert warning emission in the new dedup tests.

You already spy on console.warn; adding explicit assertions will lock in the expected observability behavior for duplicate collapse paths.

Suggested test tightening
 		const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
 		const result = await loadFileTree('/project');
+		expect(consoleSpy).toHaveBeenCalled();
 		consoleSpy.mockRestore();
@@
 		const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
 		const result = await loadFileTree('/project');
+		expect(consoleSpy).toHaveBeenCalled();
 		consoleSpy.mockRestore();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/__tests__/renderer/utils/fileExplorer.test.ts` around lines 460 - 495,
The new dedup tests spy on console.warn but don't assert it was called; update
both tests for duplicated entries ("deduplicates entries returned by readDir"
and "deduplicates entries in nested directories") to assert the warning was
emitted: after calling loadFileTree('/project') assert that the consoleSpy (the
vi.spyOn(console, 'warn') mock) was called (e.g.,
expect(consoleSpy).toHaveBeenCalled() or toHaveBeenCalledTimes(1)) and then
restore the spy as already done, keeping references to loadFileTree and
consoleSpy so the assertions target the exact spy used in each test.
src/renderer/hooks/symphony/useSymphony.ts (1)

198-225: Clear stale issueCounts when fetching fails or repo list becomes empty.

Current flow keeps old counts on failure/empty repo set, which can leave stale badges visible. Resetting counts in those paths avoids misleading UI state.

Proposed refactor
 const fetchIssueCounts = useCallback(async (repos: RegisteredRepository[]) => {
-	if (repos.length === 0) return;
+	if (repos.length === 0) {
+		setIssueCounts(null);
+		return;
+	}
 	setIsLoadingIssueCounts(true);
 	try {
 		const slugs = repos.map((r) => r.slug);
 		const response = await window.maestro.symphony.getIssueCounts(slugs);
 		if (response.counts) {
 			setIssueCounts(response.counts);
 		}
 	} catch (err) {
 		console.error('Failed to fetch issue counts:', err);
+		setIssueCounts(null);
 	} finally {
 		setIsLoadingIssueCounts(false);
 	}
 }, []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/hooks/symphony/useSymphony.ts` around lines 198 - 225, The
fetchIssueCounts flow can leave stale issueCounts displayed when the fetch fails
or when the repo list becomes empty; update the fetchIssueCounts function and
its related effect to clear state: inside fetchIssueCounts ensure that when
repos.length === 0 you call setIssueCounts({}) (or an initial empty value)
before returning, and in the catch block call setIssueCounts({}) as well as
setIsLoadingIssueCounts(false); additionally, in the useEffect that watches
repositories, if repositories.length === 0 call setIssueCounts({}) to
proactively clear counts when the repo list is emptied (refer to
fetchIssueCounts, setIssueCounts, setIsLoadingIssueCounts, and the repositories
effect).
src/renderer/components/DocumentGraph/mindMapLayouts.ts (2)

108-135: Consider cache eviction strategy for nodeHeightCache.

The nodeHeightCache Map at module scope will grow unbounded over time. While the cache keys are based on text length and limit (not the actual text), in long-running Electron apps with varying previewCharLimit values, this could accumulate entries.

Consider adding a size limit or using an LRU cache if memory becomes a concern. However, given the key structure (${truncatedLength}:${previewCharLimit}), the practical number of unique keys is bounded by reasonable text lengths and limit values, so this is likely acceptable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/DocumentGraph/mindMapLayouts.ts` around lines 108 -
135, The module-level nodeHeightCache Map can grow unbounded; modify it to
enforce a bounded size (e.g., MAX_CACHE_ENTRIES) or swap it for an LRU cache
implementation and evict the oldest entries when capacity is exceeded. Update
the nodeHeightCache initialization (replace Map with an LRU structure or a
wrapped Map) and change the calculateNodeHeight flow that calls
nodeHeightCache.set(cacheKey, height) to perform eviction when size >
MAX_CACHE_ENTRIES (or use the LRU's set method). You can implement a simple LRU
using insertion order on Map or import a small lru-cache; keep nodeHeightCache
and calculateNodeHeight names so callers remain unchanged.

47-58: Consider exporting LayoutFunction type for extensibility.

The LayoutFunction type (line 48) is currently internal. If you plan to allow custom layout algorithms in the future, exporting this type would enable type-safe implementations. For now, this is a minor consideration.

💡 Optional: Export the type
-/** Common layout function signature */
-type LayoutFunction = (
+/** Common layout function signature */
+export type LayoutFunction = (
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/DocumentGraph/mindMapLayouts.ts` around lines 47 -
58, The LayoutFunction type is currently internal; export it so external modules
can implement custom layouts with type safety. Modify the declaration of
LayoutFunction in mindMapLayouts.ts to be exported (export type LayoutFunction =
...) and ensure any related symbols used by its signature — MindMapNode,
MindMapLink, LayoutResult — are exported or at least accessible where consumers
will implement layouts; update exports if necessary so consumers can import
LayoutFunction and its dependent types.
🤖 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/__tests__/renderer/components/auto-scroll.test.tsx`:
- Line 126: Prettier is failing because the inline logs object is too long;
reformat the logs array entry in auto-scroll.test.tsx by breaking the object
into multiple lines (e.g., put id, text, timestamp, and source on their own
lines) or simply run the formatter to rewrite the file; target the logs
variable/array in the test (the object with id 'default-log') and run npx
prettier --write src/__tests__/renderer/components/auto-scroll.test.tsx to apply
the fix.

In `@src/__tests__/renderer/components/GroupChatHeader.test.tsx`:
- Around line 39-41: The mock object mockShortcuts in GroupChatHeader.test.tsx
does not conform to the Shortcut interface (it uses description and lacks id and
label) and the "as Shortcut" cast hides the mismatch; fix by replacing the
mocked entry (toggleRightPanel) with a valid Shortcut shape including id, label,
and keys (and any other required fields from src/renderer/types/index.ts) and
remove the unnecessary type cast so TypeScript enforces correctness; update any
test assertions that referenced description to use label or the new fields as
appropriate.

In `@src/__tests__/renderer/components/TerminalOutput.test.tsx`:
- Around line 1785-1901: Prettier formatting failed for the test file containing
the describe block "tool log detail extraction" and tests referencing
TerminalOutput and LogEntry (tests: "renders TodoWrite tool with task
summary...", "renders TodoWrite with first task when none in progress", "renders
Bash tool with command detail", "renders tool with no extractable detail
gracefully"); fix by running Prettier to auto-format the test file (e.g., npx
prettier --write <this test file>) and re-run CI to ensure the formatting error
is resolved.

In `@src/__tests__/renderer/hooks/useBatchProcessor.test.ts`:
- Around line 5682-5986: The CI failure is a Prettier formatting error in the
test file containing the useBatchProcessor tests; run Prettier (e.g. npx
prettier --write .) to reformat the file and commit the changes. Locate the
tests that reference useBatchProcessor and startBatchRun and assertions using
mockWorktreeSetup, mockNotifyToast, and mockOnAddHistoryEntry, reformat that
file with Prettier, then re-run tests to ensure formatting is fixed before
pushing.
- Around line 27-30: The test currently declares mockNotifyToast after the
vi.mock factory which can trigger a TDZ ReferenceError; hoist the mock by
creating it with vi.hoisted() and use that hoisted mock in the vi.mock factory
for the notificationStore notifyToast export (i.e., replace the top-level const
mockNotifyToast with a vi.hoisted() mock and reference that hoisted mock inside
the vi.mock factory that provides notifyToast).

In `@src/main/ipc/handlers/filesystem.ts`:
- Around line 170-176: The filesystem IPC handler currently returns null for
ENOENT in its catch block (the change in src/main/ipc/handlers/filesystem.ts),
but several renderer callers (MarkdownRenderer.tsx, FilePreview.tsx,
AutoRun.tsx) assume a non-null string and call .startsWith('data:') leading to
runtime errors, and global.d.ts still types the IPC as Promise<string>; fix by
either (A) adding null/undefined guards in each caller before calling
.startsWith (e.g., check value !== null && value.startsWith(...)) and update
tests, or (B) change the handler to accept an explicit option (default false) to
preserve previous behavior and only return null when callers request it, and
update global.d.ts to Promise<string | null> when opting into null returns; pick
one approach and make consistent updates to the handler, the callers
(MarkdownRenderer, FilePreview, AutoRun), and the type declaration to avoid
type/ runtime mismatches.

In `@src/main/ipc/handlers/symphony.ts`:
- Around line 1230-1239: The cache check currently returns cache.issueCounts
whenever it's fresh, but it must also validate that the cached data was fetched
for the same requested repoSlugs; update the logic around cache.issueCounts (and
the similar block at 1247-1254) to compare the requested repoSlugs parameter
with whatever identifier is stored with the cache (e.g., a
cache.issueCounts.repoSlugs or fetchedFor field) using an equality or
set-equality check before returning; if the stored slugs don’t match, treat the
cache as invalid (fall through to refresh), otherwise continue using
isCacheValid(cache.issueCounts.fetchedAt, ISSUE_COUNTS_CACHE_TTL_MS) as now and
return the cached counts when both TTL and slug match pass.
- Around line 527-559: The search currently fetches only one page (per_page=100)
and misses results beyond page 1; modify the logic around the URL/query fetch
(where query, GITHUB_API_BASE and repoSlugs are used) to paginate using the page
parameter: loop pages starting at 1, request
`${GITHUB_API_BASE}/search/issues?q=${query}&per_page=100&page=${page}`, parse
each page's data.items and increment counts[slug] (initialize counts from
repoSlugs as before), stop when a page returns fewer than per_page items or when
the cumulative results would exceed GitHub's 1000-result Search API cap (i.e.,
stop after page 10), and keep the same error handling (throw SymphonyError on
non-ok responses) and logging via logger.info when done.

In `@src/renderer/components/DocumentGraph/DocumentGraphView.tsx`:
- Around line 220-221: The local state layoutType (created with useState and
updated via setLayoutType) is only initialized from the defaultLayoutType prop
and will go out of sync if defaultLayoutType changes; add a useEffect that
watches defaultLayoutType and calls setLayoutType(defaultLayoutType) to keep the
internal state in sync (also apply the same pattern to the other instance
initialized around the same area, e.g., the related state created near lines
202-204).

In `@src/renderer/components/DocumentGraph/mindMapLayouts.ts`:
- Around line 1-12: The file fails Prettier formatting; run the formatter and
save the fixed file so the comment block and surrounding code conform to project
formatting rules. Specifically, run `npx prettier --write` (or your editor's
Prettier integration) on src/renderer/components/DocumentGraph/mindMapLayouts.ts
to reformat the header and any related exports like calculateLayout and the
LayoutResult types, then stage the changes and push the updated file.

In `@src/renderer/components/FilePreview.tsx`:
- Around line 2431-2441: Prettier formatting failed for the FilePreview
component; run the formatter (e.g., npx prettier --write) on the file containing
the FilePreview component and reformat the JSX around the Load full file button
and the setShowFullContent handler to match project Prettier rules so CI passes;
then stage the formatted file and push the commit.

In `@src/renderer/components/SymphonyModal.tsx`:
- Around line 2048-2052: Prettier formatting has drifted in SymphonyModal.tsx
causing CI to fail; run the formatter and commit the changes by running `npx
prettier --write src/renderer/components/SymphonyModal.tsx` (or your project's
formatting command) to reformat the file, then stage and push the updated file
so the span containing filteredRepositories, isLoadingIssueCounts and Loader2 is
properly formatted to satisfy Prettier.

In `@src/renderer/hooks/batch/useBatchProcessor.ts`:
- Around line 796-802: The toast call to notifyToast in useBatchProcessor.ts
uses the wrong metadata key: it passes sessionId instead of the expected
agentSessionId, so session-linked logging is lost; update the notifyToast
invocation that constructs the auto-run toast (the block calling notifyToast
with title 'Auto Run Started') to replace the sessionId property with
agentSessionId: pass agentSessionId: sessionId (or rename the variable if
appropriate) so the toast metadata matches notifyToast's contract and restores
session attribution.

---

Outside diff comments:
In `@src/renderer/App.tsx`:
- Around line 1-3244: Prettier formatting in src/renderer/App.tsx is failing CI;
run the formatter and commit the changes. Run npx prettier --write
src/renderer/App.tsx (or your repo's prettier command), verify the file
(MaestroConsoleInner / MaestroConsole) is reformatted, review any changes, then
stage and commit the updated file so the CI Prettier check passes.

In `@src/renderer/components/DocumentGraph/DocumentGraphView.tsx`:
- Around line 1-2142: Prettier formatting is failing for DocumentGraphView.tsx;
run the formatter and commit the updated file to fix CI. Specifically, run `npx
prettier --write src/renderer/components/DocumentGraph/DocumentGraphView.tsx`,
review the changes in the DocumentGraphView component (and helper
buildFileTreeFromPaths) to ensure no logic was altered, then stage and commit
the formatted file so the pipeline passes.

In `@src/renderer/components/GroupChatHeader.tsx`:
- Around line 48-55: The clickable heading in GroupChatHeader (the h1 that
renders "Group Chat: {name}" and uses onRename) lacks keyboard support; add
tabIndex={0} so it is focusable and implement an onKeyDown handler that calls
onRename when Enter or Space is pressed (checking event.key === 'Enter' ||
event.key === ' ' / 'Spacebar' as needed) and optionally preventDefault for
Space to avoid page scrolling; consider adding role="button" and an accessible
title/aria-label if not already present.

In `@src/renderer/hooks/batch/useAutoRunHandlers.ts`:
- Around line 114-127: The marker set by
markWorktreePathAsRecentlyCreated(worktreePath) isn't cleared if
window.maestro.git.worktreeSetup(...) throws; wrap the worktreeSetup call in a
try/catch (or try/finally) so that
clearRecentlyCreatedWorktreePath(worktreePath) is always invoked on error
(and/or in finally) before rethrowing or returning an error result, ensuring the
dedup marker is removed even when worktreeSetup throws.

In `@src/renderer/hooks/batch/useBatchProcessor.ts`:
- Around line 1-2014: Prettier/formatting fails due to spaces used for
indentation; reformat this file to use tabs per project rules. Run the project's
formatter (prettier or npm/yarn format script) on the file and ensure tabs are
used (not spaces) across top-level functions/hooks like useBatchProcessor,
startBatchRun, updateBatchStateAndBroadcast, pauseBatchOnError, and other
exported functions; fix any lingering lint/Prettier issues reported and re-run
CI to verify the Prettier check passes before merging.

In `@src/renderer/hooks/batch/useWorktreeManager.ts`:
- Around line 358-365: The code computes baseBranch from worktree.prTargetBranch
(falling back to window.maestro.git.getDefaultBranch(mainRepoCwd) or 'main') but
only returns/attaches targetBranch for handled outcomes; when an exception is
thrown the branch context is lost—update the catch/throw paths in
useWorktreeManager (the blocks around baseBranch / worktree.prTargetBranch and
the similar section at lines ~407-412) to include the resolved
baseBranch/targetBranch in the thrown error or the error payload so callers
always receive targetBranch information.

---

Nitpick comments:
In `@src/__tests__/renderer/components/GroupChatHeader.test.tsx`:
- Around line 43-52: Add test isolation by resetting and reinitializing the
module-level mocks before each test: add a beforeEach that calls
vi.clearAllMocks() (or vi.resetAllMocks()) and reassigns defaultProps with fresh
handler mocks (onRename, onShowInfo, onToggleRightPanel) so call counts don’t
accumulate between tests; reference the existing defaultProps object and the
mock handler names to locate where to recreate the vi.fn() instances.

In `@src/__tests__/renderer/utils/fileExplorer.test.ts`:
- Around line 460-495: The new dedup tests spy on console.warn but don't assert
it was called; update both tests for duplicated entries ("deduplicates entries
returned by readDir" and "deduplicates entries in nested directories") to assert
the warning was emitted: after calling loadFileTree('/project') assert that the
consoleSpy (the vi.spyOn(console, 'warn') mock) was called (e.g.,
expect(consoleSpy).toHaveBeenCalled() or toHaveBeenCalledTimes(1)) and then
restore the spy as already done, keeping references to loadFileTree and
consoleSpy so the assertions target the exact spy used in each test.

In `@src/renderer/components/DocumentGraph/mindMapLayouts.ts`:
- Around line 108-135: The module-level nodeHeightCache Map can grow unbounded;
modify it to enforce a bounded size (e.g., MAX_CACHE_ENTRIES) or swap it for an
LRU cache implementation and evict the oldest entries when capacity is exceeded.
Update the nodeHeightCache initialization (replace Map with an LRU structure or
a wrapped Map) and change the calculateNodeHeight flow that calls
nodeHeightCache.set(cacheKey, height) to perform eviction when size >
MAX_CACHE_ENTRIES (or use the LRU's set method). You can implement a simple LRU
using insertion order on Map or import a small lru-cache; keep nodeHeightCache
and calculateNodeHeight names so callers remain unchanged.
- Around line 47-58: The LayoutFunction type is currently internal; export it so
external modules can implement custom layouts with type safety. Modify the
declaration of LayoutFunction in mindMapLayouts.ts to be exported (export type
LayoutFunction = ...) and ensure any related symbols used by its signature —
MindMapNode, MindMapLink, LayoutResult — are exported or at least accessible
where consumers will implement layouts; update exports if necessary so consumers
can import LayoutFunction and its dependent types.

In `@src/renderer/components/FilePreview.tsx`:
- Line 2438: The onClick should guard large-file loads and surface progress:
update the FilePreview component's onClick handler (where setShowFullContent is
called) to an async handler that (1) checks the file size (use the existing
file.size or contentLength) and for sizes > 500*1024 shows a user
confirmation/warning before proceeding, (2) sets a local loading state (e.g.,
isLoadingFullContent) to true before triggering the full-content render
(setShowFullContent(true)), and (3) clear the loading state after the render
completes; optionally use requestIdleCallback or setTimeout to defer heavy
syntax-highlighting work so the spinner can display. Ensure you add and use
isLoadingFullContent state and render a spinner in FilePreview when true, and
reuse the existing setShowFullContent symbol so behavior remains consistent.

In `@src/renderer/hooks/symphony/useSymphony.ts`:
- Around line 198-225: The fetchIssueCounts flow can leave stale issueCounts
displayed when the fetch fails or when the repo list becomes empty; update the
fetchIssueCounts function and its related effect to clear state: inside
fetchIssueCounts ensure that when repos.length === 0 you call setIssueCounts({})
(or an initial empty value) before returning, and in the catch block call
setIssueCounts({}) as well as setIsLoadingIssueCounts(false); additionally, in
the useEffect that watches repositories, if repositories.length === 0 call
setIssueCounts({}) to proactively clear counts when the repo list is emptied
(refer to fetchIssueCounts, setIssueCounts, setIsLoadingIssueCounts, and the
repositories effect).

In `@src/renderer/stores/settingsStore.ts`:
- Around line 714-719: The allowed layout literals are duplicated; extract the
array ['mindmap','radial','force'] into a single constant (e.g.,
ALLOWED_DOCUMENT_GRAPH_LAYOUTS: DocumentGraphLayoutType[]) defined near the top
of the module and use that constant in setDocumentGraphLayoutType (replace the
local valid variable) and the corresponding loader code (the other occurrence
around lines 1622-1627) so both places use
ALLOWED_DOCUMENT_GRAPH_LAYOUTS.includes(value) and default to 'mindmap' if
invalid.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5e0e783 and ad9d63a.

📒 Files selected for processing (52)
  • src/__tests__/main/ipc/handlers/symphony.test.ts
  • src/__tests__/renderer/components/DocumentGraph/DocumentGraphView.test.tsx
  • src/__tests__/renderer/components/DocumentGraph/mindMapLayouts.test.ts
  • src/__tests__/renderer/components/FilePreview.test.tsx
  • src/__tests__/renderer/components/GroupChatHeader.test.tsx
  • src/__tests__/renderer/components/RightPanel.test.tsx
  • src/__tests__/renderer/components/SymphonyModal.test.tsx
  • src/__tests__/renderer/components/TerminalOutput.test.tsx
  • src/__tests__/renderer/components/auto-scroll.test.tsx
  • src/__tests__/renderer/hooks/batch/useAutoRunHandlers.worktree.test.ts
  • src/__tests__/renderer/hooks/useBatchProcessor.test.ts
  • src/__tests__/renderer/hooks/useCycleSession.test.ts
  • src/__tests__/renderer/hooks/useSettings.test.ts
  • src/__tests__/renderer/stores/settingsStore.test.ts
  • src/__tests__/renderer/utils/fileExplorer.test.ts
  • src/__tests__/renderer/utils/platformUtils.test.ts
  • src/__tests__/renderer/utils/worktreeDedup.test.ts
  • src/__tests__/shared/symphony-constants.test.ts
  • src/__tests__/shared/symphony-types.test.ts
  • src/main/agents/path-prober.ts
  • src/main/ipc/handlers/filesystem.ts
  • src/main/ipc/handlers/symphony.ts
  • src/main/preload/fs.ts
  • src/main/preload/symphony.ts
  • src/renderer/App.tsx
  • src/renderer/components/DocumentGraph/DocumentGraphView.tsx
  • src/renderer/components/DocumentGraph/MindMap.tsx
  • src/renderer/components/DocumentGraph/mindMapLayouts.ts
  • src/renderer/components/FileExplorerPanel.tsx
  • src/renderer/components/FilePreview.tsx
  • src/renderer/components/GroupChatHeader.tsx
  • src/renderer/components/GroupChatPanel.tsx
  • src/renderer/components/RightPanel.tsx
  • src/renderer/components/SessionList.tsx
  • src/renderer/components/SettingsModal.tsx
  • src/renderer/components/SymphonyModal.tsx
  • src/renderer/components/TerminalOutput.tsx
  • src/renderer/global.d.ts
  • src/renderer/hooks/batch/useAutoRunHandlers.ts
  • src/renderer/hooks/batch/useBatchProcessor.ts
  • src/renderer/hooks/batch/useWorktreeManager.ts
  • src/renderer/hooks/session/useCycleSession.ts
  • src/renderer/hooks/settings/useSettings.ts
  • src/renderer/hooks/symphony/useSymphony.ts
  • src/renderer/hooks/worktree/useWorktreeHandlers.ts
  • src/renderer/stores/settingsStore.ts
  • src/renderer/types/index.ts
  • src/renderer/utils/fileExplorer.ts
  • src/renderer/utils/platformUtils.ts
  • src/renderer/utils/worktreeDedup.ts
  • src/shared/symphony-constants.ts
  • src/shared/symphony-types.ts
💤 Files with no reviewable changes (2)
  • src/renderer/components/GroupChatPanel.tsx
  • src/main/agents/path-prober.ts

Comment on lines 796 to 802
notifyToast({
type: 'info',
title: 'Auto Run Started',
message: `${initialTotalTasks} ${initialTotalTasks === 1 ? 'task' : 'tasks'} across ${documents.length} ${documents.length === 1 ? 'document' : 'documents'}`,
project: session.name,
sessionId,
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Toast metadata key mismatch prevents session attribution.

notifyToast consumes agentSessionId metadata, but this call passes sessionId. The toast still renders, but session-linked logging/notification context is lost.

🔧 Proposed fix
 			notifyToast({
 				type: 'info',
 				title: 'Auto Run Started',
 				message: `${initialTotalTasks} ${initialTotalTasks === 1 ? 'task' : 'tasks'} across ${documents.length} ${documents.length === 1 ? 'document' : 'documents'}`,
 				project: session.name,
-				sessionId,
+				agentSessionId: sessionId,
 			});
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
notifyToast({
type: 'info',
title: 'Auto Run Started',
message: `${initialTotalTasks} ${initialTotalTasks === 1 ? 'task' : 'tasks'} across ${documents.length} ${documents.length === 1 ? 'document' : 'documents'}`,
project: session.name,
sessionId,
});
notifyToast({
type: 'info',
title: 'Auto Run Started',
message: `${initialTotalTasks} ${initialTotalTasks === 1 ? 'task' : 'tasks'} across ${documents.length} ${documents.length === 1 ? 'document' : 'documents'}`,
project: session.name,
agentSessionId: sessionId,
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/hooks/batch/useBatchProcessor.ts` around lines 796 - 802, The
toast call to notifyToast in useBatchProcessor.ts uses the wrong metadata key:
it passes sessionId instead of the expected agentSessionId, so session-linked
logging is lost; update the notifyToast invocation that constructs the auto-run
toast (the block calling notifyToast with title 'Auto Run Started') to replace
the sessionId property with agentSessionId: pass agentSessionId: sessionId (or
rename the variable if appropriate) so the toast metadata matches notifyToast's
contract and restores session attribution.

@greptile-apps
Copy link

greptile-apps bot commented Mar 1, 2026

Greptile Summary

This is a large polish/RC release (52 files) that bundles several features, fixes, and refactors across the Maestro codebase. The most significant changes are:

  • Worktree auto-run dedup fix (useBatchProcessor, useAutoRunHandlers, worktreeDedup): A race condition where the file watcher created duplicate sessions for auto-run worktrees is fixed by introducing a shared TTL-based dedup module (worktreeDedup.ts) and skipping the setupWorktree call when worktreeTarget is already resolved upstream.
  • Document Graph layout algorithms (mindMapLayouts.ts, MindMap.tsx, DocumentGraphView.tsx): The layout code is extracted from MindMap.tsx into a standalone mindMapLayouts.ts module with three interchangeable algorithms (Mind Map, Radial, Force-Directed via d3-force). The layout choice is persisted both per-session and globally.
  • Symphony issue counts (symphony.ts, useSymphony.ts, SymphonyModal.tsx): A new getIssueCounts IPC handler fetches open issue counts for all registered repos in a single GitHub Search API call, with 30-minute caching. Tiles now display live counts and repos with no issues are dimmed.
  • UX polish: "Load full file" button in truncated file previews, "View history" link in the auto-run footer, group chat close button removed, archived group chats excluded from keyboard cycling, and platform-aware "Open in Finder/Explorer" in Settings.
  • Code quality: ESLint cleanup via _-prefixed unused variables, ENOENT now returns null from fs:readFile, and file explorer deduplication at both the loader and flatten layers.

One logic bug found: In fetchIssueCounts (src/main/ipc/handlers/symphony.ts), the slug extracted from repository_url uses GitHub's canonical casing while the counts map is keyed by the registry's slug casing. A case-mismatch will silently leave all counts at 0. A case-insensitive comparison is needed when looking up slug in counts.

Confidence Score: 4/5

  • Safe to merge with one logic fix — the slug case-sensitivity bug in fetchIssueCounts will cause issue counts to silently display as 0 for all repositories.
  • The overwhelming majority of changes are well-tested refactors, polish, and targeted bug fixes with corresponding unit tests. The one actionable bug (slug case-sensitivity in the GitHub Search API response handler) affects only the new issue counts feature, not any existing functionality, and has no data-loss or security implications. All other critical paths (worktree dedup, batch processor fix, layout algorithms) look correct and are covered by tests.
  • src/main/ipc/handlers/symphony.ts — the new fetchIssueCounts function has a case-sensitive slug matching bug that will cause all issue counts to remain 0.

Important Files Changed

Filename Overview
src/main/ipc/handlers/symphony.ts New symphony:getIssueCounts IPC handler that batches issue count fetches via the GitHub Search API. Contains a case-sensitive slug comparison bug (see comment) and no pagination for >100 results.
src/renderer/components/DocumentGraph/mindMapLayouts.ts New file extracting layout algorithms from MindMap.tsx into three interchangeable implementations (Mind Map, Radial, Force-Directed) with a shared dispatcher. Clean refactor with well-tested constants and shared preamble.
src/renderer/utils/worktreeDedup.ts New module-level shared dedup mechanism using a TTL-based Set to prevent duplicate sessions when file watchers observe a just-created worktree. Well-tested with comprehensive edge cases including timer reset on re-mark and path normalization.
src/renderer/hooks/batch/useBatchProcessor.ts Critical fix to skip duplicate setupWorktree call when config.worktreeTarget is already set (worktree was created by useAutoRunHandlers). Also adds "Auto Run Started" toast and PR result history entries. Uses a direct Zustand store read to work around stale-closure timing.
src/renderer/hooks/batch/useAutoRunHandlers.ts Integrates worktreeDedup — marks path before disk creation and clears on failure, preventing the file watcher from creating a duplicate session for auto-run worktrees.
src/renderer/hooks/symphony/useSymphony.ts Adds fetchIssueCounts effect that fires once repositories are loaded, fetching all repo issue counts in a single Search API call. Exposes issueCounts and isLoadingIssueCounts state cleanly.
src/renderer/components/SymphonyModal.tsx Issue counts now displayed on repository tiles with loading indicator and dimmed appearance for zero-issue repos. Blocked issues moved to footer so they remain selectable for document exploration.
src/renderer/components/FilePreview.tsx Replaces "Use an external editor" truncation message with a "Load full file" button. State resets on file path change. Clean implementation with proper memoization guard.
src/renderer/stores/settingsStore.ts Adds documentGraphLayoutType setting with validation (rejects invalid values, falls back to 'mindmap'). Exports DocumentGraphLayoutType type alias for sharing across modules.
src/renderer/components/SessionList.tsx Wraps setActiveSessionId to also clear activeGroupChatId, preventing stale group chat state when switching to a regular session.

Sequence Diagram

sequenceDiagram
    participant UI as SymphonyModal
    participant Hook as useSymphony
    participant IPC as symphony:getIssueCounts
    participant GH as GitHub Search API
    participant Cache as SymphonyCache

    UI->>Hook: repositories loaded
    Hook->>IPC: getIssueCounts(repoSlugs)
    IPC->>Cache: readCache(app)
    alt Cache valid (< 30 min)
        Cache-->>IPC: issueCounts.data
        IPC-->>Hook: { counts, fromCache: true }
    else Cache stale or missing
        IPC->>GH: GET /search/issues?q=label:runmaestro.ai+state:open+repo:A+repo:B&per_page=100
        GH-->>IPC: { total_count, items[repository_url] }
        IPC->>IPC: count items per slug (case-sensitive ⚠️)
        IPC->>Cache: writeCache(newCache)
        IPC-->>Hook: { counts, fromCache: false }
    end
    Hook-->>UI: issueCounts, isLoadingIssueCounts

    note over UI: RepositoryTile shows count badge
    note over UI: opacity 0.45 when count === 0
Loading

Last reviewed commit: ad9d63a

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

52 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

@greptile-apps
Copy link

greptile-apps bot commented Mar 1, 2026

Additional Comments (3)

src/main/ipc/handlers/symphony.ts
Case-sensitive slug comparison silently drops all counts

The repository_url returned by the GitHub Search API uses the repository's canonical casing (e.g., "RunMaestro/Maestro"), while repoSlugs may store slugs in a different case (e.g., "runmaestro/maestro"). Since JavaScript in operator is case-sensitive, slug in counts will evaluate to false for every item, leaving all counts at 0 regardless of actual issue counts.

// Normalize casing when matching slugs
const slugLower = slug.toLowerCase();
const matchingSlug = repoSlugs.find((s) => s.toLowerCase() === slugLower);
if (matchingSlug !== undefined) {
    counts[matchingSlug]++;
}

Alternatively, normalize repoSlugs to lowercase when building counts and normalize slug before the lookup.


src/main/ipc/handlers/symphony.ts
Issue count will be inaccurate beyond 100 total results

The GitHub Search API returns at most per_page=100 results per page and this handler does not paginate. If there are more than 100 open issues matching the query across all repos, only the first 100 will be counted, causing the per-repo counts to be artificially low. While this may be rare today, adding a note or pagination guard would prevent silent undercounting as the registry grows.

Consider paginating (following Link: <url>; rel="next" headers) or adding a warning log when data.total_count > data.items.length.


src/renderer/hooks/batch/useBatchProcessor.ts
Direct Zustand store access inside render hook

useSessionStore.getState().sessions.find(...) is called directly inside the startBatchRun callback to work around a stale-closure timing issue. This is acceptable for one-off reads inside an async callback (not during render), but it's worth documenting why sessionsRef.current is insufficient here — the inline comment is good, but a TODO to revisit whether the ref update latency can be fixed at the source (e.g., by flushing the ref synchronously after session creation) would help future maintainers avoid reaching for this pattern again.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

pedramamini added a commit that referenced this pull request Mar 1, 2026
… a11y, formatting

- readFile ENOENT null return: update global.d.ts type to string | null,
  add null guards across 12 call sites (MarkdownRenderer, FilePreview,
  AutoRun, DocumentEditor, DocumentGenerationView, ImageDiffViewer,
  DocumentGraphView, DebugWizardModal, useAutoRunImageHandling,
  useFileExplorerEffects, useTabHandlers, useAppHandlers)
- Symphony issue counts: add pagination (up to 1,000 results), validate
  cache against requested repoSlugs, clear stale counts on failure/empty
- Worktree fixes: clear dedup marker on worktreeSetup throw, propagate
  targetBranch in PR creation catch path
- GroupChatHeader: add keyboard a11y (tabIndex, role, onKeyDown)
- Settings: extract DOCUMENT_GRAPH_LAYOUT_TYPES constant, dedup validation
- DocumentGraphView: sync layoutType state with defaultLayoutType prop
- Tests: fix vi.hoisted() TDZ, proper Shortcut mock shape, clearAllMocks,
  assert console.warn in dedup tests, update symphony cache type test
- Prettier: format all CI-flagged files
pedramamini and others added 18 commits March 1, 2026 00:39
Remove 7 console.log calls from useSessionDebounce that fired at high
frequency during AI streaming. Replace index-based keys with stable
identifiers in 6 components where items can be removed or filtered
(staged images, diff tabs, log entries, quit modal agents).
- QuitConfirmModal: use composite key `${name}-${index}` to prevent
  duplicate React keys when agents share display names
- extensionColors: early-return accent fallback in colorblind mode so
  we never serve non-colorblind-safe colors from EXTENSION_MAP
…y from persistence

Users with large working directories (100K+ files) caused sessions.json to
balloon to 300MB+. Root cause: the full file tree (FileTreeNode[]) was persisted
for every session with no size limits.

Three changes:
1. Strip fileTree, fileTreeStats, filePreviewHistory from persistence — these
   are ephemeral cache data that re-scan automatically on session activation
2. Add configurable local ignore patterns setting (default: .git, node_modules,
   __pycache__) with UI in Settings > Display, reusing a new generic
   IgnorePatternsSection component extracted from SshRemoteIgnoreSection
3. Wire local ignore patterns through loadFileTree for local (non-SSH) scans
Add localHonorGitignore setting (default: true) that parses .gitignore
files and merges their patterns with the user's local ignore patterns
when building file trees. Mirrors the existing SSH remote gitignore
support for local filesystem scans.
…s object, and rescan effect

- Extract parseGitignoreContent() shared between local/remote gitignore handling
- Replace 7 positional params in loadFileTree with LocalFileTreeOptions object
- Export DEFAULT_LOCAL_IGNORE_PATTERNS from settingsStore, use in SettingsModal
- Fix deprecated onKeyPress -> onKeyDown in IgnorePatternsSection
- Add role="checkbox" and aria-checked to honor-gitignore toggle
- Add useEffect to auto-rescan file tree when local ignore patterns change
- Update tests for new loadFileTree signature
React.memo wraps for 29 components (7 core + 22 UsageDashboard charts)
to prevent unnecessary re-renders on parent state changes.

Add compound DB indexes (start_time, agent_type/project_path/source)
for common query patterns in stats aggregation.

Parallelize sequential git rev-parse calls in worktreeInfo and
worktreeSetup handlers with Promise.all (~4x fewer round-trips).

Cache Date.getTime() results in useFilteredAndSortedSessions sort
comparator to avoid repeated date parsing during sort operations.

Replace inline debounce in useStats with shared useDebouncedCallback
hook, adding proper cleanup on unmount.
Blocked issues were completely non-interactive. Now they can be clicked
and keyboard-navigated to explore their documents, while the Start
Symphony button remains hidden to prevent starting blocked work.
Group @mentions: typing @ in group chat now shows agent groups in the
dropdown (visually differentiated with icon and member count badge).
Selecting a group expands to individual @mentions for all member agents.

Navigation history: group chats now participate in back/forward breadcrumb
navigation. Fixed pre-existing dual-instance bug where useNavigationHistory
was instantiated independently in App.tsx and useSessionLifecycle.ts,
creating separate stacks that couldn't communicate.
Replace the read-only provider display with a dropdown selector.
When provider changes, tabs reset to a fresh session and provider-
specific config (path, args, env, model) is cleared. History data
persists since it's keyed by Maestro session ID.
* feat: add archive state to group-chats

* fix: deduplicate GroupChat return type in global.d.ts, make archive callback optional

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Bug fixes: use tasksTotal for AutoRunStats count, add division-by-zero
guard in WeekdayComparisonChart, fix useStats effect cleanup, add v4
stats migration for compound indexes.

Sentry compliance: replace console.error with captureException in
QRCode, AutoRunStats, LongestAutoRunsTable, TasksByHourChart.

Accessibility: add keyboard handlers to AutoRunStats bar items, add
target guard and focus-visible ring to SymphonyModal.

Performance: memo MetricCard, useMemo for PeakHoursChart hasData,
useId for DurationTrendsChart gradient, stable opacities in
ChartSkeletons, stable onRemove ref in Toast, consistent variable
naming in git handler.
Removed inline blocked info box from the detail header. The footer now
shows for blocked issues with a disabled Start Symphony button and a
lock-icon message explaining the dependency block, consistent with the
normal footer layout.
Replace ad-hoc hardcoded pixel buffers across 6 context menu
implementations with a shared useContextMenuPosition hook that
measures actual rendered menu size via getBoundingClientRect in
useLayoutEffect, clamping position to keep menus fully visible.

Two menus (SessionActivityGraph, GroupChatHistoryPanel) previously
had no viewport detection at all.
File preview rendering was independent of inputMode, so switching to
terminal mode (via authenticateAfterError or remote mode switch) without
clearing activeFileTabId left the file preview visible with no tab bar.
The corrupted state persisted across app restarts.

Guard file preview with inputMode='ai', clear activeFileTabId on all
terminal-mode transitions, and auto-heal on session restoration.
- Clear activeFileTabId in onRemoteCommand mode-sync path (same bug
  as onRemoteSwitchMode, caught by CodeRabbit)
- Fix "preserves activeFileTabId" test to use non-null starting value
  so it actually tests preservation behavior
- Add test for onRemoteCommand clearing activeFileTabId on terminal sync
…rector's Notes beta badge

- Add z-20 to header container to prevent overlap issues
- Dim agent status dots for inactive sessions (0.45 opacity)
- Add beta badge to Director's Notes in Encore Features settings
Cache no longer keys on lookbackDays — once generated, notes persist
until explicit Regenerate click. During regeneration, old notes stay
visible and scrollable with stats bar, timestamp, and Save/Copy buttons
remaining enabled. Progress bar only shows on first-ever generation.
Renamed "Refresh" to "Regenerate" for clearer intent.
Add ~37 missing props to useMainPanelProps interface, useMemo return,
and dependency array. Remove state props from useRightPanelProps call
in App.tsx that the refactored hook no longer accepts (RightPanel now
reads state directly from Zustand stores).
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (1)
src/renderer/hooks/batch/useBatchProcessor.ts (1)

792-799: ⚠️ Potential issue | 🟡 Minor

Use agentSessionId in toast metadata (still unresolved).

At Line 798, notifyToast is still passed sessionId instead of agentSessionId, which can break session-linked attribution for notifications. This appears to be the same issue previously reported; please also update the related assertion in src/__tests__/renderer/hooks/useBatchProcessor.test.ts (toast expectation block around Lines 5799-5804).

🔧 Proposed fix
 			notifyToast({
 				type: 'info',
 				title: 'Auto Run Started',
 				message: `${initialTotalTasks} ${initialTotalTasks === 1 ? 'task' : 'tasks'} across ${documents.length} ${documents.length === 1 ? 'document' : 'documents'}`,
 				project: session.name,
-				sessionId,
+				agentSessionId: sessionId,
 			});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/hooks/batch/useBatchProcessor.ts` around lines 792 - 799, The
toast metadata is using sessionId instead of agentSessionId; update the
notifyToast call in useBatchProcessor (the call to notifyToast that sets
project/sessionId) to pass agentSessionId as the sessionId field, and then
update the corresponding assertion in useBatchProcessor.test.ts (the toast
expectation block) to assert agentSessionId is present in the toast metadata
instead of sessionId.
🧹 Nitpick comments (1)
src/renderer/components/DocumentGraph/DocumentGraphView.tsx (1)

1357-1414: Consider adding aria-expanded and keyboard navigation for accessibility.

The layout dropdown button lacks aria-expanded and aria-haspopup attributes, and the dropdown items don't support arrow-key navigation. While functional, these additions would improve keyboard accessibility.

♿ Suggested accessibility improvements
 <button
 	onClick={() => setShowLayoutDropdown(!showLayoutDropdown)}
 	className="flex items-center gap-1.5 px-3 py-1.5 rounded text-sm transition-colors"
+	aria-expanded={showLayoutDropdown}
+	aria-haspopup="listbox"
 	style={{
 		backgroundColor: `${theme.colors.accent}10`,
 		color: theme.colors.textDim,
 	}}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/DocumentGraph/DocumentGraphView.tsx` around lines
1357 - 1414, The layout dropdown lacks accessibility attributes and keyboard
support: update the toggle button (where setShowLayoutDropdown and
showLayoutDropdown are used) to include aria-haspopup="menu" and aria-expanded
tied to showLayoutDropdown, and add an onKeyDown handler on the button to
open/close on Enter/Space and move focus into the list; mark the dropdown
container with role="menu" and each mapped button (the items that call
handleLayoutTypeChange and read layoutType/LAYOUT_LABELS) with role="menuitem"
and make them keyboard-focusable (manage tabIndex or programmatic focus), then
implement arrow-up/arrow-down handling to move focus between items and
Enter/Space to select so keyboard-only users can navigate the layout selector.
🤖 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/symphony.ts`:
- Around line 1279-1286: The fallback currently returns cache.issueCounts.data
without ensuring it corresponds to the requested repo set; update the
catch/fallback to compare the requested repoSlugs (the input parameter or local
variable representing the requested repos) with cache.issueCounts.repoSlugs
(and/or a normalized sorted representation) before returning counts, and only
return { counts: cache.issueCounts.data, fromCache: true, cacheAge } when they
match; if they don’t match, avoid returning the cached counts (e.g., throw the
error or return an explicit miss) so callers don’t receive counts for the wrong
repo set.
- Around line 525-536: The query currently builds repoQualifiers by joining
repoSlugs with '+' which creates an implicit AND; update repoQualifiers to build
an OR-grouped expression like `(repo:owner1/repo1 OR repo:owner2/repo2 ...)`
(use repoSlugs to create each `repo:${slug}` and join with ' OR '), then build
the full query string using SYMPHONY_ISSUE_LABEL and state:open, wrap the entire
query in parentheses where appropriate, and pass the entire assembled query
through encodeURIComponent when constructing `query`/the URL so the Search API
sees the grouped OR expression correctly; adjust the code around the
`repoQualifiers`, `query`, and URL construction to use this encoded grouped OR
form.

In `@src/renderer/components/ImageDiffViewer.tsx`:
- Around line 58-60: The current truthy guard around setNewImage after calling
window.maestro.fs.readFile can leave a stale "after" image when read returns
null; update the logic in the try/catch surrounding
window.maestro.fs.readFile(fullPath, sshRemoteId) so that when content is falsy
(null/undefined) you explicitly clear the "after" image state (call
setNewImage(null) or equivalent) and also ensure the catch block likewise clears
setNewImage and/or sets an error state; update references to setNewImage and the
read call in ImageDiffViewer.tsx to implement this behavior so a failed or empty
read never leaves the previous image displayed.

---

Duplicate comments:
In `@src/renderer/hooks/batch/useBatchProcessor.ts`:
- Around line 792-799: The toast metadata is using sessionId instead of
agentSessionId; update the notifyToast call in useBatchProcessor (the call to
notifyToast that sets project/sessionId) to pass agentSessionId as the sessionId
field, and then update the corresponding assertion in useBatchProcessor.test.ts
(the toast expectation block) to assert agentSessionId is present in the toast
metadata instead of sessionId.

---

Nitpick comments:
In `@src/renderer/components/DocumentGraph/DocumentGraphView.tsx`:
- Around line 1357-1414: The layout dropdown lacks accessibility attributes and
keyboard support: update the toggle button (where setShowLayoutDropdown and
showLayoutDropdown are used) to include aria-haspopup="menu" and aria-expanded
tied to showLayoutDropdown, and add an onKeyDown handler on the button to
open/close on Enter/Space and move focus into the list; mark the dropdown
container with role="menu" and each mapped button (the items that call
handleLayoutTypeChange and read layoutType/LAYOUT_LABELS) with role="menuitem"
and make them keyboard-focusable (manage tabIndex or programmatic focus), then
implement arrow-up/arrow-down handling to move focus between items and
Enter/Space to select so keyboard-only users can navigate the layout selector.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ad9d63a and 435aba8.

📒 Files selected for processing (31)
  • src/__tests__/renderer/components/DocumentGraph/mindMapLayouts.test.ts
  • src/__tests__/renderer/components/GroupChatHeader.test.tsx
  • src/__tests__/renderer/components/TerminalOutput.test.tsx
  • src/__tests__/renderer/components/auto-scroll.test.tsx
  • src/__tests__/renderer/hooks/useBatchProcessor.test.ts
  • src/__tests__/renderer/utils/fileExplorer.test.ts
  • src/__tests__/shared/symphony-types.test.ts
  • src/main/ipc/handlers/symphony.ts
  • src/renderer/App.tsx
  • src/renderer/components/AutoRun.tsx
  • src/renderer/components/DebugWizardModal.tsx
  • src/renderer/components/DocumentGraph/DocumentGraphView.tsx
  • src/renderer/components/DocumentGraph/mindMapLayouts.ts
  • src/renderer/components/FilePreview.tsx
  • src/renderer/components/GroupChatHeader.tsx
  • src/renderer/components/ImageDiffViewer.tsx
  • src/renderer/components/InlineWizard/DocumentGenerationView.tsx
  • src/renderer/components/MarkdownRenderer.tsx
  • src/renderer/components/SymphonyModal.tsx
  • src/renderer/components/Wizard/shared/DocumentEditor.tsx
  • src/renderer/global.d.ts
  • src/renderer/hooks/batch/useAutoRunHandlers.ts
  • src/renderer/hooks/batch/useAutoRunImageHandling.ts
  • src/renderer/hooks/batch/useBatchProcessor.ts
  • src/renderer/hooks/batch/useWorktreeManager.ts
  • src/renderer/hooks/git/useFileExplorerEffects.ts
  • src/renderer/hooks/symphony/useSymphony.ts
  • src/renderer/hooks/tabs/useTabHandlers.ts
  • src/renderer/hooks/ui/useAppHandlers.ts
  • src/renderer/stores/settingsStore.ts
  • src/shared/symphony-types.ts
🚧 Files skipped from review as they are similar to previous changes (7)
  • src/tests/renderer/components/auto-scroll.test.tsx
  • src/tests/shared/symphony-types.test.ts
  • src/tests/renderer/utils/fileExplorer.test.ts
  • src/tests/renderer/components/GroupChatHeader.test.tsx
  • src/renderer/hooks/batch/useWorktreeManager.ts
  • src/renderer/hooks/symphony/useSymphony.ts
  • src/renderer/hooks/batch/useAutoRunHandlers.ts

Comment on lines 525 to 536
const repoQualifiers = repoSlugs.map((s) => `repo:${s}`).join('+');
const query = `label:${encodeURIComponent(SYMPHONY_ISSUE_LABEL)}+state:open+${repoQualifiers}`;
// Initialize all slugs to 0, then count from paginated results
const counts: Record<string, number> = {};
for (const slug of repoSlugs) {
counts[slug] = 0;
}

let page = 1;
while (true) {
const url = `${GITHUB_API_BASE}/search/issues?q=${query}&per_page=100&page=${page}`;
const response = await fetch(url, {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

GitHub Search API multiple repositories repo qualifier syntax AND OR

💡 Result:

For the GitHub Search API (e.g., GET /search/code?q=...), the q string uses GitHub’s search syntax:

  • Whitespace = AND for code search (e.g., foo bar means foo AND bar). AND, OR, and NOT are supported, and you can group with parentheses. [1]
  • A single item can’t be in two repos at once, so to search multiple repositories you must combine repo: qualifiers with OR (optionally grouped with parentheses). [1]

Examples:

  • Two repos:
    • q=searchterm (repo:owner1/repo1 OR repo:owner2/repo2) [1]
  • Many repos:
    • q=searchterm (repo:a/r1 OR repo:a/r2 OR repo:b/r3) [1]
  • Alternative (broader) scopes:
    • q=searchterm org:my-org or q=searchterm user:my-user [1]

Sources:
[1] Understanding GitHub Code Search syntax (GitHub Docs) https://archive.vn/2026.01.11-051612/https%3A/docs.github.com/en/search-github/github-code-search/understanding-github-code-search-syntax


Use explicit OR grouping for multi-repository Search API queries.

At Line 525–526, concatenating multiple repo: qualifiers with + creates an implicit AND condition. GitHub's Search API treats whitespace (which + decodes to) as AND, making it impossible for a single issue to satisfy multiple repo: constraints simultaneously. This results in zero matches instead of the intended multi-repository query.

Multiple repositories must be combined with explicit OR and grouped with parentheses. Additionally, the entire query string should be URL-encoded as a single unit.

Suggested fix
-	const repoQualifiers = repoSlugs.map((s) => `repo:${s}`).join('+');
-	const query = `label:${encodeURIComponent(SYMPHONY_ISSUE_LABEL)}+state:open+${repoQualifiers}`;
+	const repoExpression = repoSlugs.map((s) => `repo:${s}`).join(' OR ');
+	const query = encodeURIComponent(
+		`label:${SYMPHONY_ISSUE_LABEL} state:open (${repoExpression})`
+	);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/ipc/handlers/symphony.ts` around lines 525 - 536, The query
currently builds repoQualifiers by joining repoSlugs with '+' which creates an
implicit AND; update repoQualifiers to build an OR-grouped expression like
`(repo:owner1/repo1 OR repo:owner2/repo2 ...)` (use repoSlugs to create each
`repo:${slug}` and join with ' OR '), then build the full query string using
SYMPHONY_ISSUE_LABEL and state:open, wrap the entire query in parentheses where
appropriate, and pass the entire assembled query through encodeURIComponent when
constructing `query`/the URL so the Search API sees the grouped OR expression
correctly; adjust the code around the `repoQualifiers`, `query`, and URL
construction to use this encoded grouped OR form.

Comment on lines 1279 to 1286
// Fallback to expired cache if available
if (cache?.issueCounts?.data) {
const cacheAge = Date.now() - cache.issueCounts.fetchedAt;
return {
counts: cache.issueCounts.data,
fromCache: true,
cacheAge,
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fallback path can return counts for the wrong repo set.

If fresh fetch fails, the catch block returns cache.issueCounts.data without validating that cached repoSlugs matches the current request. That can surface incorrect counts.

💡 Suggested fix
-					// Fallback to expired cache if available
-					if (cache?.issueCounts?.data) {
+					// Fallback to expired cache only if slug set still matches request
+					const fallbackSlugsMatch =
+						cache?.issueCounts?.repoSlugs &&
+						requestedSlugs.length === cache.issueCounts.repoSlugs.length &&
+						requestedSlugs.every((s) => cache.issueCounts!.repoSlugs.includes(s));
+					if (cache?.issueCounts?.data && fallbackSlugsMatch) {
 						const cacheAge = Date.now() - cache.issueCounts.fetchedAt;
 						return {
 							counts: cache.issueCounts.data,
 							fromCache: true,
 							cacheAge,
 						};
 					}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/ipc/handlers/symphony.ts` around lines 1279 - 1286, The fallback
currently returns cache.issueCounts.data without ensuring it corresponds to the
requested repo set; update the catch/fallback to compare the requested repoSlugs
(the input parameter or local variable representing the requested repos) with
cache.issueCounts.repoSlugs (and/or a normalized sorted representation) before
returning counts, and only return { counts: cache.issueCounts.data, fromCache:
true, cacheAge } when they match; if they don’t match, avoid returning the
cached counts (e.g., throw the error or return an explicit miss) so callers
don’t receive counts for the wrong repo set.

Comment on lines 58 to 60
const content = await window.maestro.fs.readFile(fullPath, sshRemoteId);
setNewImage(content);
if (content) setNewImage(content);
} catch (err) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Truthy guard can leave stale “After” image displayed.

At Line 59, skipping setNewImage when content is null keeps the previous image in state. This can show the wrong file after a failed read.

💡 Suggested fix
-					const content = await window.maestro.fs.readFile(fullPath, sshRemoteId);
-					if (content) setNewImage(content);
+					const content = await window.maestro.fs.readFile(fullPath, sshRemoteId);
+					if (content === null) {
+						setNewImage(null);
+					} else {
+						setNewImage(content);
+					}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const content = await window.maestro.fs.readFile(fullPath, sshRemoteId);
setNewImage(content);
if (content) setNewImage(content);
} catch (err) {
const content = await window.maestro.fs.readFile(fullPath, sshRemoteId);
if (content === null) {
setNewImage(null);
} else {
setNewImage(content);
}
} catch (err) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/ImageDiffViewer.tsx` around lines 58 - 60, The
current truthy guard around setNewImage after calling window.maestro.fs.readFile
can leave a stale "after" image when read returns null; update the logic in the
try/catch surrounding window.maestro.fs.readFile(fullPath, sshRemoteId) so that
when content is falsy (null/undefined) you explicitly clear the "after" image
state (call setNewImage(null) or equivalent) and also ensure the catch block
likewise clears setNewImage and/or sets an error state; update references to
setNewImage and the read call in ImageDiffViewer.tsx to implement this behavior
so a failed or empty read never leaves the previous image displayed.

…l, Force-Directed)

Adds a dropdown in the Document Graph header to switch between three
layout algorithms. Layout choice persists per-agent (via Session) with
global default fallback (via settingsStore).

- Extract layout code from MindMap.tsx into mindMapLayouts.ts
- Add Radial layout (concentric rings) and Force-Directed layout (d3-force)
- Add layout type dropdown UI with layer stack Escape handling
- Wire persistence through App.tsx (session + global default)
- Add tests for all three algorithms, settingsStore, and props
…provements

- Fix ESLint errors: empty catch block comment, unused eslint-disable directive,
  unused variable prefixing in App.tsx and DirectorNotesModal
- Hide auto-scroll button when tab has no content (filteredLogs.length > 0 guard)
- Fix worktree auto-run: skip setupWorktree when worktreeTarget already created
  the worktree, preventing "belongs to different repository" false positive
- Add "Auto Run Started" toast notification with task/document counts
- Record PR creation results in agent history panel
- Update auto-scroll and batch processor tests for new behavior
Uses a single GitHub Search API call to batch-fetch open issue counts
for all registered repos (instead of 2 calls per repo). Tiles now show
"View #N Issues" when issues exist, "No Issues" (ghosted at 45% opacity)
when none, and the original "View Issues" while counts are loading.

- New IPC handler: symphony:getIssueCounts (30-min cache TTL)
- New fetchIssueCounts() using /search/issues with multiple repo: qualifiers
- Counts fetched automatically when registry loads
- Updated RepositoryTile, useSymphony hook, preload bridge, global.d.ts
… a11y, formatting

- readFile ENOENT null return: update global.d.ts type to string | null,
  add null guards across 12 call sites (MarkdownRenderer, FilePreview,
  AutoRun, DocumentEditor, DocumentGenerationView, ImageDiffViewer,
  DocumentGraphView, DebugWizardModal, useAutoRunImageHandling,
  useFileExplorerEffects, useTabHandlers, useAppHandlers)
- Symphony issue counts: add pagination (up to 1,000 results), validate
  cache against requested repoSlugs, clear stale counts on failure/empty
- Worktree fixes: clear dedup marker on worktreeSetup throw, propagate
  targetBranch in PR creation catch path
- GroupChatHeader: add keyboard a11y (tabIndex, role, onKeyDown)
- Settings: extract DOCUMENT_GRAPH_LAYOUT_TYPES constant, dedup validation
- DocumentGraphView: sync layoutType state with defaultLayoutType prop
- Tests: fix vi.hoisted() TDZ, proper Shortcut mock shape, clearAllMocks,
  assert console.warn in dedup tests, update symphony cache type test
- Prettier: format all CI-flagged files
@pedramamini pedramamini force-pushed the 0.15.0-RC-polish-round-two branch from 435aba8 to 293180c Compare March 1, 2026 06:53
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

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/batch/useWorktreeManager.ts (1)

356-365: ⚠️ Potential issue | 🟡 Minor

targetBranch can still be undefined on early default-branch lookup failure

At Line 360, if getDefaultBranch() throws, control jumps to the outer catch before Line 361-364 assigns a fallback, so Line 412 returns targetBranch: undefined. That weakens the new “always propagate target branch” behavior.

💡 Proposed fix
-			let baseBranch: string | undefined = worktree.prTargetBranch;
+			let baseBranch: string = worktree.prTargetBranch ?? 'main';
 			try {
 				// Use the user-selected target branch, or fall back to default branch detection
-				if (!baseBranch) {
-					const defaultBranchResult = await window.maestro.git.getDefaultBranch(mainRepoCwd);
-					baseBranch =
-						defaultBranchResult.success && defaultBranchResult.branch
-							? defaultBranchResult.branch
-							: 'main';
+				if (!worktree.prTargetBranch) {
+					try {
+						const defaultBranchResult = await window.maestro.git.getDefaultBranch(mainRepoCwd);
+						if (defaultBranchResult.success && defaultBranchResult.branch) {
+							baseBranch = defaultBranchResult.branch;
+						}
+					} catch (err) {
+						// Keep default 'main' when branch detection fails
+						captureException(err, { extra: { mainRepoCwd, operation: 'git.getDefaultBranch' } });
+					}
 				}

Also applies to: 407-413

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/hooks/batch/useWorktreeManager.ts` around lines 356 - 365, The
code can leave baseBranch/targetBranch undefined if
window.maestro.git.getDefaultBranch throws; wrap the default-branch lookup in
its own try/catch and ensure you assign a fallback (e.g., 'main') on error so
baseBranch is always defined afterwards. Specifically, update the block using
baseBranch and window.maestro.git.getDefaultBranch in useWorktreeManager (the
local variable baseBranch and the awaited getDefaultBranch call) to catch errors
from getDefaultBranch and set baseBranch = 'main' (or the desired default) in
the catch. Apply the same pattern to the other similar lookup (the block around
the other getDefaultBranch usage referenced at lines ~407-413) so targetBranch
cannot be left undefined.
♻️ Duplicate comments (1)
src/main/ipc/handlers/symphony.ts (1)

1279-1286: ⚠️ Potential issue | 🟠 Major

Fallback can still return counts for the wrong slug set.

At Line [1280], the expired-cache fallback does not validate repoSlugs against requestedSlugs, so mismatched counts can still be returned after fetch failures.

Suggested fix
 				} catch (error) {
 					logger.warn('Failed to fetch issue counts from GitHub Search API', LOG_CONTEXT, {
 						error,
 					});
 
-					// Fallback to expired cache if available
-					if (cache?.issueCounts?.data) {
+					// Fallback to expired cache only when slug set matches request
+					const fallbackSlugsMatch =
+						cache?.issueCounts?.repoSlugs &&
+						requestedSlugs.length === cache.issueCounts.repoSlugs.length &&
+						requestedSlugs.every((s) => cache.issueCounts!.repoSlugs.includes(s));
+					if (cache?.issueCounts?.data && fallbackSlugsMatch) {
 						const cacheAge = Date.now() - cache.issueCounts.fetchedAt;
 						return {
 							counts: cache.issueCounts.data,
 							fromCache: true,
 							cacheAge,
 						};
 					}
 
 					throw error;
 				}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/ipc/handlers/symphony.ts` around lines 1279 - 1286, The
expired-cache fallback is returning counts without verifying they match the
requested repo slugs, so update the fallback in the handler (where
cache.issueCounts is used) to compare the cached repoSlugs with the current
requestedSlugs (or perform set equality) before returning counts; only return
the cached counts (with cacheAge) when the cached issueCounts.repoSlugs exactly
match requestedSlugs, otherwise treat as no valid cache and propagate the fetch
failure path. Ensure you reference cache.issueCounts,
cache.issueCounts.fetchedAt, repoSlugs and requestedSlugs when adding the check.
🤖 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/renderer/hooks/symphony/useSymphony.ts`:
- Around line 199-202: The empty-repo reset (setIssueCounts(null)) is never
reached because the effect early-returns when the guard if (repositories.length
> 0) prevents calling fetchIssueCounts([]); update the effect that references
repositories and fetchIssueCounts so it explicitly handles the empty case: when
repositories.length === 0 call setIssueCounts(null) (or call
fetchIssueCounts([]) if you prefer consistent flow) before the early-return,
otherwise proceed to call fetchIssueCounts(repositories); adjust the guard
around repositories so that fetchIssueCounts and setIssueCounts are always
invoked for the empty-repo path (affecting the effect containing repositories,
fetchIssueCounts, and setIssueCounts).

---

Outside diff comments:
In `@src/renderer/hooks/batch/useWorktreeManager.ts`:
- Around line 356-365: The code can leave baseBranch/targetBranch undefined if
window.maestro.git.getDefaultBranch throws; wrap the default-branch lookup in
its own try/catch and ensure you assign a fallback (e.g., 'main') on error so
baseBranch is always defined afterwards. Specifically, update the block using
baseBranch and window.maestro.git.getDefaultBranch in useWorktreeManager (the
local variable baseBranch and the awaited getDefaultBranch call) to catch errors
from getDefaultBranch and set baseBranch = 'main' (or the desired default) in
the catch. Apply the same pattern to the other similar lookup (the block around
the other getDefaultBranch usage referenced at lines ~407-413) so targetBranch
cannot be left undefined.

---

Duplicate comments:
In `@src/main/ipc/handlers/symphony.ts`:
- Around line 1279-1286: The expired-cache fallback is returning counts without
verifying they match the requested repo slugs, so update the fallback in the
handler (where cache.issueCounts is used) to compare the cached repoSlugs with
the current requestedSlugs (or perform set equality) before returning counts;
only return the cached counts (with cacheAge) when the cached
issueCounts.repoSlugs exactly match requestedSlugs, otherwise treat as no valid
cache and propagate the fetch failure path. Ensure you reference
cache.issueCounts, cache.issueCounts.fetchedAt, repoSlugs and requestedSlugs
when adding the check.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 435aba8 and 293180c.

📒 Files selected for processing (31)
  • src/__tests__/renderer/components/DocumentGraph/mindMapLayouts.test.ts
  • src/__tests__/renderer/components/GroupChatHeader.test.tsx
  • src/__tests__/renderer/components/TerminalOutput.test.tsx
  • src/__tests__/renderer/components/auto-scroll.test.tsx
  • src/__tests__/renderer/hooks/useBatchProcessor.test.ts
  • src/__tests__/renderer/utils/fileExplorer.test.ts
  • src/__tests__/shared/symphony-types.test.ts
  • src/main/ipc/handlers/symphony.ts
  • src/renderer/App.tsx
  • src/renderer/components/AutoRun.tsx
  • src/renderer/components/DebugWizardModal.tsx
  • src/renderer/components/DocumentGraph/DocumentGraphView.tsx
  • src/renderer/components/DocumentGraph/mindMapLayouts.ts
  • src/renderer/components/FilePreview.tsx
  • src/renderer/components/GroupChatHeader.tsx
  • src/renderer/components/ImageDiffViewer.tsx
  • src/renderer/components/InlineWizard/DocumentGenerationView.tsx
  • src/renderer/components/MarkdownRenderer.tsx
  • src/renderer/components/SymphonyModal.tsx
  • src/renderer/components/Wizard/shared/DocumentEditor.tsx
  • src/renderer/global.d.ts
  • src/renderer/hooks/batch/useAutoRunHandlers.ts
  • src/renderer/hooks/batch/useAutoRunImageHandling.ts
  • src/renderer/hooks/batch/useBatchProcessor.ts
  • src/renderer/hooks/batch/useWorktreeManager.ts
  • src/renderer/hooks/git/useFileExplorerEffects.ts
  • src/renderer/hooks/symphony/useSymphony.ts
  • src/renderer/hooks/tabs/useTabHandlers.ts
  • src/renderer/hooks/ui/useAppHandlers.ts
  • src/renderer/stores/settingsStore.ts
  • src/shared/symphony-types.ts
✅ Files skipped from review due to trivial changes (2)
  • src/tests/renderer/components/TerminalOutput.test.tsx
  • src/renderer/components/SymphonyModal.tsx
🚧 Files skipped from review as they are similar to previous changes (18)
  • src/renderer/components/GroupChatHeader.tsx
  • src/tests/renderer/hooks/useBatchProcessor.test.ts
  • src/renderer/components/FilePreview.tsx
  • src/renderer/components/AutoRun.tsx
  • src/tests/renderer/components/auto-scroll.test.tsx
  • src/renderer/global.d.ts
  • src/renderer/hooks/batch/useAutoRunHandlers.ts
  • src/renderer/hooks/batch/useBatchProcessor.ts
  • src/renderer/stores/settingsStore.ts
  • src/tests/renderer/components/DocumentGraph/mindMapLayouts.test.ts
  • src/renderer/hooks/git/useFileExplorerEffects.ts
  • src/tests/renderer/utils/fileExplorer.test.ts
  • src/tests/shared/symphony-types.test.ts
  • src/renderer/components/DocumentGraph/mindMapLayouts.ts
  • src/renderer/hooks/tabs/useTabHandlers.ts
  • src/shared/symphony-types.ts
  • src/renderer/components/DocumentGraph/DocumentGraphView.tsx
  • src/renderer/components/InlineWizard/DocumentGenerationView.tsx

Comment on lines +199 to +202
if (repos.length === 0) {
setIssueCounts(null);
return;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Empty-repo reset path is not reached from the current effect flow.

At Line [225], the guard if (repositories.length > 0) prevents calling fetchIssueCounts([]), so the new clear-at-empty logic at Line [199] won’t run and stale counts can persist when repositories become empty.

Suggested fix
 const fetchIssueCounts = useCallback(async (repos: RegisteredRepository[]) => {
 	if (repos.length === 0) {
 		setIssueCounts(null);
+		setIsLoadingIssueCounts(false);
 		return;
 	}
 	setIsLoadingIssueCounts(true);
@@
 	// Fetch issue counts once repositories are loaded
 	useEffect(() => {
-		if (repositories.length > 0) {
-			fetchIssueCounts(repositories);
-		}
+		fetchIssueCounts(repositories);
 	}, [repositories, fetchIssueCounts]);

Also applies to: 225-228

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/hooks/symphony/useSymphony.ts` around lines 199 - 202, The
empty-repo reset (setIssueCounts(null)) is never reached because the effect
early-returns when the guard if (repositories.length > 0) prevents calling
fetchIssueCounts([]); update the effect that references repositories and
fetchIssueCounts so it explicitly handles the empty case: when
repositories.length === 0 call setIssueCounts(null) (or call
fetchIssueCounts([]) if you prefer consistent flow) before the early-return,
otherwise proceed to call fetchIssueCounts(repositories); adjust the guard
around repositories so that fetchIssueCounts and setIssueCounts are always
invoked for the empty-repo path (affecting the effect containing repositories,
fetchIssueCounts, and setIssueCounts).

@pedramamini pedramamini merged commit 72c43b4 into main Mar 1, 2026
3 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants