Desktop app boot overlay and recovery fixes
NousResearch/hermes-agent · cluster 54545 · apps/desktop
Data as of
Nothing open on GitHub. Fix merged in #63030. Check it covers your case.
The short answer
- Known?
- Yes. 5 related items grouped here, 2 issues already closed.
- Fix event
- A fix was merged to main in PR #63030. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
resolved, deciding PRs #63030, #122035. That is separate from the fix event. - Still open
- Nothing open on GitHub.
These items relate to the desktop app's handling of startup issues, specifically improving the boot overlay to be dismissible and allowing recovery from a gateway error state.
Open issues0
Open PRs0
Fixes on record
-
#63030 fix(desktop): recover a failed gateway from the boot-failure screen
files:
apps/desktop/src/app/settings/gateway-settings.tsx,apps/desktop/src/app/settings/primitives.tsx,apps/desktop/src/components/boot-failure-overlay.test.tsx,apps/desktop/src/components/boot-failure-overlay.tsx,apps/desktop/src/components/boot-failure-reauth.test.ts,apps/desktop/src/components/boot-failure-reauth.ts,apps/desktop/src/components/gateway-connecting-overlay.test.tsx,apps/desktop/src/i18n/en.ts,apps/desktop/src/i18n/ja.ts,apps/desktop/src/i18n/types.ts,apps/desktop/src/i18n/zh-hant.ts,apps/desktop/src/i18n/zh.tsShow the diff
-
apps/desktop/src/app/settings/gateway-settings.tsx@@ -110,7 +110,11 @@ function ScopeChip({ active, label, onSelect }: { active: boolean; label: string ) } -export function GatewaySettings() { +// `embedded` trims the page chrome for reuse inside the boot-failure recovery +// card: the outer title/intro, the "Save for next restart" action, and the +// Diagnostics row are redundant there (the card owns its header + a single +// reconnect action), so only the connection controls render. +export function GatewaySettings({ embedded = false }: { embedded?: boolean } = {}) { const { t } = useI18n() const g = t.settings.gateway const [loading, setLoading] = useState(true) @@ -707,17 +711,19 @@ export function GatewaySettings() { } return ( - <SettingsContent> - <div className="mb-5"> - <div className="flex items-center gap-2 text-[length:var(--conversation-text-font-size)] font-medium"> - <Globe className="size-4 text-muted-foreground" /> - {g.title} - {state.envOverride ? <Pill tone="primary">{g.envOverride}</Pill> : null} + <SettingsContent bare={embedded}> + {embedded ? null : ( + <div className="mb-5"> + <div className="flex items-center gap-2 text-[length:var(--conversation-text-font-size)] font-medium"> + <Globe className="size-4 text-muted-foreground" /> + {g.title} + {state.envOverride ? <Pill tone="primary">{g.envOverride}</Pill> : null} + </div> + <p className="mt-2 max-w-2xl text-[length:var(--conversation-caption-font-size)] leading-(--conversation-caption-line-height) text-(--ui-text-tertiary)"> + {g.intro} + </p> </div> - <p className="mt-2 max-w-2xl text-[length:var(--conversation-caption-font-size)] leading-(--conversation-caption-line-height) text-(--ui-text-tertiary)"> - {g.intro} - </p> - </div> + )} {namedProfiles.length > 0 ? ( <div className="mb-5 grid gap-2"> @@ -1037,14 +1043,16 @@ export function GatewaySettings() { {g.testRemote} </Button> ) : null} - <Button - disabled={state.envOverride || saving} - onClick={() => void save(false)} - size="sm" - variant="textStrong" - > - {g.saveForRestart} - </Button> + {embedded ? null : ( + <Button + disabled={state.envOverride || saving} + onClick={() => void save(false)} + size="sm" + variant="textStrong" + > + {g.saveForRestart} + </Button> + )} <Button disabled={state.envOverride || saving} onClick={() => void save(true)} size="sm"> {saving ? <Loader2 className="animate-spin" /> : null} {g.saveAndReconnect} @@ -1052,18 +1060,20 @@ export function GatewaySettings() { </div> ) : null} - <div className="mt-6 grid gap-1"> - <ListRow - action={ - <Button onClick={() => void window.hermesDesktop?.revealLogs()} size="sm" variant="textStrong"> - <FileText /> - {g.openLogs} - </Button> - } - description={g.diagnosticsDesc} - title={g.diagnostics} - /> - </div> + {embedded ? null : ( + <div className="mt-6 grid gap-1"> + <ListRow + action={ + <Button onClick={() => void window.hermesDesktop?.revealLogs()} size="sm" variant="textStrong"> + <FileText /> + {g.openLogs} + </Button> + } + description={g.diagnosticsDesc} + title={g.diagnostics} + /> + </div> + )} </SettingsContent> ) } -
apps/desktop/src/app/settings/primitives.tsx@@ -8,10 +8,14 @@ import { cn } from '@/lib/utils' import { PAGE_INSET_X } from '../layout-constants' -export function SettingsContent({ children }: { children: ReactNode }) { +// `bare` drops the page gutters + tall bottom pad for embedding in a tighter +// surface (e.g. the boot-failure recovery card owns its own padding). +export function SettingsContent({ children, bare = false }: { children: ReactNode; bare?: boolean }) { return ( <section className="min-h-0 overflow-hidden"> - <div className={cn('h-full min-h-0 overflow-y-auto pb-20', PAGE_INSET_X)}>{children}</div> + <div className={cn('h-full min-h-0 overflow-y-auto', bare ? 'px-5 pb-6' : cn('pb-20', PAGE_INSET_X))}> + {children} + </div> </section> ) } -
apps/desktop/src/components/boot-failure-overlay.test.tsx@@ -0,0 +1,101 @@ +import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react' +import { afterEach, beforeEach, describe, expect, it } from 'vitest' + +import { $desktopBoot } from '@/store/boot' +import { $desktopOnboarding } from '@/store/onboarding' + +import { BootFailureOverlay } from './boot-failure-overlay' + +// Remote-backend users hit a hard boot failure that isn't OAuth reauth (token +// auth, wrong URL, unreachable host). The recovery screen must let them fix the +// remote connection in place — the "Connection settings" action swaps the card +// to an in-line connect form — instead of stranding them (the old bug forced a +// hand-edit of connection.json). + +function failBoot() { + $desktopBoot.set({ + error: 'Could not connect to Hermes gateway', + fakeMode: false, + message: 'boot failed', + phase: 'renderer.error', + progress: 40, + running: false, + timestamp: Date.now(), + visible: true + }) +} + +function stubDesktop(config: Record<string, unknown>) { + const original = window.hermesDesktop + Object.defineProperty(window, 'hermesDesktop', { + configurable: true, + value: { getRecentLogs: async () => ({ lines: [] }), getConnectionConfig: async () => config } + }) + + return () => Object.defineProperty(window, 'hermesDesktop', { configurable: true, value: original }) +} + +const remoteToken = { + envOverride: false, + mode: 'remote', + profile: null, + remoteAuthMode: 'token', + remoteOauthConnected: false, + remoteTokenPreview: null, + remoteTokenSet: true, + remoteUrl: 'http://100.116.104.53:9191', + cloudOrg: '' +} + +beforeEach(() => { + $desktopOnboarding.set({ + configured: true, + flow: { status: 'idle' }, + mode: 'oauth', + providers: null, + reason: null, + requested: false, + firstRunSkipped: false, + manual: false, + localEndpoint: false + }) + failBoot() +}) + +afterEach(cleanup) + +describe('BootFailureOverlay', () => { + it('swaps to the in-place gateway settings view (no route nav) and back', async () => { + render(<BootFailureOverlay />) + + fireEvent.click(screen.getByRole('button', { name: /gateway settings/i })) + // Recovery actions give way to the embedded panel (behind a Back control). + expect(await screen.findByRole('button', { name: /back/i })).toBeTruthy() + expect(screen.queryByRole('button', { name: /retry/i })).toBeNull() + + fireEvent.click(screen.getByRole('button', { name: /back/i })) + expect(screen.getByRole('button', { name: /retry/i })).toBeTruthy() + expect(screen.queryByRole('button', { name: /back/i })).toBeNull() + }) + + it('drops local-only Repair and Use-local-gateway on a local failure', () => { + render(<BootFailureOverlay />) + // No connection config stub → treated as a local failure. + expect(screen.getByRole('button', { name: /retry/i })).toBeTruthy() + expect(screen.getByRole('button', { name: /repair/i })).toBeTruthy() + expect(screen.queryByRole('button', { name: /use local gateway/i })).toBeNull() + }) + + it('leads with Gateway settings and drops Repair for a remote (token) failure', async () => { + const restore = stubDesktop(remoteToken) + + try { + render(<BootFailureOverlay />) + await waitFor(() => expect(screen.queryByRole('button', { name: /repair/i })).toBeNull()) + expect(screen.getByRole('button', { name: /gateway settings/i })).toBeTruthy() + expect(screen.getByRole('button', { name: /use local gateway/i })).toBeTruthy() + } finally { + restore() + } + }) +}) -
apps/desktop/src/components/boot-failure-overlay.tsx@@ -1,20 +1,29 @@ import { useStore } from '@nanostores/react' -import { useEffect, useState } from 'react' +import { type ComponentProps, lazy, type ReactNode, Suspense, useEffect, useState } from 'react' import { Button } from '@/components/ui/button' import { ErrorIcon } from '@/components/ui/error-state' +import { Loader } from '@/components/ui/loader' import { LogView } from '@/components/ui/log-view' import type { DesktopConnectionConfig } from '@/global' import { useI18n } from '@/i18n' -import { FileText, Loader2, LogIn, RefreshCw, Wrench } from '@/lib/icons' +import { ChevronLeft, FileText, Loader2, LogIn, RefreshCw, SlidersHorizontal, Wrench } from '@/lib/icons' import { $desktopBoot } from '@/store/boot' import { notify, notifyError } from '@/store/notifications' import { $desktopOnboarding } from '@/store/onboarding' import type { RemoteReauth } from './boot-failure-reauth' -import { deriveProviderShape, isRemoteReauthFailure, signInLabel } from './boot-failure-reauth' +import { deriveProviderShape, isRemoteConfig, isRemoteReauthFailure, signInLabel } from './boot-failure-reauth' + +// The recovery "Gateway settings" view embeds the real Settings → Gateway panel +// (identical URL/auth/test/save controls — no parallel form to drift). Lazy so +// it stays out of the always-mounted overlay's bundle until opened. +const GatewaySettings = lazy(() => + import('@/app/settings/gateway-settings').then(module => ({ default: module.GatewaySettings })) +) type BusyAction = 'local' | 'repair' | 'retry' | 'signin' | null +type RecoveryView = 'connect' | 'recovery' // A remote gateway whose access cookie has lapsed (e.g. the dashboard // restarted on the remote box) boots into this overlay with a reauth-shaped @@ -35,6 +44,13 @@ export function BootFailureOverlay() { const [logs, setLogs] = useState<string[]>([]) const [showLogs, setShowLogs] = useState(false) const [remoteReauth, setRemoteReauth] = useState<RemoteReauth | null>(null) + // A remote/cloud backend that failed to boot is fixable from gateway settings, + // so the escape hatch earns emphasis (local failures keep it as a quiet ghost). + const [remoteFailure, setRemoteFailure] = useState(false) + // Swap the card body to the embedded Gateway settings panel in place of routing + // to the full Settings page (keeps the user on the recovery surface, no z-index + // juggling, no second connection form to maintain). + const [view, setView] = useState<RecoveryView>('recovery') const visible = Boolean(boot.error) && !boot.running // While first-run onboarding owns the picker/flow we let it surface its own @@ -51,7 +67,7 @@ export function BootFailureOverlay() { ?.getRecentLogs() .then(res => setLogs(res.lines ?? [])) .catch(() => undefined) - }, [visible]) + }, [boot.error, visible]) // Resolve whether this boot failure is a remote-gateway reauth so we can // offer the actionable "Sign in" path instead of the local-only recovery @@ -59,6 +75,8 @@ export function BootFailureOverlay() { useEffect(() => { if (!visible) { setRemoteReauth(null) + setRemoteFailure(false) + setView('recovery') return } @@ -80,7 +98,13 @@ export function BootFailureOverlay() { return } - if (cancelled || !isRemoteReauthFailure(config)) { + if (cancelled) { + return + } + + setRemoteFailure(isRemoteConfig(config)) + + if (!isRemoteReauthFailure(config, boot.error)) { return } @@ -104,7 +128,7 @@ export function BootFailureOverlay() { return () => { cancelled = true } - }, [visible]) + }, [boot.error, visible]) if (!visible || suppressed) { return null @@ -129,11 +153,12 @@ export function BootFailureOverlay() { setBusy(null) } - // Open the gateway's login window (renders the username/password form for a - // basic gateway, or the OAuth redirect otherwise — the desktop drives both - // through the same window). On a successful sign-in the session cookie is - // re-established in the persistent partition; reload so boot re-runs and the - // reconnect now mints a ticket against a live session. + // Clear the OAuth partition first, then open the gateway's login window + // (username/password form or OAuth redirect — the desktop drives both). A + // partition-wide sign-out drops stale gateway AND identity-provider cookies so + // an expired session can't silently bounce us back into the same state. On a + // successful sign-in the cookie is re-established; reload so boot mints a fresh + // ticket against a live session. const signInRemote = async () => { if (!remoteReauth) { return @@ -142,6 +167,7 @@ export function BootFailureOverlay() { setBusy('signin') try { + await window.hermesDesktop?.oauthLogoutConnectionConfig?.() const result = await window.hermesDesktop?.oauthLoginConnectionConfig(remoteReauth.url) if (result?.connected) { @@ -172,6 +198,92 @@ export function BootFailureOverlay() { withProvider: copy.signInWithProvider }) + // Recovery actions are shaped by the failure kind so the leading (primary) + // button is the one that actually fixes it: Sign in for a lapsed remote + // session, Connection settings for any other remote failure (local Retry / + // Repair can't revive a dead remote — Repair is dropped there), Retry for a + // local backend. Open logs is always appended. + type RecoveryVariant = ComponentProps<typeof Button>['variant'] + interface RecoveryAction { + key: string + label: string + onClick: () => void + icon?: ReactNode + variant?: RecoveryVariant + busy?: Exclude<BusyAction, null> + } + + const settingsAction: RecoveryAction = { + key: 'settings', + label: copy.gatewaySettings, + onClick: () => setView('connect'), + icon: <SlidersHorizontal /> + } + + const retryAction: RecoveryAction = { + key: 'retry', + label: copy.retry, + onClick: () => void retry(), + icon: <RefreshCw />, + busy: 'retry' + } + + const localAction: RecoveryAction = { + key: 'local', + label: copy.useLocalGateway, + onClick: () => void switchToLocalGateway(), + variant: 'secondary', + busy: 'local' + } + + let actions: RecoveryAction[] + let hint: string + + if (remoteReauth) { + actions = [ + { key: 'signin', label: copy.signOutAndSignIn, onClick: () => void signInRemote(), icon: <LogIn />, busy: 'signin' }, + { ...settingsAction, variant: 'secondary' }, + localAction + ] + hint = copy.remoteSignInHint(label) + } else if (remoteFailure) { + actions = [settingsAction, { ...retryAction, variant: 'secondary' }, localAction] + hint = copy.remoteFailureHint + } else { + // Local failure: Use-local is redundant with Retry (both re-target local), so + // it's dropped here; keep it for remote failures where it's the fall-back. + actions = [ + retryAction, + { key: 'repair', label: copy.repairInstall, onClick: () => void repair(), icon: <Wrench />, variant: 'secondary', busy: 'repair' }, + { ...settingsAction, variant: 'ghost' } + ] + hint = copy.repairHint + } + + if (view === 'connect') { + return ( + <div className="fixed inset-0 z-[1400] flex items-center justify-center bg-(--ui-chat-surface-background) p-6"> + <div className="flex max-h-[86vh] w-full max-w-[46rem] flex-col overflow-hidden rounded-xl border border-(--stroke-nous) bg-(--ui-chat-bubble-background) shadow-nous"> + {/* Subtle back affordance (projects/overlay idiom): muted → foreground + on hover, no divider. */} + <button + className="flex w-full items-center gap-1.5 px-4 pt-4 text-left text-xs text-muted-foreground transition-colors hover:text-foreground" + onClick={() => setView('recovery')} + type="button" + > + <ChevronLeft className="size-3.5" /> + {copy.back} + </button> + <div className="min-h-0 flex-1 pt-4"> + <Suspense fallback={<Loader className="mx-auto my-16 size-6 text-(--ui-text-tertiary)" />}> + <GatewaySettings embedded /> + </Suspense> + </div> + </div> + </div> + ) + } + return ( <div className="fixed inset-0 z-[1400] flex items-center justify-center bg-(--ui-chat-surface-background) p-6"> <div className="w-full max-w-[40rem] overflow-hidden rounded-xl border border-(--stroke-nous) bg-(--ui-chat-bubble-background) shadow-nous"> @@ -187,40 +299,25 @@ export function BootFailureOverlay() { </div> </div> - <div className="grid gap-4 p-5"> + <div className="grid gap-4 p-5 pt-0"> <div className="rounded-2xl border border-destructive/30 bg-destructive/10 px-4 py-3 text-xs text-destructive"> {boot.error} </div> <div className="grid gap-2"> <div className="flex flex-wrap gap-2"> - {remoteReauth ? ( - <Button disabled={Boolean(busy)} onClick={() => void signInRemote()}> - {busy === 'signin' ? <Loader2 className="animate-spin" /> : <LogIn />} - {label} - </Button> - ) : ( - <Button disabled={Boolean(busy)} onClick={() => void retry()}> - {busy === 'retry' ? <Loader2 className="animate-spin" /> : <RefreshCw />} - {copy.retry} + {actions.map(action => ( + <Button disabled={Boolean(busy)} key={action.key} onClick={action.onClick} variant={action.variant}> + {action.busy && busy === action.busy ? <Loader2 className="animate-spin" /> : action.icon} + {action.label} </Button> - )} - {!remoteReauth ? ( - <Button disabled={Boolean(busy)} onClick={() => void repair()} variant="secondary"> - {busy === 'repair' ? <Loader2 className="animate-spin" /> : <Wrench />} - {copy.repairInstall} - </Button> - ) : null} - <Button disabled={Boolean(busy)} onClick={() => void switchToLocalGateway()} variant="secondary"> - {busy === 'local' ? <Loader2 className="animate-spin" /> : null} - {copy.useLocalGateway} - </Button> + ))} <Button onClick={openLogs} variant="ghost"> <FileText /> {copy.openLogs} </Button> </div> - <p className="text-xs text-muted-foreground">{remoteReauth ? copy.remoteSignInHint : copy.repairHint}</p> + <p className="text-xs text-muted-foreground">{hint}</p> </div> {logs.length > 0 ? ( -
apps/desktop/src/components/boot-failure-reauth.test.ts@@ -2,7 +2,13 @@ import { describe, expect, it } from 'vitest' import type { DesktopConnectionConfig } from '@/global' -import { deriveProviderShape, isRemoteReauthFailure, signInLabel } from './boot-failure-reauth' +import { + deriveProviderShape, + isRemoteConfig, + isRemoteReauthError, + isRemoteReauthFailure, + signInLabel +} from './boot-failure-reauth' function config(overrides: Partial<DesktopConnectionConfig> = {}): DesktopConnectionConfig { return { @@ -19,13 +25,32 @@ function config(overrides: Partial<DesktopConnectionConfig> = {}): DesktopConnec } } +describe('isRemoteConfig', () => { + it('true for remote/cloud with a URL, regardless of auth mode or connection', () => { + expect(isRemoteConfig(config({ remoteAuthMode: 'token', remoteOauthConnected: false }))).toBe(true) + expect(isRemoteConfig(config({ mode: 'cloud', remoteOauthConnected: true }))).toBe(true) + }) + + it('false for local, for a remote with no URL, and for nullish', () => { + expect(isRemoteConfig(config({ mode: 'local' }))).toBe(false) + expect(isRemoteConfig(config({ remoteUrl: '' }))).toBe(false) + expect(isRemoteConfig(null)).toBe(false) + }) +}) + describe('isRemoteReauthFailure', () => { it('true for a remote, gated, disconnected gateway with a URL', () => { expect(isRemoteReauthFailure(config())).toBe(true) }) - it('false when the oauth session is still connected', () => { - expect(isRemoteReauthFailure(config({ remoteOauthConnected: true }))).toBe(false) + it('false when connected and the boot error is not auth-shaped', () => { + expect(isRemoteReauthFailure(config({ remoteOauthConnected: true }), 'Python exploded')).toBe(false) + }) + + it('true when the indicator reads connected but the boot error is auth-shaped (expired session)', () => { + expect( + isRemoteReauthFailure(config({ remoteOauthConnected: true }), 'Your remote gateway session has expired.') + ).toBe(true) }) it('false for a local gateway', () => { @@ -56,6 +81,18 @@ describe('isRemoteReauthFailure', () => { }) }) +describe('isRemoteReauthError', () => { + it('recognizes auth-shaped boot errors', () => { + expect(isRemoteReauthError('Your remote gateway session has expired.')).toBe(true) + expect(isRemoteReauthError('OAuth: please sign in')).toBe(true) + }) + + it('ignores non-auth boot errors and nullish', () => { + expect(isRemoteReauthError('Hermes background process exited during startup.')).toBe(false) + expect(isRemoteReauthError(null)).toBe(false) + }) +}) + describe('deriveProviderShape', () => { it('generic copy when there are no providers', () => { expect(deriveProviderShape([])).toEqual({ isPassword: [redacted:secret-assignment] providerLabel: 'your identity provider' }) -
apps/desktop/src/components/boot-failure-reauth.ts@@ -26,24 +26,40 @@ const DEFAULT_SIGN_IN_COPY: SignInCopy = { withProvider: provider => `Sign in with ${provider}` } -// A remote, gated (oauth-bucket), not-currently-connected gateway is a -// remote-reauth boot failure: the access cookie lapsed (e.g. the remote -// dashboard restarted) and the local-recovery buttons (Retry/Repair) can't -// fix it — only re-establishing the remote session can. A connected oauth -// session, or a token/local gateway, boots for some other reason the -// local-recovery buttons address, so those return false here. 'cloud' counts -// as remote here — it resolves to a remote oauth backend (cloud-auto-discovery -// Q6), so a lapsed cloud session is the same reauth failure. -export function isRemoteReauthFailure(config: DesktopConnectionConfig | null | undefined): boolean { - if (!config) { - return false - } +// True when the app is pointed at a remote/cloud backend (either resolves to a +// remote URL). Any boot failure in this shape is fixable from Settings → +// Gateway (edit URL / token / sign in) — the local Retry/Repair buttons target +// the bundled backend and can't help. Drives the escape-hatch emphasis. +export function isRemoteConfig(config: DesktopConnectionConfig | null | undefined): boolean { + return Boolean(config && (config.mode === 'remote' || config.mode === 'cloud') && config.remoteUrl) +} + +// True when a boot error is auth-shaped — the refresh token was rejected or the +// remote couldn't mint a websocket ticket. The Settings indicator can still read +// "connected" (a stale RT cookie exists), so the error text is part of the +// signal; without it a connected-but-expired session drops into the local-only +// recovery buttons for a problem only reauth can fix. +export function isRemoteReauthError(error: string | null | undefined): boolean { + const text = String(error || '').toLowerCase() + + return ( + text.includes('remote gateway session has expired') || + text.includes('gateway sign-in required') || + text.includes('needs oauth login') || + (text.includes('oauth') && (text.includes('not signed in') || text.includes('sign in'))) + ) +} +// A remote, gated (oauth-bucket) gateway is a remote-reauth boot failure when the +// session isn't connected OR the boot error is auth-shaped (connected-but-expired +// — see isRemoteReauthError). Only re-establishing the remote session fixes it; +// the local Retry/Repair buttons can't. 'cloud' counts as remote (it resolves to +// a remote oauth backend), so a lapsed cloud session is the same failure. +export function isRemoteReauthFailure(config: DesktopConnectionConfig | null | undefined, error?: string | null): boolean { return ( - (config.mode === 'remote' || config.mode === 'cloud') && - config.remoteAuthMode === 'oauth' && - !config.remoteOauthConnected && - Boolean(config.remoteUrl) + isRemoteConfig(config) && + config!.remoteAuthMode === 'oauth' && + (!config!.remoteOauthConnected || isRemoteReauthError(error)) ) } -
apps/desktop/src/components/gateway-connecting-overlay.test.tsx@@ -177,7 +177,7 @@ describe('connecting overlay vs recovery surface', () => { // Escape hatch is now reachable; the connecting overlay bows out. expect(isRecoveryShown()).toBe(true) - expect(screen.getByText(/use local gateway/i)).toBeTruthy() + expect(screen.getByRole('button', { name: /gateway settings/i })).toBeTruthy() expect(isConnectingShown()).toBe(false) }) }) -
apps/desktop/src/i18n/en.ts@@ -90,9 +90,14 @@ export const en: Translations = { retry: 'Retry', repairInstall: 'Repair install', useLocalGateway: 'Use local gateway', + gatewaySettings: 'Gateway settings', + back: 'Back', openLogs: 'Open logs', repairHint: 'Repair re-runs the installer and can take a few minutes on a fresh machine.', - remoteSignInHint: 'Opens the gateway login window. Use local gateway to switch to the bundled backend instead.', + remoteSignInHint: signInLabel => + `Signs out of the saved remote browser session, then opens ${signInLabel}. Use local gateway to switch to the bundled backend instead.`, + signOutAndSignIn: 'Sign out & sign in', + remoteFailureHint: 'Check the gateway URL and sign-in under Gateway settings, or switch to the local gateway.', hideRecentLogs: 'Hide recent logs', showRecentLogs: 'Show recent logs', signedInTitle: 'Signed in', -
apps/desktop/src/i18n/ja.ts@@ -90,10 +90,14 @@ export const ja = defineLocale({ retry: '再試行', repairInstall: 'インストールを修復', useLocalGateway: 'ローカルゲートウェイを使用', + gatewaySettings: 'ゲートウェイ設定', + back: '戻る', openLogs: 'ログを開く', repairHint: '修復はインストーラーを再実行します。新しいマシンでは数分かかる場合があります。', - remoteSignInHint: - 'ゲートウェイのログインウィンドウを開きます。代わりにバンドルされたバックエンドに切り替えるには「ローカルゲートウェイを使用」を選択してください。', + remoteSignInHint: signInLabel => + `保存済みのリモートブラウザセッションからサインアウトし、${signInLabel}を開きます。代わりにバンドルされたバックエンドに切り替えるには「ローカルゲートウェイを使用」を選択してください。`, + signOutAndSignIn: 'サインアウトして再サインイン', + remoteFailureHint: '「ゲートウェイ設定」でゲートウェイの URL とサインインを確認するか、ローカルゲートウェイに切り替えてください。', hideRecentLogs: '最近のログを非表示', showRecentLogs: '最近のログを表示', signedInTitle: 'サインインしました', -
apps/desktop/src/i18n/types.ts@@ -133,9 +133,13 @@ export interface Translations { retry: string repairInstall: string useLocalGateway: string + gatewaySettings: string + back: string openLogs: string repairHint: string - remoteSignInHint: string + remoteSignInHint: (signInLabel: string) => string + signOutAndSignIn: string + remoteFailureHint: string hideRecentLogs: string showRecentLogs: string signedInTitle: string -
apps/desktop/src/i18n/zh-hant.ts@@ -88,9 +88,14 @@ export const zhHant = defineLocale({ retry: '重試', repairInstall: '修復安裝', useLocalGateway: '使用本機閘道', + gatewaySettings: '閘道設定', + back: '返回', openLogs: '開啟記錄', repairHint: '修復會重新執行安裝程式,在新機器上可能需要幾分鐘。', - remoteSignInHint: '開啟閘道登入視窗。使用本機閘道可切換至內建後端。', + remoteSignInHint: signInLabel => + `先登出已儲存的遠端瀏覽器工作階段,然後開啟${signInLabel}。使用本機閘道可切換至內建後端。`, + signOutAndSignIn: '登出並重新登入', + remoteFailureHint: '在「閘道設定」中檢查閘道 URL 與登入,或切換至本機閘道。', hideRecentLogs: '隱藏最近記錄', showRecentLogs: '顯示最近記錄', signedInTitle: '已登入', -
apps/desktop/src/i18n/zh.ts@@ -88,9 +88,14 @@ export const zh: Translations = { retry: '重试', repairInstall: '修复安装', useLocalGateway: '使用本地网关', + gatewaySettings: '网关设置', + back: '返回', openLogs: '打开日志', repairHint: '修复会重新运行安装器,在新机器上可能需要几分钟。', - remoteSignInHint: '打开网关登录窗口。也可以使用本地网关切换到随应用提供的后端。', + remoteSignInHint: signInLabel => + `先退出已保存的远程浏览器会话,然后打开${signInLabel}。也可以使用本地网关切换到随应用提供的后端。`, + signOutAndSignIn: '退出并重新登录', + remoteFailureHint: '在“网关设置”中检查网关 URL 和登录,或切换到本地网关。', hideRecentLogs: '隐藏最近日志', showRecentLogs: '显示最近日志', signedInTitle: '已登录',
-
-
#122035 fix(desktop): let the boot-failure overlay be dismissed
files:
apps/desktop/src/components/boot-failure-overlay.test.tsx,apps/desktop/src/components/boot-failure-overlay.tsx
1 PR closed without merging
From the close comments: superseded by another PR 1
-
#62559 fix(desktop): make boot failure overlay dismissible · superseded by another PR →
#122035
Closed issues · 2
How this cluster was grouped
-
Link evidence (
partly structural links): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text. - Members outside that structural core may belong to a different underlying problem.
-
Pairwise link density
0.34: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36.1.00means every pair is linked.
Pick up a Lab task
Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.
Open the Lab