Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion electron/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,13 @@ app.setName('Modly')

process.on('uncaughtException', (err) => {
logger.error(`Uncaught exception: ${err.stack ?? err.message}`)
mainWindow?.webContents.send('app:error', err.stack ?? err.message)
})

process.on('unhandledRejection', (reason) => {
logger.error(`Unhandled rejection: ${String(reason)}`)
const msg = String(reason)
logger.error(`Unhandled rejection: ${msg}`)
mainWindow?.webContents.send('app:error', msg)
})

app.whenReady().then(async () => {
Expand Down
3 changes: 1 addition & 2 deletions electron/main/ipc-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,8 @@ export function setupIpcHandlers(pythonBridge: PythonBridge, getWindow: WindowGe

// Setup handlers — skipped in dev (uses .venv instead of python-embed)
ipcMain.handle('setup:check', async () => {
const defaultDataDir = join(app.getPath('documents'), 'LocalMeshy')
if (!app.isPackaged) return { needed: false, defaultDataDir }
const userData = app.getPath('userData')
const defaultDataDir = join(app.getPath('documents'), 'Modly')
return { needed: checkSetupNeeded(userData), defaultDataDir }
})

Expand Down
21 changes: 8 additions & 13 deletions electron/main/python-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,19 +186,14 @@ export class PythonBridge {
: join(app.getAppPath(), 'resources')
const userData = app.getPath('userData')

const candidates = app.isPackaged
? [
join(resourcesPath, 'python-embed', 'python.exe'), // Windows embeddable
join(userData, 'venv', 'bin', 'python'), // Linux/macOS venv (packaged)
'python3',
'python',
]
: [
join(apiDir, '.venv', 'Scripts', 'python.exe'), // Windows venv (dev)
join(apiDir, '.venv', 'bin', 'python'), // Unix/Mac venv (dev)
'python',
'python3',
]
const candidates = [
join(resourcesPath, 'python-embed', 'python.exe'), // Windows embedded (dev + packaged)
join(userData, 'venv', 'bin', 'python'), // Linux/macOS venv
join(apiDir, '.venv', 'Scripts', 'python.exe'), // legacy dev fallback
join(apiDir, '.venv', 'bin', 'python'), // legacy dev fallback
'python3',
'python',
]

for (const candidate of candidates) {
if (existsSync(candidate)) {
Expand Down
6 changes: 5 additions & 1 deletion electron/preload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,11 @@ contextBridge.exposeInMainWorld('electron', {
// App metadata
app: {
info: (): Promise<{ version: string; userData: string; modelsDir: string; apiUrl: string }> =>
ipcRenderer.invoke('app:info')
ipcRenderer.invoke('app:info'),
onError: (cb: (message: string) => void) => {
ipcRenderer.on('app:error', (_event, message) => cb(message))
},
offError: () => ipcRenderer.removeAllListeners('app:error'),
},

// Logging
Expand Down
14 changes: 12 additions & 2 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useAppStore } from '@shared/stores/appStore'
import FirstRunSetup from '@areas/setup/FirstRunSetup'
import MainLayout from '@shared/components/layout/MainLayout'
import { UpdateModal } from '@shared/components/ui/UpdateModal'
import { ErrorModal } from '@shared/components/ui/ErrorModal'

function compareSemver(a: string, b: string): number {
const pa = a.replace(/^v/, '').split('.').map(Number)
Expand All @@ -15,12 +16,15 @@ function compareSemver(a: string, b: string): number {
}

export default function App(): JSX.Element {
const { checkSetup, setupStatus, initApp, backendStatus } = useAppStore()
const { checkSetup, setupStatus, initApp, backendStatus, showError } = useAppStore()
const [updateVersion, setUpdateVersion] = useState<string | null>(null)
const [currentVersion, setCurrentVersion] = useState<string>('')

useEffect(() => {
checkSetup()
window.electron.app.onError((message) => showError(message))

return () => { window.electron.app.offError() }
}, [])

useEffect(() => {
Expand Down Expand Up @@ -51,7 +55,13 @@ export default function App(): JSX.Element {
onDismiss={() => setUpdateVersion(null)}
/>
)}
<ErrorModal />
</>
)
return (
<>
<FirstRunSetup />
<ErrorModal />
</>
)
return <FirstRunSetup />
}
57 changes: 57 additions & 0 deletions src/shared/components/ui/ErrorModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { useState } from 'react'
import { useAppStore } from '@shared/stores/appStore'

export function ErrorModal(): JSX.Element | null {
const { errorModal, hideError } = useAppStore()
const [copied, setCopied] = useState(false)

if (!errorModal) return null

const handleCopy = () => {
navigator.clipboard.writeText(errorModal).then(() => {
setCopied(true)
setTimeout(() => setCopied(false), 2000)
})
}

return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
<div className="w-full max-w-lg mx-4 bg-surface-300 border border-zinc-700/60 rounded-xl shadow-2xl flex flex-col">
{/* Header */}
<div className="flex items-center gap-3 px-5 py-4 border-b border-zinc-700/50">
<div className="w-7 h-7 rounded-full bg-red-500/15 border border-red-500/30 flex items-center justify-center shrink-0">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="text-red-400">
<circle cx="12" cy="12" r="10" />
<line x1="12" y1="8" x2="12" y2="12" />
<line x1="12" y1="16" x2="12.01" y2="16" />
</svg>
</div>
<span className="text-sm font-semibold text-zinc-100">An error occurred</span>
</div>

{/* Error message — selectable */}
<div className="px-5 py-4">
<pre className="text-xs text-red-400 bg-red-950/30 border border-red-900/30 rounded-lg px-4 py-3 max-h-60 overflow-y-auto whitespace-pre-wrap break-words select-text font-mono leading-relaxed">
{errorModal}
</pre>
</div>

{/* Actions */}
<div className="flex items-center justify-end gap-2 px-5 pb-4">
<button
onClick={handleCopy}
className="px-4 py-2 text-xs font-semibold rounded-lg bg-zinc-700/60 hover:bg-zinc-700 text-zinc-200 transition-colors"
>
{copied ? 'Copied!' : 'Copy'}
</button>
<button
onClick={hideError}
className="px-4 py-2 text-xs font-semibold rounded-lg bg-accent hover:bg-accent-dark text-white transition-colors"
>
Dismiss
</button>
</div>
</div>
</div>
)
}
22 changes: 16 additions & 6 deletions src/shared/stores/appStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,11 @@ interface AppState {
runSetup: () => Promise<void>
saveDataDir: (baseDir: string) => Promise<void>

// Error modal
errorModal: string | null
showError: (message: string) => void
hideError: () => void

// Actions
initApp: () => Promise<void>
setCurrentJob: (job: GenerationJob | null) => void
Expand Down Expand Up @@ -140,6 +145,10 @@ export const useAppStore = create<AppState>()(
window.electron.setup.run()
},

errorModal: null,
showError: (message) => set({ errorModal: message }),
hideError: () => set({ errorModal: null }),

currentJob: null,
selectedImagePath: null,
setSelectedImagePath: (path) => set({ selectedImagePath: path }),
Expand All @@ -157,8 +166,10 @@ export const useAppStore = create<AppState>()(
set({ backendStatus: 'starting', backendError: null })

window.electron.python.offCrashed()
window.electron.python.onCrashed(() => {
set({ backendStatus: 'error', apiUrl: '', backendError: 'FastAPI crashed unexpectedly' })
window.electron.python.onCrashed(({ code }) => {
const msg = `FastAPI process crashed unexpectedly (exit code: ${code ?? 'unknown'})`
set({ backendStatus: 'error', apiUrl: '', backendError: msg })
get().showError(msg)
})

try {
Expand All @@ -167,10 +178,9 @@ export const useAppStore = create<AppState>()(
const { apiUrl } = await window.electron.app.info()
set({ backendStatus: 'ready', apiUrl })
} catch (err) {
set({
backendStatus: 'error',
backendError: err instanceof Error ? err.message : String(err),
})
const msg = err instanceof Error ? err.message : String(err)
set({ backendStatus: 'error', backendError: msg })
get().showError(msg)
}
},

Expand Down
2 changes: 2 additions & 0 deletions src/shared/types/electron.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ declare global {
modelsDir: string
apiUrl: string
}>
onError: (cb: (message: string) => void) => void
offError: () => void
}
log: {
error: (message: string) => void
Expand Down