perf: reduce idle desktop load by gating plan-mode checks
- Add startup feature flag for plan mode from OPENCODE_EXPERIMENTAL* env vars. - Stop recurring app health polling and keep a one-time startup check. - Skip plan-mode synthetic parsing and plan tab/file work when flag is off.
This commit is contained in:
+24
-5
@@ -40,12 +40,12 @@ import { registerRuntimeAPIs } from '@/contexts/runtimeAPIRegistry';
|
|||||||
import { VoiceProvider } from '@/components/voice';
|
import { VoiceProvider } from '@/components/voice';
|
||||||
import { useUIStore } from '@/stores/useUIStore';
|
import { useUIStore } from '@/stores/useUIStore';
|
||||||
import { useGitHubAuthStore } from '@/stores/useGitHubAuthStore';
|
import { useGitHubAuthStore } from '@/stores/useGitHubAuthStore';
|
||||||
|
import { useFeatureFlagsStore } from '@/stores/useFeatureFlagsStore';
|
||||||
import type { RuntimeAPIs } from '@/lib/api/types';
|
import type { RuntimeAPIs } from '@/lib/api/types';
|
||||||
import { TooltipProvider } from '@/components/ui/tooltip';
|
import { TooltipProvider } from '@/components/ui/tooltip';
|
||||||
|
|
||||||
const CLI_MISSING_ERROR_REGEX =
|
const CLI_MISSING_ERROR_REGEX =
|
||||||
/ENOENT|spawn\s+opencode|Unable\s+to\s+locate\s+the\s+opencode\s+CLI|OpenCode\s+CLI\s+not\s+found|opencode(\.exe)?\s+not\s+found|opencode(\.exe)?:\s*command\s+not\s+found|not\s+recognized\s+as\s+an\s+internal\s+or\s+external\s+command|env:\s*['"]?(node|bun)['"]?:\s*No\s+such\s+file\s+or\s+directory|(node|bun):\s*No\s+such\s+file\s+or\s+directory/i;
|
/ENOENT|spawn\s+opencode|Unable\s+to\s+locate\s+the\s+opencode\s+CLI|OpenCode\s+CLI\s+not\s+found|opencode(\.exe)?\s+not\s+found|opencode(\.exe)?:\s*command\s+not\s+found|not\s+recognized\s+as\s+an\s+internal\s+or\s+external\s+command|env:\s*['"]?(node|bun)['"]?:\s*No\s+such\s+file\s+or\s+directory|(node|bun):\s*No\s+such\s+file\s+or\s+directory/i;
|
||||||
const CLI_ONBOARDING_HEALTH_POLL_MS = 1500;
|
|
||||||
|
|
||||||
const AboutDialogWrapper: React.FC = () => {
|
const AboutDialogWrapper: React.FC = () => {
|
||||||
const isAboutDialogOpen = useUIStore((s) => s.isAboutDialogOpen);
|
const isAboutDialogOpen = useUIStore((s) => s.isAboutDialogOpen);
|
||||||
@@ -176,6 +176,7 @@ function App({ apis }: AppProps) {
|
|||||||
const [showCliOnboarding, setShowCliOnboarding] = React.useState(false);
|
const [showCliOnboarding, setShowCliOnboarding] = React.useState(false);
|
||||||
const [isEmbeddedVisible, setIsEmbeddedVisible] = React.useState(true);
|
const [isEmbeddedVisible, setIsEmbeddedVisible] = React.useState(true);
|
||||||
const isDesktopRuntime = React.useMemo(() => isDesktopShell(), []);
|
const isDesktopRuntime = React.useMemo(() => isDesktopShell(), []);
|
||||||
|
const setPlanModeEnabled = useFeatureFlagsStore((state) => state.setPlanModeEnabled);
|
||||||
const appReadyDispatchedRef = React.useRef(false);
|
const appReadyDispatchedRef = React.useRef(false);
|
||||||
const embeddedSessionChat = React.useMemo<EmbeddedSessionChatConfig | null>(() => readEmbeddedSessionChatConfig(), []);
|
const embeddedSessionChat = React.useMemo<EmbeddedSessionChatConfig | null>(() => readEmbeddedSessionChatConfig(), []);
|
||||||
const embeddedBackgroundWorkEnabled = !embeddedSessionChat || isEmbeddedVisible;
|
const embeddedBackgroundWorkEnabled = !embeddedSessionChat || isEmbeddedVisible;
|
||||||
@@ -256,6 +257,28 @@ function App({ apis }: AppProps) {
|
|||||||
return () => clearTimeout(fallbackTimer);
|
return () => clearTimeout(fallbackTimer);
|
||||||
}, [isInitialized]);
|
}, [isInitialized]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
|
const run = async () => {
|
||||||
|
const res = await fetch('/health', { method: 'GET' }).catch(() => null);
|
||||||
|
if (!res || !res.ok || cancelled) return;
|
||||||
|
const data = (await res.json().catch(() => null)) as null | {
|
||||||
|
planModeExperimentalEnabled?: unknown;
|
||||||
|
};
|
||||||
|
if (!data || cancelled) return;
|
||||||
|
const raw = data.planModeExperimentalEnabled;
|
||||||
|
const enabled = raw === true || raw === 1 || raw === '1' || raw === 'true';
|
||||||
|
setPlanModeEnabled(enabled);
|
||||||
|
};
|
||||||
|
|
||||||
|
void run();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [setPlanModeEnabled]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const init = async () => {
|
const init = async () => {
|
||||||
// VS Code runtime bootstraps config + sessions after the managed OpenCode instance reports "connected".
|
// VS Code runtime bootstraps config + sessions after the managed OpenCode instance reports "connected".
|
||||||
@@ -480,13 +503,9 @@ function App({ apis }: AppProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
void run();
|
void run();
|
||||||
const interval = window.setInterval(() => {
|
|
||||||
void run();
|
|
||||||
}, CLI_ONBOARDING_HEALTH_POLL_MS);
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
window.clearInterval(interval);
|
|
||||||
};
|
};
|
||||||
}, [embeddedSessionChat]);
|
}, [embeddedSessionChat]);
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useShallow } from 'zustand/react/shallow';
|
|||||||
import { defaultCodeDark, defaultCodeLight } from '@/lib/codeTheme';
|
import { defaultCodeDark, defaultCodeLight } from '@/lib/codeTheme';
|
||||||
import { MessageFreshnessDetector } from '@/lib/messageFreshness';
|
import { MessageFreshnessDetector } from '@/lib/messageFreshness';
|
||||||
import { useConfigStore } from '@/stores/useConfigStore';
|
import { useConfigStore } from '@/stores/useConfigStore';
|
||||||
|
import { useFeatureFlagsStore } from '@/stores/useFeatureFlagsStore';
|
||||||
import { useUIStore } from '@/stores/useUIStore';
|
import { useUIStore } from '@/stores/useUIStore';
|
||||||
import { useContextStore } from '@/stores/contextStore';
|
import { useContextStore } from '@/stores/contextStore';
|
||||||
import { useStreamingStore } from '@/sync/streaming';
|
import { useStreamingStore } from '@/sync/streaming';
|
||||||
@@ -214,6 +215,7 @@ const ChatMessage: React.FC<ChatMessageProps> = ({
|
|||||||
const showStickyInlineHoverRow = isUser && !isMobile && stickyUserHeader && !useExternalUserActionsRow;
|
const showStickyInlineHoverRow = isUser && !isMobile && stickyUserHeader && !useExternalUserActionsRow;
|
||||||
|
|
||||||
const sessionId = message.info.sessionID;
|
const sessionId = message.info.sessionID;
|
||||||
|
const planModeEnabled = useFeatureFlagsStore((state) => state.planModeEnabled);
|
||||||
|
|
||||||
// Keep non-active-turn rows detached from context-store churn.
|
// Keep non-active-turn rows detached from context-store churn.
|
||||||
const { currentContextAgent, savedSessionAgentSelection } = useContextStore(
|
const { currentContextAgent, savedSessionAgentSelection } = useContextStore(
|
||||||
@@ -228,8 +230,8 @@ const ChatMessage: React.FC<ChatMessageProps> = ({
|
|||||||
return message.parts;
|
return message.parts;
|
||||||
}
|
}
|
||||||
|
|
||||||
return normalizeUserDisplayParts(message.parts);
|
return normalizeUserDisplayParts(message.parts, { planModeEnabled });
|
||||||
}, [isUser, message.parts]);
|
}, [isUser, message.parts, planModeEnabled]);
|
||||||
|
|
||||||
const previousUserMetadata = React.useMemo(() => {
|
const previousUserMetadata = React.useMemo(() => {
|
||||||
if (isUser || !previousMessage) {
|
if (isUser || !previousMessage) {
|
||||||
@@ -269,6 +271,7 @@ const ChatMessage: React.FC<ChatMessageProps> = ({
|
|||||||
}, [isUser, previousMessage]);
|
}, [isUser, previousMessage]);
|
||||||
|
|
||||||
const previousIsModeSwitchMessage = React.useMemo(() => {
|
const previousIsModeSwitchMessage = React.useMemo(() => {
|
||||||
|
if (!planModeEnabled) return false;
|
||||||
if (isUser || !previousMessage) return false;
|
if (isUser || !previousMessage) return false;
|
||||||
const parts = Array.isArray(previousMessage.parts) ? previousMessage.parts : [];
|
const parts = Array.isArray(previousMessage.parts) ? previousMessage.parts : [];
|
||||||
for (let i = 0; i < parts.length; i++) {
|
for (let i = 0; i < parts.length; i++) {
|
||||||
@@ -281,7 +284,7 @@ const ChatMessage: React.FC<ChatMessageProps> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}, [isUser, previousMessage]);
|
}, [isUser, planModeEnabled, previousMessage]);
|
||||||
|
|
||||||
const agentName = React.useMemo(() => {
|
const agentName = React.useMemo(() => {
|
||||||
if (isUser) return undefined;
|
if (isUser) return undefined;
|
||||||
|
|||||||
@@ -81,15 +81,16 @@ const buildGitHubAttachmentPart = (text: string): Part | null => {
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const shouldKeepSyntheticUserText = (text: string): boolean => {
|
const shouldKeepSyntheticUserText = (text: string, planModeEnabled: boolean): boolean => {
|
||||||
const trimmed = text.trim();
|
const trimmed = text.trim();
|
||||||
if (trimmed.startsWith('User has requested to enter plan mode')) return true;
|
if (planModeEnabled && trimmed.startsWith('User has requested to enter plan mode')) return true;
|
||||||
if (trimmed.startsWith('The plan at ')) return true;
|
if (planModeEnabled && trimmed.startsWith('The plan at ')) return true;
|
||||||
if (trimmed.startsWith('The following tool was executed by the user')) return true;
|
if (trimmed.startsWith('The following tool was executed by the user')) return true;
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const normalizeUserDisplayParts = (parts: Part[]): Part[] => {
|
export const normalizeUserDisplayParts = (parts: Part[], options?: { planModeEnabled?: boolean }): Part[] => {
|
||||||
|
const planModeEnabled = options?.planModeEnabled === true;
|
||||||
return parts
|
return parts
|
||||||
.filter((part) => {
|
.filter((part) => {
|
||||||
const synthetic = (part as { synthetic?: boolean }).synthetic === true;
|
const synthetic = (part as { synthetic?: boolean }).synthetic === true;
|
||||||
@@ -101,7 +102,7 @@ export const normalizeUserDisplayParts = (parts: Part[]): Part[] => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const normalizedText = text.trimStart();
|
const normalizedText = text.trimStart();
|
||||||
return shouldKeepSyntheticUserText(text)
|
return shouldKeepSyntheticUserText(text, planModeEnabled)
|
||||||
|| normalizedText.startsWith(GITHUB_ISSUE_CONTEXT_PREFIX)
|
|| normalizedText.startsWith(GITHUB_ISSUE_CONTEXT_PREFIX)
|
||||||
|| normalizedText.startsWith(GITHUB_PR_CONTEXT_PREFIX);
|
|| normalizedText.startsWith(GITHUB_PR_CONTEXT_PREFIX);
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import { useFilesViewTabsStore } from '@/stores/useFilesViewTabsStore';
|
|||||||
import { useGitStore } from '@/stores/useGitStore';
|
import { useGitStore } from '@/stores/useGitStore';
|
||||||
import { useGitHubPrStatusStore } from '@/stores/useGitHubPrStatusStore';
|
import { useGitHubPrStatusStore } from '@/stores/useGitHubPrStatusStore';
|
||||||
import { useGlobalSessionsStore } from '@/stores/useGlobalSessionsStore';
|
import { useGlobalSessionsStore } from '@/stores/useGlobalSessionsStore';
|
||||||
|
import { useFeatureFlagsStore } from '@/stores/useFeatureFlagsStore';
|
||||||
|
|
||||||
import { useGitHubAuthStore } from '@/stores/useGitHubAuthStore';
|
import { useGitHubAuthStore } from '@/stores/useGitHubAuthStore';
|
||||||
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
|
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
|
||||||
@@ -807,7 +808,8 @@ export const Header: React.FC<HeaderProps> = ({
|
|||||||
|
|
||||||
|
|
||||||
const [planTabAvailable, setPlanTabAvailable] = React.useState(false);
|
const [planTabAvailable, setPlanTabAvailable] = React.useState(false);
|
||||||
const showPlanTab = planTabAvailable;
|
const planModeEnabled = useFeatureFlagsStore((state) => state.planModeEnabled);
|
||||||
|
const showPlanTab = planModeEnabled && planTabAvailable;
|
||||||
const lastPlanSessionKeyRef = React.useRef<string>('');
|
const lastPlanSessionKeyRef = React.useRef<string>('');
|
||||||
|
|
||||||
const handleGitHubAccountSwitch = React.useCallback(async (accountId: string) => {
|
const handleGitHubAccountSwitch = React.useCallback(async (accountId: string) => {
|
||||||
@@ -843,6 +845,14 @@ export const Header: React.FC<HeaderProps> = ({
|
|||||||
}, [isSwitchingGitHubAccount, runtimeApis.github, setGitHubAuthStatus]);
|
}, [isSwitchingGitHubAccount, runtimeApis.github, setGitHubAuthStatus]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
if (!planModeEnabled) {
|
||||||
|
setPlanTabAvailable(false);
|
||||||
|
if (useUIStore.getState().activeMainTab === 'plan') {
|
||||||
|
useUIStore.getState().setActiveMainTab('chat');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
|
|
||||||
const checkExists = async (directory: string, fileName: string): Promise<boolean> => {
|
const checkExists = async (directory: string, fileName: string): Promise<boolean> => {
|
||||||
@@ -903,6 +913,7 @@ export const Header: React.FC<HeaderProps> = ({
|
|||||||
window.clearInterval(interval);
|
window.clearInterval(interval);
|
||||||
};
|
};
|
||||||
}, [
|
}, [
|
||||||
|
planModeEnabled,
|
||||||
sessionDirectory,
|
sessionDirectory,
|
||||||
currentSession?.slug,
|
currentSession?.slug,
|
||||||
currentSession?.time?.created,
|
currentSession?.time?.created,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { RiCheckLine, RiClipboardLine, RiFileCopy2Line } from '@remixicon/react'
|
|||||||
import { useSessionUIStore } from '@/sync/session-ui-store';
|
import { useSessionUIStore } from '@/sync/session-ui-store';
|
||||||
import { useSessions } from '@/sync/sync-context';
|
import { useSessions } from '@/sync/sync-context';
|
||||||
import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
||||||
|
import { useFeatureFlagsStore } from '@/stores/useFeatureFlagsStore';
|
||||||
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
|
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
|
||||||
import { EditorView } from '@codemirror/view';
|
import { EditorView } from '@codemirror/view';
|
||||||
import { copyTextToClipboard } from '@/lib/clipboard';
|
import { copyTextToClipboard } from '@/lib/clipboard';
|
||||||
@@ -84,6 +85,7 @@ export const PlanView: React.FC = () => {
|
|||||||
const currentSessionId = useSessionUIStore((state) => state.currentSessionId);
|
const currentSessionId = useSessionUIStore((state) => state.currentSessionId);
|
||||||
const sessions = useSessions();
|
const sessions = useSessions();
|
||||||
const homeDirectory = useDirectoryStore((state) => state.homeDirectory);
|
const homeDirectory = useDirectoryStore((state) => state.homeDirectory);
|
||||||
|
const planModeEnabled = useFeatureFlagsStore((state) => state.planModeEnabled);
|
||||||
const runtimeApis = useRuntimeAPIs();
|
const runtimeApis = useRuntimeAPIs();
|
||||||
useUIStore();
|
useUIStore();
|
||||||
const { isMobile } = useDeviceInfo();
|
const { isMobile } = useDeviceInfo();
|
||||||
@@ -256,6 +258,13 @@ export const PlanView: React.FC = () => {
|
|||||||
}, [currentTheme, resolvedPath]);
|
}, [currentTheme, resolvedPath]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
if (!planModeEnabled) {
|
||||||
|
setResolvedPath(null);
|
||||||
|
setContent('');
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
|
|
||||||
const readText = async (path: string): Promise<string> => {
|
const readText = async (path: string): Promise<string> => {
|
||||||
@@ -339,7 +348,7 @@ export const PlanView: React.FC = () => {
|
|||||||
cancelled = true;
|
cancelled = true;
|
||||||
window.clearInterval(interval);
|
window.clearInterval(interval);
|
||||||
};
|
};
|
||||||
}, [sessionDirectory, session?.slug, session?.time?.created, homeDirectory, runtimeApis.files]);
|
}, [planModeEnabled, sessionDirectory, session?.slug, session?.time?.created, homeDirectory, runtimeApis.files]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
|
||||||
|
type FeatureFlagsStore = {
|
||||||
|
planModeEnabled: boolean;
|
||||||
|
setPlanModeEnabled: (enabled: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useFeatureFlagsStore = create<FeatureFlagsStore>((set) => ({
|
||||||
|
planModeEnabled: false,
|
||||||
|
setPlanModeEnabled: (enabled) => set({ planModeEnabled: enabled }),
|
||||||
|
}));
|
||||||
@@ -98,6 +98,18 @@ const OPENCHAMBER_VERSION = (() => {
|
|||||||
}
|
}
|
||||||
return 'unknown';
|
return 'unknown';
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
const isEnvFlagEnabled = (value) => {
|
||||||
|
if (value === true || value === 1) return true;
|
||||||
|
if (typeof value !== 'string') return false;
|
||||||
|
const normalized = value.trim().toLowerCase();
|
||||||
|
return normalized === '1' || normalized === 'true';
|
||||||
|
};
|
||||||
|
|
||||||
|
const PLAN_MODE_EXPERIMENT_ENABLED =
|
||||||
|
isEnvFlagEnabled(process.env.OPENCODE_EXPERIMENTAL_PLAN_MODE)
|
||||||
|
|| isEnvFlagEnabled(process.env.OPENCODE_EXPERIMENTAL);
|
||||||
|
|
||||||
const fsPromises = fs.promises;
|
const fsPromises = fs.promises;
|
||||||
|
|
||||||
const settingsNormalizationRuntime = createSettingsNormalizationRuntime({
|
const settingsNormalizationRuntime = createSettingsNormalizationRuntime({
|
||||||
@@ -850,6 +862,7 @@ async function main(options = {}) {
|
|||||||
opencodeWslDistro: resolvedWslDistro || null,
|
opencodeWslDistro: resolvedWslDistro || null,
|
||||||
nodeBinaryResolved: resolvedNodeBinary || null,
|
nodeBinaryResolved: resolvedNodeBinary || null,
|
||||||
bunBinaryResolved: resolvedBunBinary || null,
|
bunBinaryResolved: resolvedBunBinary || null,
|
||||||
|
planModeExperimentalEnabled: PLAN_MODE_EXPERIMENT_ENABLED,
|
||||||
}),
|
}),
|
||||||
uiPassword,
|
uiPassword,
|
||||||
tunnelAuthController,
|
tunnelAuthController,
|
||||||
|
|||||||
Reference in New Issue
Block a user