mirror of
https://github.com/jlengrand/Maestro.git
synced 2026-03-10 08:31:19 +00:00
- Symphony now tracks PR status for any active contribution with PRs 🧭
- Draft PR creation is now deferred until the first commit lands ⏳ - PR info now syncs from contribution metadata into state automatically 🔄 - Creating a draft PR now updates both metadata.json and state.json reliably 🗃️ - Symphony adds new `completed` contribution status across types and UI ✅ - Active contribution duration now displays real timeSpent-based timing accurately ⌛ - Symphony issue cards now link directly to claimed PRs externally 🔗 - Playbook Exchange adds richer keyboard navigation and cross-platform shortcuts ⌨️ - Playbook imports now include optional assets/ folder and remote-session guidance 📦 - Troubleshooting upgrades: richer logs, process tree monitor, and error recovery 🛠️
This commit is contained in:
11
docs/CLAUDE.md
Normal file
11
docs/CLAUDE.md
Normal file
@@ -0,0 +1,11 @@
|
||||
<claude-mem-context>
|
||||
# Recent Activity
|
||||
|
||||
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
|
||||
|
||||
### Jan 11, 2026
|
||||
|
||||
| ID | Time | T | Title | Read |
|
||||
|----|------|---|-------|------|
|
||||
| #413 | 5:34 AM | 🔵 | History Documentation Content Loaded for Verification | ~412 |
|
||||
</claude-mem-context>
|
||||
@@ -81,14 +81,16 @@ View your in-progress Symphony sessions:
|
||||

