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:
@@ -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();
|
||||
|
||||
32
src/screens/replStartupGates.test.ts
Normal file
32
src/screens/replStartupGates.test.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
27
src/screens/replStartupGates.ts
Normal file
27
src/screens/replStartupGates.ts
Normal 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 }
|
||||
Reference in New Issue
Block a user