mirror of
https://github.com/stablyai/orca.git
synced 2026-10-02 00:02:05 +00:00
Replace the copy-a-command startup dialog for diverged JSON/SQLite profile state with Use SQLite / Use JSON buttons. The choice relaunches Orca into the locked recovery preflight, applies it, then starts normally. Adds a current-sqlite recovery selector (and --current-sqlite CLI flag) that archives the diverged JSON and republishes it from SQLite. Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
175 lines
7.0 KiB
TypeScript
175 lines
7.0 KiB
TypeScript
import { app, clipboard, dialog, type BrowserWindow } from 'electron'
|
|
import { parseSkillShareId } from '../shared/skill-share-link'
|
|
import { createMacAppActivationHandler } from './window/macos-app-activation'
|
|
import { isBackgroundLaunch } from './window/foreground-activation-policy'
|
|
import {
|
|
focusExistingWindow as focusExistingWindowAction,
|
|
setMainWindowOpener
|
|
} from './startup/main-window-actions'
|
|
import { openMainWindow as openMainWindowController } from './startup/main-window-controller'
|
|
import { mainProcessState as state } from './startup/main-process-state'
|
|
import { runMainProcessPreflight } from './startup/main-process-preflight'
|
|
import { registerMainProcessIpcHandlers } from './startup/main-process-ipc-bootstrap'
|
|
import { initializeMainProcessReady } from './startup/main-process-ready'
|
|
import { installMainProcessQuitHandlers } from './startup/main-process-quit'
|
|
import { shouldActivateDesktopForSecondInstance } from './startup/single-instance-lock'
|
|
import { resolveOpenedMarkdownDocuments } from './startup/os-opened-markdown-files'
|
|
import {
|
|
formatProfileStateStartupFailure,
|
|
isDivergedProfileStateFailure,
|
|
profileStateStartupFailureClass
|
|
} from './persistence/profile-state/profile-state-startup-failure'
|
|
import { recordDurableCrashBreadcrumb } from './crash-reporting/durable-crash-breadcrumb'
|
|
import {
|
|
chooseProfileStateCopy,
|
|
presentProfileStateStartupRecoveryDialog,
|
|
readProfileStateCopySavedTimes
|
|
} from './persistence/profile-state/profile-state-startup-recovery-dialog'
|
|
import { profileStateDesktopRecoveryArgs } from './startup/profile-state-recovery-preflight'
|
|
|
|
function openMainWindow(options: { revealOnDidFinishLoad?: boolean } = {}): BrowserWindow {
|
|
return openMainWindowController(options)
|
|
}
|
|
|
|
setMainWindowOpener(openMainWindow)
|
|
|
|
function focusExistingWindow(): void {
|
|
focusExistingWindowAction()
|
|
}
|
|
|
|
function requestDesktopActivation(argv: readonly string[] = []): void {
|
|
state.skillShareDeepLinks.capture(argv, (shareId) => {
|
|
state.mainWindow?.webContents.send('ui:openSkillShare', shareId)
|
|
})
|
|
state.osOpenedMarkdownFiles.capture(argv, publishOsOpenedMarkdownFiles)
|
|
// Why: a duplicate `orca serve` must not drag a headless server into opening a desktop window (#11935).
|
|
if (!shouldActivateDesktopForSecondInstance(argv)) {
|
|
return
|
|
}
|
|
state.desktopActivationGate?.requestActivation()
|
|
}
|
|
|
|
/**
|
|
* Hands buffered OS-opened markdown paths to a renderer that has proven it is listening.
|
|
*
|
|
* Until that proof arrives the paths stay buffered, because `webContents.send` to a renderer
|
|
* with no listener attached is dropped silently and the queue would be gone.
|
|
*/
|
|
function publishOsOpenedMarkdownFiles(): void {
|
|
const targetWindow = state.mainWindow
|
|
if (!state.markdownFileOpenListenerReady || !targetWindow || targetWindow.isDestroyed()) {
|
|
return
|
|
}
|
|
// Why consumed before the await: a renderer pull racing this resolve must not take the same
|
|
// batch again. The restore() calls hand it back if delivery turns out to be impossible.
|
|
const filePaths = state.osOpenedMarkdownFiles.consume()
|
|
if (filePaths.length === 0) {
|
|
return
|
|
}
|
|
void resolveOpenedMarkdownDocuments(filePaths)
|
|
.then((documents) => {
|
|
if (targetWindow.isDestroyed() || targetWindow.webContents.isDestroyed()) {
|
|
state.osOpenedMarkdownFiles.restore(filePaths)
|
|
return
|
|
}
|
|
if (documents.length > 0) {
|
|
targetWindow.webContents.send('ui:openMarkdownFiles', documents)
|
|
}
|
|
})
|
|
.catch((error) => {
|
|
state.osOpenedMarkdownFiles.restore(filePaths)
|
|
console.warn('[os-open] Failed to resolve OS-opened markdown files:', error)
|
|
})
|
|
}
|
|
|
|
const handleMacAppActivation = createMacAppActivationHandler({
|
|
getWindow: () => state.mainWindow,
|
|
requestActivation: requestDesktopActivation
|
|
})
|
|
|
|
const preflightReady = runMainProcessPreflight({
|
|
focusExistingWindow,
|
|
requestDesktopActivation
|
|
})
|
|
|
|
// Why: when another process holds the lock we've already exited; skip file-writing side effects so this transient process never touches userData.
|
|
if (preflightReady) {
|
|
app.on('open-url', (event, url) => {
|
|
if (!parseSkillShareId(url)) {
|
|
return
|
|
}
|
|
event.preventDefault()
|
|
requestDesktopActivation([url])
|
|
})
|
|
// Why: macOS delivers "Open With" as open-file, often before `ready`, and only to a handler
|
|
// that claims the event. Non-markdown paths stay unclaimed so the OS default handler wins.
|
|
app.on('open-file', (event, filePath) => {
|
|
if (!state.osOpenedMarkdownFiles.captureFilePaths([filePath], publishOsOpenedMarkdownFiles)) {
|
|
return
|
|
}
|
|
event.preventDefault()
|
|
// Why gated on isReady: pre-ready the cold-start window is already on its way, and
|
|
// activating the gate here would try to open one before Electron can.
|
|
if (app.isReady()) {
|
|
requestDesktopActivation()
|
|
}
|
|
})
|
|
state.skillShareDeepLinks.capture(process.argv)
|
|
// Why no publish: nothing is listening this early, so the first renderer pulls these on mount.
|
|
state.osOpenedMarkdownFiles.capture(process.argv)
|
|
registerMainProcessIpcHandlers()
|
|
installMainProcessQuitHandlers()
|
|
void app.whenReady().then(async () => {
|
|
try {
|
|
await initializeMainProcessReady({
|
|
openMainWindow,
|
|
handleMacAppActivation
|
|
})
|
|
} catch (error) {
|
|
const message =
|
|
formatProfileStateStartupFailure(error) ??
|
|
`Orca could not finish starting: ${error instanceof Error ? error.message : String(error)}`
|
|
const failureClass = profileStateStartupFailureClass(error)
|
|
if (failureClass !== undefined) {
|
|
recordDurableCrashBreadcrumb('profile_state_startup_failed', {
|
|
failure_class: failureClass
|
|
})
|
|
}
|
|
console.error(`[profile-state] ${message}`)
|
|
if (!state.isServeMode && !isBackgroundLaunch()) {
|
|
try {
|
|
if (isDivergedProfileStateFailure(error)) {
|
|
const userDataPath = app.getPath('userData')
|
|
const choice = await chooseProfileStateCopy({
|
|
...readProfileStateCopySavedTimes(userDataPath),
|
|
showMessageBox: (options) => dialog.showMessageBox(options)
|
|
})
|
|
if (choice !== undefined) {
|
|
// Recovery needs both profile locks, which only a fresh process can own safely.
|
|
app.relaunch({
|
|
args: profileStateDesktopRecoveryArgs(process.argv, {
|
|
userDataPath,
|
|
selector: { kind: choice }
|
|
})
|
|
})
|
|
}
|
|
app.exit(1)
|
|
return
|
|
}
|
|
await presentProfileStateStartupRecoveryDialog({
|
|
message,
|
|
...(failureClass === 'recovery-required' || failureClass === 'ambiguous-authority'
|
|
? { recoveryCommand: 'orca profile state exports' }
|
|
: {}),
|
|
showMessageBox: (options) => dialog.showMessageBox(options),
|
|
copyToClipboard: (text) => clipboard.writeText(text)
|
|
})
|
|
} catch (dialogError) {
|
|
console.warn('[profile-state] Recovery dialog failed; exiting safely:', dialogError)
|
|
}
|
|
}
|
|
app.exit(1)
|
|
}
|
|
})
|
|
}
|