mirror of
https://github.com/jlengrand/Maestro.git
synced 2026-03-10 08:31:19 +00:00
I'd be happy to help you create a clean update summary for your GitHub project! However, I don't see any input provided after "INPUT:" in your message.
Could you please share the changelog, commit history, pull request information, or any other details about what has changed in your project since the last release? This could include: - Git commit messages - Pull request descriptions - Release notes - Feature additions - Bug fixes - Performance improvements - Breaking changes - Dependency updates Once you provide this information, I'll create an exciting CHANGES section with clean, 10-word bullets and relevant emojis as requested!
This commit is contained in:
104
package-lock.json
generated
104
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "maestro",
|
||||
"version": "0.8.5",
|
||||
"version": "0.8.7",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "maestro",
|
||||
"version": "0.8.5",
|
||||
"version": "0.8.7",
|
||||
"hasInstallScript": true,
|
||||
"license": "AGPL 3.0",
|
||||
"dependencies": {
|
||||
@@ -26,6 +26,7 @@
|
||||
"diff": "^8.0.2",
|
||||
"dompurify": "^3.3.0",
|
||||
"electron-store": "^8.1.0",
|
||||
"electron-updater": "^6.6.2",
|
||||
"fastify": "^4.25.2",
|
||||
"js-tiktoken": "^1.0.21",
|
||||
"mermaid": "^11.12.1",
|
||||
@@ -3767,7 +3768,6 @@
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||
"dev": true,
|
||||
"license": "Python-2.0"
|
||||
},
|
||||
"node_modules/aria-query": {
|
||||
@@ -6714,6 +6714,82 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/electron-updater": {
|
||||
"version": "6.6.2",
|
||||
"resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.6.2.tgz",
|
||||
"integrity": "sha512-Cr4GDOkbAUqRHP5/oeOmH/L2Bn6+FQPxVLZtPbcmKZC63a1F3uu5EefYOssgZXG3u/zBlubbJ5PJdITdMVggbw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"builder-util-runtime": "9.3.1",
|
||||
"fs-extra": "^10.1.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"lazy-val": "^1.0.5",
|
||||
"lodash.escaperegexp": "^4.1.2",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"semver": "^7.6.3",
|
||||
"tiny-typed-emitter": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/electron-updater/node_modules/builder-util-runtime": {
|
||||
"version": "9.3.1",
|
||||
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.3.1.tgz",
|
||||
"integrity": "sha512-2/egrNDDnRaxVwK3A+cJq6UOlqOdedGA7JPqCeJjN2Zjk1/QB/6QUi3b714ScIGS7HafFXTyzJEOr5b44I3kvQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.3.4",
|
||||
"sax": "^1.2.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/electron-updater/node_modules/fs-extra": {
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
|
||||
"integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.0",
|
||||
"jsonfile": "^6.0.1",
|
||||
"universalify": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/electron-updater/node_modules/jsonfile": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
|
||||
"integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"universalify": "^2.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"graceful-fs": "^4.1.6"
|
||||
}
|
||||
},
|
||||
"node_modules/electron-updater/node_modules/semver": {
|
||||
"version": "7.7.3",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
||||
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/electron-updater/node_modules/universalify": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
|
||||
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/electron/node_modules/@types/node": {
|
||||
"version": "18.19.130",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz",
|
||||
@@ -8644,7 +8720,6 @@
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1"
|
||||
@@ -8876,7 +8951,6 @@
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz",
|
||||
"integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lazystream": {
|
||||
@@ -9008,6 +9082,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.escaperegexp": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz",
|
||||
"integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.flatten": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
|
||||
@@ -9015,6 +9095,13 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isequal": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
|
||||
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
|
||||
"deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isplainobject": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
||||
@@ -12559,7 +12646,6 @@
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.3.tgz",
|
||||
"integrity": "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0"
|
||||
},
|
||||
"node_modules/saxes": {
|
||||
@@ -13412,6 +13498,12 @@
|
||||
"real-require": "^0.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tiny-typed-emitter": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz",
|
||||
"integrity": "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinybench": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
||||
|
||||
@@ -43,6 +43,11 @@
|
||||
"build": {
|
||||
"appId": "com.maestro.app",
|
||||
"productName": "Maestro",
|
||||
"publish": {
|
||||
"provider": "github",
|
||||
"owner": "pedramamini",
|
||||
"repo": "Maestro"
|
||||
},
|
||||
"directories": {
|
||||
"output": "release",
|
||||
"buildResources": "build"
|
||||
@@ -143,6 +148,7 @@
|
||||
"diff": "^8.0.2",
|
||||
"dompurify": "^3.3.0",
|
||||
"electron-store": "^8.1.0",
|
||||
"electron-updater": "^6.6.2",
|
||||
"fastify": "^4.25.2",
|
||||
"js-tiktoken": "^1.0.21",
|
||||
"mermaid": "^11.12.1",
|
||||
|
||||
@@ -17,6 +17,10 @@ import React from 'react';
|
||||
if (!(window.maestro as any).updates) {
|
||||
(window.maestro as any).updates = {
|
||||
check: vi.fn(),
|
||||
download: vi.fn(),
|
||||
install: vi.fn(),
|
||||
getStatus: vi.fn(),
|
||||
onStatus: vi.fn(() => vi.fn()), // Returns an unsubscribe function
|
||||
};
|
||||
}
|
||||
|
||||
@@ -212,19 +216,6 @@ describe('UpdateCheckModal', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('shows upgrade instructions', async () => {
|
||||
render(
|
||||
<UpdateCheckModal
|
||||
theme={createMockTheme()}
|
||||
onClose={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('How to Upgrade')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('shows release notes section', async () => {
|
||||
render(
|
||||
<UpdateCheckModal
|
||||
@@ -247,11 +238,13 @@ describe('UpdateCheckModal', () => {
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Download Latest Release')).toBeInTheDocument();
|
||||
expect(screen.getByText('Download and Install Update')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('opens releases URL when download button clicked', async () => {
|
||||
it('starts download when download button clicked', async () => {
|
||||
(window.maestro.updates.download as any).mockResolvedValue({ success: true });
|
||||
|
||||
render(
|
||||
<UpdateCheckModal
|
||||
theme={createMockTheme()}
|
||||
@@ -260,14 +253,25 @@ describe('UpdateCheckModal', () => {
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Download Latest Release')).toBeInTheDocument();
|
||||
expect(screen.getByText('Download and Install Update')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByText('Download Latest Release'));
|
||||
fireEvent.click(screen.getByText('Download and Install Update'));
|
||||
|
||||
expect(window.maestro.shell.openExternal).toHaveBeenCalledWith(
|
||||
'https://github.com/pedramamini/Maestro/releases'
|
||||
expect(window.maestro.updates.download).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('shows fallback link to GitHub', async () => {
|
||||
render(
|
||||
<UpdateCheckModal
|
||||
theme={createMockTheme()}
|
||||
onClose={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Or download manually from GitHub')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -890,4 +894,557 @@ describe('UpdateCheckModal', () => {
|
||||
expect(screen.queryByTestId('react-markdown')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// AUTO-UPDATE DOWNLOAD FLOW
|
||||
// =============================================================================
|
||||
|
||||
describe('auto-update download flow', () => {
|
||||
beforeEach(() => {
|
||||
// Reset download mock
|
||||
(window.maestro.updates.download as any).mockReset();
|
||||
(window.maestro.updates.install as any).mockReset();
|
||||
});
|
||||
|
||||
it('calls download API when download button is clicked', async () => {
|
||||
(window.maestro.updates.download as any).mockResolvedValue({ success: true });
|
||||
|
||||
render(
|
||||
<UpdateCheckModal
|
||||
theme={createMockTheme()}
|
||||
onClose={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Download and Install Update')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByText('Download and Install Update'));
|
||||
|
||||
expect(window.maestro.updates.download).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('shows downloading state when download starts', async () => {
|
||||
// Make download take some time
|
||||
(window.maestro.updates.download as any).mockImplementation(
|
||||
() => new Promise((resolve) => setTimeout(() => resolve({ success: true }), 100))
|
||||
);
|
||||
|
||||
render(
|
||||
<UpdateCheckModal
|
||||
theme={createMockTheme()}
|
||||
onClose={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Download and Install Update')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByText('Download and Install Update'));
|
||||
|
||||
// Should show downloading state
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Downloading...')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('disables download button while downloading', async () => {
|
||||
(window.maestro.updates.download as any).mockImplementation(
|
||||
() => new Promise((resolve) => setTimeout(() => resolve({ success: true }), 100))
|
||||
);
|
||||
|
||||
render(
|
||||
<UpdateCheckModal
|
||||
theme={createMockTheme()}
|
||||
onClose={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Download and Install Update')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const downloadButton = screen.getByText('Download and Install Update').closest('button');
|
||||
fireEvent.click(downloadButton!);
|
||||
|
||||
await waitFor(() => {
|
||||
const downloadingButton = screen.getByText('Downloading...').closest('button');
|
||||
expect(downloadingButton).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
it('disables refresh button while downloading', async () => {
|
||||
(window.maestro.updates.download as any).mockImplementation(
|
||||
() => new Promise((resolve) => setTimeout(() => resolve({ success: true }), 100))
|
||||
);
|
||||
|
||||
render(
|
||||
<UpdateCheckModal
|
||||
theme={createMockTheme()}
|
||||
onClose={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Download and Install Update')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByText('Download and Install Update'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Downloading...')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const refreshButton = screen.getByTitle('Refresh');
|
||||
expect(refreshButton).toBeDisabled();
|
||||
});
|
||||
|
||||
it('shows download error when download fails', async () => {
|
||||
(window.maestro.updates.download as any).mockResolvedValue({
|
||||
success: false,
|
||||
error: 'Download failed: network error',
|
||||
});
|
||||
|
||||
render(
|
||||
<UpdateCheckModal
|
||||
theme={createMockTheme()}
|
||||
onClose={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Download and Install Update')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByText('Download and Install Update'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Download failed')).toBeInTheDocument();
|
||||
expect(screen.getByText('Download failed: network error')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('shows manual download link on error', async () => {
|
||||
(window.maestro.updates.download as any).mockResolvedValue({
|
||||
success: false,
|
||||
error: 'Some error',
|
||||
});
|
||||
|
||||
render(
|
||||
<UpdateCheckModal
|
||||
theme={createMockTheme()}
|
||||
onClose={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Download and Install Update')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByText('Download and Install Update'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Download manually from GitHub')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('opens GitHub releases when manual download link is clicked after error', async () => {
|
||||
(window.maestro.updates.download as any).mockResolvedValue({
|
||||
success: false,
|
||||
error: 'Some error',
|
||||
});
|
||||
|
||||
render(
|
||||
<UpdateCheckModal
|
||||
theme={createMockTheme()}
|
||||
onClose={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Download and Install Update')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByText('Download and Install Update'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Download manually from GitHub')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByText('Download manually from GitHub'));
|
||||
|
||||
expect(window.maestro.shell.openExternal).toHaveBeenCalledWith(
|
||||
'https://github.com/pedramamini/Maestro/releases'
|
||||
);
|
||||
});
|
||||
|
||||
it('subscribes to status updates on mount', async () => {
|
||||
render(
|
||||
<UpdateCheckModal
|
||||
theme={createMockTheme()}
|
||||
onClose={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(window.maestro.updates.onStatus).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('unsubscribes from status updates on unmount', async () => {
|
||||
const mockUnsubscribe = vi.fn();
|
||||
(window.maestro.updates.onStatus as any).mockReturnValue(mockUnsubscribe);
|
||||
|
||||
const { unmount } = render(
|
||||
<UpdateCheckModal
|
||||
theme={createMockTheme()}
|
||||
onClose={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
unmount();
|
||||
|
||||
expect(mockUnsubscribe).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// AUTO-UPDATE INSTALL FLOW
|
||||
// =============================================================================
|
||||
|
||||
describe('auto-update install flow', () => {
|
||||
it('calls install API when restart button is clicked', async () => {
|
||||
// Simulate downloaded state via onStatus callback
|
||||
let statusCallback: ((status: any) => void) | null = null;
|
||||
(window.maestro.updates.onStatus as any).mockImplementation((cb: any) => {
|
||||
statusCallback = cb;
|
||||
return vi.fn();
|
||||
});
|
||||
|
||||
render(
|
||||
<UpdateCheckModal
|
||||
theme={createMockTheme()}
|
||||
onClose={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Download and Install Update')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Simulate update downloaded status
|
||||
statusCallback!({ status: 'downloaded', info: { version: '1.1.0' } });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Restart to Update')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByText('Restart to Update'));
|
||||
|
||||
expect(window.maestro.updates.install).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('shows restart button after download completes', async () => {
|
||||
let statusCallback: ((status: any) => void) | null = null;
|
||||
(window.maestro.updates.onStatus as any).mockImplementation((cb: any) => {
|
||||
statusCallback = cb;
|
||||
return vi.fn();
|
||||
});
|
||||
|
||||
render(
|
||||
<UpdateCheckModal
|
||||
theme={createMockTheme()}
|
||||
onClose={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Download and Install Update')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Simulate download completed
|
||||
statusCallback!({ status: 'downloaded', info: { version: '1.1.0' } });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Restart to Update')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Download button should be replaced
|
||||
expect(screen.queryByText('Download and Install Update')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('restart button has success styling', async () => {
|
||||
let statusCallback: ((status: any) => void) | null = null;
|
||||
(window.maestro.updates.onStatus as any).mockImplementation((cb: any) => {
|
||||
statusCallback = cb;
|
||||
return vi.fn();
|
||||
});
|
||||
|
||||
const theme = createMockTheme();
|
||||
|
||||
render(
|
||||
<UpdateCheckModal
|
||||
theme={theme}
|
||||
onClose={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Download and Install Update')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
statusCallback!({ status: 'downloaded', info: { version: '1.1.0' } });
|
||||
|
||||
await waitFor(() => {
|
||||
const restartButton = screen.getByText('Restart to Update').closest('button');
|
||||
expect(restartButton).toHaveStyle({ backgroundColor: theme.colors.success });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// DOWNLOAD PROGRESS
|
||||
// =============================================================================
|
||||
|
||||
describe('download progress', () => {
|
||||
it('shows progress bar during download', async () => {
|
||||
let statusCallback: ((status: any) => void) | null = null;
|
||||
(window.maestro.updates.onStatus as any).mockImplementation((cb: any) => {
|
||||
statusCallback = cb;
|
||||
return vi.fn();
|
||||
});
|
||||
|
||||
render(
|
||||
<UpdateCheckModal
|
||||
theme={createMockTheme()}
|
||||
onClose={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Download and Install Update')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Simulate downloading with progress
|
||||
statusCallback!({
|
||||
status: 'downloading',
|
||||
progress: { percent: 50, bytesPerSecond: 1000000, total: 10000000, transferred: 5000000 },
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Downloading update...')).toBeInTheDocument();
|
||||
expect(screen.getByText('50%')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('shows download speed', async () => {
|
||||
let statusCallback: ((status: any) => void) | null = null;
|
||||
(window.maestro.updates.onStatus as any).mockImplementation((cb: any) => {
|
||||
statusCallback = cb;
|
||||
return vi.fn();
|
||||
});
|
||||
|
||||
render(
|
||||
<UpdateCheckModal
|
||||
theme={createMockTheme()}
|
||||
onClose={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Download and Install Update')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
statusCallback!({
|
||||
status: 'downloading',
|
||||
progress: { percent: 25, bytesPerSecond: 1048576, total: 10485760, transferred: 2621440 },
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('1 MB/s')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('shows transferred and total bytes', async () => {
|
||||
let statusCallback: ((status: any) => void) | null = null;
|
||||
(window.maestro.updates.onStatus as any).mockImplementation((cb: any) => {
|
||||
statusCallback = cb;
|
||||
return vi.fn();
|
||||
});
|
||||
|
||||
render(
|
||||
<UpdateCheckModal
|
||||
theme={createMockTheme()}
|
||||
onClose={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Download and Install Update')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
statusCallback!({
|
||||
status: 'downloading',
|
||||
progress: { percent: 50, bytesPerSecond: 500000, total: 10485760, transferred: 5242880 },
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('5 MB / 10 MB')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('updates progress bar width based on percent', async () => {
|
||||
let statusCallback: ((status: any) => void) | null = null;
|
||||
(window.maestro.updates.onStatus as any).mockImplementation((cb: any) => {
|
||||
statusCallback = cb;
|
||||
return vi.fn();
|
||||
});
|
||||
|
||||
render(
|
||||
<UpdateCheckModal
|
||||
theme={createMockTheme()}
|
||||
onClose={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Download and Install Update')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
statusCallback!({
|
||||
status: 'downloading',
|
||||
progress: { percent: 75, bytesPerSecond: 1000000, total: 10000000, transferred: 7500000 },
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('75%')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Check progress bar width
|
||||
const progressContainer = screen.getByText('75%').closest('div')?.parentElement;
|
||||
const progressBar = progressContainer?.querySelector('[class*="h-full"]');
|
||||
expect(progressBar).toHaveStyle({ width: '75%' });
|
||||
});
|
||||
|
||||
it('handles error status from onStatus callback', async () => {
|
||||
let statusCallback: ((status: any) => void) | null = null;
|
||||
(window.maestro.updates.onStatus as any).mockImplementation((cb: any) => {
|
||||
statusCallback = cb;
|
||||
return vi.fn();
|
||||
});
|
||||
|
||||
render(
|
||||
<UpdateCheckModal
|
||||
theme={createMockTheme()}
|
||||
onClose={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Download and Install Update')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Simulate error during download
|
||||
statusCallback!({
|
||||
status: 'error',
|
||||
error: 'Connection timed out',
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Download failed')).toBeInTheDocument();
|
||||
expect(screen.getByText('Connection timed out')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// FALLBACK LINK
|
||||
// =============================================================================
|
||||
|
||||
describe('fallback link', () => {
|
||||
it('always shows fallback link when update is available', async () => {
|
||||
render(
|
||||
<UpdateCheckModal
|
||||
theme={createMockTheme()}
|
||||
onClose={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Or download manually from GitHub')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('opens GitHub when fallback link is clicked', async () => {
|
||||
render(
|
||||
<UpdateCheckModal
|
||||
theme={createMockTheme()}
|
||||
onClose={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Or download manually from GitHub')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByText('Or download manually from GitHub'));
|
||||
|
||||
expect(window.maestro.shell.openExternal).toHaveBeenCalledWith(
|
||||
'https://github.com/pedramamini/Maestro/releases'
|
||||
);
|
||||
});
|
||||
|
||||
it('fallback link is visible during download', async () => {
|
||||
let statusCallback: ((status: any) => void) | null = null;
|
||||
(window.maestro.updates.onStatus as any).mockImplementation((cb: any) => {
|
||||
statusCallback = cb;
|
||||
return vi.fn();
|
||||
});
|
||||
|
||||
render(
|
||||
<UpdateCheckModal
|
||||
theme={createMockTheme()}
|
||||
onClose={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Download and Install Update')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
statusCallback!({
|
||||
status: 'downloading',
|
||||
progress: { percent: 50, bytesPerSecond: 1000000, total: 10000000, transferred: 5000000 },
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Or download manually from GitHub')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('fallback link is visible after download completes', async () => {
|
||||
let statusCallback: ((status: any) => void) | null = null;
|
||||
(window.maestro.updates.onStatus as any).mockImplementation((cb: any) => {
|
||||
statusCallback = cb;
|
||||
return vi.fn();
|
||||
});
|
||||
|
||||
render(
|
||||
<UpdateCheckModal
|
||||
theme={createMockTheme()}
|
||||
onClose={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Download and Install Update')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
statusCallback!({ status: 'downloaded', info: { version: '1.1.0' } });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Or download manually from GitHub')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
132
src/main/auto-updater.ts
Normal file
132
src/main/auto-updater.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* Auto-updater module for Maestro
|
||||
* Uses electron-updater to download and install updates from GitHub releases
|
||||
*/
|
||||
|
||||
import { autoUpdater, UpdateInfo, ProgressInfo } from 'electron-updater';
|
||||
import { BrowserWindow, ipcMain } from 'electron';
|
||||
import { logger } from './utils/logger';
|
||||
|
||||
// Don't auto-download - we want user to initiate
|
||||
autoUpdater.autoDownload = false;
|
||||
autoUpdater.autoInstallOnAppQuit = true;
|
||||
|
||||
export interface UpdateStatus {
|
||||
status: 'idle' | 'checking' | 'available' | 'not-available' | 'downloading' | 'downloaded' | 'error';
|
||||
info?: UpdateInfo;
|
||||
progress?: ProgressInfo;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
let mainWindow: BrowserWindow | null = null;
|
||||
let currentStatus: UpdateStatus = { status: 'idle' };
|
||||
|
||||
/**
|
||||
* Initialize the auto-updater and set up event handlers
|
||||
*/
|
||||
export function initAutoUpdater(window: BrowserWindow): void {
|
||||
mainWindow = window;
|
||||
|
||||
// Update available
|
||||
autoUpdater.on('update-available', (info: UpdateInfo) => {
|
||||
logger.info(`Update available: ${info.version}`, 'AutoUpdater');
|
||||
currentStatus = { status: 'available', info };
|
||||
sendStatusToRenderer();
|
||||
});
|
||||
|
||||
// No update available
|
||||
autoUpdater.on('update-not-available', (info: UpdateInfo) => {
|
||||
logger.debug(`No update available (current: ${info.version})`, 'AutoUpdater');
|
||||
currentStatus = { status: 'not-available', info };
|
||||
sendStatusToRenderer();
|
||||
});
|
||||
|
||||
// Download progress
|
||||
autoUpdater.on('download-progress', (progress: ProgressInfo) => {
|
||||
currentStatus = { ...currentStatus, status: 'downloading', progress };
|
||||
sendStatusToRenderer();
|
||||
});
|
||||
|
||||
// Update downloaded
|
||||
autoUpdater.on('update-downloaded', (info: UpdateInfo) => {
|
||||
logger.info(`Update downloaded: ${info.version}`, 'AutoUpdater');
|
||||
currentStatus = { status: 'downloaded', info };
|
||||
sendStatusToRenderer();
|
||||
});
|
||||
|
||||
// Error
|
||||
autoUpdater.on('error', (err: Error) => {
|
||||
logger.error(`Auto-update error: ${err.message}`, 'AutoUpdater');
|
||||
currentStatus = { status: 'error', error: err.message };
|
||||
sendStatusToRenderer();
|
||||
});
|
||||
|
||||
// Set up IPC handlers
|
||||
setupIpcHandlers();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send current status to renderer
|
||||
*/
|
||||
function sendStatusToRenderer(): void {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send('updates:status', currentStatus);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up IPC handlers for update operations
|
||||
*/
|
||||
function setupIpcHandlers(): void {
|
||||
// Check for updates using electron-updater (different from manual GitHub API check)
|
||||
ipcMain.handle('updates:checkAutoUpdater', async () => {
|
||||
try {
|
||||
currentStatus = { status: 'checking' };
|
||||
sendStatusToRenderer();
|
||||
const result = await autoUpdater.checkForUpdates();
|
||||
return { success: true, updateInfo: result?.updateInfo };
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
currentStatus = { status: 'error', error: errorMessage };
|
||||
sendStatusToRenderer();
|
||||
return { success: false, error: errorMessage };
|
||||
}
|
||||
});
|
||||
|
||||
// Download update
|
||||
ipcMain.handle('updates:download', async () => {
|
||||
try {
|
||||
currentStatus = { status: 'downloading', progress: { percent: 0, bytesPerSecond: 0, total: 0, transferred: 0, delta: 0 } };
|
||||
sendStatusToRenderer();
|
||||
await autoUpdater.downloadUpdate();
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
currentStatus = { status: 'error', error: errorMessage };
|
||||
sendStatusToRenderer();
|
||||
return { success: false, error: errorMessage };
|
||||
}
|
||||
});
|
||||
|
||||
// Install update (quit and install)
|
||||
ipcMain.handle('updates:install', () => {
|
||||
autoUpdater.quitAndInstall(false, true);
|
||||
});
|
||||
|
||||
// Get current status
|
||||
ipcMain.handle('updates:getStatus', () => {
|
||||
return currentStatus;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually trigger update check (can be called from main process)
|
||||
*/
|
||||
export async function checkForUpdatesManual(): Promise<UpdateInfo | null> {
|
||||
try {
|
||||
const result = await autoUpdater.checkForUpdates();
|
||||
return result?.updateInfo || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -412,6 +412,42 @@ export class HistoryManager {
|
||||
return paginateEntries(entries, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update sessionName for all entries matching a given claudeSessionId.
|
||||
* This is used when a tab is renamed to retroactively update past history entries.
|
||||
*/
|
||||
updateSessionNameByClaudeSessionId(claudeSessionId: string, sessionName: string): number {
|
||||
const sessions = this.listSessionsWithHistory();
|
||||
let updatedCount = 0;
|
||||
|
||||
for (const sessionId of sessions) {
|
||||
const filePath = this.getSessionFilePath(sessionId);
|
||||
if (!fs.existsSync(filePath)) continue;
|
||||
|
||||
try {
|
||||
const data: HistoryFileData = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
||||
let modified = false;
|
||||
|
||||
for (const entry of data.entries) {
|
||||
if (entry.claudeSessionId === claudeSessionId && entry.sessionName !== sessionName) {
|
||||
entry.sessionName = sessionName;
|
||||
modified = true;
|
||||
updatedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (modified) {
|
||||
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
|
||||
logger.debug(`Updated ${updatedCount} entries for claudeSessionId ${claudeSessionId} in session ${sessionId}`, LOG_CONTEXT);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(`Failed to update sessionName in session ${sessionId}: ${error}`, LOG_CONTEXT);
|
||||
}
|
||||
}
|
||||
|
||||
return updatedCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all sessions for a specific project
|
||||
*/
|
||||
|
||||
@@ -13,6 +13,7 @@ import Store from 'electron-store';
|
||||
import { getHistoryManager } from './history-manager';
|
||||
import { registerGitHandlers, registerAutorunHandlers, registerPlaybooksHandlers, registerHistoryHandlers, registerAgentsHandlers, registerProcessHandlers, registerPersistenceHandlers, registerSystemHandlers, setupLoggerEventForwarding } from './ipc/handlers';
|
||||
import { DEMO_MODE, DEMO_DATA_PATH, CLAUDE_SESSION_PARSE_LIMITS, CLAUDE_PRICING } from './constants';
|
||||
import { initAutoUpdater } from './auto-updater';
|
||||
import {
|
||||
SessionStatsCache,
|
||||
GlobalStatsCache,
|
||||
@@ -533,6 +534,12 @@ function createWindow() {
|
||||
logger.info('Browser window closed', 'Window');
|
||||
mainWindow = null;
|
||||
});
|
||||
|
||||
// Initialize auto-updater (only in production)
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
initAutoUpdater(mainWindow);
|
||||
logger.info('Auto-updater initialized', 'Window');
|
||||
}
|
||||
}
|
||||
|
||||
// Set up global error handlers for uncaught exceptions
|
||||
|
||||
@@ -166,6 +166,13 @@ export function registerHistoryHandlers(): void {
|
||||
return false;
|
||||
});
|
||||
|
||||
// Update sessionName for all entries matching a claudeSessionId (used when renaming tabs)
|
||||
ipcMain.handle('history:updateSessionName', async (_event, claudeSessionId: string, sessionName: string) => {
|
||||
const count = historyManager.updateSessionNameByClaudeSessionId(claudeSessionId, sessionName);
|
||||
logger.info(`Updated sessionName for ${count} history entries with claudeSessionId ${claudeSessionId}`, LOG_CONTEXT);
|
||||
return count;
|
||||
});
|
||||
|
||||
// NEW: Get history file path for AI context integration
|
||||
ipcMain.handle('history:getFilePath', async (_event, sessionId: string) => {
|
||||
return historyManager.getHistoryFilePath(sessionId);
|
||||
|
||||
@@ -375,6 +375,26 @@ contextBridge.exposeInMainWorld('maestro', {
|
||||
releasesUrl: string;
|
||||
error?: string;
|
||||
}>,
|
||||
// Auto-updater APIs (electron-updater)
|
||||
download: () => ipcRenderer.invoke('updates:download') as Promise<{ success: boolean; error?: string }>,
|
||||
install: () => ipcRenderer.invoke('updates:install') as Promise<void>,
|
||||
getStatus: () => ipcRenderer.invoke('updates:getStatus') as Promise<{
|
||||
status: 'idle' | 'checking' | 'available' | 'not-available' | 'downloading' | 'downloaded' | 'error';
|
||||
info?: { version: string };
|
||||
progress?: { percent: number; bytesPerSecond: number; total: number; transferred: number };
|
||||
error?: string;
|
||||
}>,
|
||||
// Subscribe to update status changes
|
||||
onStatus: (callback: (status: {
|
||||
status: 'idle' | 'checking' | 'available' | 'not-available' | 'downloading' | 'downloaded' | 'error';
|
||||
info?: { version: string };
|
||||
progress?: { percent: number; bytesPerSecond: number; total: number; transferred: number };
|
||||
error?: string;
|
||||
}) => void) => {
|
||||
const handler = (_: any, status: any) => callback(status);
|
||||
ipcRenderer.on('updates:status', handler);
|
||||
return () => ipcRenderer.removeListener('updates:status', handler);
|
||||
},
|
||||
},
|
||||
|
||||
// Logger API
|
||||
@@ -523,6 +543,9 @@ contextBridge.exposeInMainWorld('maestro', {
|
||||
ipcRenderer.invoke('history:delete', entryId, sessionId),
|
||||
update: (entryId: string, updates: { validated?: boolean }, sessionId?: string) =>
|
||||
ipcRenderer.invoke('history:update', entryId, updates, sessionId),
|
||||
// Update sessionName for all entries matching a claudeSessionId (used when renaming tabs)
|
||||
updateSessionName: (claudeSessionId: string, sessionName: string) =>
|
||||
ipcRenderer.invoke('history:updateSessionName', claudeSessionId, sessionName),
|
||||
// NEW: Get history file path for AI context integration
|
||||
getFilePath: (sessionId: string) =>
|
||||
ipcRenderer.invoke('history:getFilePath', sessionId),
|
||||
@@ -876,6 +899,20 @@ export interface MaestroAPI {
|
||||
releasesUrl: string;
|
||||
error?: string;
|
||||
}>;
|
||||
download: () => Promise<{ success: boolean; error?: string }>;
|
||||
install: () => Promise<void>;
|
||||
getStatus: () => Promise<{
|
||||
status: 'idle' | 'checking' | 'available' | 'not-available' | 'downloading' | 'downloaded' | 'error';
|
||||
info?: { version: string };
|
||||
progress?: { percent: number; bytesPerSecond: number; total: number; transferred: number };
|
||||
error?: string;
|
||||
}>;
|
||||
onStatus: (callback: (status: {
|
||||
status: 'idle' | 'checking' | 'available' | 'not-available' | 'downloading' | 'downloaded' | 'error';
|
||||
info?: { version: string };
|
||||
progress?: { percent: number; bytesPerSecond: number; total: number; transferred: number };
|
||||
error?: string;
|
||||
}) => void) => () => void;
|
||||
};
|
||||
logger: {
|
||||
log: (level: string, message: string, context?: string, data?: unknown) => Promise<void>;
|
||||
@@ -1079,6 +1116,8 @@ export interface MaestroAPI {
|
||||
clear: (projectPath?: string) => Promise<boolean>;
|
||||
delete: (entryId: string, sessionId?: string) => Promise<boolean>;
|
||||
update: (entryId: string, updates: { validated?: boolean }, sessionId?: string) => Promise<boolean>;
|
||||
// Update sessionName for all entries matching a claudeSessionId (used when renaming tabs)
|
||||
updateSessionName: (claudeSessionId: string, sessionName: string) => Promise<number>;
|
||||
onExternalChange: (handler: () => void) => () => void;
|
||||
reload: () => Promise<boolean>;
|
||||
// NEW: Get history file path for AI context integration
|
||||
|
||||
@@ -4745,6 +4745,11 @@ export default function MaestroConsole() {
|
||||
tab.claudeSessionId,
|
||||
newName || ''
|
||||
).catch(err => console.error('Failed to persist tab name:', err));
|
||||
// Also update past history entries with this claudeSessionId
|
||||
window.maestro.history.updateSessionName(
|
||||
tab.claudeSessionId,
|
||||
newName || ''
|
||||
).catch(err => console.error('Failed to update history session names:', err));
|
||||
}
|
||||
return {
|
||||
...s,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useRef, useState, useCallback } from 'react';
|
||||
import { X, Download, ExternalLink, Loader2, CheckCircle2, AlertCircle, RefreshCw, ChevronDown, ChevronRight } from 'lucide-react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { X, Download, ExternalLink, Loader2, CheckCircle2, AlertCircle, RefreshCw, ChevronDown, ChevronRight, RotateCcw } from 'lucide-react';
|
||||
import type { Theme } from '../types';
|
||||
import { useLayerStack } from '../contexts/LayerStackContext';
|
||||
import { MODAL_PRIORITIES } from '../constants/modalPriorities';
|
||||
@@ -23,6 +23,13 @@ interface UpdateCheckResult {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
interface UpdateStatus {
|
||||
status: 'idle' | 'checking' | 'available' | 'not-available' | 'downloading' | 'downloaded' | 'error';
|
||||
info?: { version: string };
|
||||
progress?: { percent: number; bytesPerSecond: number; total: number; transferred: number };
|
||||
error?: string;
|
||||
}
|
||||
|
||||
interface UpdateCheckModalProps {
|
||||
theme: Theme;
|
||||
onClose: () => void;
|
||||
@@ -36,6 +43,10 @@ export function UpdateCheckModal({ theme, onClose }: UpdateCheckModalProps) {
|
||||
const [result, setResult] = useState<UpdateCheckResult | null>(null);
|
||||
const [expandedReleases, setExpandedReleases] = useState<Set<string>>(new Set());
|
||||
|
||||
// Auto-updater state
|
||||
const [downloadStatus, setDownloadStatus] = useState<UpdateStatus>({ status: 'idle' });
|
||||
const [downloadError, setDownloadError] = useState<string | null>(null);
|
||||
|
||||
const onCloseRef = useRef(onClose);
|
||||
onCloseRef.current = onClose;
|
||||
|
||||
@@ -44,8 +55,20 @@ export function UpdateCheckModal({ theme, onClose }: UpdateCheckModalProps) {
|
||||
checkForUpdates();
|
||||
}, []);
|
||||
|
||||
// Subscribe to update status changes
|
||||
useEffect(() => {
|
||||
const unsubscribe = window.maestro.updates.onStatus((status) => {
|
||||
setDownloadStatus(status);
|
||||
if (status.status === 'error' && status.error) {
|
||||
setDownloadError(status.error);
|
||||
}
|
||||
});
|
||||
return () => unsubscribe();
|
||||
}, []);
|
||||
|
||||
const checkForUpdates = async () => {
|
||||
setLoading(true);
|
||||
setDownloadError(null);
|
||||
try {
|
||||
const updateResult = await window.maestro.updates.check();
|
||||
setResult(updateResult);
|
||||
@@ -90,6 +113,29 @@ export function UpdateCheckModal({ theme, onClose }: UpdateCheckModalProps) {
|
||||
});
|
||||
};
|
||||
|
||||
const formatBytes = (bytes: number) => {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
const handleDownloadUpdate = async () => {
|
||||
setDownloadError(null);
|
||||
setDownloadStatus({ status: 'downloading', progress: { percent: 0, bytesPerSecond: 0, total: 0, transferred: 0 } });
|
||||
|
||||
const downloadResult = await window.maestro.updates.download();
|
||||
if (!downloadResult.success && downloadResult.error) {
|
||||
setDownloadError(downloadResult.error);
|
||||
setDownloadStatus({ status: 'error', error: downloadResult.error });
|
||||
}
|
||||
};
|
||||
|
||||
const handleInstallUpdate = () => {
|
||||
window.maestro.updates.install();
|
||||
};
|
||||
|
||||
// Register layer on mount
|
||||
useEffect(() => {
|
||||
const id = registerLayer({
|
||||
@@ -112,6 +158,9 @@ export function UpdateCheckModal({ theme, onClose }: UpdateCheckModalProps) {
|
||||
};
|
||||
}, [registerLayer, unregisterLayer]);
|
||||
|
||||
const isDownloading = downloadStatus.status === 'downloading';
|
||||
const isDownloaded = downloadStatus.status === 'downloaded';
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
@@ -139,7 +188,7 @@ export function UpdateCheckModal({ theme, onClose }: UpdateCheckModalProps) {
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={checkForUpdates}
|
||||
disabled={loading}
|
||||
disabled={loading || isDownloading}
|
||||
className="p-1 rounded hover:bg-white/10 transition-colors disabled:opacity-50"
|
||||
title="Refresh"
|
||||
>
|
||||
@@ -210,27 +259,6 @@ export function UpdateCheckModal({ theme, onClose }: UpdateCheckModalProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Upgrade Instructions */}
|
||||
<div
|
||||
className="p-4 rounded border"
|
||||
style={{ borderColor: theme.colors.border, backgroundColor: theme.colors.bgActivity }}
|
||||
>
|
||||
<div className="text-sm font-bold mb-2" style={{ color: theme.colors.textMain }}>
|
||||
How to Upgrade
|
||||
</div>
|
||||
<div className="text-xs space-y-1" style={{ color: theme.colors.textDim }}>
|
||||
<p>Upgrading is simple - just download and replace the app binary:</p>
|
||||
<ol className="list-decimal list-inside ml-2 space-y-1">
|
||||
<li>Download the latest release for your platform</li>
|
||||
<li>Replace the existing Maestro app</li>
|
||||
<li>Restart Maestro</li>
|
||||
</ol>
|
||||
<p className="mt-2 pt-2 border-t" style={{ borderColor: theme.colors.border, color: theme.colors.success }}>
|
||||
All your data (sessions, settings, history) will persist automatically.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Release Notes */}
|
||||
<div>
|
||||
<div className="text-sm font-bold mb-3" style={{ color: theme.colors.textMain }}>
|
||||
@@ -313,16 +341,100 @@ export function UpdateCheckModal({ theme, onClose }: UpdateCheckModalProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Download Button */}
|
||||
<button
|
||||
onClick={() => window.maestro.shell.openExternal(result.releasesUrl)}
|
||||
className="w-full flex items-center justify-center gap-2 p-3 rounded-lg font-bold text-sm transition-colors hover:opacity-90"
|
||||
style={{ backgroundColor: theme.colors.accent, color: theme.colors.bgMain }}
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
Download Latest Release
|
||||
<ExternalLink className="w-3 h-3" />
|
||||
</button>
|
||||
{/* Download Error */}
|
||||
{downloadError && (
|
||||
<div
|
||||
className="p-3 rounded border text-xs"
|
||||
style={{
|
||||
backgroundColor: `${theme.colors.error}15`,
|
||||
borderColor: theme.colors.error,
|
||||
color: theme.colors.error
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<AlertCircle className="w-4 h-4" />
|
||||
<span className="font-bold">Download failed</span>
|
||||
</div>
|
||||
<p style={{ color: theme.colors.textDim }}>{downloadError}</p>
|
||||
<button
|
||||
onClick={() => window.maestro.shell.openExternal(result.releasesUrl)}
|
||||
className="flex items-center gap-1 mt-2 hover:underline"
|
||||
style={{ color: theme.colors.accent }}
|
||||
>
|
||||
Download manually from GitHub
|
||||
<ExternalLink className="w-3 h-3" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Download Progress */}
|
||||
{isDownloading && downloadStatus.progress && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between text-xs" style={{ color: theme.colors.textDim }}>
|
||||
<span>Downloading update...</span>
|
||||
<span>{Math.round(downloadStatus.progress.percent)}%</span>
|
||||
</div>
|
||||
<div
|
||||
className="h-2 rounded-full overflow-hidden"
|
||||
style={{ backgroundColor: theme.colors.bgActivity }}
|
||||
>
|
||||
<div
|
||||
className="h-full transition-all duration-300 rounded-full"
|
||||
style={{
|
||||
width: `${downloadStatus.progress.percent}%`,
|
||||
backgroundColor: theme.colors.accent
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-xs" style={{ color: theme.colors.textDim }}>
|
||||
<span>{formatBytes(downloadStatus.progress.transferred)} / {formatBytes(downloadStatus.progress.total)}</span>
|
||||
<span>{formatBytes(downloadStatus.progress.bytesPerSecond)}/s</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="space-y-2">
|
||||
{isDownloaded ? (
|
||||
<button
|
||||
onClick={handleInstallUpdate}
|
||||
className="w-full flex items-center justify-center gap-2 p-3 rounded-lg font-bold text-sm transition-colors hover:opacity-90"
|
||||
style={{ backgroundColor: theme.colors.success, color: theme.colors.bgMain }}
|
||||
>
|
||||
<RotateCcw className="w-4 h-4" />
|
||||
Restart to Update
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={handleDownloadUpdate}
|
||||
disabled={isDownloading}
|
||||
className="w-full flex items-center justify-center gap-2 p-3 rounded-lg font-bold text-sm transition-colors hover:opacity-90 disabled:opacity-50"
|
||||
style={{ backgroundColor: theme.colors.accent, color: theme.colors.bgMain }}
|
||||
>
|
||||
{isDownloading ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
Downloading...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Download className="w-4 h-4" />
|
||||
Download and Install Update
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Fallback link */}
|
||||
<button
|
||||
onClick={() => window.maestro.shell.openExternal(result.releasesUrl)}
|
||||
className="w-full flex items-center justify-center gap-2 p-2 rounded text-xs transition-colors hover:bg-white/5"
|
||||
style={{ color: theme.colors.textDim }}
|
||||
>
|
||||
Or download manually from GitHub
|
||||
<ExternalLink className="w-3 h-3" />
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center py-8 gap-3">
|
||||
|
||||
Reference in New Issue
Block a user