diff --git a/src/main/opencode/hook-service.ts b/src/main/opencode/hook-service.ts index 648a297a..71e201cf 100644 --- a/src/main/opencode/hook-service.ts +++ b/src/main/opencode/hook-service.ts @@ -208,6 +208,16 @@ export class OpenCodeHookService { } const configDir = this.writePluginConfig(ptyId) + if (!configDir) { + // Why: plugin config is best-effort — return hook vars without + // OPENCODE_CONFIG_DIR so the PTY still spawns and manually launched + // opencode sessions can still report status via the hook server. + return { + ORCA_OPENCODE_HOOK_PORT: String(this.port), + ORCA_OPENCODE_HOOK_TOKEN: this.token, + ORCA_OPENCODE_PTY_ID: ptyId + } + } // Why: OpenCode only reads the extra plugin directory at process startup. // Inject these vars into every Orca PTY so manually launched `opencode` @@ -221,11 +231,18 @@ export class OpenCodeHookService { } } - private writePluginConfig(ptyId: string): string { + private writePluginConfig(ptyId: string): string | null { const configDir = join(app.getPath('userData'), 'opencode-hooks', ptyId) const pluginsDir = join(configDir, 'plugins') - mkdirSync(pluginsDir, { recursive: true }) - writeFileSync(join(pluginsDir, ORCA_OPENCODE_PLUGIN_FILE), getOpenCodePluginSource()) + try { + mkdirSync(pluginsDir, { recursive: true }) + writeFileSync(join(pluginsDir, ORCA_OPENCODE_PLUGIN_FILE), getOpenCodePluginSource()) + } catch { + // Why: on Windows, userData directories can be locked by antivirus or + // indexers (EPERM/EBUSY). Plugin config is non-critical — the PTY should + // still spawn without the OpenCode status plugin. + return null + } return configDir } } diff --git a/src/main/pi/titlebar-extension-service.ts b/src/main/pi/titlebar-extension-service.ts index 1bb0f2f9..62f9ba88 100644 --- a/src/main/pi/titlebar-extension-service.ts +++ b/src/main/pi/titlebar-extension-service.ts @@ -142,17 +142,36 @@ export class PiTitlebarExtensionService { const sourceAgentDir = existingAgentDir || getDefaultPiAgentDir() const overlayDir = this.getOverlayDir(ptyId) - rmSync(overlayDir, { recursive: true, force: true }) - mkdirSync(overlayDir, { recursive: true }) - this.mirrorAgentDir(sourceAgentDir, overlayDir) + try { + rmSync(overlayDir, { recursive: true, force: true }) + } catch { + // Why: on Windows the overlay directory can be locked by another process + // (e.g. antivirus, indexer, or a previous Orca session that didn't clean up). + // rmSync with force:true handles ENOENT but not EPERM/EBUSY. If we can't + // remove the stale overlay, fall back to the user's own Pi agent dir so the + // terminal still spawns — the titlebar spinner is not worth blocking the PTY. + return existingAgentDir ? { PI_CODING_AGENT_DIR: existingAgentDir } : {} + } - const extensionsDir = join(overlayDir, 'extensions') - mkdirSync(extensionsDir, { recursive: true }) - // Why: Pi auto-loads global extensions from PI_CODING_AGENT_DIR/extensions. - // Add Orca's titlebar extension alongside the user's existing extensions - // instead of replacing that directory, otherwise Orca terminals would - // silently disable the user's Pi customization inside Orca only. - writeFileSync(join(extensionsDir, ORCA_PI_EXTENSION_FILE), getPiTitlebarExtensionSource()) + try { + mkdirSync(overlayDir, { recursive: true }) + this.mirrorAgentDir(sourceAgentDir, overlayDir) + + const extensionsDir = join(overlayDir, 'extensions') + mkdirSync(extensionsDir, { recursive: true }) + // Why: Pi auto-loads global extensions from PI_CODING_AGENT_DIR/extensions. + // Add Orca's titlebar extension alongside the user's existing extensions + // instead of replacing that directory, otherwise Orca terminals would + // silently disable the user's Pi customization inside Orca only. + writeFileSync(join(extensionsDir, ORCA_PI_EXTENSION_FILE), getPiTitlebarExtensionSource()) + } catch { + // Why: overlay creation is best-effort — permission errors (EPERM/EACCES) + // on Windows can occur when the userData directory is restricted or when + // symlink/junction creation fails without developer mode. Fall back to the + // user's Pi agent dir so the terminal spawns without the Orca extension. + this.clearPty(ptyId) + return existingAgentDir ? { PI_CODING_AGENT_DIR: existingAgentDir } : {} + } return { PI_CODING_AGENT_DIR: overlayDir @@ -160,7 +179,13 @@ export class PiTitlebarExtensionService { } clearPty(ptyId: string): void { - rmSync(this.getOverlayDir(ptyId), { recursive: true, force: true }) + try { + rmSync(this.getOverlayDir(ptyId), { recursive: true, force: true }) + } catch { + // Why: on Windows the overlay dir can be locked (EPERM/EBUSY) by antivirus + // or indexers. Overlay cleanup is best-effort — a stale directory in userData + // is harmless and will be overwritten on the next PTY spawn attempt. + } } }