fix: defer startup plugin checks and suppress recommendation dialogs during startup window (issue #363)

Root cause: performStartupChecks() fires immediately on REPL mount,
triggering plugin loading which populates trackedFiles, which triggers
useLspPluginRecommendation to surface an LSP recommendation dialog.
Since promptTypingSuppressionActive is false before any user input,
getFocusedInputDialog() returns the dialog, unmounting PromptInput
entirely and making the CLI appear frozen.

Fix: Two-pronged approach:
1. Defer performStartupChecks by 1500ms and gate on
   promptTypingSuppressionActive so startup checks dont run while
   the user is typing or has early input buffered
2. Suppress lower-priority startup dialogs (LSP recommendation,
   plugin hint, desktop upsell) until startupChecksStartedRef is
   true, preventing them from stealing focus during the vulnerable
   startup window

This also explains why --bare mode and disabling plugins work:
--bare mode skips plugin loading entirely, and disabling the
autoresearch plugin eliminates the LSP match, so lspRecommendation
stays null and PromptInput renders normally.
This commit is contained in:
OpenClaude Worker 3
2026-04-08 11:24:36 +05:30
parent 3188f6ac66
commit 106f85d0bf
3 changed files with 79 additions and 8 deletions

View File

@@ -238,6 +238,7 @@ import { usePromptsFromClaudeInChrome } from 'src/hooks/usePromptsFromClaudeInCh
import { getTipToShowOnSpinner, recordShownTip } from 'src/services/tips/tipScheduler.js';
import type { Theme } from 'src/utils/theme.js';
import { isPromptTypingSuppressionActive } from './replInputSuppression.js';
import { shouldRunStartupChecks, STARTUP_CHECK_DELAY_MS } from './replStartupGates.js';
import { checkAndDisableBypassPermissionsIfNeeded, checkAndDisableAutoModeIfNeeded, useKickOffCheckAndDisableBypassPermissionsIfNeeded, useKickOffCheckAndDisableAutoModeIfNeeded } from 'src/utils/permissions/bypassPermissionsKillswitch.js';
import { SandboxManager } from 'src/utils/sandbox/sandbox-adapter.js';
import { SANDBOX_NETWORK_ACCESS_TOOL_NAME } from 'src/cli/structuredIO.js';
@@ -792,10 +793,20 @@ export function REPL({
// accepts, and only then is the REPL component mounted and this effect runs.
// This ensures that plugin installations from repository and user settings only
// happen after explicit user consent to trust the current working directory.
// We defer startup checks by STARTUP_CHECK_DELAY_MS and gate on
// promptTypingSuppressionActive so that plugin loading doesn't steal focus
// from the prompt during the vulnerable startup window (issue #363).
const startupChecksStartedRef = React.useRef(false)
useEffect(() => {
if (isRemoteSession) return;
void performStartupChecks(setAppState);
}, [setAppState, isRemoteSession]);
if (isRemoteSession) return
if (startupChecksStartedRef.current) return
const timer = setTimeout(() => {
if (!shouldRunStartupChecks(isRemoteSession, startupChecksStartedRef.current, promptTypingSuppressionActive)) return
startupChecksStartedRef.current = true
void performStartupChecks(setAppState)
}, STARTUP_CHECK_DELAY_MS)
return () => clearTimeout(timer)
}, [setAppState, isRemoteSession, promptTypingSuppressionActive])
// Allow Claude in Chrome MCP to send prompts through MCP notifications
// and sync permission mode changes to the Chrome extension
@@ -2061,13 +2072,14 @@ export function REPL({
if (allowDialogsWithAnimation && showRemoteCallout) return 'remote-callout';
// LSP plugin recommendation (lowest priority - non-blocking suggestion)
if (allowDialogsWithAnimation && lspRecommendation) return 'lsp-recommendation';
// Suppress during startup window to prevent stealing focus from the prompt (issue #363)
if (allowDialogsWithAnimation && lspRecommendation && startupChecksStartedRef.current) return 'lsp-recommendation';
// Plugin hint from CLI/SDK stderr (same priority band as LSP rec)
if (allowDialogsWithAnimation && hintRecommendation) return 'plugin-hint';
// Plugin hint from CLI/SDK stderr (same priority band as LSP rec)
if (allowDialogsWithAnimation && hintRecommendation && startupChecksStartedRef.current) return 'plugin-hint';
// Desktop app upsell (max 3 launches, lowest priority)
if (allowDialogsWithAnimation && showDesktopUpsellStartup) return 'desktop-upsell';
// Desktop app upsell (max 3 launches, lowest priority)
if (allowDialogsWithAnimation && showDesktopUpsellStartup && startupChecksStartedRef.current) return 'desktop-upsell';
return undefined;
}
const focusedInputDialog = getFocusedInputDialog();

View File

@@ -0,0 +1,32 @@
import { describe, expect, test } from 'bun:test'
import { shouldRunStartupChecks, STARTUP_CHECK_DELAY_MS } from './replStartupGates.js'
describe('shouldRunStartupChecks', () => {
test('runs checks when not remote, not started, and not typing', () => {
expect(shouldRunStartupChecks(false, false, false)).toBe(true)
})
test('skips checks in remote sessions', () => {
expect(shouldRunStartupChecks(true, false, false)).toBe(false)
})
test('skips checks if already started', () => {
expect(shouldRunStartupChecks(false, true, false)).toBe(false)
})
test('skips checks while user is typing', () => {
expect(shouldRunStartupChecks(false, false, true)).toBe(false)
})
test('skips checks when remote even if started and typing', () => {
expect(shouldRunStartupChecks(true, true, true)).toBe(false)
})
})
describe('STARTUP_CHECK_DELAY_MS', () => {
test('delay is positive and reasonable', () => {
expect(STARTUP_CHECK_DELAY_MS).toBeGreaterThan(0)
expect(STARTUP_CHECK_DELAY_MS).toBeLessThanOrEqual(5000)
})
})

View File

@@ -0,0 +1,27 @@
/**
* Startup gates for the REPL.
*
* Prevents startup plugin checks and recommendation dialogs from stealing
* focus while the user is typing or has early input buffered.
*
* This addresses the root cause of issue #363: on mount, performStartupChecks
* triggers plugin loading, which populates trackedFiles, which triggers
* useLspPluginRecommendation to surface an LSP recommendation dialog. Since
* promptTypingSuppressionActive is false before the user has typed anything,
* getFocusedInputDialog() returns the dialog, unmounting PromptInput entirely.
*/
const STARTUP_CHECK_DELAY_MS = 1500
export function shouldRunStartupChecks(
isRemoteSession: boolean,
hasStarted: boolean,
promptTypingSuppressionActive: boolean,
): boolean {
if (isRemoteSession) return false
if (hasStarted) return false
if (promptTypingSuppressionActive) return false
return true
}
export { STARTUP_CHECK_DELAY_MS }