From 106f85d0bfbc4a01b7d510721cd01a0a9c9e4720 Mon Sep 17 00:00:00 2001 From: OpenClaude Worker 3 Date: Wed, 8 Apr 2026 11:24:36 +0530 Subject: [PATCH] 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. --- src/screens/REPL.tsx | 28 +++++++++++++++++------- src/screens/replStartupGates.test.ts | 32 ++++++++++++++++++++++++++++ src/screens/replStartupGates.ts | 27 +++++++++++++++++++++++ 3 files changed, 79 insertions(+), 8 deletions(-) create mode 100644 src/screens/replStartupGates.test.ts create mode 100644 src/screens/replStartupGates.ts diff --git a/src/screens/REPL.tsx b/src/screens/REPL.tsx index 1ee59943..bcfd6a4d 100644 --- a/src/screens/REPL.tsx +++ b/src/screens/REPL.tsx @@ -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(); diff --git a/src/screens/replStartupGates.test.ts b/src/screens/replStartupGates.test.ts new file mode 100644 index 00000000..cd4ae96a --- /dev/null +++ b/src/screens/replStartupGates.test.ts @@ -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) + }) +}) \ No newline at end of file diff --git a/src/screens/replStartupGates.ts b/src/screens/replStartupGates.ts new file mode 100644 index 00000000..6f43ae5a --- /dev/null +++ b/src/screens/replStartupGates.ts @@ -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 } \ No newline at end of file