diff --git a/docs/CLAUDE.md b/docs/CLAUDE.md new file mode 100644 index 00000000..64247ebb --- /dev/null +++ b/docs/CLAUDE.md @@ -0,0 +1,11 @@ + +# Recent Activity + + + +### Jan 11, 2026 + +| ID | Time | T | Title | Read | +|----|------|---|-------|------| +| #413 | 5:34 AM | 🔵 | History Documentation Content Loaded for Verification | ~412 | + \ No newline at end of file diff --git a/docs/symphony.md b/docs/symphony.md index ae013abc..ecbe9ef1 100644 --- a/docs/symphony.md +++ b/docs/symphony.md @@ -81,14 +81,16 @@ View your in-progress Symphony sessions: ![Active Contributions](./screenshots/symphony-active.png) Each active contribution shows: +- **Issue title and repository** — The GitHub issue being worked on +- **Status badge** — Running, Paused, Creating PR, etc. +- **Progress bar** — Documents completed vs. total +- **Current document** — The document being processed +- **Time elapsed** — How long the contribution has been running +- **Token usage** — Input/output tokens and estimated cost +- **Draft PR link** — Once created on first commit +- **Controls** — Pause/Resume, Cancel, Finalize PR -- Status indicators (Running, Paused, Creating PR, etc.) -- Progress bar showing documents completed vs. total -- Current document being processed -- Token usage (input/output tokens, estimated cost) -- Draft PR link (once created on first commit) -- Controls: Pause/Resume, Cancel, Finalize PR -- **Check PR Status** button to detect merged/closed PRs +Click **Check PR Status** to verify your draft PR on GitHub and detect merged/closed PRs. ### History Tab diff --git a/src/__tests__/main/ipc/handlers/symphony.test.ts b/src/__tests__/main/ipc/handlers/symphony.test.ts index 9a5f98c3..0b400306 100644 --- a/src/__tests__/main/ipc/handlers/symphony.test.ts +++ b/src/__tests__/main/ipc/handlers/symphony.test.ts @@ -3110,7 +3110,7 @@ describe('Symphony IPC handlers', () => { }); describe('active contribution checking', () => { - it('should check active ready_for_review contributions', async () => { + it('should check all active contributions with a draft PR', async () => { const state = { active: [ { @@ -3124,7 +3124,7 @@ describe('Symphony IPC handlers', () => { draftPrNumber: 500, draftPrUrl: 'https://github.com/owner/repo/pull/500', startedAt: '2024-01-01T00:00:00Z', - status: 'ready_for_review', // Only this status is checked + status: 'ready_for_review', progress: { totalDocuments: 1, completedDocuments: 1, totalTasks: 5, completedTasks: 5 }, tokenUsage: { inputTokens: 1000, outputTokens: 500, estimatedCost: 0.10 }, timeSpent: 60000, @@ -3137,7 +3137,15 @@ describe('Symphony IPC handlers', () => { repoName: 'repo', issueNumber: 2, draftPrNumber: 501, - status: 'running', // Not ready_for_review - should not be checked + status: 'running', // Running contributions with PR should also be checked + }, + { + id: 'active_3', + repoSlug: 'owner/repo', + repoName: 'repo', + issueNumber: 3, + // No draftPrNumber - should not be checked + status: 'running', }, ], history: [], @@ -3153,8 +3161,8 @@ describe('Symphony IPC handlers', () => { const handler = getCheckPRStatusesHandler(); const result = await handler!({} as any); - // Should only check the ready_for_review contribution - expect(result.checked).toBe(1); + // Should check all contributions with a draft PR (both ready_for_review and running) + expect(result.checked).toBe(2); }); it('should move merged active contributions to history', async () => { @@ -4025,10 +4033,23 @@ describe('Symphony IPC handlers', () => { describe('commit counting', () => { it('should count commits on branch vs base branch', async () => { const metadata = createValidMetadata(); + const stateWithActiveContrib = { + active: [{ + id: 'contrib_draft_test', + repoSlug: 'owner/repo', + issueNumber: 42, + status: 'running', + }], + history: [], + stats: {}, + }; vi.mocked(fs.readFile).mockImplementation(async (filePath) => { if ((filePath as string).includes('metadata.json')) { return JSON.stringify(metadata); } + if ((filePath as string).includes('state.json')) { + return JSON.stringify(stateWithActiveContrib); + } throw new Error('ENOENT'); }); vi.mocked(execFileNoThrow).mockImplementation(async (cmd, args, cwd) => { @@ -4090,10 +4111,23 @@ describe('Symphony IPC handlers', () => { describe('PR creation', () => { it('should push branch and create draft PR when commits exist', async () => { const metadata = createValidMetadata(); + const stateWithActiveContrib = { + active: [{ + id: 'contrib_draft_test', + repoSlug: 'owner/repo', + issueNumber: 42, + status: 'running', + }], + history: [], + stats: {}, + }; vi.mocked(fs.readFile).mockImplementation(async (filePath) => { if ((filePath as string).includes('metadata.json')) { return JSON.stringify(metadata); } + if ((filePath as string).includes('state.json')) { + return JSON.stringify(stateWithActiveContrib); + } throw new Error('ENOENT'); }); vi.mocked(execFileNoThrow).mockImplementation(async (cmd, args) => { @@ -4135,10 +4169,23 @@ describe('Symphony IPC handlers', () => { describe('metadata updates', () => { it('should update metadata.json with PR info', async () => { const metadata = createValidMetadata(); + const stateWithActiveContrib = { + active: [{ + id: 'contrib_draft_test', + repoSlug: 'owner/repo', + issueNumber: 42, + status: 'running', + }], + history: [], + stats: {}, + }; vi.mocked(fs.readFile).mockImplementation(async (filePath) => { if ((filePath as string).includes('metadata.json')) { return JSON.stringify(metadata); } + if ((filePath as string).includes('state.json')) { + return JSON.stringify(stateWithActiveContrib); + } throw new Error('ENOENT'); }); vi.mocked(execFileNoThrow).mockImplementation(async (cmd, args) => { @@ -4165,15 +4212,75 @@ describe('Symphony IPC handlers', () => { expect(updatedMetadata.draftPrNumber).toBe(77); expect(updatedMetadata.draftPrUrl).toBe('https://github.com/owner/repo/pull/77'); }); + + it('should update state.json active contribution with PR info', async () => { + const metadata = createValidMetadata(); + const stateWithActiveContrib = { + active: [{ + id: 'contrib_draft_test', + repoSlug: 'owner/repo', + issueNumber: 42, + status: 'running', + }], + history: [], + stats: {}, + }; + vi.mocked(fs.readFile).mockImplementation(async (filePath) => { + if ((filePath as string).includes('metadata.json')) { + return JSON.stringify(metadata); + } + if ((filePath as string).includes('state.json')) { + return JSON.stringify(stateWithActiveContrib); + } + throw new Error('ENOENT'); + }); + vi.mocked(execFileNoThrow).mockImplementation(async (cmd, args) => { + if (cmd === 'gh' && args?.[0] === 'auth') return { stdout: 'Logged in', stderr: '', exitCode: 0 }; + if (cmd === 'git' && args?.[0] === 'symbolic-ref') return { stdout: 'refs/remotes/origin/main', stderr: '', exitCode: 0 }; + if (cmd === 'git' && args?.[0] === 'rev-list') return { stdout: '1', stderr: '', exitCode: 0 }; + if (cmd === 'git' && args?.[0] === 'rev-parse') return { stdout: 'symphony/issue-42-abc123', stderr: '', exitCode: 0 }; + if (cmd === 'git' && args?.[0] === 'push') return { stdout: '', stderr: '', exitCode: 0 }; + if (cmd === 'gh' && args?.[0] === 'pr') return { stdout: 'https://github.com/owner/repo/pull/100', stderr: '', exitCode: 0 }; + return { stdout: '', stderr: '', exitCode: 0 }; + }); + + const handler = getCreateDraftPRHandler(); + await handler!({} as any, { contributionId: 'contrib_draft_test' }); + + // Verify state.json was updated with PR info + const stateWriteCall = vi.mocked(fs.writeFile).mock.calls.find( + call => (call[0] as string).includes('state.json') + ); + expect(stateWriteCall).toBeDefined(); + + const updatedState = JSON.parse(stateWriteCall![1] as string); + const activeContrib = updatedState.active.find((c: any) => c.id === 'contrib_draft_test'); + expect(activeContrib).toBeDefined(); + expect(activeContrib.draftPrNumber).toBe(100); + expect(activeContrib.draftPrUrl).toBe('https://github.com/owner/repo/pull/100'); + }); }); describe('event broadcasting', () => { it('should broadcast symphony:prCreated event', async () => { const metadata = createValidMetadata(); + const stateWithActiveContrib = { + active: [{ + id: 'contrib_draft_test', + repoSlug: 'owner/repo', + issueNumber: 42, + status: 'running', + }], + history: [], + stats: {}, + }; vi.mocked(fs.readFile).mockImplementation(async (filePath) => { if ((filePath as string).includes('metadata.json')) { return JSON.stringify(metadata); } + if ((filePath as string).includes('state.json')) { + return JSON.stringify(stateWithActiveContrib); + } throw new Error('ENOENT'); }); vi.mocked(execFileNoThrow).mockImplementation(async (cmd, args) => { @@ -4205,10 +4312,23 @@ describe('Symphony IPC handlers', () => { describe('return values', () => { it('should return draftPrNumber and draftPrUrl on success', async () => { const metadata = createValidMetadata(); + const stateWithActiveContrib = { + active: [{ + id: 'contrib_draft_test', + repoSlug: 'owner/repo', + issueNumber: 42, + status: 'running', + }], + history: [], + stats: {}, + }; vi.mocked(fs.readFile).mockImplementation(async (filePath) => { if ((filePath as string).includes('metadata.json')) { return JSON.stringify(metadata); } + if ((filePath as string).includes('state.json')) { + return JSON.stringify(stateWithActiveContrib); + } throw new Error('ENOENT'); }); vi.mocked(execFileNoThrow).mockImplementation(async (cmd, args) => { diff --git a/src/__tests__/shared/symphony-types.test.ts b/src/__tests__/shared/symphony-types.test.ts index ec005953..edd6780d 100644 --- a/src/__tests__/shared/symphony-types.test.ts +++ b/src/__tests__/shared/symphony-types.test.ts @@ -110,6 +110,7 @@ describe('shared/symphony-types', () => { 'creating_pr', 'running', 'paused', + 'completed', 'completing', 'ready_for_review', 'failed', @@ -121,8 +122,8 @@ describe('shared/symphony-types', () => { expect(testStatus).toBe(status); }); - it('should have 8 valid contribution statuses', () => { - expect(validStatuses).toHaveLength(8); + it('should have 9 valid contribution statuses', () => { + expect(validStatuses).toHaveLength(9); }); }); diff --git a/src/main/CLAUDE.md b/src/main/CLAUDE.md new file mode 100644 index 00000000..beb80924 --- /dev/null +++ b/src/main/CLAUDE.md @@ -0,0 +1,11 @@ + +# Recent Activity + + + +### Jan 11, 2026 + +| ID | Time | T | Title | Read | +|----|------|---|-------|------| +| #421 | 5:35 AM | 🔵 | History Manager Implementation Details Verified | ~454 | + \ No newline at end of file diff --git a/src/main/ipc/handlers/symphony.ts b/src/main/ipc/handlers/symphony.ts index 2cfa0303..b8c0ed70 100644 --- a/src/main/ipc/handlers/symphony.ts +++ b/src/main/ipc/handlers/symphony.ts @@ -1522,12 +1522,42 @@ This PR will be updated automatically when the Auto Run completes.`; } } - // Also check active contributions that are ready_for_review + // First, sync PR info from metadata.json for any active contributions missing it + // This handles cases where PR was created but state.json wasn't updated (migration) + let prInfoSynced = false; + for (const contribution of state.active) { + if (!contribution.draftPrNumber) { + try { + const metadataPath = path.join(getSymphonyDir(app), 'contributions', contribution.id, 'metadata.json'); + const metadataContent = await fs.readFile(metadataPath, 'utf-8'); + const metadata = JSON.parse(metadataContent) as { + prCreated?: boolean; + draftPrNumber?: number; + draftPrUrl?: string; + }; + if (metadata.prCreated && metadata.draftPrNumber) { + // Sync PR info from metadata to state + contribution.draftPrNumber = metadata.draftPrNumber; + contribution.draftPrUrl = metadata.draftPrUrl; + prInfoSynced = true; + logger.info('Synced PR info from metadata to state', LOG_CONTEXT, { + contributionId: contribution.id, + draftPrNumber: metadata.draftPrNumber, + }); + } + } catch { + // Metadata file might not exist - that's okay + } + } + } + + // Also check active contributions that have a draft PR // These might have been merged/closed externally const activeToMove: number[] = []; for (let i = 0; i < state.active.length; i++) { const contribution = state.active[i]; - if (!contribution.draftPrNumber || contribution.status !== 'ready_for_review') continue; + // Check any active contribution with a PR (not just ready_for_review) + if (!contribution.draftPrNumber) continue; results.checked++; @@ -1601,11 +1631,11 @@ This PR will be updated automatically when the Auto Run completes.`; await writeState(app, state); - if (results.merged > 0 || results.closed > 0) { + if (results.merged > 0 || results.closed > 0 || prInfoSynced) { broadcastSymphonyUpdate(getMainWindow); } - logger.info('PR status check complete', LOG_CONTEXT, results); + logger.info('PR status check complete', LOG_CONTEXT, { ...results, prInfoSynced }); return results; } @@ -1950,6 +1980,16 @@ This PR will be updated automatically when the Auto Run completes.`; metadata.draftPrUrl = prResult.prUrl; await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2)); + // Also update the active contribution in state with PR info + // This is critical for checkPRStatuses to find the PR + const state = await readState(app); + const activeContrib = state.active.find(c => c.id === contributionId); + if (activeContrib) { + activeContrib.draftPrNumber = prResult.prNumber; + activeContrib.draftPrUrl = prResult.prUrl; + await writeState(app, state); + } + // Broadcast PR creation event const mainWindow = getMainWindow?.(); if (mainWindow && !mainWindow.isDestroyed()) { diff --git a/src/renderer/components/CLAUDE.md b/src/renderer/components/CLAUDE.md new file mode 100644 index 00000000..1f429a64 --- /dev/null +++ b/src/renderer/components/CLAUDE.md @@ -0,0 +1,15 @@ + +# Recent Activity + + + +### Jan 11, 2026 + +| ID | Time | T | Title | Read | +|----|------|---|-------|------| +| #444 | 5:37 AM | 🔵 | History Detail Modal Implementation Verified | ~517 | +| #443 | " | 🔵 | History Help Modal Content Verified | ~418 | +| #441 | " | 🔵 | Activity Graph Time Range Options Verified | ~292 | +| #437 | 5:36 AM | 🔵 | History Default Setting UI Label Verified | ~314 | +| #426 | " | 🔵 | History Panel UI Implementation Verified | ~529 | + \ No newline at end of file diff --git a/src/renderer/components/SymphonyModal.tsx b/src/renderer/components/SymphonyModal.tsx index 4f208745..e57efb8a 100644 --- a/src/renderer/components/SymphonyModal.tsx +++ b/src/renderer/components/SymphonyModal.tsx @@ -93,6 +93,7 @@ const STATUS_COLORS: Record = { creating_pr: COLORBLIND_AGENT_PALETTE[0], // #0077BB running: COLORBLIND_AGENT_PALETTE[2], // #009988 (Teal - success) paused: COLORBLIND_AGENT_PALETTE[1], // #EE7733 (Orange - warning) + completed: COLORBLIND_AGENT_PALETTE[2], // #009988 (Teal - success) completing: COLORBLIND_AGENT_PALETTE[0], // #0077BB ready_for_review: COLORBLIND_AGENT_PALETTE[8], // #AA4499 (Purple) failed: COLORBLIND_AGENT_PALETTE[3], // #CC3311 (Vermillion - error) @@ -113,12 +114,11 @@ function formatCacheAge(cacheAgeMs: number | null): string { return 'just now'; } -function formatDuration(startedAt: string): string { - const start = new Date(startedAt).getTime(); - const diff = Math.floor((Date.now() - start) / 1000); - if (diff < 60) return `${diff}s`; - if (diff < 3600) return `${Math.floor(diff / 60)}m`; - return `${Math.floor(diff / 3600)}h ${Math.floor((diff % 3600) / 60)}m`; +function formatDurationMs(ms: number): string { + const totalSeconds = Math.floor(ms / 1000); + if (totalSeconds < 60) return `${totalSeconds}s`; + if (totalSeconds < 3600) return `${Math.floor(totalSeconds / 60)}m`; + return `${Math.floor(totalSeconds / 3600)}h ${Math.floor((totalSeconds % 3600) / 60)}m`; } function formatDate(isoString: string): string { @@ -135,6 +135,7 @@ function getStatusInfo(status: ContributionStatus): { label: string; color: stri creating_pr: , running: , paused: , + completed: , completing: , ready_for_review: , failed: , @@ -145,6 +146,7 @@ function getStatusInfo(status: ContributionStatus): { label: string; color: stri creating_pr: 'Creating PR', running: 'Running', paused: 'Paused', + completed: 'Completed', completing: 'Completing', ready_for_review: 'Ready for Review', failed: 'Failed', @@ -298,18 +300,22 @@ function IssueCard({ {issue.documentPaths.length} {issue.documentPaths.length === 1 ? 'document' : 'documents'} {isClaimed && issue.claimedByPr && ( - { + e.preventDefault(); e.stopPropagation(); - window.maestro.shell?.openExternal?.(issue.claimedByPr!.url); + window.maestro.shell.openExternal(issue.claimedByPr!.url); }} > {issue.claimedByPr.isDraft ? 'Draft ' : ''}PR #{issue.claimedByPr.number} by @{issue.claimedByPr.author} - + )} @@ -383,7 +389,7 @@ function RepositoryDetailView({ () => createMarkdownComponents({ theme, - onExternalLinkClick: (href) => window.maestro.shell?.openExternal?.(href), + onExternalLinkClick: (href) => window.maestro.shell.openExternal(href), }), [theme] ); @@ -446,7 +452,7 @@ function RepositoryDetailView({ }; const handleOpenExternal = useCallback((url: string) => { - window.maestro.shell?.openExternal?.(url); + window.maestro.shell.openExternal(url); }, []); return ( @@ -766,16 +772,10 @@ function RepositoryDetailView({ function ActiveContributionCard({ contribution, theme, - onPause, - onResume, - onCancel, onFinalize, }: { contribution: ActiveContribution; theme: Theme; - onPause: () => void; - onResume: () => void; - onCancel: () => void; onFinalize: () => void; }) { const statusInfo = getStatusInfo(contribution.status); @@ -783,13 +783,10 @@ function ActiveContributionCard({ ? Math.round((contribution.progress.completedDocuments / contribution.progress.totalDocuments) * 100) : 0; - const canPause = contribution.status === 'running'; - const canResume = contribution.status === 'paused'; const canFinalize = contribution.status === 'ready_for_review'; - const canCancel = !['ready_for_review', 'completing', 'cancelled'].includes(contribution.status); const handleOpenExternal = useCallback((url: string) => { - window.maestro.shell?.openExternal?.(url); + window.maestro.shell.openExternal(url); }, []); return ( @@ -841,7 +838,7 @@ function ActiveContributionCard({ - {formatDuration(contribution.startedAt)} + {formatDurationMs(contribution.timeSpent)}
@@ -871,45 +868,15 @@ function ActiveContributionCard({

)} -
- {canPause && ( - - )} - {canResume && ( - - )} - {canFinalize && ( - - )} - {canCancel && ( - - )} -
+ {canFinalize && ( + + )}
); } @@ -926,7 +893,7 @@ function CompletedContributionCard({ theme: Theme; }) { const handleOpenPR = useCallback(() => { - window.maestro.shell?.openExternal?.(contribution.prUrl); + window.maestro.shell.openExternal(contribution.prUrl); }, [contribution.prUrl]); return ( @@ -991,11 +958,15 @@ function AchievementCard({ }) { return (
-
{achievement.icon}
+
{achievement.icon}

{achievement.title} @@ -1056,7 +1027,6 @@ export function SymphonyModal({ startContribution, activeContributions, completedContributions, - cancelContribution, finalizeContribution, } = useSymphony(); @@ -1230,18 +1200,6 @@ export function SymphonyModal({ }, [selectedRepo, selectedIssue, startContribution, onStartContribution, handleBack]); // Contribution actions - const handlePause = useCallback(async (contributionId: string) => { - await window.maestro.symphony.updateStatus({ contributionId, status: 'paused' }); - }, []); - - const handleResume = useCallback(async (contributionId: string) => { - await window.maestro.symphony.updateStatus({ contributionId, status: 'running' }); - }, []); - - const handleCancel = useCallback(async (contributionId: string) => { - await cancelContribution(contributionId, true); - }, [cancelContribution]); - const handleFinalize = useCallback(async (contributionId: string) => { await finalizeContribution(contributionId); }, [finalizeContribution]); @@ -1672,9 +1630,6 @@ export function SymphonyModal({ key={contribution.id} contribution={contribution} theme={theme} - onPause={() => handlePause(contribution.id)} - onResume={() => handleResume(contribution.id)} - onCancel={() => handleCancel(contribution.id)} onFinalize={() => handleFinalize(contribution.id)} /> ))} diff --git a/src/renderer/hooks/settings/CLAUDE.md b/src/renderer/hooks/settings/CLAUDE.md new file mode 100644 index 00000000..5a56a44e --- /dev/null +++ b/src/renderer/hooks/settings/CLAUDE.md @@ -0,0 +1,11 @@ + +# Recent Activity + + + +### Jan 11, 2026 + +| ID | Time | T | Title | Read | +|----|------|---|-------|------| +| #433 | 5:36 AM | 🔵 | History Default Setting Verified in Settings Hook | ~323 | + \ No newline at end of file diff --git a/src/renderer/types/CLAUDE.md b/src/renderer/types/CLAUDE.md new file mode 100644 index 00000000..d09de6f5 --- /dev/null +++ b/src/renderer/types/CLAUDE.md @@ -0,0 +1,11 @@ + +# Recent Activity + + + +### Jan 11, 2026 + +| ID | Time | T | Title | Read | +|----|------|---|-------|------| +| #442 | 5:37 AM | 🔵 | Achievement Action Extension Verified in Renderer Types | ~311 | + \ No newline at end of file diff --git a/src/shared/CLAUDE.md b/src/shared/CLAUDE.md new file mode 100644 index 00000000..7c9f7da2 --- /dev/null +++ b/src/shared/CLAUDE.md @@ -0,0 +1,12 @@ + +# Recent Activity + + + +### Jan 11, 2026 + +| ID | Time | T | Title | Read | +|----|------|---|-------|------| +| #428 | 5:36 AM | 🔵 | HistoryEntry Interface Fields Verified | ~378 | +| #419 | 5:35 AM | 🔵 | History Constants and Types Verified in Shared Module | ~384 | + \ No newline at end of file diff --git a/src/shared/symphony-types.ts b/src/shared/symphony-types.ts index 1a0ad33e..217bdab4 100644 --- a/src/shared/symphony-types.ts +++ b/src/shared/symphony-types.ts @@ -216,6 +216,7 @@ export type ContributionStatus = | 'creating_pr' // Creating draft PR | 'running' // Auto Run in progress | 'paused' // User paused + | 'completed' // Auto Run finished, PR still in draft | 'completing' // Pushing final changes | 'ready_for_review' // PR marked ready | 'failed' // Failed (see error field)