mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 16:02:32 +00:00
Label macOS dev Dock instances by branch (#2068)
* fix: label macOS dev Dock instances * fix: keep dev badge visible in Dock labels * chore: clean up dev identity plumbing * feat: add stable-name dev command
This commit is contained in:
@@ -1,6 +1,14 @@
|
||||
import { spawn } from 'node:child_process'
|
||||
import { execFileSync, spawn } from 'node:child_process'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { chmodSync, mkdirSync, writeFileSync } from 'node:fs'
|
||||
import {
|
||||
chmodSync,
|
||||
cpSync,
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
rmSync,
|
||||
writeFileSync
|
||||
} from 'node:fs'
|
||||
import net from 'node:net'
|
||||
import { createRequire } from 'node:module'
|
||||
import path from 'node:path'
|
||||
@@ -14,6 +22,188 @@ delete process.env.ELECTRON_RUN_AS_NODE
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..')
|
||||
const STABLE_NAME_FLAG = '--stable-name'
|
||||
const rawForwardedArgs = process.argv.slice(2)
|
||||
// Why: keep an escape hatch for tools that key off Electron's stock app name.
|
||||
// The flag is runner-only and must not leak into Chromium/electron-vite.
|
||||
const useStableElectronName =
|
||||
process.env.ORCA_DEV_STABLE_NAME === '1' || rawForwardedArgs.includes(STABLE_NAME_FLAG)
|
||||
const forwardedRaw = rawForwardedArgs.filter((arg) => arg !== STABLE_NAME_FLAG)
|
||||
if (useStableElectronName) {
|
||||
process.env.ORCA_DEV_STABLE_NAME = '1'
|
||||
}
|
||||
|
||||
function readGitValue(args) {
|
||||
try {
|
||||
const value = execFileSync('git', ['-C', repoRoot, ...args], {
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'ignore']
|
||||
}).trim()
|
||||
return value || null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function lastBranchSegment(value) {
|
||||
return value.replace(/\\/g, '/').split('/').filter(Boolean).at(-1) ?? value
|
||||
}
|
||||
|
||||
function formatDevInstanceLabel(branch, worktreeName) {
|
||||
if (branch && worktreeName) {
|
||||
if (branch === worktreeName || lastBranchSegment(branch) === worktreeName) {
|
||||
return worktreeName
|
||||
}
|
||||
return `${worktreeName} @ ${branch}`
|
||||
}
|
||||
return branch || worktreeName || null
|
||||
}
|
||||
|
||||
function createBadgeSuffix(seed) {
|
||||
const n = parseInt(createHash('sha1').update(seed).digest('hex').slice(0, 8), 16)
|
||||
return (n % 1296).toString(36).toUpperCase().padStart(2, '0')
|
||||
}
|
||||
|
||||
function createDockBadgeLabel(displayValue, identitySeed) {
|
||||
const source = lastBranchSegment(displayValue || identitySeed || '')
|
||||
const words = source.split(/[^a-zA-Z0-9]+/).filter(Boolean)
|
||||
const prefix =
|
||||
words.length > 1
|
||||
? words
|
||||
.slice(0, 2)
|
||||
.map((word) => word[0])
|
||||
.join('')
|
||||
: (words[0] ?? 'D').slice(0, 2)
|
||||
const normalizedPrefix = (prefix.replace(/[^a-zA-Z0-9]/g, '').toUpperCase() || 'D').slice(0, 2)
|
||||
return `${normalizedPrefix}${createBadgeSuffix(identitySeed || displayValue || source)}`
|
||||
}
|
||||
|
||||
function createDockTitle(branch, label, badgeLabel) {
|
||||
const title = `Orca Dev${badgeLabel ? ` [${badgeLabel}]` : ''}`
|
||||
return `${title}: ${branch || label || 'dev'}`
|
||||
}
|
||||
|
||||
function seedDevInstanceIdentityEnv() {
|
||||
const branch =
|
||||
process.env.ORCA_DEV_BRANCH ||
|
||||
readGitValue(['symbolic-ref', '--quiet', '--short', 'HEAD']) ||
|
||||
readGitValue(['rev-parse', '--short', 'HEAD'])
|
||||
const worktreeName = process.env.ORCA_DEV_WORKTREE_NAME || path.basename(repoRoot)
|
||||
const label = process.env.ORCA_DEV_INSTANCE_LABEL || formatDevInstanceLabel(branch, worktreeName)
|
||||
const identitySeed = process.env.ORCA_DEV_INSTANCE_KEY || repoRoot
|
||||
const badgeLabel =
|
||||
process.env.ORCA_DEV_DOCK_BADGE_LABEL ||
|
||||
createDockBadgeLabel(worktreeName || branch || label, identitySeed)
|
||||
const dockTitle = process.env.ORCA_DEV_DOCK_TITLE || createDockTitle(branch, label, badgeLabel)
|
||||
|
||||
process.env.ORCA_DEV_REPO_ROOT ||= repoRoot
|
||||
process.env.ORCA_DEV_INSTANCE_KEY ||= identitySeed
|
||||
if (branch) {
|
||||
process.env.ORCA_DEV_BRANCH ||= branch
|
||||
}
|
||||
if (worktreeName) {
|
||||
process.env.ORCA_DEV_WORKTREE_NAME ||= worktreeName
|
||||
}
|
||||
if (label) {
|
||||
// Why: parallel `pn dev` runs need a stable origin label for window titles,
|
||||
// Dock badges, and automation sessions without re-running git in Electron.
|
||||
process.env.ORCA_DEV_INSTANCE_LABEL ||= label
|
||||
}
|
||||
if (badgeLabel) {
|
||||
process.env.ORCA_DEV_DOCK_BADGE_LABEL ||= badgeLabel
|
||||
}
|
||||
process.env.ORCA_DEV_DOCK_TITLE ||= dockTitle
|
||||
}
|
||||
|
||||
function setPlistValue(plistPath, key, value) {
|
||||
execFileSync('/usr/bin/plutil', ['-replace', key, '-string', value, plistPath])
|
||||
}
|
||||
|
||||
function sanitizeBundleIdPart(value) {
|
||||
return (
|
||||
value
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9.-]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '')
|
||||
.slice(0, 80) || 'dev'
|
||||
)
|
||||
}
|
||||
|
||||
function sanitizeMacAppBundleName(value) {
|
||||
return (
|
||||
Array.from(value, (char) => {
|
||||
const code = char.charCodeAt(0)
|
||||
return code < 32 || code === 127 || char === ':' || char === '/' || char === '\\' ? '-' : char
|
||||
})
|
||||
.join('')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
.slice(0, 120) || 'Orca Dev'
|
||||
)
|
||||
}
|
||||
|
||||
function prepareMacDevElectronApp() {
|
||||
if (process.platform !== 'darwin') {
|
||||
return
|
||||
}
|
||||
|
||||
const sourceAppPath = path.join(repoRoot, 'node_modules', 'electron', 'dist', 'Electron.app')
|
||||
const electronPackagePath = path.join(repoRoot, 'node_modules', 'electron', 'package.json')
|
||||
if (!existsSync(sourceAppPath)) {
|
||||
return
|
||||
}
|
||||
|
||||
let electronVersion = null
|
||||
try {
|
||||
electronVersion = JSON.parse(readFileSync(electronPackagePath, 'utf8')).version ?? null
|
||||
} catch {}
|
||||
|
||||
const title = process.env.ORCA_DEV_DOCK_TITLE || 'Orca Dev'
|
||||
const identityKey = process.env.ORCA_DEV_INSTANCE_KEY || repoRoot
|
||||
const bundleLayoutVersion = 'dock-title-app-filename-v1'
|
||||
const hash = createHash('sha1')
|
||||
.update(
|
||||
`${sourceAppPath}\0${electronVersion ?? ''}\0${title}\0${identityKey}\0${bundleLayoutVersion}`
|
||||
)
|
||||
.digest('hex')
|
||||
.slice(0, 12)
|
||||
const distDir = path.join(repoRoot, 'out', 'electron-dev', hash)
|
||||
// Why: macOS Dock hover uses the bundle's filesystem display name for
|
||||
// electron-vite's direct binary launch path, even when Info.plist is patched.
|
||||
const appBundleName = `${sanitizeMacAppBundleName(title)}.app`
|
||||
const appPath = path.join(distDir, appBundleName)
|
||||
const markerPath = path.join(distDir, 'orca-dev-electron-app.json')
|
||||
const bundleId = `com.stablyai.orca.dev.${sanitizeBundleIdPart(hash)}`
|
||||
const expectedMarker = JSON.stringify(
|
||||
{ title, appBundleName, bundleId, sourceAppPath, electronVersion, bundleLayoutVersion },
|
||||
null,
|
||||
2
|
||||
)
|
||||
|
||||
if (existsSync(markerPath) && existsSync(appPath)) {
|
||||
try {
|
||||
if (readFileSync(markerPath, 'utf8') === expectedMarker) {
|
||||
process.env.ELECTRON_EXEC_PATH = path.join(appPath, 'Contents', 'MacOS', 'Electron')
|
||||
return
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
rmSync(distDir, { recursive: true, force: true })
|
||||
mkdirSync(distDir, { recursive: true })
|
||||
cpSync(sourceAppPath, appPath, { recursive: true })
|
||||
|
||||
const plistPath = path.join(appPath, 'Contents', 'Info.plist')
|
||||
setPlistValue(plistPath, 'CFBundleName', title)
|
||||
setPlistValue(plistPath, 'CFBundleDisplayName', title)
|
||||
setPlistValue(plistPath, 'CFBundleIdentifier', bundleId)
|
||||
|
||||
// Why no re-sign: dev launches execute the copied Electron binary directly,
|
||||
// and Electron's framework bundle is ambiguous to codesign when deep-signing
|
||||
// an already-built distribution. Avoid blocking `pn dev` on local signing.
|
||||
writeFileSync(markerPath, expectedMarker, 'utf8')
|
||||
process.env.ELECTRON_EXEC_PATH = path.join(appPath, 'Contents', 'MacOS', 'Electron')
|
||||
}
|
||||
|
||||
function getDevUserDataPath() {
|
||||
if (process.env.ORCA_DEV_USER_DATA_PATH) {
|
||||
@@ -72,6 +262,11 @@ if (process.env.ORCA_SKIP_DEV_CLI_PREPARE !== '1') {
|
||||
prepareDevCliWrapper()
|
||||
}
|
||||
|
||||
seedDevInstanceIdentityEnv()
|
||||
if (!useStableElectronName && process.env.ORCA_SKIP_DEV_ELECTRON_APP_PREPARE !== '1') {
|
||||
prepareMacDevElectronApp()
|
||||
}
|
||||
|
||||
// Why: tests inject a tiny fake CLI here so they can verify Ctrl+C tears down
|
||||
// the full child tree without depending on a real electron-vite install.
|
||||
const electronViteCli =
|
||||
@@ -82,7 +277,6 @@ const electronViteCli =
|
||||
// without manual port juggling. Pick a best-effort deterministic port per
|
||||
// worktree; falls back to a probe sweep if the deterministic pick or its
|
||||
// neighbors are busy (multiple worktrees may share a machine).
|
||||
const forwardedRaw = process.argv.slice(2)
|
||||
function isPortFree(port) {
|
||||
return new Promise((resolve) => {
|
||||
const srv = net.createServer()
|
||||
@@ -131,6 +325,12 @@ const userPassedPort = forwardedRaw.some(
|
||||
// Why: --help/--version exit immediately; binding a probe socket and printing
|
||||
// a debug-port line would be noise.
|
||||
const isHelpOrVersion = forwardedRaw.some((a) => a === '--help' || a === '-h' || a === '--version')
|
||||
if (!isHelpOrVersion && process.env.ORCA_DEV_INSTANCE_LABEL) {
|
||||
const badge = process.env.ORCA_DEV_DOCK_BADGE_LABEL
|
||||
? ` [${process.env.ORCA_DEV_DOCK_BADGE_LABEL}]`
|
||||
: ''
|
||||
console.error(`[orca-dev] Instance: ${process.env.ORCA_DEV_INSTANCE_LABEL}${badge}`)
|
||||
}
|
||||
let forwardedExtras = []
|
||||
if (!userPassedPort && !isHelpOrVersion) {
|
||||
const envPortRaw = process.env.REMOTE_DEBUGGING_PORT
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
"typecheck:tsc": "tsc --noEmit -p config/tsconfig.node.json --composite false && tsc --noEmit -p config/tsconfig.cli.json --composite false && tsc --noEmit -p config/tsconfig.web.json --composite false",
|
||||
"start": "electron-vite preview",
|
||||
"dev": "node config/scripts/run-electron-vite-dev.mjs",
|
||||
"dev-stable-name": "node config/scripts/run-electron-vite-dev.mjs --stable-name",
|
||||
"dev:web": "vite --config vite.web.config.ts --host 127.0.0.1",
|
||||
"build:relay": "node config/scripts/build-relay.mjs",
|
||||
"build:computer-macos": "node config/scripts/build-computer-macos.mjs",
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const { setBadgeMock } = vi.hoisted(() => ({
|
||||
setBadgeMock: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('electron', () => ({
|
||||
app: {
|
||||
dock: {
|
||||
setBadge: setBadgeMock
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
describe('unread Dock badge', () => {
|
||||
const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform')
|
||||
|
||||
afterEach(() => {
|
||||
if (originalPlatform) {
|
||||
Object.defineProperty(process, 'platform', originalPlatform)
|
||||
}
|
||||
setBadgeMock.mockReset()
|
||||
vi.resetModules()
|
||||
})
|
||||
|
||||
it('shows the dev identity badge when unread count is zero', async () => {
|
||||
Object.defineProperty(process, 'platform', { configurable: true, value: 'darwin' })
|
||||
const { setIdleDockBadgeLabel, setUnreadDockBadgeCount } = await import('./unread-badge')
|
||||
|
||||
setIdleDockBadgeLabel('DI')
|
||||
expect(setBadgeMock).toHaveBeenLastCalledWith('DI')
|
||||
|
||||
setUnreadDockBadgeCount(5)
|
||||
expect(setBadgeMock).toHaveBeenLastCalledWith('5')
|
||||
|
||||
setUnreadDockBadgeCount(0)
|
||||
expect(setBadgeMock).toHaveBeenLastCalledWith('DI')
|
||||
})
|
||||
|
||||
it('caps unread counts while preserving the idle badge', async () => {
|
||||
Object.defineProperty(process, 'platform', { configurable: true, value: 'darwin' })
|
||||
const { setIdleDockBadgeLabel, setUnreadDockBadgeCount } = await import('./unread-badge')
|
||||
|
||||
setIdleDockBadgeLabel('DI')
|
||||
setUnreadDockBadgeCount(104)
|
||||
expect(setBadgeMock).toHaveBeenLastCalledWith('99+')
|
||||
})
|
||||
})
|
||||
@@ -1,14 +1,32 @@
|
||||
import { app } from 'electron'
|
||||
|
||||
let idleDockBadgeLabel = ''
|
||||
let unreadCount = 0
|
||||
|
||||
function applyDockBadge(): void {
|
||||
if (process.platform !== 'darwin') {
|
||||
return
|
||||
}
|
||||
|
||||
const label =
|
||||
unreadCount === 0 ? idleDockBadgeLabel : unreadCount > 99 ? '99+' : String(unreadCount)
|
||||
|
||||
app.dock?.setBadge(label)
|
||||
}
|
||||
|
||||
export function setIdleDockBadgeLabel(label: string | null | undefined): void {
|
||||
idleDockBadgeLabel = label ?? ''
|
||||
applyDockBadge()
|
||||
}
|
||||
|
||||
export function setUnreadDockBadgeCount(count: number): void {
|
||||
if (process.platform !== 'darwin') {
|
||||
return
|
||||
}
|
||||
|
||||
const normalizedCount = Number.isFinite(count) ? Math.max(0, Math.trunc(count)) : 0
|
||||
const label = normalizedCount === 0 ? '' : normalizedCount > 99 ? '99+' : String(normalizedCount)
|
||||
unreadCount = Number.isFinite(count) ? Math.max(0, Math.trunc(count)) : 0
|
||||
|
||||
// Why: unread counts belong on the native Dock tile on macOS.
|
||||
// Windows/Linux are skipped until we define the right platform behavior.
|
||||
app.dock?.setBadge(label)
|
||||
// Why: unread counts own the native badge while active; otherwise dev builds
|
||||
// keep their worktree badge visible so parallel `pn dev` windows stay distinct.
|
||||
applyDockBadge()
|
||||
}
|
||||
|
||||
+8
-4
@@ -39,6 +39,7 @@ import {
|
||||
patchPackagedProcessPath
|
||||
} from './startup/configure-process'
|
||||
import { startFirstWindowStartupServices } from './startup/first-window-startup-services'
|
||||
import { getDevInstanceIdentity } from './startup/dev-instance-identity'
|
||||
import { hydrateShellPath, mergePathSegments } from './startup/hydrate-shell-path'
|
||||
import { acquireSingleInstanceLock } from './startup/single-instance-lock'
|
||||
import { RateLimitService } from './rate-limits/service'
|
||||
@@ -66,7 +67,7 @@ import {
|
||||
} from './ipc/pty'
|
||||
import { AgentBrowserBridge } from './browser/agent-browser-bridge'
|
||||
import { browserManager } from './browser/browser-manager'
|
||||
import { setUnreadDockBadgeCount } from './dock/unread-badge'
|
||||
import { setIdleDockBadgeLabel, setUnreadDockBadgeCount } from './dock/unread-badge'
|
||||
import { registerFeatureWallFirstAgentTour } from './feature-wall/first-agent-tour'
|
||||
import { AutomationService } from './automations/service'
|
||||
import { AgentAwakeService } from './agent-awake-service'
|
||||
@@ -96,6 +97,7 @@ let watcherShutdownPromise: Promise<void> | null = null
|
||||
let watcherShutdownDone = false
|
||||
let automations: AutomationService | null = null
|
||||
const isServeMode = process.argv.includes('--serve')
|
||||
const devInstanceIdentity = getDevInstanceIdentity(is.dev)
|
||||
|
||||
installUncaughtPipeErrorGuard()
|
||||
// Why: propagate the Orca app version into `process.env` so PTY-env
|
||||
@@ -255,7 +257,8 @@ function openMainWindow(): BrowserWindow {
|
||||
onQuitAborted: () => {
|
||||
isQuitting = false
|
||||
},
|
||||
deferLoad: true
|
||||
deferLoad: true,
|
||||
title: devInstanceIdentity.name
|
||||
})
|
||||
|
||||
// Why: telemetry-plan.md§First-launch experience anchors default-on
|
||||
@@ -643,12 +646,13 @@ function driveSyntheticTitleFromHook(
|
||||
}
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
electronApp.setAppUserModelId('com.stablyai.orca')
|
||||
app.setName('Orca')
|
||||
electronApp.setAppUserModelId(devInstanceIdentity.appUserModelId)
|
||||
app.setName(devInstanceIdentity.name)
|
||||
|
||||
if (process.platform === 'darwin' && is.dev) {
|
||||
const dockIcon = nativeImage.createFromPath(devIcon)
|
||||
app.dock?.setIcon(dockIcon)
|
||||
setIdleDockBadgeLabel(devInstanceIdentity.dockBadgeLabel)
|
||||
}
|
||||
|
||||
store = new Store()
|
||||
|
||||
@@ -5,7 +5,10 @@ import path from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { promisify } from 'node:util'
|
||||
import { app, ipcMain } from 'electron'
|
||||
import { is } from '@electron-toolkit/utils'
|
||||
import type { AppIdentity } from '../../shared/app-identity'
|
||||
import type { FloatingTerminalCwdRequest } from '../../shared/types'
|
||||
import { getDevInstanceIdentity } from '../startup/dev-instance-identity'
|
||||
import { isPwshAvailable } from '../pwsh'
|
||||
import { isWslAvailable } from '../wsl'
|
||||
import { setUnreadDockBadgeCount } from '../dock/unread-badge'
|
||||
@@ -73,6 +76,19 @@ function resolveDevFeatureWallAssetDir(): string {
|
||||
export function registerAppHandlers(): void {
|
||||
ipcMain.handle('app:getFeatureWallAssetBaseUrl', (): string => getFeatureWallAssetBaseUrl())
|
||||
|
||||
ipcMain.handle('app:getIdentity', (): AppIdentity => {
|
||||
const identity = getDevInstanceIdentity(is.dev)
|
||||
return {
|
||||
name: identity.name,
|
||||
isDev: identity.isDev,
|
||||
devLabel: identity.devLabel,
|
||||
devBranch: identity.devBranch,
|
||||
devWorktreeName: identity.devWorktreeName,
|
||||
devRepoRoot: identity.devRepoRoot,
|
||||
dockBadgeLabel: identity.dockBadgeLabel
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('wsl:isAvailable', (): boolean => isWslAvailable())
|
||||
ipcMain.handle('pwsh:isAvailable', (): boolean => isPwshAvailable())
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { fileURLToPath } from 'node:url'
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
const grandchildPath = path.join(__dirname, 'electron-vite-dev-grandchild.mjs')
|
||||
const pidFile = process.env.ORCA_DEV_WRAPPER_TEST_PID_FILE
|
||||
const envFile = process.env.ORCA_DEV_WRAPPER_TEST_ENV_FILE
|
||||
|
||||
const grandchild = spawn(process.execPath, [grandchildPath], {
|
||||
stdio: 'ignore'
|
||||
@@ -16,4 +17,25 @@ if (!pidFile) {
|
||||
}
|
||||
|
||||
writeFileSync(pidFile, `${grandchild.pid ?? ''}\n`, 'utf8')
|
||||
if (envFile) {
|
||||
writeFileSync(
|
||||
envFile,
|
||||
JSON.stringify(
|
||||
{
|
||||
args: process.argv.slice(2),
|
||||
label: process.env.ORCA_DEV_INSTANCE_LABEL ?? null,
|
||||
branch: process.env.ORCA_DEV_BRANCH ?? null,
|
||||
worktreeName: process.env.ORCA_DEV_WORKTREE_NAME ?? null,
|
||||
repoRoot: process.env.ORCA_DEV_REPO_ROOT ?? null,
|
||||
badgeLabel: process.env.ORCA_DEV_DOCK_BADGE_LABEL ?? null,
|
||||
dockTitle: process.env.ORCA_DEV_DOCK_TITLE ?? null,
|
||||
stableName: process.env.ORCA_DEV_STABLE_NAME ?? null,
|
||||
electronExecPath: process.env.ELECTRON_EXEC_PATH ?? null
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
'utf8'
|
||||
)
|
||||
}
|
||||
setInterval(() => {}, 1000)
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { createDevDockBadgeLabel, getDevInstanceIdentity } from './dev-instance-identity'
|
||||
|
||||
describe('dev-instance-identity', () => {
|
||||
it('keeps packaged identity stable', () => {
|
||||
expect(getDevInstanceIdentity(false, {})).toMatchObject({
|
||||
name: 'Orca',
|
||||
isDev: false,
|
||||
devLabel: null,
|
||||
dockBadgeLabel: null,
|
||||
appUserModelId: 'com.stablyai.orca'
|
||||
})
|
||||
})
|
||||
|
||||
it('derives a readable dev label from worktree and branch env', () => {
|
||||
const identity = getDevInstanceIdentity(true, {
|
||||
ORCA_DEV_REPO_ROOT: '/repo/worktrees/dev-indicator',
|
||||
ORCA_DEV_WORKTREE_NAME: 'dev-indicator',
|
||||
ORCA_DEV_BRANCH: 'nwparker/dev-indicator'
|
||||
})
|
||||
|
||||
expect(identity).toMatchObject({
|
||||
isDev: true,
|
||||
devLabel: 'dev-indicator',
|
||||
devBranch: 'nwparker/dev-indicator',
|
||||
devWorktreeName: 'dev-indicator',
|
||||
devRepoRoot: '/repo/worktrees/dev-indicator'
|
||||
})
|
||||
expect(identity.name).toMatch(/^Orca Dev \[DI[A-Z0-9]{2}\]: nwparker\/dev-indicator$/)
|
||||
expect(identity.dockBadgeLabel).toMatch(/^DI[A-Z0-9]{2}$/)
|
||||
expect(identity.appUserModelId).toMatch(/^com\.stablyai\.orca\.dev\.[a-f0-9]{10}$/)
|
||||
})
|
||||
|
||||
it('includes the branch when it differs from the worktree basename', () => {
|
||||
const identity = getDevInstanceIdentity(true, {
|
||||
ORCA_DEV_REPO_ROOT: '/repo/worktrees/payment-ui',
|
||||
ORCA_DEV_WORKTREE_NAME: 'payment-ui',
|
||||
ORCA_DEV_BRANCH: 'feature/billing-shell'
|
||||
})
|
||||
|
||||
expect(identity.devLabel).toBe('payment-ui @ feature/billing-shell')
|
||||
expect(identity.name).toMatch(/^Orca Dev \[PU[A-Z0-9]{2}\]: feature\/billing-shell$/)
|
||||
expect(identity.dockBadgeLabel).toMatch(/^PU[A-Z0-9]{2}$/)
|
||||
})
|
||||
|
||||
it('allows an explicit label override', () => {
|
||||
const identity = getDevInstanceIdentity(true, {
|
||||
ORCA_DEV_INSTANCE_LABEL: 'manual label',
|
||||
ORCA_DEV_WORKTREE_NAME: 'dev-indicator',
|
||||
ORCA_DEV_BRANCH: 'feature/other'
|
||||
})
|
||||
|
||||
expect(identity.devLabel).toBe('manual label')
|
||||
expect(identity.name).toMatch(/^Orca Dev \[DI[A-Z0-9]{2}\]: feature\/other$/)
|
||||
expect(identity.dockBadgeLabel).toMatch(/^DI[A-Z0-9]{2}$/)
|
||||
})
|
||||
|
||||
it('creates compact alphanumeric Dock labels with stable collision suffixes', () => {
|
||||
expect(createDevDockBadgeLabel('dev-indicator', '/repo/a')).toMatch(/^DI[A-Z0-9]{2}$/)
|
||||
expect(createDevDockBadgeLabel('feature/singleword', '/repo/a')).toMatch(/^SI[A-Z0-9]{2}$/)
|
||||
expect(createDevDockBadgeLabel('pr-123', '/repo/a')).toMatch(/^P1[A-Z0-9]{2}$/)
|
||||
expect(createDevDockBadgeLabel('dev-indicator', '/repo/a')).not.toBe(
|
||||
createDevDockBadgeLabel('dev-indicator', '/repo/b')
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,113 @@
|
||||
import { createHash } from 'crypto'
|
||||
import path from 'path'
|
||||
import type { AppIdentity } from '../../shared/app-identity'
|
||||
|
||||
const BASE_APP_NAME = 'Orca'
|
||||
const BASE_APP_USER_MODEL_ID = 'com.stablyai.orca'
|
||||
const MAX_LABEL_LENGTH = 80
|
||||
|
||||
export type DevInstanceIdentity = AppIdentity & {
|
||||
appUserModelId: string
|
||||
}
|
||||
|
||||
function cleanEnvValue(value: string | undefined): string | null {
|
||||
const trimmed = value?.replace(/\s+/g, ' ').trim()
|
||||
if (!trimmed) {
|
||||
return null
|
||||
}
|
||||
return trimmed.length > MAX_LABEL_LENGTH
|
||||
? `${trimmed.slice(0, MAX_LABEL_LENGTH - 3)}...`
|
||||
: trimmed
|
||||
}
|
||||
|
||||
function lastPathSegment(value: string): string {
|
||||
const normalized = value.replace(/\\/g, '/')
|
||||
return normalized.split('/').filter(Boolean).at(-1) ?? value
|
||||
}
|
||||
|
||||
function formatLabel(branch: string | null, worktreeName: string | null): string | null {
|
||||
if (branch && worktreeName) {
|
||||
if (branch === worktreeName || lastPathSegment(branch) === worktreeName) {
|
||||
return worktreeName
|
||||
}
|
||||
return `${worktreeName} @ ${branch}`
|
||||
}
|
||||
return branch ?? worktreeName
|
||||
}
|
||||
|
||||
function createBadgeSuffix(seed: string): string {
|
||||
const n = parseInt(createHash('sha1').update(seed).digest('hex').slice(0, 8), 16)
|
||||
return (n % 1296).toString(36).toUpperCase().padStart(2, '0')
|
||||
}
|
||||
|
||||
export function createDevDockBadgeLabel(
|
||||
value: string | null,
|
||||
identitySeed?: string | null
|
||||
): string | null {
|
||||
if (!value) {
|
||||
return null
|
||||
}
|
||||
|
||||
const source = lastPathSegment(value)
|
||||
const words = source.split(/[^a-zA-Z0-9]+/).filter(Boolean)
|
||||
const label =
|
||||
words.length > 1
|
||||
? words
|
||||
.slice(0, 2)
|
||||
.map((word) => word[0])
|
||||
.join('')
|
||||
: (words[0] ?? source).slice(0, 2)
|
||||
const prefix = (label.replace(/[^a-zA-Z0-9]/g, '').toUpperCase() || 'D').slice(0, 2)
|
||||
return `${prefix}${createBadgeSuffix(identitySeed ?? value)}`
|
||||
}
|
||||
|
||||
function createDevAppUserModelId(identityKey: string | null): string {
|
||||
if (!identityKey) {
|
||||
return BASE_APP_USER_MODEL_ID
|
||||
}
|
||||
const hash = createHash('sha1').update(identityKey).digest('hex').slice(0, 10)
|
||||
return `${BASE_APP_USER_MODEL_ID}.dev.${hash}`
|
||||
}
|
||||
|
||||
export function getDevInstanceIdentity(
|
||||
isDev: boolean,
|
||||
env: NodeJS.ProcessEnv = process.env
|
||||
): DevInstanceIdentity {
|
||||
if (!isDev) {
|
||||
return {
|
||||
name: BASE_APP_NAME,
|
||||
isDev: false,
|
||||
devLabel: null,
|
||||
devBranch: null,
|
||||
devWorktreeName: null,
|
||||
devRepoRoot: null,
|
||||
dockBadgeLabel: null,
|
||||
appUserModelId: BASE_APP_USER_MODEL_ID
|
||||
}
|
||||
}
|
||||
|
||||
const repoRoot = cleanEnvValue(env.ORCA_DEV_REPO_ROOT)
|
||||
const branch = cleanEnvValue(env.ORCA_DEV_BRANCH)
|
||||
const worktreeName =
|
||||
cleanEnvValue(env.ORCA_DEV_WORKTREE_NAME) ??
|
||||
cleanEnvValue(path.basename(repoRoot ?? process.cwd()))
|
||||
const devLabel = cleanEnvValue(env.ORCA_DEV_INSTANCE_LABEL) ?? formatLabel(branch, worktreeName)
|
||||
const identitySeed = cleanEnvValue(env.ORCA_DEV_INSTANCE_KEY) ?? repoRoot ?? devLabel
|
||||
const dockBadgeLabel =
|
||||
cleanEnvValue(env.ORCA_DEV_DOCK_BADGE_LABEL) ??
|
||||
createDevDockBadgeLabel(worktreeName ?? branch ?? devLabel, identitySeed)
|
||||
const dockTitle =
|
||||
cleanEnvValue(env.ORCA_DEV_DOCK_TITLE) ??
|
||||
`${BASE_APP_NAME} Dev${dockBadgeLabel ? ` [${dockBadgeLabel}]` : ''}: ${branch ?? devLabel ?? 'dev'}`
|
||||
|
||||
return {
|
||||
name: dockTitle,
|
||||
isDev: true,
|
||||
devLabel,
|
||||
devBranch: branch,
|
||||
devWorktreeName: worktreeName,
|
||||
devRepoRoot: repoRoot,
|
||||
dockBadgeLabel,
|
||||
appUserModelId: createDevAppUserModelId(repoRoot ?? devLabel)
|
||||
}
|
||||
}
|
||||
@@ -64,6 +64,7 @@ describe('run-electron-vite-dev', () => {
|
||||
...process.env,
|
||||
ORCA_ELECTRON_VITE_CLI: fakeCliPath,
|
||||
ORCA_SKIP_DEV_CLI_PREPARE: '1',
|
||||
ORCA_SKIP_DEV_ELECTRON_APP_PREPARE: '1',
|
||||
ORCA_DEV_WRAPPER_TEST_PID_FILE: pidFile
|
||||
},
|
||||
stdio: 'ignore'
|
||||
@@ -100,4 +101,120 @@ describe('run-electron-vite-dev', () => {
|
||||
processesToCleanUp.delete(wrapper.pid!)
|
||||
}
|
||||
)
|
||||
|
||||
it('forwards dev instance identity to electron-vite', async () => {
|
||||
const tempDir = mkdtempSync(join(tmpdir(), 'orca-dev-wrapper-'))
|
||||
const pidFile = join(tempDir, 'grandchild.pid')
|
||||
const envFile = join(tempDir, 'env.json')
|
||||
const wrapperPath = resolve('config/scripts/run-electron-vite-dev.mjs')
|
||||
const fakeCliPath = resolve('src/main/startup/__fixtures__/fake-electron-vite-dev-cli.mjs')
|
||||
|
||||
const wrapper = spawn(process.execPath, [wrapperPath, '--remote-debugging-port=9444'], {
|
||||
cwd: resolve('.'),
|
||||
env: {
|
||||
...process.env,
|
||||
ORCA_ELECTRON_VITE_CLI: fakeCliPath,
|
||||
ORCA_SKIP_DEV_CLI_PREPARE: '1',
|
||||
ORCA_SKIP_DEV_ELECTRON_APP_PREPARE: '1',
|
||||
ORCA_DEV_WRAPPER_TEST_PID_FILE: pidFile,
|
||||
ORCA_DEV_WRAPPER_TEST_ENV_FILE: envFile,
|
||||
ORCA_DEV_BRANCH: 'feature/billing-shell',
|
||||
ORCA_DEV_WORKTREE_NAME: 'payment-ui'
|
||||
},
|
||||
stdio: 'ignore'
|
||||
})
|
||||
|
||||
expect(wrapper.pid).toBeTypeOf('number')
|
||||
processesToCleanUp.add(wrapper.pid!)
|
||||
|
||||
await waitFor(() => {
|
||||
try {
|
||||
return readFileSync(envFile, 'utf8').trim().length > 0
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
const grandchildPid = Number.parseInt(readFileSync(pidFile, 'utf8').trim(), 10)
|
||||
if (Number.isFinite(grandchildPid)) {
|
||||
processesToCleanUp.add(grandchildPid)
|
||||
}
|
||||
|
||||
const envSnapshot = JSON.parse(readFileSync(envFile, 'utf8')) as {
|
||||
args: string[]
|
||||
label: string
|
||||
branch: string
|
||||
worktreeName: string
|
||||
repoRoot: string
|
||||
badgeLabel: string
|
||||
dockTitle: string
|
||||
stableName: string | null
|
||||
electronExecPath: string | null
|
||||
}
|
||||
expect(envSnapshot.args).toContain('--remote-debugging-port=9444')
|
||||
expect(envSnapshot.label).toBe('payment-ui @ feature/billing-shell')
|
||||
expect(envSnapshot.branch).toBe('feature/billing-shell')
|
||||
expect(envSnapshot.worktreeName).toBe('payment-ui')
|
||||
expect(envSnapshot.repoRoot).toBe(resolve('.'))
|
||||
expect(envSnapshot.badgeLabel).toMatch(/^PU[A-Z0-9]{2}$/)
|
||||
expect(envSnapshot.dockTitle).toMatch(/^Orca Dev \[PU[A-Z0-9]{2}\]: feature\/billing-shell$/)
|
||||
expect(envSnapshot.stableName).toBeNull()
|
||||
expect(envSnapshot.electronExecPath).toBeNull()
|
||||
|
||||
wrapper.kill('SIGINT')
|
||||
})
|
||||
|
||||
it('consumes the stable-name flag before forwarding args to electron-vite', async () => {
|
||||
const tempDir = mkdtempSync(join(tmpdir(), 'orca-dev-wrapper-'))
|
||||
const pidFile = join(tempDir, 'grandchild.pid')
|
||||
const envFile = join(tempDir, 'env.json')
|
||||
const wrapperPath = resolve('config/scripts/run-electron-vite-dev.mjs')
|
||||
const fakeCliPath = resolve('src/main/startup/__fixtures__/fake-electron-vite-dev-cli.mjs')
|
||||
|
||||
const wrapper = spawn(
|
||||
process.execPath,
|
||||
[wrapperPath, '--stable-name', '--remote-debugging-port=9445'],
|
||||
{
|
||||
cwd: resolve('.'),
|
||||
env: {
|
||||
...process.env,
|
||||
ORCA_ELECTRON_VITE_CLI: fakeCliPath,
|
||||
ORCA_SKIP_DEV_CLI_PREPARE: '1',
|
||||
ORCA_DEV_WRAPPER_TEST_PID_FILE: pidFile,
|
||||
ORCA_DEV_WRAPPER_TEST_ENV_FILE: envFile,
|
||||
ORCA_DEV_BRANCH: 'feature/stable-name',
|
||||
ORCA_DEV_WORKTREE_NAME: 'stable-ui'
|
||||
},
|
||||
stdio: 'ignore'
|
||||
}
|
||||
)
|
||||
|
||||
expect(wrapper.pid).toBeTypeOf('number')
|
||||
processesToCleanUp.add(wrapper.pid!)
|
||||
|
||||
await waitFor(() => {
|
||||
try {
|
||||
return readFileSync(envFile, 'utf8').trim().length > 0
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
const grandchildPid = Number.parseInt(readFileSync(pidFile, 'utf8').trim(), 10)
|
||||
if (Number.isFinite(grandchildPid)) {
|
||||
processesToCleanUp.add(grandchildPid)
|
||||
}
|
||||
|
||||
const envSnapshot = JSON.parse(readFileSync(envFile, 'utf8')) as {
|
||||
args: string[]
|
||||
stableName: string | null
|
||||
electronExecPath: string | null
|
||||
}
|
||||
expect(envSnapshot.args).not.toContain('--stable-name')
|
||||
expect(envSnapshot.args).toContain('--remote-debugging-port=9445')
|
||||
expect(envSnapshot.stableName).toBe('1')
|
||||
expect(envSnapshot.electronExecPath).toBeNull()
|
||||
|
||||
wrapper.kill('SIGINT')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -71,6 +71,7 @@ type CreateMainWindowOptions = {
|
||||
/** Why: main-process startup must register IPC handlers before the renderer
|
||||
* begins booting, or eager renderer calls can race into missing channels. */
|
||||
deferLoad?: boolean
|
||||
title?: string
|
||||
}
|
||||
|
||||
export function loadMainWindow(mainWindow: BrowserWindow): void {
|
||||
@@ -172,6 +173,7 @@ export function createMainWindow(
|
||||
...(savedBounds ? { x: savedBounds.x, y: savedBounds.y } : {}),
|
||||
minWidth: MIN_WIDTH,
|
||||
minHeight: MIN_HEIGHT,
|
||||
title: opts?.title ?? 'Orca',
|
||||
show: false,
|
||||
// Why: on macOS the menu lives in the system menu bar, so the in-window
|
||||
// menu bar is irrelevant. On Windows/Linux we auto-hide so the menu bar
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
HostedReviewForBranchArgs,
|
||||
HostedReviewInfo
|
||||
} from '../shared/hosted-review'
|
||||
import type { AppIdentity } from '../shared/app-identity'
|
||||
import type {
|
||||
BaseRefDefaultResult,
|
||||
BrowserCookieImportResult,
|
||||
@@ -488,6 +489,8 @@ export type OpenCodeUsageApi = {
|
||||
}
|
||||
|
||||
export type AppApi = {
|
||||
/** Returns the app identity currently exposed to native chrome and the titlebar. */
|
||||
getIdentity: () => Promise<AppIdentity>
|
||||
/** Returns a URL base for feature-wall assets. In dev this is Vite /@fs;
|
||||
* in packaged builds this is file:// resources. Renderer appends filenames. */
|
||||
getFeatureWallAssetBaseUrl: () => Promise<string>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { contextBridge, ipcRenderer, webFrame, webUtils } from 'electron'
|
||||
import { electronAPI } from '@electron-toolkit/preload'
|
||||
import { preloadE2EConfig } from './e2e-config'
|
||||
import { glApi } from './gitlab'
|
||||
import type { AppIdentity } from '../shared/app-identity'
|
||||
import type { CliInstallStatus } from '../shared/cli-install-types'
|
||||
import type { AgentHookInstallStatus } from '../shared/agent-hook-types'
|
||||
import type {
|
||||
@@ -324,6 +325,7 @@ document.addEventListener(
|
||||
// Custom APIs for renderer
|
||||
const api = {
|
||||
app: {
|
||||
getIdentity: (): Promise<AppIdentity> => ipcRenderer.invoke('app:getIdentity'),
|
||||
getFeatureWallAssetBaseUrl: (): Promise<string> =>
|
||||
ipcRenderer.invoke('app:getFeatureWallAssetBaseUrl'),
|
||||
relaunch: (): Promise<void> => ipcRenderer.invoke('app:relaunch'),
|
||||
|
||||
@@ -89,6 +89,7 @@ import {
|
||||
import type { VirtualizedScrollAnchor } from './hooks/useVirtualizedScrollAnchor'
|
||||
import type { RemoteWorkspacePatchResult } from '../../shared/remote-workspace-types'
|
||||
import type { OnboardingState } from '../../shared/types'
|
||||
import type { AppIdentity } from '../../shared/app-identity'
|
||||
|
||||
const isMac = navigator.userAgent.includes('Mac')
|
||||
const isWindows = !isMac && navigator.userAgent.includes('Windows')
|
||||
@@ -350,10 +351,27 @@ function App(): React.JSX.Element {
|
||||
const [collapsedSidebarHeaderWidth, setCollapsedSidebarHeaderWidth] = useState(0)
|
||||
const [mountedLazyModalIds, setMountedLazyModalIds] = useState(() => new Set<string>())
|
||||
const [onboarding, setOnboarding] = useState<OnboardingState | null>(null)
|
||||
const [appIdentity, setAppIdentity] = useState<AppIdentity | null>(null)
|
||||
|
||||
// Subscribe to IPC push events
|
||||
useIpcEvents()
|
||||
useAutomationDispatchEvents()
|
||||
|
||||
useEffect(() => {
|
||||
let disposed = false
|
||||
void window.api.app
|
||||
.getIdentity()
|
||||
.then((identity) => {
|
||||
if (!disposed) {
|
||||
document.title = identity.name
|
||||
setAppIdentity(identity)
|
||||
}
|
||||
})
|
||||
.catch(console.error)
|
||||
return () => {
|
||||
disposed = true
|
||||
}
|
||||
}, [])
|
||||
// Why: retention must run at App level so the inline per-card agents list
|
||||
// always sees retained entries. If retention ran inside the sidebar-card
|
||||
// subtree, "done" agents would vanish any time the user collapsed a card's
|
||||
@@ -1103,6 +1121,17 @@ function App(): React.JSX.Element {
|
||||
})
|
||||
}, [activeModal])
|
||||
|
||||
const appIdentityTitle =
|
||||
appIdentity?.isDev === true
|
||||
? [
|
||||
appIdentity.name,
|
||||
appIdentity.devBranch ? `Branch: ${appIdentity.devBranch}` : null,
|
||||
appIdentity.devRepoRoot ? `Path: ${appIdentity.devRepoRoot}` : null
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' | ')
|
||||
: undefined
|
||||
|
||||
// Why: extracted so both the full-width titlebar (settings/landing) and
|
||||
// the sidebar-width left header (workspace view) can share the same
|
||||
// controls without duplicating the agent badge popover.
|
||||
@@ -1147,8 +1176,18 @@ function App(): React.JSX.Element {
|
||||
{settings?.showTitlebarAppName !== false && (
|
||||
<ContextMenu>
|
||||
<ContextMenuTrigger asChild>
|
||||
<div className="titlebar-app-name" aria-label="Orca">
|
||||
Orca
|
||||
<div
|
||||
className="titlebar-app-name"
|
||||
aria-label={appIdentity?.name ?? 'Orca'}
|
||||
title={appIdentityTitle}
|
||||
>
|
||||
<span className="titlebar-app-name-main">Orca</span>
|
||||
{appIdentity?.isDev && appIdentity.devLabel ? (
|
||||
<span className="titlebar-dev-label">{appIdentity.devLabel}</span>
|
||||
) : null}
|
||||
{appIdentity?.isDev && appIdentity.dockBadgeLabel ? (
|
||||
<span className="titlebar-dev-badge">{appIdentity.dockBadgeLabel}</span>
|
||||
) : null}
|
||||
</div>
|
||||
</ContextMenuTrigger>
|
||||
<ContextMenuContent>
|
||||
|
||||
@@ -553,7 +553,9 @@
|
||||
-webkit-app-region: no-drag;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
height: 24px;
|
||||
max-width: min(280px, 32vw);
|
||||
margin-left: 2px;
|
||||
margin-right: 4px;
|
||||
padding: 0 6px;
|
||||
@@ -563,9 +565,42 @@
|
||||
font-weight: 600;
|
||||
line-height: 24px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.titlebar-app-name-main {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.titlebar-dev-label {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
color: var(--foreground);
|
||||
font-family: var(--font-mono);
|
||||
font-weight: 500;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.titlebar-dev-label::before {
|
||||
content: 'Dev:';
|
||||
margin-right: 4px;
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
.titlebar-dev-badge {
|
||||
flex: 0 0 auto;
|
||||
height: 16px;
|
||||
padding: 0 4px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
color: var(--muted-foreground);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
.titlebar-icon-button {
|
||||
-webkit-app-region: no-drag;
|
||||
background: none;
|
||||
|
||||
@@ -61,6 +61,16 @@ export function installWebPreloadApi(): void {
|
||||
function createWebPreloadApi(): Partial<PreloadApi> {
|
||||
return {
|
||||
app: {
|
||||
getIdentity: () =>
|
||||
Promise.resolve({
|
||||
name: 'Orca',
|
||||
isDev: false,
|
||||
devLabel: null,
|
||||
devBranch: null,
|
||||
devWorktreeName: null,
|
||||
devRepoRoot: null,
|
||||
dockBadgeLabel: null
|
||||
}),
|
||||
getFeatureWallAssetBaseUrl: () => Promise.resolve('/'),
|
||||
relaunch: () => Promise.resolve(window.location.reload()),
|
||||
getKeyboardInputSourceId: () => Promise.resolve(null),
|
||||
@@ -816,9 +826,7 @@ function createCliApi(): NonNullable<Partial<PreloadApi>['cli']> {
|
||||
}
|
||||
|
||||
function createAgentHooksApi(): NonNullable<Partial<PreloadApi>['agentHooks']> {
|
||||
const status = (
|
||||
agent: 'claude' | 'codex' | 'gemini' | 'cursor' | 'droid' | 'grok' | 'hermes'
|
||||
) =>
|
||||
const status = (agent: 'claude' | 'codex' | 'gemini' | 'cursor' | 'droid' | 'grok' | 'hermes') =>
|
||||
Promise.resolve({
|
||||
agent,
|
||||
state: 'not_installed',
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
export type AppIdentity = {
|
||||
name: string
|
||||
isDev: boolean
|
||||
devLabel: string | null
|
||||
devBranch: string | null
|
||||
devWorktreeName: string | null
|
||||
devRepoRoot: string | null
|
||||
dockBadgeLabel: string | null
|
||||
}
|
||||
Reference in New Issue
Block a user