fix(desktop): keep composer usable during reconnect (#45488)
* feat(cli): add --safe-mode troubleshooting flag Inspired by Claude Code v2.1.169 (June 2026): run Hermes with all customizations disabled to isolate setup problems from product bugs. --safe-mode implies --ignore-user-config and --ignore-rules, and additionally skips plugin discovery (hermes_cli/plugins.py) and MCP server loading (tools/mcp_tool.py) via the internal HERMES_SAFE_MODE env bridge. * fix(desktop): keep composer usable during reconnect
This commit is contained in:
@@ -3,23 +3,23 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
||||
|
||||
import { $desktopBoot } from '@/store/boot'
|
||||
import { $desktopOnboarding } from '@/store/onboarding'
|
||||
import { $gatewayState, setGatewayState } from '@/store/session'
|
||||
import { setGatewayState } from '@/store/session'
|
||||
|
||||
import { BootFailureOverlay } from './boot-failure-overlay'
|
||||
import { GatewayConnectingOverlay } from './gateway-connecting-overlay'
|
||||
|
||||
// Repro for the "remote gateway → stuck on CONNECTING, no way to settings"
|
||||
// report. The connecting overlay (z-1200, full-screen, pointer-events on) is
|
||||
// shown whenever `gatewayState !== 'open' && !boot.error`. The ONLY escape
|
||||
// report. The connecting overlay (z-1200, full-screen, pointer-events on) used
|
||||
// to be shown whenever `gatewayState !== 'open' && !boot.error`. The ONLY escape
|
||||
// hatch — BootFailureOverlay, which has "Use local gateway" / "Sign in" /
|
||||
// "Retry" — only renders when `boot.error` is set.
|
||||
//
|
||||
// useGatewayBoot only calls failDesktopBoot() (which sets boot.error) when the
|
||||
// INITIAL boot() throws. After the first successful connect (bootCompleted),
|
||||
// any later socket drop goes through scheduleReconnect(), which loops FOREVER
|
||||
// against the dead remote and never sets boot.error. So gatewayState sits at
|
||||
// 'closed'/'error' with boot.error null → CONNECTING forever, recovery overlay
|
||||
// never appears, settings unreachable.
|
||||
// against the dead remote. So gatewayState sits at 'closed'/'error' with
|
||||
// boot.error null. The fix keeps the initial-boot overlay out of post-boot
|
||||
// reconnects, leaving chat/settings usable while the reconnect loop runs.
|
||||
|
||||
function resetStores() {
|
||||
setGatewayState('idle')
|
||||
@@ -75,7 +75,7 @@ describe('connecting overlay vs recovery surface', () => {
|
||||
expect(isConnectingShown()).toBe(false)
|
||||
})
|
||||
|
||||
it('REPRO: remote socket drops AFTER a successful boot → stuck on CONNECTING, no recovery, no settings', () => {
|
||||
it('post-boot socket drops do not re-cover the app with the initial CONNECTING overlay', () => {
|
||||
// 1. Initial boot succeeded: gateway opened, boot completed (no error).
|
||||
setGatewayState('open')
|
||||
const { rerender } = render(
|
||||
@@ -97,14 +97,14 @@ describe('connecting overlay vs recovery surface', () => {
|
||||
</>
|
||||
)
|
||||
|
||||
// The connecting overlay reappears and latches...
|
||||
expect(isConnectingShown()).toBe(true)
|
||||
// ...with NO recovery surface, because boot.error was never set.
|
||||
// The initial-boot connecting overlay stays out of the way, so settings and
|
||||
// the composer remain reachable during the reconnect loop.
|
||||
expect(isConnectingShown()).toBe(false)
|
||||
expect(isRecoveryShown()).toBe(false)
|
||||
|
||||
// 3. Reconnect loops forever against the dead remote: gatewayState bounces
|
||||
// closed → error → closed, boot.error never gets set. The user is
|
||||
// pinned on CONNECTING with no path to Settings indefinitely.
|
||||
// 3. Reconnect loops against the dead remote: gatewayState bounces closed
|
||||
// → error → closed. Until the escalation path sets boot.error, the app
|
||||
// remains usable instead of modal-blocked.
|
||||
setGatewayState('error')
|
||||
rerender(
|
||||
<>
|
||||
@@ -113,7 +113,7 @@ describe('connecting overlay vs recovery surface', () => {
|
||||
</>
|
||||
)
|
||||
expect($desktopBoot.get().error).toBeNull()
|
||||
expect(isConnectingShown()).toBe(true)
|
||||
expect(isConnectingShown()).toBe(false)
|
||||
expect(isRecoveryShown()).toBe(false)
|
||||
})
|
||||
|
||||
|
||||
@@ -52,7 +52,13 @@ export function GatewayConnectingOverlay() {
|
||||
const [tail, setTail] = useState(TAIL)
|
||||
const [phase, setPhase] = useState<Phase>('live')
|
||||
|
||||
const connecting = gatewayState !== 'open' && !boot.error
|
||||
// The full-screen connecting overlay is for initial boot only. After a
|
||||
// healthy boot, flaky networks / sleep-wake can drop the socket and flip the
|
||||
// gateway state back to closed/error while the app reconnects. Do not cover
|
||||
// the chat then — users should still be able to type drafts, open settings,
|
||||
// and recover instead of staring at a modal CONNECTING screen.
|
||||
const initialBootActive = boot.visible || boot.running || boot.progress < 100
|
||||
const connecting = gatewayState !== 'open' && !boot.error && initialBootActive
|
||||
// Latches once we've actually shown the overlay, so the brief frame where
|
||||
// gatewayState flips to "open" (connecting -> false) before the exit phase
|
||||
// kicks in doesn't unmount us and cause a flash.
|
||||
|
||||
Reference in New Issue
Block a user