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:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user