Improve startup with lazy dialog loading and resilient CLI resolution

- Defer SkillFreshnessUpdateDialog and LinearAgentSkillSetupDialog load
  until first request, removing ~120ms of module parsing from critical path
- Keep dialogs mounted after first open to allow Radix exit animation,
  fixing early unmount that was cutting off close transitions
- Gracefully degrade CLI resolution when nvm directory is unreadable
  (stale mount, dangling symlink), falling back to PATH instead of
  aborting command detection
This commit is contained in:
Jinjing
2026-07-16 19:21:39 -07:00
parent 811a4fd248
commit 1e1f3b7214
6 changed files with 54 additions and 6 deletions
+19
View File
@@ -327,6 +327,25 @@ describe('resolveCliCommands', () => {
expect(resolved.get('codex')).toBe(join(pathDir, 'codex'))
})
it('still resolves PATH commands when the nvm versions dir is unreadable', async () => {
const root = mkdtempSync(join(tmpdir(), 'orca-cli-commands-'))
const pathDir = join(root, 'bin')
const pathClaude = join(pathDir, 'claude')
makeExecutable(pathClaude)
// Why: a file where a directory is expected makes readdir throw ENOTDIR,
// standing in for stale-mount/EACCES failures on ~/.nvm/versions/node.
makeNonExecutableFile(join(root, '.nvm', 'versions', 'node'))
const resolved = await resolveCliCommands(['claude', 'missing'], {
platform: 'darwin',
pathEnv: pathDir,
homePath: root
})
expect(resolved.get('claude')).toBe(pathClaude)
expect(resolved.get('missing')).toBe('missing')
})
it('keeps Linux executable matching case-sensitive', async () => {
const root = mkdtempSync(join(tmpdir(), 'orca-cli-commands-'))
const pathDir = join(root, 'bin')
+12 -2
View File
@@ -1,4 +1,4 @@
import { accessSync, constants, existsSync, readdirSync, statSync } from 'node:fs'
import { accessSync, constants, existsSync, readdirSync, statSync, type Dirent } from 'node:fs'
import { access, readdir, stat } from 'node:fs/promises'
import { homedir } from 'node:os'
import { delimiter, dirname, join } from 'node:path'
@@ -214,7 +214,17 @@ function getNvmVersionDirectories(homePath: string): string[] {
return []
}
return readdirSync(nvmVersionsDir, { withFileTypes: true })
// Why: bulk detection folds PATH and install dirs into one pass, so an
// unreadable nvm dir (stale mount, dangling symlink) must degrade to "no
// nvm dirs" instead of aborting resolution and discarding PATH-found CLIs.
let entries: Dirent[]
try {
entries = readdirSync(nvmVersionsDir, { withFileTypes: true })
} catch {
return []
}
return entries
.filter((entry) => entry.isDirectory())
.map((entry) => entry.name)
.sort(compareVersionDesc)
+8 -1
View File
@@ -326,7 +326,14 @@ function SkillFreshnessUpdateDialogGate(): React.JSX.Element | null {
getSkillFreshnessUpdateDialogRequest,
getSkillFreshnessUpdateDialogRequest
)
if (!requested) {
// Why: stay mounted after the first request — closing consumes the request
// in the same commit `open` flips false, and unmounting then would cut off
// Radix's exit animation.
const everRequestedRef = useRef(false)
if (requested) {
everRequestedRef.current = true
}
if (!everRequestedRef.current) {
return null
}
return (
+1 -1
View File
@@ -238,7 +238,7 @@ describe('renderer startup runtime routing', () => {
expect(source).toContain("import('./LinearAgentSkillSetupDialog').then")
expect(source).not.toContain("from './LinearAgentSkillSetupDialog'")
expect(source).toContain('const setupDialog = setupDialogOpen ?')
expect(source).toContain('const setupDialog = setupDialogEverOpenedRef.current ?')
})
it('loads worktree note Markdown only with the hover detail surface', () => {
@@ -86,6 +86,12 @@ export function LinearAgentSkillSetupPrompt({
const [cliStatus, setCliStatus] = useState<CliInstallStatus | null>(null)
const [cliLoading, setCliLoading] = useState(linked)
const [setupDialogOpen, setSetupDialogOpen] = useState(false)
// Why: keep the dialog mounted once opened so closing can play Radix's exit
// animation; only the very first mount is deferred off the startup path.
const setupDialogEverOpenedRef = useRef(false)
if (setupDialogOpen) {
setupDialogEverOpenedRef.current = true
}
const [setupCheckResult, setSetupCheckResult] = useState<SetupCheckResult>('idle')
const [activeSetupCheckIdentity, setActiveSetupCheckIdentity] = useState<string | null>(null)
const agentRuntime = useMemo(
@@ -287,8 +293,9 @@ export function LinearAgentSkillSetupPrompt({
}
// Why: rendering a lazy component with open=false still fetches its module.
// Gate the element itself so the inline installer terminal stays off startup.
const setupDialog = setupDialogOpen ? (
// Gate the element until the first open so the inline installer terminal
// stays off startup.
const setupDialog = setupDialogEverOpenedRef.current ? (
<Suspense fallback={null}>
<LinearAgentSkillSetupDialog
open={setupDialogOpen}
@@ -519,6 +519,11 @@ export function ensureStartupBenchmarkFixture(fixtureDir, options) {
['Service Worker', 'CacheStorage'],
['terminal-scrollback-snapshots']
]
// Why: rebuilding with a smaller file count must not inherit higher-index
// files from a prior larger fixture, or the icacls walk measures the old size.
for (const bucketRoot of new Set(buckets.map(([root]) => root))) {
rmSync(join(fixtureDir, bucketRoot), { recursive: true, force: true })
}
const payload = 'x'.repeat(1024)
let written = 0
const started = Date.now()