mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-26 12:35:35 +02:00
merge: resolve conflicts with origin/main
Keep both features: - App focus event (HEAD) - Auto-update functionality (origin/main) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
3
dev-app-update.yml
Normal file
3
dev-app-update.yml
Normal file
@@ -0,0 +1,3 @@
|
||||
provider: github
|
||||
owner: multica-ai
|
||||
repo: multica
|
||||
@@ -19,6 +19,7 @@ nsis:
|
||||
uninstallDisplayName: ${productName}
|
||||
createDesktopShortcut: always
|
||||
mac:
|
||||
artifactName: ${productName}-${version}-${arch}-mac.${ext}
|
||||
entitlementsInherit: build/entitlements.mac.plist
|
||||
extendInfo:
|
||||
- NSCameraUsageDescription: Application requests access to the device's camera.
|
||||
@@ -39,5 +40,6 @@ appImage:
|
||||
artifactName: ${name}-${version}.${ext}
|
||||
npmRebuild: false
|
||||
publish:
|
||||
provider: generic
|
||||
url: https://example.com/auto-updates
|
||||
provider: github
|
||||
owner: multica-ai
|
||||
repo: multica
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "multica",
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.2",
|
||||
"description": "Multiplexed Information and Computing Agent - A GUI client that brings coding agent capabilities to everyone",
|
||||
"main": "./out/main/index.js",
|
||||
"author": "Multica",
|
||||
@@ -45,6 +45,7 @@
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"electron-updater": "^6.7.3",
|
||||
"fix-path": "^5.0.0",
|
||||
"lucide-react": "^0.562.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
|
||||
35
pnpm-lock.yaml
generated
35
pnpm-lock.yaml
generated
@@ -56,6 +56,9 @@ importers:
|
||||
clsx:
|
||||
specifier: ^2.1.1
|
||||
version: 2.1.1
|
||||
electron-updater:
|
||||
specifier: ^6.7.3
|
||||
version: 6.7.3
|
||||
fix-path:
|
||||
specifier: ^5.0.0
|
||||
version: 5.0.0
|
||||
@@ -2073,6 +2076,9 @@ packages:
|
||||
electron-to-chromium@1.5.267:
|
||||
resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==}
|
||||
|
||||
electron-updater@6.7.3:
|
||||
resolution: {integrity: sha512-EgkT8Z9noqXKbwc3u5FkJA+r48jwZ5DTUiOkJMOTEEH//n5Am6wfQGz7nvSFEA2oIAMv9jRzn5JKTyWeSKOPgg==}
|
||||
|
||||
electron-vite@5.0.0:
|
||||
resolution: {integrity: sha512-OHp/vjdlubNlhNkPkL/+3JD34ii5ov7M0GpuXEVdQeqdQ3ulvVR7Dg/rNBLfS5XPIFwgoBLDf9sjjrL+CuDyRQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
@@ -2947,6 +2953,13 @@ packages:
|
||||
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
lodash.escaperegexp@4.1.2:
|
||||
resolution: {integrity: sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==}
|
||||
|
||||
lodash.isequal@4.5.0:
|
||||
resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==}
|
||||
deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead.
|
||||
|
||||
lodash.merge@4.6.2:
|
||||
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
|
||||
|
||||
@@ -3909,6 +3922,9 @@ packages:
|
||||
tiny-async-pool@1.3.0:
|
||||
resolution: {integrity: sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==}
|
||||
|
||||
tiny-typed-emitter@2.1.0:
|
||||
resolution: {integrity: sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==}
|
||||
|
||||
tinybench@2.9.0:
|
||||
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
|
||||
|
||||
@@ -6193,6 +6209,19 @@ snapshots:
|
||||
|
||||
electron-to-chromium@1.5.267: {}
|
||||
|
||||
electron-updater@6.7.3:
|
||||
dependencies:
|
||||
builder-util-runtime: 9.5.1
|
||||
fs-extra: 10.1.0
|
||||
js-yaml: 4.1.1
|
||||
lazy-val: 1.0.5
|
||||
lodash.escaperegexp: 4.1.2
|
||||
lodash.isequal: 4.5.0
|
||||
semver: 7.7.3
|
||||
tiny-typed-emitter: 2.1.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
electron-vite@5.0.0(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)):
|
||||
dependencies:
|
||||
'@babel/core': 7.28.6
|
||||
@@ -7254,6 +7283,10 @@ snapshots:
|
||||
dependencies:
|
||||
p-locate: 5.0.0
|
||||
|
||||
lodash.escaperegexp@4.1.2: {}
|
||||
|
||||
lodash.isequal@4.5.0: {}
|
||||
|
||||
lodash.merge@4.6.2: {}
|
||||
|
||||
lodash@4.17.21: {}
|
||||
@@ -8547,6 +8580,8 @@ snapshots:
|
||||
dependencies:
|
||||
semver: 5.7.2
|
||||
|
||||
tiny-typed-emitter@2.1.0: {}
|
||||
|
||||
tinybench@2.9.0: {}
|
||||
|
||||
tinyexec@1.0.2: {}
|
||||
|
||||
@@ -13,11 +13,13 @@ import { Conductor } from './conductor/Conductor'
|
||||
import { IPC_CHANNELS } from '../shared/ipc-channels'
|
||||
import type { PermissionResponse } from '../shared/electron-api'
|
||||
import { PermissionManager } from './permission'
|
||||
import { createUpdater, AutoUpdater } from './updater'
|
||||
|
||||
// Global instances
|
||||
let conductor: Conductor
|
||||
let mainWindow: BrowserWindow | null = null
|
||||
let permissionManager: PermissionManager
|
||||
let updater: AutoUpdater
|
||||
|
||||
function createWindow(): BrowserWindow {
|
||||
const window = new BrowserWindow({
|
||||
@@ -134,6 +136,32 @@ app.whenReady().then(async () => {
|
||||
}
|
||||
})
|
||||
|
||||
// Initialize auto-updater
|
||||
// Set FORCE_DEV_UPDATE=true to test updates in dev mode
|
||||
const forceDevUpdate = process.env.FORCE_DEV_UPDATE === 'true'
|
||||
updater = createUpdater(forceDevUpdate)
|
||||
updater.setMainWindow(() => mainWindow)
|
||||
|
||||
// Auto-check for updates (in production or when forced)
|
||||
if (!is.dev || forceDevUpdate) {
|
||||
mainWindow.once('ready-to-show', () => {
|
||||
updater.checkForUpdates()
|
||||
})
|
||||
}
|
||||
|
||||
// Register update IPC handlers
|
||||
ipcMain.handle(IPC_CHANNELS.UPDATE_CHECK, async () => {
|
||||
await updater.checkForUpdates()
|
||||
})
|
||||
|
||||
ipcMain.handle(IPC_CHANNELS.UPDATE_DOWNLOAD, async () => {
|
||||
await updater.downloadUpdate()
|
||||
})
|
||||
|
||||
ipcMain.handle(IPC_CHANNELS.UPDATE_INSTALL, () => {
|
||||
updater.quitAndInstall()
|
||||
})
|
||||
|
||||
app.on('activate', function () {
|
||||
// On macOS it's common to re-create a window when dock icon is clicked
|
||||
if (BrowserWindow.getAllWindows().length === 0) createWindow()
|
||||
|
||||
122
src/main/updater/index.ts
Normal file
122
src/main/updater/index.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* Auto-updater module using electron-updater
|
||||
* Checks for updates from GitHub releases and handles download/install
|
||||
*/
|
||||
import { autoUpdater, UpdateInfo, ProgressInfo } from 'electron-updater'
|
||||
import { BrowserWindow } from 'electron'
|
||||
import { IPC_CHANNELS } from '../../shared/ipc-channels'
|
||||
|
||||
export interface UpdateStatus {
|
||||
status: 'checking' | 'available' | 'not-available' | 'downloading' | 'downloaded' | 'error'
|
||||
info?: UpdateInfo
|
||||
progress?: ProgressInfo
|
||||
error?: string
|
||||
}
|
||||
|
||||
export class AutoUpdater {
|
||||
private mainWindow: (() => BrowserWindow | null) | null = null
|
||||
|
||||
constructor(forceDevUpdateConfig = false) {
|
||||
// Configure auto-updater
|
||||
autoUpdater.autoDownload = false
|
||||
autoUpdater.autoInstallOnAppQuit = true
|
||||
|
||||
// Enable update checking in dev mode for testing
|
||||
if (forceDevUpdateConfig) {
|
||||
autoUpdater.forceDevUpdateConfig = true
|
||||
console.log('[AutoUpdater] Force dev update config enabled')
|
||||
}
|
||||
|
||||
// Enable logging
|
||||
autoUpdater.logger = {
|
||||
info: (msg) => console.log('[AutoUpdater]', msg),
|
||||
warn: (msg) => console.warn('[AutoUpdater]', msg),
|
||||
error: (msg) => console.error('[AutoUpdater]', msg),
|
||||
debug: (msg) => console.log('[AutoUpdater:debug]', msg)
|
||||
}
|
||||
|
||||
// Set up event handlers
|
||||
autoUpdater.on('checking-for-update', () => {
|
||||
this.sendStatus({ status: 'checking' })
|
||||
})
|
||||
|
||||
autoUpdater.on('update-available', (info: UpdateInfo) => {
|
||||
this.sendStatus({ status: 'available', info })
|
||||
})
|
||||
|
||||
autoUpdater.on('update-not-available', (info: UpdateInfo) => {
|
||||
this.sendStatus({ status: 'not-available', info })
|
||||
})
|
||||
|
||||
autoUpdater.on('download-progress', (progress: ProgressInfo) => {
|
||||
this.sendStatus({ status: 'downloading', progress })
|
||||
})
|
||||
|
||||
autoUpdater.on('update-downloaded', (info: UpdateInfo) => {
|
||||
this.sendStatus({ status: 'downloaded', info })
|
||||
})
|
||||
|
||||
autoUpdater.on('error', (err: Error) => {
|
||||
this.sendStatus({ status: 'error', error: err.message })
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the main window reference for sending IPC messages
|
||||
*/
|
||||
setMainWindow(getWindow: () => BrowserWindow | null): void {
|
||||
this.mainWindow = getWindow
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for updates
|
||||
*/
|
||||
async checkForUpdates(): Promise<void> {
|
||||
try {
|
||||
await autoUpdater.checkForUpdates()
|
||||
} catch (err) {
|
||||
console.error('[AutoUpdater] Check for updates failed:', err)
|
||||
this.sendStatus({
|
||||
status: 'error',
|
||||
error: err instanceof Error ? err.message : 'Unknown error'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download the available update
|
||||
*/
|
||||
async downloadUpdate(): Promise<void> {
|
||||
try {
|
||||
await autoUpdater.downloadUpdate()
|
||||
} catch (err) {
|
||||
console.error('[AutoUpdater] Download update failed:', err)
|
||||
this.sendStatus({
|
||||
status: 'error',
|
||||
error: err instanceof Error ? err.message : 'Download failed'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Quit and install the downloaded update
|
||||
*/
|
||||
quitAndInstall(): void {
|
||||
autoUpdater.quitAndInstall()
|
||||
}
|
||||
|
||||
/**
|
||||
* Send update status to renderer
|
||||
*/
|
||||
private sendStatus(status: UpdateStatus): void {
|
||||
const window = this.mainWindow?.()
|
||||
if (window && !window.isDestroyed()) {
|
||||
window.webContents.send(IPC_CHANNELS.UPDATE_STATUS, status)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Factory function to create updater with options
|
||||
export function createUpdater(forceDevUpdateConfig = false): AutoUpdater {
|
||||
return new AutoUpdater(forceDevUpdateConfig)
|
||||
}
|
||||
@@ -110,6 +110,18 @@ const electronAPI: ElectronAPI = {
|
||||
const listener = () => callback()
|
||||
ipcRenderer.on(IPC_CHANNELS.APP_FOCUS, listener)
|
||||
return () => ipcRenderer.removeListener(IPC_CHANNELS.APP_FOCUS, listener)
|
||||
},
|
||||
|
||||
// Auto-update
|
||||
checkForUpdates: () => ipcRenderer.invoke(IPC_CHANNELS.UPDATE_CHECK),
|
||||
downloadUpdate: () => ipcRenderer.invoke(IPC_CHANNELS.UPDATE_DOWNLOAD),
|
||||
installUpdate: () => ipcRenderer.invoke(IPC_CHANNELS.UPDATE_INSTALL),
|
||||
|
||||
onUpdateStatus: (callback) => {
|
||||
const listener = (_event: Electron.IpcRendererEvent, status: unknown) =>
|
||||
callback(status as Parameters<typeof callback>[0])
|
||||
ipcRenderer.on(IPC_CHANNELS.UPDATE_STATUS, listener)
|
||||
return () => ipcRenderer.removeListener(IPC_CHANNELS.UPDATE_STATUS, listener)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
import { useState } from 'react'
|
||||
import { useApp } from './hooks/useApp'
|
||||
import { ChatView, MessageInput, StatusBar } from './components'
|
||||
import { ChatView, MessageInput, StatusBar, UpdateNotification } from './components'
|
||||
import { AppSidebar } from './components/AppSidebar'
|
||||
import { Modals } from './components/Modals'
|
||||
import { ThemeProvider } from './contexts/ThemeContext'
|
||||
@@ -181,6 +181,9 @@ function AppContent(): React.JSX.Element {
|
||||
|
||||
{/* Toast notifications */}
|
||||
<Toaster position="bottom-right" />
|
||||
|
||||
{/* Update notification */}
|
||||
<UpdateNotification />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
118
src/renderer/src/components/UpdateNotification.tsx
Normal file
118
src/renderer/src/components/UpdateNotification.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* Update notification component
|
||||
* Shows when a new version is available and allows user to download/install
|
||||
*/
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Download, RefreshCw, X, CheckCircle, AlertCircle } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import type { UpdateStatus } from '../../../shared/electron-api'
|
||||
|
||||
export function UpdateNotification(): React.JSX.Element | null {
|
||||
const [updateStatus, setUpdateStatus] = useState<UpdateStatus | null>(null)
|
||||
const [dismissed, setDismissed] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = window.electronAPI.onUpdateStatus((status) => {
|
||||
setUpdateStatus(status)
|
||||
// Reset dismissed state when a new update becomes available
|
||||
if (status.status === 'available') {
|
||||
setDismissed(false)
|
||||
}
|
||||
})
|
||||
|
||||
return () => unsubscribe()
|
||||
}, [])
|
||||
|
||||
const handleDownload = async () => {
|
||||
await window.electronAPI.downloadUpdate()
|
||||
}
|
||||
|
||||
const handleInstall = () => {
|
||||
window.electronAPI.installUpdate()
|
||||
}
|
||||
|
||||
const handleDismiss = () => {
|
||||
setDismissed(true)
|
||||
}
|
||||
|
||||
// Don't show if dismissed or no relevant status
|
||||
if (dismissed) return null
|
||||
if (!updateStatus) return null
|
||||
if (updateStatus.status === 'checking' || updateStatus.status === 'not-available') return null
|
||||
|
||||
const version = updateStatus.info?.version
|
||||
const isError = updateStatus.status === 'error'
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-4 right-4 z-50 animate-in slide-in-from-bottom-2 fade-in duration-300">
|
||||
<div className="flex items-center gap-3 rounded-lg border bg-card p-3 shadow-lg">
|
||||
{/* Icon */}
|
||||
<div
|
||||
className={`flex h-8 w-8 items-center justify-center rounded-full ${isError ? 'bg-destructive/10' : 'bg-primary/10'}`}
|
||||
>
|
||||
{isError ? (
|
||||
<AlertCircle className="h-4 w-4 text-destructive" />
|
||||
) : updateStatus.status === 'downloaded' ? (
|
||||
<CheckCircle className="h-4 w-4 text-primary" />
|
||||
) : updateStatus.status === 'downloading' ? (
|
||||
<RefreshCw className="h-4 w-4 text-primary animate-spin" />
|
||||
) : (
|
||||
<Download className="h-4 w-4 text-primary" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span className="text-sm font-medium">
|
||||
{isError
|
||||
? 'Update failed'
|
||||
: updateStatus.status === 'downloaded'
|
||||
? 'Update ready'
|
||||
: updateStatus.status === 'downloading'
|
||||
? 'Downloading update...'
|
||||
: 'Update available'}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{isError
|
||||
? 'Please download manually from GitHub'
|
||||
: updateStatus.status === 'downloading' && updateStatus.progress
|
||||
? `${Math.round(updateStatus.progress.percent)}%`
|
||||
: version
|
||||
? `Version ${version}`
|
||||
: 'New version available'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center gap-1 ml-2">
|
||||
{updateStatus.status === 'available' && (
|
||||
<Button size="sm" variant="default" onClick={handleDownload}>
|
||||
Download
|
||||
</Button>
|
||||
)}
|
||||
{updateStatus.status === 'downloaded' && (
|
||||
<Button size="sm" variant="default" onClick={handleInstall}>
|
||||
Restart
|
||||
</Button>
|
||||
)}
|
||||
{isError && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() =>
|
||||
window.open('https://github.com/multica-ai/multica/releases', '_blank')
|
||||
}
|
||||
>
|
||||
View Releases
|
||||
</Button>
|
||||
)}
|
||||
{updateStatus.status !== 'downloading' && (
|
||||
<Button size="icon" variant="ghost" className="h-7 w-7" onClick={handleDismiss}>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -4,3 +4,4 @@ export { ChatView } from './ChatView'
|
||||
export { MessageInput } from './MessageInput'
|
||||
export { StatusBar } from './StatusBar'
|
||||
export { Settings } from './Settings'
|
||||
export { UpdateNotification } from './UpdateNotification'
|
||||
|
||||
27
src/shared/electron-api.d.ts
vendored
27
src/shared/electron-api.d.ts
vendored
@@ -127,6 +127,27 @@ export interface InstallResult {
|
||||
error?: string
|
||||
}
|
||||
|
||||
// Auto-update types
|
||||
export interface UpdateInfo {
|
||||
version: string
|
||||
releaseDate?: string
|
||||
releaseNotes?: string | null
|
||||
}
|
||||
|
||||
export interface UpdateProgress {
|
||||
percent: number
|
||||
bytesPerSecond: number
|
||||
total: number
|
||||
transferred: number
|
||||
}
|
||||
|
||||
export interface UpdateStatus {
|
||||
status: 'checking' | 'available' | 'not-available' | 'downloading' | 'downloaded' | 'error'
|
||||
info?: UpdateInfo
|
||||
progress?: UpdateProgress
|
||||
error?: string
|
||||
}
|
||||
|
||||
export interface ElectronAPI {
|
||||
// Agent status (per-session agents)
|
||||
getAgentStatus(): Promise<RunningSessionsStatus>
|
||||
@@ -180,6 +201,12 @@ export interface ElectronAPI {
|
||||
|
||||
// App lifecycle
|
||||
onAppFocus(callback: () => void): () => void
|
||||
|
||||
// Auto-update
|
||||
checkForUpdates(): Promise<void>
|
||||
downloadUpdate(): Promise<void>
|
||||
installUpdate(): Promise<void>
|
||||
onUpdateStatus(callback: (status: UpdateStatus) => void): () => void
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -52,7 +52,13 @@ export const IPC_CHANNELS = {
|
||||
TERMINAL_RUN: 'terminal:run',
|
||||
|
||||
// App lifecycle
|
||||
APP_FOCUS: 'app:focus'
|
||||
APP_FOCUS: 'app:focus',
|
||||
|
||||
// Auto-update
|
||||
UPDATE_CHECK: 'update:check',
|
||||
UPDATE_DOWNLOAD: 'update:download',
|
||||
UPDATE_INSTALL: 'update:install',
|
||||
UPDATE_STATUS: 'update:status'
|
||||
} as const
|
||||
|
||||
export type IPCChannel = (typeof IPC_CHANNELS)[keyof typeof IPC_CHANNELS]
|
||||
|
||||
Reference in New Issue
Block a user