diff --git a/src/browser/components/ChatInput/CreationControls.tsx b/src/browser/components/ChatInput/CreationControls.tsx
index e53b7bcd30..0fa829aa1e 100644
--- a/src/browser/components/ChatInput/CreationControls.tsx
+++ b/src/browser/components/ChatInput/CreationControls.tsx
@@ -164,11 +164,10 @@ function RuntimeButtonGroup(props: RuntimeButtonGroupProps) {
const state = props.runtimeAvailabilityState;
const availabilityMap = state?.status === "loaded" ? state.data : null;
- // Hide only during loading (prevents flash), show on failed (allows fallback selection)
+ // Hide devcontainer only when confirmed missing (not during loading - would cause layout flash)
const hideDevcontainer =
- state?.status === "loading" ||
- (availabilityMap?.devcontainer?.available === false &&
- availabilityMap.devcontainer.reason === "No devcontainer.json found");
+ availabilityMap?.devcontainer?.available === false &&
+ availabilityMap.devcontainer.reason === "No devcontainer.json found";
const runtimeOptions = hideDevcontainer
? RUNTIME_OPTIONS.filter((option) => option.value !== RUNTIME_MODE.DEVCONTAINER)
@@ -254,6 +253,8 @@ export function CreationControls(props: CreationControlsProps) {
const availabilityMap =
runtimeAvailabilityState.status === "loaded" ? runtimeAvailabilityState.data : null;
const showTrunkBranchSelector = props.branches.length > 0 && runtimeMode !== RUNTIME_MODE.LOCAL;
+ // Show loading skeleton while branches are loading to avoid layout flash
+ const showBranchLoadingPlaceholder = !props.branchesLoaded && runtimeMode !== RUNTIME_MODE.LOCAL;
// Centralized devcontainer selection logic
const devcontainerSelection = resolveDevcontainerSelection({
@@ -493,25 +494,35 @@ export function CreationControls(props: CreationControlsProps) {
/>
)}
-
- {/* SSH Host Input - hidden when Coder is enabled */}
- {selectedRuntime.mode === "ssh" && !props.coderProps?.enabled && (
+ {/* Loading placeholder - reserves space while branches load to avoid layout flash */}
+ {showBranchLoadingPlaceholder && (
-
host
-
onSelectedRuntimeChange({ mode: "ssh", host: e.target.value })}
- placeholder="user@host"
- disabled={props.disabled}
- className={cn(
- "bg-bg-dark text-foreground border-border-medium focus:border-accent h-7 w-36 rounded-md border px-2 text-sm focus:outline-none disabled:opacity-50",
- props.runtimeFieldError === "ssh" && "border-red-500"
- )}
- />
+
from
+
)}
+ {/* SSH Host Input - hidden when Coder is enabled or will be enabled after checking */}
+ {selectedRuntime.mode === "ssh" &&
+ !props.coderProps?.enabled &&
+ // Also hide when Coder is still checking but has saved config (will enable after check)
+ !(props.coderProps?.coderInfo === null && props.coderProps?.coderConfig) && (
+
+ host
+ onSelectedRuntimeChange({ mode: "ssh", host: e.target.value })}
+ placeholder="user@host"
+ disabled={props.disabled}
+ className={cn(
+ "bg-bg-dark text-foreground border-border-medium focus:border-accent h-7 w-36 rounded-md border px-2 text-sm focus:outline-none disabled:opacity-50",
+ props.runtimeFieldError === "ssh" && "border-red-500"
+ )}
+ />
+
+ )}
+
{/* Runtime-specific config inputs */}
{selectedRuntime.mode === "docker" && (
diff --git a/src/browser/components/ProjectMCPOverview.tsx b/src/browser/components/ProjectMCPOverview.tsx
index 0efa1c1f63..6cfe5e6edf 100644
--- a/src/browser/components/ProjectMCPOverview.tsx
+++ b/src/browser/components/ProjectMCPOverview.tsx
@@ -4,6 +4,8 @@ import type { MCPServerInfo } from "@/common/types/mcp";
import { useAPI } from "@/browser/contexts/API";
import { useSettings } from "@/browser/contexts/SettingsContext";
import { Button } from "@/browser/components/ui/button";
+import { getMCPServersKey } from "@/common/constants/storage";
+import { readPersistedState, updatePersistedState } from "@/browser/hooks/usePersistedState";
interface ProjectMCPOverviewProps {
projectPath: string;
@@ -16,7 +18,10 @@ export const ProjectMCPOverview: React.FC = (props) =>
const [loading, setLoading] = React.useState(false);
const [error, setError] = React.useState(null);
- const [servers, setServers] = React.useState>({});
+ // Initialize from localStorage cache to avoid flash
+ const [servers, setServers] = React.useState>(() =>
+ readPersistedState>(getMCPServersKey(projectPath), {})
+ );
React.useEffect(() => {
if (!api || settings.isOpen) return;
@@ -27,7 +32,10 @@ export const ProjectMCPOverview: React.FC = (props) =>
.list({ projectPath })
.then((result) => {
if (cancelled) return;
- setServers(result ?? {});
+ const newServers = result ?? {};
+ setServers(newServers);
+ // Cache for next load
+ updatePersistedState(getMCPServersKey(projectPath), newServers);
setError(null);
})
.catch((err) => {
diff --git a/src/browser/components/ProjectPage.tsx b/src/browser/components/ProjectPage.tsx
index 37ce343e72..400313cb01 100644
--- a/src/browser/components/ProjectPage.tsx
+++ b/src/browser/components/ProjectPage.tsx
@@ -17,10 +17,15 @@ import { ConfigureProvidersPrompt } from "./ConfigureProvidersPrompt";
import { useProvidersConfig } from "@/browser/hooks/useProvidersConfig";
import type { ProvidersConfigMap } from "@/common/orpc/types";
import { AgentsInitBanner } from "./AgentsInitBanner";
-import { usePersistedState, updatePersistedState } from "@/browser/hooks/usePersistedState";
+import {
+ usePersistedState,
+ updatePersistedState,
+ readPersistedState,
+} from "@/browser/hooks/usePersistedState";
import {
getAgentIdKey,
getAgentsInitNudgeKey,
+ getArchivedWorkspacesKey,
getInputKey,
getPendingScopeId,
getProjectScopeId,
@@ -72,7 +77,10 @@ export const ProjectPage: React.FC = ({
const { api } = useAPI();
const chatInputRef = useRef(null);
const pendingAgentsInitSendRef = useRef(false);
- const [archivedWorkspaces, setArchivedWorkspaces] = useState([]);
+ // Initialize from localStorage cache to avoid flash when archived workspaces appear
+ const [archivedWorkspaces, setArchivedWorkspaces] = useState(() =>
+ readPersistedState(getArchivedWorkspacesKey(projectPath), [])
+ );
const [showAgentsInitNudge, setShowAgentsInitNudge] = usePersistedState(
getAgentsInitNudgeKey(projectPath),
false,
@@ -128,8 +136,13 @@ export const ProjectPage: React.FC = ({
const syncArchivedState = useCallback(() => {
const next = Array.from(archivedMapRef.current.values());
- setArchivedWorkspaces((prev) => (archivedListsEqual(prev, next) ? prev : next));
- }, []);
+ setArchivedWorkspaces((prev) => {
+ if (archivedListsEqual(prev, next)) return prev;
+ // Persist to localStorage for optimistic cache on next load
+ updatePersistedState(getArchivedWorkspacesKey(projectPath), next);
+ return next;
+ });
+ }, [projectPath]);
// Fetch archived workspaces for this project on mount
useEffect(() => {
@@ -289,8 +302,16 @@ export const ProjectPage: React.FC = ({
/>
)}
{/* Configured providers bar - compact icon carousel */}
- {providersConfig && hasProviders && (
-
+ {providersLoading ? (
+ // Skeleton placeholder matching ConfiguredProvidersBar height
+
+ ) : (
+ hasProviders &&
+ providersConfig && (
+
+ )
)}
{/* ChatInput for workspace creation - includes section selector */}
(null);
useEffect(() => {
const prevMode = prevSelectedRuntimeModeRef.current;
- if (prevMode !== selectedRuntime.mode) {
+ if (prevMode !== null && prevMode !== selectedRuntime.mode) {
if (selectedRuntime.mode === RUNTIME_MODE.SSH) {
const needsHostRestore = !selectedRuntime.host.trim() && lastSshHost.trim();
const needsCoderRestore =
diff --git a/src/common/constants/storage.ts b/src/common/constants/storage.ts
index d1753675ed..b4e2a3f123 100644
--- a/src/common/constants/storage.ts
+++ b/src/common/constants/storage.ts
@@ -73,6 +73,24 @@ export function getMCPTestResultsKey(projectPath: string): string {
return `mcpTestResults:${projectPath}`;
}
+/**
+ * Get the localStorage key for cached archived workspaces per project
+ * Format: "archivedWorkspaces:{projectPath}"
+ * Stores: Array of workspace metadata objects (optimistic cache)
+ */
+export function getArchivedWorkspacesKey(projectPath: string): string {
+ return `archivedWorkspaces:${projectPath}`;
+}
+
+/**
+ * Get the localStorage key for cached MCP servers per project
+ * Format: "mcpServers:{projectPath}"
+ * Stores: Record (optimistic cache)
+ */
+export function getMCPServersKey(projectPath: string): string {
+ return `mcpServers:${projectPath}`;
+}
+
/**
* Get the localStorage key for thinking level preference per scope (workspace/project).
* Format: "thinkingLevel:{scopeId}"