perf: reduce re-renders, fix mobile keyboard handling, add chunk load recovery, and improve PATH management (#1028)
* fix: exclude file content from reverted prompt text Revert and fork now restore only the user's original prompt, not server-injected file content Uses existing isSyntheticPart helper for type-safe filtering * fix: keep scrollbar visible when hovering over thumb * fix: prevent ESC abort from triggering when terminal is focused * fix: pass directory to permission/question reply calls so approvals actually resolve * fix: default model selection not responding after Base UI migration * fix: prevent modal content from shifting and clipping footer buttons * fix: improve session switching performance and add sub-agent export with prompt collapse Defer viewport anchor saving to eliminate ~800ms UI freeze when switching sessions Add export dialog to include sub-agent tasks recursively in markdown export Add collapse chevron button for expanded user prompts in sticky header * fix: resolve sidebar scroll and TDZ crash in session sidebar * perf: reduce CPU overhead and re-renders across chat, layout, and settings * fix: position collapse button at top of message and prevent ESC abort in terminal * fix: position collapse button at top and add padding only when expanded * refactor: extract shared PATH utilities and mobile keyboard hook * refactor: import shared path-utils in electron, use module-level style constants - Electron now imports pathLooksUserConfigured/mergePathValues from shared path-utils.js instead of inline duplication - ToolPart collapsedCustomStyle moved from useMemo([]) to module const * fix: resolve remaining merge conflicts and type errors - Remove duplicate variable declarations in SessionNodeItem - Remove orphaned export callback body from conflict resolution - Fix HelpDialog description -> descriptionKey (i18n rename) * fix: resolve type-check and lint errors in session-actions.test.ts - Added missing bun:test type declarations (beforeEach, mock, mock.module) - Removed unused State import - Replaced 'as any' casts with proper OpencodeClient and ChildStoreManager types - Added eslint-disable for unused _ parameter in mock function * fix PR 1028 export and PATH edge cases * fix startup retry exhaustion state * remove opencode package lock change * fix sub-session rename cancellation --------- Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
committed by
GitHub
co-authored by
Bohdan Triapitsyn
parent
632e6cc97b
commit
4523e9c486
+99
-6
@@ -5,6 +5,7 @@ import { AgentManagerView } from '@/components/views/agent-manager';
|
||||
import { ChatView } from '@/components/views';
|
||||
import { FireworksProvider } from '@/contexts/FireworksContext';
|
||||
import { Toaster } from '@/components/ui/sonner';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { MemoryDebugPanel } from '@/components/ui/MemoryDebugPanel';
|
||||
import { setStreamPerfEnabled } from '@/stores/utils/streamDebug';
|
||||
import { ErrorBoundary } from '@/components/ui/ErrorBoundary';
|
||||
@@ -56,6 +57,7 @@ import { QuickOpenDialog } from '@/components/ui/QuickOpenDialog';
|
||||
import { McpOAuthCallbackPage } from '@/components/sections/mcp/McpOAuthCallbackPage';
|
||||
import { MCP_OAUTH_CALLBACK_PATH } from '@/components/sections/mcp/mcpOAuth';
|
||||
import { lazyWithChunkRecovery } from '@/lib/chunkLoadRecovery';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
|
||||
// Lazy-loaded heavy views — loaded on demand to reduce initial bundle size.
|
||||
const OnboardingScreen = lazyWithChunkRecovery(() =>
|
||||
@@ -73,6 +75,27 @@ const AboutDialogWrapper: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const StartupInitializationRecovery: React.FC<{
|
||||
onRetry: () => void;
|
||||
isRetrying: boolean;
|
||||
}> = ({ onRetry, isRetrying }) => {
|
||||
const { t } = useI18n();
|
||||
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center bg-background px-6 text-foreground">
|
||||
<div className="flex max-w-md flex-col items-center gap-4 text-center">
|
||||
<div className="flex flex-col gap-2">
|
||||
<h1 className="typography-title text-foreground">{t('startup.initRecovery.title')}</h1>
|
||||
<p className="typography-body text-muted-foreground">{t('startup.initRecovery.description')}</p>
|
||||
</div>
|
||||
<Button type="button" onClick={onRetry} disabled={isRetrying}>
|
||||
{isRetrying ? t('startup.initRecovery.retrying') : t('startup.initRecovery.retry')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type AppProps = {
|
||||
apis: RuntimeAPIs;
|
||||
};
|
||||
@@ -195,6 +218,9 @@ function App({ apis }: AppProps) {
|
||||
const refreshGitHubAuthStatus = useGitHubAuthStore((state) => state.refreshStatus);
|
||||
const [isVSCodeRuntime, setIsVSCodeRuntime] = React.useState<boolean>(() => apis.runtime.isVSCode);
|
||||
const [isEmbeddedVisible, setIsEmbeddedVisible] = React.useState(true);
|
||||
const [initRetryExhausted, setInitRetryExhausted] = React.useState(false);
|
||||
const [initRetryEpoch, setInitRetryEpoch] = React.useState(0);
|
||||
const [manualInitRetrying, setManualInitRetrying] = React.useState(false);
|
||||
const isDesktopRuntime = React.useMemo(() => isDesktopShell(), []);
|
||||
const setPlanModeEnabled = useFeatureFlagsStore((state) => state.setPlanModeEnabled);
|
||||
const [bootInjectionStatus, setBootInjectionStatus] = React.useState<BootInjectionStatus>(() => {
|
||||
@@ -366,16 +392,27 @@ function App({ apis }: AppProps) {
|
||||
|
||||
let active = true;
|
||||
let retryTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
let retryCount = 0;
|
||||
const MAX_RETRIES = 10;
|
||||
const BASE_DELAY_MS = 1000;
|
||||
|
||||
const retryInitialization = async () => {
|
||||
if (!active) return;
|
||||
if (retryCount >= MAX_RETRIES) {
|
||||
setInitRetryExhausted(true);
|
||||
return;
|
||||
}
|
||||
const state = useConfigStore.getState();
|
||||
if (state.isInitialized) return;
|
||||
if (state.isInitialized) {
|
||||
setInitRetryExhausted(false);
|
||||
return;
|
||||
}
|
||||
if (initializationInFlightRef.current) {
|
||||
retryTimer = setTimeout(retryInitialization, 1000);
|
||||
retryTimer = setTimeout(retryInitialization, BASE_DELAY_MS);
|
||||
return;
|
||||
}
|
||||
|
||||
retryCount += 1;
|
||||
initializationInFlightRef.current = true;
|
||||
try {
|
||||
await state.initializeApp();
|
||||
@@ -384,17 +421,44 @@ function App({ apis }: AppProps) {
|
||||
}
|
||||
|
||||
const next = useConfigStore.getState();
|
||||
if (!active || next.isInitialized) return;
|
||||
retryTimer = setTimeout(retryInitialization, 1000);
|
||||
if (!active) return;
|
||||
if (next.isInitialized) {
|
||||
setInitRetryExhausted(false);
|
||||
return;
|
||||
}
|
||||
if (retryCount >= MAX_RETRIES) {
|
||||
setInitRetryExhausted(true);
|
||||
return;
|
||||
}
|
||||
const delay = Math.min(BASE_DELAY_MS * Math.pow(2, retryCount - 1), 16000);
|
||||
retryTimer = setTimeout(retryInitialization, delay);
|
||||
};
|
||||
|
||||
retryTimer = setTimeout(retryInitialization, 1000);
|
||||
retryTimer = setTimeout(retryInitialization, BASE_DELAY_MS);
|
||||
|
||||
return () => {
|
||||
active = false;
|
||||
if (retryTimer) clearTimeout(retryTimer);
|
||||
};
|
||||
}, [isInitialized, isVSCodeRuntime]);
|
||||
}, [initRetryEpoch, isInitialized, isVSCodeRuntime]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isInitialized) {
|
||||
setInitRetryExhausted(false);
|
||||
}
|
||||
}, [isInitialized]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!initRetryExhausted) return;
|
||||
|
||||
const loadingElement = document.getElementById('initial-loading');
|
||||
if (loadingElement) {
|
||||
loadingElement.classList.add('fade-out');
|
||||
setTimeout(() => {
|
||||
loadingElement.remove();
|
||||
}, 300);
|
||||
}
|
||||
}, [initRetryExhausted]);
|
||||
|
||||
// Startup recovery: poll until providers AND agents are loaded.
|
||||
// loadProviders/loadAgents resolve normally even on failure (errors swallowed),
|
||||
@@ -679,6 +743,24 @@ function App({ apis }: AppProps) {
|
||||
window.location.reload();
|
||||
}, []);
|
||||
|
||||
const handleManualInitRetry = React.useCallback(async () => {
|
||||
if (manualInitRetrying || initializationInFlightRef.current) return;
|
||||
|
||||
setInitRetryExhausted(false);
|
||||
setManualInitRetrying(true);
|
||||
initializationInFlightRef.current = true;
|
||||
try {
|
||||
await useConfigStore.getState().initializeApp();
|
||||
} finally {
|
||||
initializationInFlightRef.current = false;
|
||||
setManualInitRetrying(false);
|
||||
}
|
||||
|
||||
if (!useConfigStore.getState().isInitialized) {
|
||||
setInitRetryEpoch((value) => value + 1);
|
||||
}
|
||||
}, [manualInitRetrying]);
|
||||
|
||||
// Map boot outcome kind to recovery variant
|
||||
const mapBootViewToRecoveryVariant = (view: DesktopBootView): RecoveryVariant | undefined => {
|
||||
if (view.screen === 'recovery') {
|
||||
@@ -758,6 +840,17 @@ function App({ apis }: AppProps) {
|
||||
);
|
||||
}
|
||||
|
||||
if (initRetryExhausted && !isInitialized && !isVSCodeRuntime && !embeddedSessionChat) {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<StartupInitializationRecovery
|
||||
onRetry={() => { void handleManualInitRetry(); }}
|
||||
isRetrying={manualInitRetrying}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
// VS Code runtime - simplified layout without git/terminal views
|
||||
if (isVSCodeRuntime) {
|
||||
// Check if this is the Agent Manager panel
|
||||
|
||||
Reference in New Issue
Block a user