|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
11
src/main/CLAUDE.md
Normal file
11
src/main/CLAUDE.md
Normal file
@@ -0,0 +1,11 @@
|
||||
<claude-mem-context>
|
||||
# Recent Activity
|
||||
|
||||
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
|
||||
|
||||
### Jan 11, 2026
|
||||
|
||||
| ID | Time | T | Title | Read |
|
||||
|----|------|---|-------|------|
|
||||
| #421 | 5:35 AM | 🔵 | History Manager Implementation Details Verified | ~454 |
|
||||
</claude-mem-context>
|
||||
@@ -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()) {
|
||||
|
||||
15
src/renderer/components/CLAUDE.md
Normal file
15
src/renderer/components/CLAUDE.md
Normal file
@@ -0,0 +1,15 @@
|
||||
<claude-mem-context>
|
||||
# Recent Activity
|
||||
|
||||
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
|
||||
|
||||
### 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 |
|
||||
</claude-mem-context>
|
||||
@@ -93,6 +93,7 @@ const STATUS_COLORS: Record<string, string> = {
|
||||
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: <Loader2 className="w-3 h-3 animate-spin" />,
|
||||
running: <Play className="w-3 h-3" />,
|
||||
paused: <Pause className="w-3 h-3" />,
|
||||
completed: <CheckCircle className="w-3 h-3" />,
|
||||
completing: <Loader2 className="w-3 h-3 animate-spin" />,
|
||||
ready_for_review: <GitPullRequest className="w-3 h-3" />,
|
||||
failed: <AlertCircle className="w-3 h-3" />,
|
||||
@@ -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'}
|
||||
</span>
|
||||
{isClaimed && issue.claimedByPr && (
|
||||
<span
|
||||
<a
|
||||
href={issue.claimedByPr.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-1 cursor-pointer hover:underline"
|
||||
style={{ color: theme.colors.accent }}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
window.maestro.shell?.openExternal?.(issue.claimedByPr!.url);
|
||||
window.maestro.shell.openExternal(issue.claimedByPr!.url);
|
||||
}}
|
||||
>
|
||||
<GitPullRequest className="w-3 h-3" />
|
||||
{issue.claimedByPr.isDraft ? 'Draft ' : ''}PR #{issue.claimedByPr.number} by @{issue.claimedByPr.author}
|
||||
<ExternalLink className="w-2.5 h-2.5" />
|
||||
</span>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -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({
|
||||
</span>
|
||||
<span style={{ color: theme.colors.textDim }}>
|
||||
<Clock className="w-3 h-3 inline mr-1" />
|
||||
{formatDuration(contribution.startedAt)}
|
||||
{formatDurationMs(contribution.timeSpent)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-1.5 rounded-full overflow-hidden" style={{ backgroundColor: theme.colors.bgMain }}>
|
||||
@@ -871,45 +868,15 @@ function ActiveContributionCard({
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{canPause && (
|
||||
<button
|
||||
onClick={onPause}
|
||||
className="flex-1 py-1.5 rounded text-xs flex items-center justify-center gap-1 hover:bg-white/10"
|
||||
style={{ backgroundColor: theme.colors.bgMain, color: theme.colors.textDim }}
|
||||
>
|
||||
<Pause className="w-3 h-3" /> Pause
|
||||
</button>
|
||||
)}
|
||||
{canResume && (
|
||||
<button
|
||||
onClick={onResume}
|
||||
className="flex-1 py-1.5 rounded text-xs flex items-center justify-center gap-1"
|
||||
style={{ backgroundColor: theme.colors.accent + '20', color: theme.colors.accent }}
|
||||
>
|
||||
<Play className="w-3 h-3" /> Resume
|
||||
</button>
|
||||
)}
|
||||
{canFinalize && (
|
||||
<button
|
||||
onClick={onFinalize}
|
||||
className="flex-1 py-1.5 rounded text-xs flex items-center justify-center gap-1"
|
||||
style={{ backgroundColor: theme.colors.accent, color: theme.colors.accentForeground }}
|
||||
>
|
||||
<GitPullRequest className="w-3 h-3" /> Finalize PR
|
||||
</button>
|
||||
)}
|
||||
{canCancel && (
|
||||
<button
|
||||
onClick={onCancel}
|
||||
className="py-1.5 px-2 rounded text-xs hover:bg-white/10"
|
||||
style={{ color: theme.colors.textDim }}
|
||||
title="Cancel"
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{canFinalize && (
|
||||
<button
|
||||
onClick={onFinalize}
|
||||
className="w-full py-1.5 rounded text-xs flex items-center justify-center gap-1"
|
||||
style={{ backgroundColor: theme.colors.accent, color: theme.colors.accentForeground }}
|
||||
>
|
||||
<GitPullRequest className="w-3 h-3" /> Finalize PR
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<div
|
||||
className={`p-3 rounded-lg border ${achievement.earned ? '' : 'opacity-50'}`}
|
||||
style={{ backgroundColor: theme.colors.bgActivity, borderColor: theme.colors.border }}
|
||||
className="p-3 rounded-lg border"
|
||||
style={{
|
||||
backgroundColor: theme.colors.bgActivity,
|
||||
borderColor: achievement.earned ? theme.colors.accent : theme.colors.border,
|
||||
opacity: achievement.earned ? 1 : 0.5,
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="text-2xl">{achievement.icon}</div>
|
||||
<div className="text-2xl" style={{ opacity: achievement.earned ? 1 : 0.7 }}>{achievement.icon}</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h4 className="font-medium text-sm" style={{ color: theme.colors.textMain }}>
|
||||
{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)}
|
||||
/>
|
||||
))}
|
||||
|
||||
11
src/renderer/hooks/settings/CLAUDE.md
Normal file
11
src/renderer/hooks/settings/CLAUDE.md
Normal file
@@ -0,0 +1,11 @@
|
||||
<claude-mem-context>
|
||||
# Recent Activity
|
||||
|
||||
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
|
||||
|
||||
### Jan 11, 2026
|
||||
|
||||
| ID | Time | T | Title | Read |
|
||||
|----|------|---|-------|------|
|
||||
| #433 | 5:36 AM | 🔵 | History Default Setting Verified in Settings Hook | ~323 |
|
||||
</claude-mem-context>
|
||||
11
src/renderer/types/CLAUDE.md
Normal file
11
src/renderer/types/CLAUDE.md
Normal file
@@ -0,0 +1,11 @@
|
||||
<claude-mem-context>
|
||||
# Recent Activity
|
||||
|
||||
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
|
||||
|
||||
### Jan 11, 2026
|
||||
|
||||
| ID | Time | T | Title | Read |
|
||||
|----|------|---|-------|------|
|
||||
| #442 | 5:37 AM | 🔵 | Achievement Action Extension Verified in Renderer Types | ~311 |
|
||||
</claude-mem-context>
|
||||
12
src/shared/CLAUDE.md
Normal file
12
src/shared/CLAUDE.md
Normal file
@@ -0,0 +1,12 @@
|
||||
<claude-mem-context>
|
||||
# Recent Activity
|
||||
|
||||
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
|
||||
|
||||
### 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 |
|
||||
</claude-mem-context>
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user