From fab37014fa8e575bd02a099b709d0704cd6d41e1 Mon Sep 17 00:00:00 2001
From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
Date: Wed, 27 May 2026 15:45:52 -0700
Subject: [PATCH] Fix Resolve with AI icon (#2918)
---
.github/workflows/pr.yml | 4 +-
.../install-electron-package-binary.mjs | 185 ++++++++++++++++++
...package-electron-runtime-contract.test.mjs | 19 +-
.../right-sidebar/CommitArea.test.tsx | 16 ++
.../right-sidebar/SourceControl.tsx | 2 +-
.../checks-panel-content.test.tsx | 8 +
.../right-sidebar/checks-panel-content.tsx | 4 +-
7 files changed, 230 insertions(+), 8 deletions(-)
create mode 100644 config/scripts/install-electron-package-binary.mjs
diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml
index b2985a7aaad..72bd00aec0b 100644
--- a/.github/workflows/pr.yml
+++ b/.github/workflows/pr.yml
@@ -89,9 +89,7 @@ jobs:
# Why: install intentionally blocks Electron's package postinstall, but
# some unit tests import `electron` under Node and require path.txt.
- name: Install Electron package binary for tests
- run: |
- pnpm rebuild electron --pending
- node -e "require('electron')"
+ run: node config/scripts/install-electron-package-binary.mjs
- name: Test
run: pnpm test
diff --git a/config/scripts/install-electron-package-binary.mjs b/config/scripts/install-electron-package-binary.mjs
new file mode 100644
index 00000000000..d21186ccefc
--- /dev/null
+++ b/config/scripts/install-electron-package-binary.mjs
@@ -0,0 +1,185 @@
+#!/usr/bin/env node
+
+import { createHash } from 'node:crypto'
+import {
+ createReadStream,
+ createWriteStream,
+ existsSync,
+ mkdtempSync,
+ readdirSync,
+ readFileSync,
+ renameSync,
+ rmSync,
+ writeFileSync
+} from 'node:fs'
+import { createRequire } from 'node:module'
+import { platform as osPlatform, tmpdir } from 'node:os'
+import { dirname, resolve } from 'node:path'
+import { Readable } from 'node:stream'
+import { pipeline } from 'node:stream/promises'
+import { fileURLToPath } from 'node:url'
+
+const require = createRequire(import.meta.url)
+const projectDir = resolve(dirname(fileURLToPath(import.meta.url)), '../..')
+const electronPackageDir = resolve(projectDir, 'node_modules/electron')
+const electronRequire = createRequire(resolve(electronPackageDir, 'package.json'))
+const { version: electronVersion } = electronRequire('./package.json')
+const extract = electronRequire('extract-zip')
+const platformPath = getElectronPlatformPath()
+
+if (electronPackageLoads()) {
+ process.exit(0)
+}
+
+// Why: PR tests run under system Node after native modules are rebuilt for
+// Node. Install only Electron's npm package binary here; do not run the full
+// Electron native-module rebuild path, which would undo the Node ABI rebuild.
+console.log('[electron-package] Electron package binary is missing; running Electron install.')
+await installElectronPackageBinary()
+
+repairElectronPathFile()
+
+if (!electronPackageLoads()) {
+ logElectronInstallDiagnostics()
+ console.error('[electron-package] Electron package is still unavailable after install.')
+ process.exit(1)
+}
+
+function electronPackageLoads() {
+ try {
+ require('electron')
+ return true
+ } catch {
+ return false
+ }
+}
+
+function repairElectronPathFile() {
+ const electronExecutable = resolve(electronPackageDir, 'dist', platformPath)
+ if (!existsSync(electronExecutable)) {
+ return
+ }
+
+ const pathFile = resolve(electronPackageDir, 'path.txt')
+ let currentPath = ''
+ try {
+ currentPath = readFileSync(pathFile, 'utf8')
+ } catch {
+ // Missing path.txt is the common CI failure this script repairs.
+ }
+
+ if (currentPath !== platformPath) {
+ writeFileSync(pathFile, platformPath)
+ console.log(`[electron-package] Repaired Electron path.txt -> ${platformPath}`)
+ }
+}
+
+async function installElectronPackageBinary() {
+ const electronDistDir = resolve(electronPackageDir, 'dist')
+ const artifactName = getElectronArtifactName()
+ const tempDir = mkdtempSync(resolve(tmpdir(), 'orca-electron-'))
+ const zipPath = resolve(tempDir, artifactName)
+
+ try {
+ await downloadElectronArtifact(artifactName, zipPath)
+ await verifyElectronArtifactChecksum(artifactName, zipPath)
+
+ rmSync(electronDistDir, { recursive: true, force: true })
+ await extract(zipPath, { dir: electronDistDir })
+
+ const srcTypeDefPath = resolve(electronDistDir, 'electron.d.ts')
+ if (existsSync(srcTypeDefPath)) {
+ renameSync(srcTypeDefPath, resolve(electronPackageDir, 'electron.d.ts'))
+ }
+ } finally {
+ rmSync(tempDir, { recursive: true, force: true })
+ }
+}
+
+async function downloadElectronArtifact(artifactName, zipPath) {
+ const artifactUrl = new URL(`v${electronVersion}/${artifactName}`, getElectronReleaseBaseUrl())
+ console.log(`[electron-package] Downloading ${artifactUrl}`)
+
+ const response = await fetch(artifactUrl)
+ if (!response.ok) {
+ throw new Error(`Failed to download ${artifactName}: ${response.status} ${response.statusText}`)
+ }
+ if (!response.body) {
+ throw new Error(`Failed to download ${artifactName}: empty response body`)
+ }
+
+ await pipeline(Readable.fromWeb(response.body), createWriteStream(zipPath))
+}
+
+async function verifyElectronArtifactChecksum(artifactName, zipPath) {
+ if (
+ process.env.electron_use_remote_checksums ||
+ process.env.npm_config_electron_use_remote_checksums
+ ) {
+ return
+ }
+
+ const expected = electronRequire('./checksums.json')[artifactName]
+ if (!expected) {
+ throw new Error(`Missing Electron checksum for ${artifactName}`)
+ }
+
+ const hash = createHash('sha256')
+ for await (const chunk of createReadStream(zipPath)) {
+ hash.update(chunk)
+ }
+ const actual = hash.digest('hex')
+ if (actual !== expected) {
+ throw new Error(`Checksum mismatch for ${artifactName}: expected ${expected}, got ${actual}`)
+ }
+}
+
+function getElectronArtifactName() {
+ return `electron-v${electronVersion}-${process.env.npm_config_platform || osPlatform()}-${
+ process.env.npm_config_arch || process.arch
+ }.zip`
+}
+
+function getElectronReleaseBaseUrl() {
+ const configuredMirror = process.env.ELECTRON_MIRROR || process.env.npm_config_electron_mirror
+ const baseUrl = configuredMirror || 'https://github.com/electron/electron/releases/download/'
+ return baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`
+}
+
+function logElectronInstallDiagnostics() {
+ const electronDistDir = resolve(electronPackageDir, 'dist')
+ const pathFile = resolve(electronPackageDir, 'path.txt')
+ console.error('[electron-package] Electron install diagnostics:')
+ console.error(` packageDir=${electronPackageDir} exists=${existsSync(electronPackageDir)}`)
+ console.error(` distDir=${electronDistDir} exists=${existsSync(electronDistDir)}`)
+ console.error(` pathFile=${pathFile} exists=${existsSync(pathFile)}`)
+ console.error(` platformPath=${platformPath}`)
+ if (existsSync(electronDistDir)) {
+ console.error(` distEntries=${safeReaddir(electronDistDir).join(', ')}`)
+ }
+}
+
+function safeReaddir(targetPath) {
+ try {
+ return readdirSync(targetPath).slice(0, 40)
+ } catch {
+ return []
+ }
+}
+
+function getElectronPlatformPath() {
+ const targetPlatform = process.env.npm_config_platform || osPlatform()
+ switch (targetPlatform) {
+ case 'mas':
+ case 'darwin':
+ return 'Electron.app/Contents/MacOS/Electron'
+ case 'freebsd':
+ case 'openbsd':
+ case 'linux':
+ return 'electron'
+ case 'win32':
+ return 'electron.exe'
+ default:
+ throw new Error(`Electron builds are not available on platform: ${targetPlatform}`)
+ }
+}
diff --git a/config/scripts/package-electron-runtime-contract.test.mjs b/config/scripts/package-electron-runtime-contract.test.mjs
index 8a52e7bb406..178c8706a8c 100644
--- a/config/scripts/package-electron-runtime-contract.test.mjs
+++ b/config/scripts/package-electron-runtime-contract.test.mjs
@@ -34,7 +34,10 @@ describe('Electron runtime package contract', () => {
})
it('guards release publishing before electron-builder runs', () => {
- const releaseWorkflow = readFileSync(join(projectDir, '.github/workflows/release-cut.yml'), 'utf8')
+ const releaseWorkflow = readFileSync(
+ join(projectDir, '.github/workflows/release-cut.yml'),
+ 'utf8'
+ )
const parsedWorkflow = parse(releaseWorkflow)
const releaseCommands = new Map(
parsedWorkflow.jobs.build.strategy.matrix.include.map(({ platform, release_command }) => [
@@ -47,7 +50,9 @@ describe('Electron runtime package contract', () => {
for (const command of releaseCommands.values()) {
expect(command).toContain('node config/scripts/ensure-native-runtime.mjs --runtime=electron')
expect(command).toContain('electron-builder')
- expect(command.indexOf('ensure-native-runtime')).toBeLessThan(command.indexOf('electron-builder'))
+ expect(command.indexOf('ensure-native-runtime')).toBeLessThan(
+ command.indexOf('electron-builder')
+ )
}
expect(releaseCommands.get('mac')).toContain(' && ORCA_MAC_RELEASE=1 ')
expect(releaseCommands.get('linux')).toContain(' && pnpm exec electron-builder ')
@@ -55,4 +60,14 @@ describe('Electron runtime package contract', () => {
'; if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }; pnpm exec electron-builder '
)
})
+
+ it('installs the Electron package binary in PR checks without changing native module ABI', () => {
+ const prWorkflow = readFileSync(join(projectDir, '.github/workflows/pr.yml'), 'utf8')
+ const parsedWorkflow = parse(prWorkflow)
+ const installStep = parsedWorkflow.jobs.verify.steps.find(
+ (step) => step.name === 'Install Electron package binary for tests'
+ )
+
+ expect(installStep.run).toBe('node config/scripts/install-electron-package-binary.mjs')
+ })
})
diff --git a/src/renderer/src/components/right-sidebar/CommitArea.test.tsx b/src/renderer/src/components/right-sidebar/CommitArea.test.tsx
index 085612f6626..ab144841679 100644
--- a/src/renderer/src/components/right-sidebar/CommitArea.test.tsx
+++ b/src/renderer/src/components/right-sidebar/CommitArea.test.tsx
@@ -359,4 +359,20 @@ describe('ConflictSummaryCard', () => {
expect(mergeMarkup).toContain('Abort merge')
expect(rebaseMarkup).not.toContain('Abort merge')
})
+
+ it('renders the Sparkles icon on the idle Resolve with AI button', () => {
+ const markup = renderToStaticMarkup(
+
+ )
+
+ expect(markup).toContain('Resolve with AI')
+ expect(markup).toContain('lucide-sparkles')
+ expect(markup).not.toMatch(/\blucide-sparkle(?!s)\b/)
+ })
})
diff --git a/src/renderer/src/components/right-sidebar/SourceControl.tsx b/src/renderer/src/components/right-sidebar/SourceControl.tsx
index 8792fe14e33..66409f6c6fa 100644
--- a/src/renderer/src/components/right-sidebar/SourceControl.tsx
+++ b/src/renderer/src/components/right-sidebar/SourceControl.tsx
@@ -5811,7 +5811,7 @@ export function ConflictSummaryCard({
{isResolvingWithAI ? (
) : (
-
+
)}
Resolve with AI
diff --git a/src/renderer/src/components/right-sidebar/checks-panel-content.test.tsx b/src/renderer/src/components/right-sidebar/checks-panel-content.test.tsx
index 90b847f2a5e..234a6b79e2e 100644
--- a/src/renderer/src/components/right-sidebar/checks-panel-content.test.tsx
+++ b/src/renderer/src/components/right-sidebar/checks-panel-content.test.tsx
@@ -56,4 +56,12 @@ describe('MergeConflictNotice', () => {
expect(markup).toBe('')
})
+
+ it('renders the Sparkles icon on the idle Resolve with AI button', () => {
+ const markup = renderNotice(makePR())
+
+ expect(markup).toContain('Resolve with AI')
+ expect(markup).toContain('lucide-sparkles')
+ expect(markup).not.toMatch(/\blucide-sparkle(?!s)\b/)
+ })
})
diff --git a/src/renderer/src/components/right-sidebar/checks-panel-content.tsx b/src/renderer/src/components/right-sidebar/checks-panel-content.tsx
index a3f522843f1..8f64b1ab9cb 100644
--- a/src/renderer/src/components/right-sidebar/checks-panel-content.tsx
+++ b/src/renderer/src/components/right-sidebar/checks-panel-content.tsx
@@ -13,7 +13,7 @@ import {
Check,
MessageSquare,
ChevronDown,
- Sparkle,
+ Sparkles,
RefreshCw,
Wrench
} from 'lucide-react'
@@ -95,7 +95,7 @@ function ResolveConflictsWithAIButton({
{isResolvingWithAI ? (
) : (
-
+
)}
Resolve with AI