Restore features lost during main merge and fix workspace setup flow

Restores all features dropped when merging main into dev:
- Keyboard shortcuts (send, dictation-confirm) with full e2e chain
- Feature toggles (plan/fast mode) in agent status bar
- Sidebar kebab menu (Remove Project) on project rows
- Combined model selector (sticky header, sizing, favorites sort, search)
- Question form Enter key submit with guard
- Input focus restoration (composer + AgentStatusBar onDropdownClose)
- Draft-agent feature toggles and focus restoration
- Provider diagnostics UI (settings section, diagnostic sheet, status badge)
- Provider diagnostics server (snapshot manager, diagnostic utils, RPC)

Fixes dev-branch regressions:
- New workspace button now opens composer in setup dialog via beginWorkspaceSetup
- Setup tab auto-opens when workspace setup is running
This commit is contained in:
Mohamed Boudra
2026-04-05 21:10:16 +07:00
parent 8e482b1613
commit 4dca672711
26 changed files with 2002 additions and 147 deletions
@@ -6,6 +6,7 @@ export function buildWorkspaceDraftAgentConfig(input: {
modeId?: string;
model?: string;
thinkingOptionId?: string;
featureValues?: Record<string, unknown>;
}): AgentSessionConfig {
return {
provider: input.provider,
@@ -13,5 +14,6 @@ export function buildWorkspaceDraftAgentConfig(input: {
...(input.modeId ? { modeId: input.modeId } : {}),
...(input.model ? { model: input.model } : {}),
...(input.thinkingOptionId ? { thinkingOptionId: input.thinkingOptionId } : {}),
...(input.featureValues ? { featureValues: input.featureValues } : {}),
};
}
@@ -147,6 +147,7 @@ export function WorkspaceDraftAgentTab({
title: "Agent",
cwd: workspaceDirectory,
model,
features: composerState.statusControls.features,
thinkingOptionId,
labels: {},
};
@@ -166,6 +167,7 @@ export function WorkspaceDraftAgentTab({
: {}),
model: composerState.effectiveModelId || undefined,
thinkingOptionId: composerState.effectiveThinkingOptionId || undefined,
featureValues: composerState.featureValues,
});
const imagesData = await encodeImages(images);
@@ -195,6 +197,63 @@ export function WorkspaceDraftAgentTab({
addImagesRef.current = addImages;
}, []);
const focusInputRef = useRef<(() => void) | null>(null);
const handleFocusInputCallback = useCallback((focus: () => void) => {
focusInputRef.current = focus;
}, []);
const handleProviderSelectWithFocus = useCallback(
(provider: Parameters<typeof composerState.setProviderFromUser>[0]) => {
composerState.setProviderFromUser(provider);
focusInputRef.current?.();
},
[composerState],
);
const handleModeSelectWithFocus = useCallback(
(modeId: string) => {
composerState.setModeFromUser(modeId);
focusInputRef.current?.();
},
[composerState],
);
const handleModelSelectWithFocus = useCallback(
(modelId: string) => {
composerState.setModelFromUser(modelId);
focusInputRef.current?.();
},
[composerState],
);
const handleProviderAndModelSelectWithFocus = useCallback(
(
provider: Parameters<typeof composerState.setProviderAndModelFromUser>[0],
modelId: string,
) => {
composerState.setProviderAndModelFromUser(provider, modelId);
focusInputRef.current?.();
},
[composerState],
);
const handleThinkingOptionSelectWithFocus = useCallback(
(optionId: string) => {
composerState.setThinkingOptionFromUser(optionId);
focusInputRef.current?.();
},
[composerState],
);
const handleSetFeatureWithFocus = useCallback(
(featureId: string, value: unknown) => {
composerState.statusControls.onSetFeature?.(featureId, value);
focusInputRef.current?.();
},
[composerState],
);
return (
<FileDropZone onFilesDropped={handleFilesDropped}>
<View style={styles.container}>
@@ -242,9 +301,17 @@ export function WorkspaceDraftAgentTab({
clearDraft={draftInput.clear}
autoFocus={shouldAutoFocusWorkspaceDraftComposer({ isPaneFocused, isSubmitting })}
onAddImages={handleAddImagesCallback}
onFocusInput={handleFocusInputCallback}
commandDraftConfig={composerState.commandDraftConfig}
statusControls={{
...composerState.statusControls,
onSelectProvider: handleProviderSelectWithFocus,
onSelectMode: handleModeSelectWithFocus,
onSelectModel: handleModelSelectWithFocus,
onSelectProviderAndModel: handleProviderAndModelSelectWithFocus,
onSelectThinkingOption: handleThinkingOptionSelectWithFocus,
onSetFeature: handleSetFeatureWithFocus,
onDropdownClose: () => focusInputRef.current?.(),
disabled: isSubmitting,
}}
/>
@@ -70,6 +70,7 @@ import {
} from "@/utils/workspace-tab-identity";
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
import { useProviderModels } from "@/hooks/use-provider-models";
import { useWorkspaceSetupStore } from "@/stores/workspace-setup-store";
import { useWorkspaceTerminalSessionRetention } from "@/terminal/hooks/use-workspace-terminal-session-retention";
import {
checkoutStatusQueryKey,
@@ -121,6 +122,7 @@ import { findAdjacentPane } from "@/utils/split-navigation";
import { isCompactFormFactor, supportsDesktopPaneSplits } from "@/constants/layout";
const TERMINALS_QUERY_STALE_TIME = 5_000;
const WORKSPACE_SETUP_AUTO_OPEN_WINDOW_MS = 30_000;
const EMPTY_UI_TABS: WorkspaceTab[] = [];
const EMPTY_SET = new Set<string>();
@@ -858,6 +860,9 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
const workspaceLayout = useWorkspaceLayoutStore((state) =>
persistenceKey ? (state.layoutByWorkspace[persistenceKey] ?? null) : null,
);
const workspaceSetupSnapshot = useWorkspaceSetupStore((state) =>
persistenceKey ? state.snapshots[persistenceKey] ?? null : null,
);
const uiTabs = useMemo(
() => (workspaceLayout ? collectAllTabs(workspaceLayout.root) : EMPTY_UI_TABS),
[workspaceLayout],
@@ -1013,6 +1018,14 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
() => focusedPaneTabState.tabs.map((tab) => tab.descriptor),
[focusedPaneTabState.tabs],
);
const hasSetupTab = useMemo(
() =>
uiTabs.some(
(tab) =>
tab.target.kind === "setup" && tab.target.workspaceId === normalizedWorkspaceId,
),
[normalizedWorkspaceId, uiTabs],
);
const navigateToTabId = useCallback(
function navigateToTabId(tabId: string) {
@@ -1025,6 +1038,7 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
);
const emptyWorkspaceSeedRef = useRef<string | null>(null);
const autoOpenedSetupTabWorkspaceRef = useRef<string | null>(null);
useEffect(() => {
if (!persistenceKey) {
return;
@@ -1053,6 +1067,56 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
workspaceAgentVisibility.activeAgentIds.size,
]);
useEffect(() => {
if (!persistenceKey) {
return;
}
if (!workspaceSetupSnapshot) {
if (autoOpenedSetupTabWorkspaceRef.current === persistenceKey) {
autoOpenedSetupTabWorkspaceRef.current = null;
}
return;
}
const snapshotAge = Date.now() - workspaceSetupSnapshot.updatedAt;
const shouldAutoOpen =
workspaceSetupSnapshot.status === "running" ||
snapshotAge <= WORKSPACE_SETUP_AUTO_OPEN_WINDOW_MS;
if (!shouldAutoOpen) {
return;
}
if (hasSetupTab) {
autoOpenedSetupTabWorkspaceRef.current = persistenceKey;
return;
}
if (autoOpenedSetupTabWorkspaceRef.current === persistenceKey) {
return;
}
const target = normalizeWorkspaceTabTarget({
kind: "setup",
workspaceId: normalizedWorkspaceId,
});
if (!target) {
return;
}
const tabId = openWorkspaceTab(persistenceKey, target);
if (!tabId) {
return;
}
focusWorkspaceTab(persistenceKey, tabId);
autoOpenedSetupTabWorkspaceRef.current = persistenceKey;
}, [
focusWorkspaceTab,
hasSetupTab,
normalizedWorkspaceId,
openWorkspaceTab,
persistenceKey,
workspaceSetupSnapshot,
]);
const handleOpenFileFromExplorer = useCallback(
function handleOpenFileFromExplorer(filePath: string) {
if (isMobile) {