refactor(surface): remove the main-area surface concept entirely

activeSurface was permanently 'chat' after the legacy mobile layout
removal, so the whole concept is gone: the store field, surfaceGuard,
setActiveSurface/setSurfaceGuard, the per-runtime surface memory in
prepare/restoreForRuntimeSwitch, and WorkspaceSurface itself. All ~30
setActiveSurface('chat') call sites were no-ops and are deleted;
always-true 'is the chat active' checks in keyboard shortcuts, Header
and ChatContainer are unconditional now. FilesView's dirty-file guard
kept its file-switch and close protection but drops the surface-switch
branch nothing could trigger. TerminalView visibility comes only from
its callers. The router keeps parsing legacy ?tab= links (they open the
matching context-panel surface) via its own RouteTab type and no longer
serializes a tab or diff file into URLs — desktop URLs never carried
them anyway.
This commit is contained in:
Bohdan Triapitsyn
2026-08-24 16:36:41 +03:00
parent c6e39f15fe
commit c82f188fc8
31 changed files with 49 additions and 276 deletions
@@ -28,7 +28,6 @@ export function ArchiveView(): React.ReactNode {
const { t } = useI18n();
const open = useUIStore((state) => state.isArchivePageOpen);
const setOpen = useUIStore((state) => state.setArchivePageOpen);
const setActiveSurface = useUIStore((state) => state.setActiveSurface);
const setCurrentSession = useSessionUIStore((state) => state.setCurrentSession);
const unarchiveSession = useSessionUIStore((state) => state.unarchiveSession);
const homeDirectory = useDirectoryStore((state) => state.homeDirectory);
@@ -86,9 +85,8 @@ export function ArchiveView(): React.ReactNode {
const openSession = React.useCallback((session: Session) => {
const directory = normalizePath(resolveGlobalSessionDirectory(session));
setCurrentSession(session.id, directory ?? undefined);
setActiveSurface('chat');
setOpen(false);
}, [setActiveSurface, setCurrentSession, setOpen]);
}, [setCurrentSession, setOpen]);
const restoreSession = React.useCallback((session: Session) => {
void unarchiveSession(session.id).then((success) => {
+3 -44
View File
@@ -939,7 +939,6 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
const [confirmDiscardOpen, setConfirmDiscardOpen] = React.useState(false);
const pendingSelectFileRef = React.useRef<FileNode | null>(null);
const pendingTabRef = React.useRef<import('@/stores/useUIStore').WorkspaceSurface | null>(null);
const pendingClosePathRef = React.useRef<string | null>(null);
const skipDirtyOnceRef = React.useRef(false);
const copiedContentTimeoutRef = React.useRef<number | null>(null);
@@ -1029,7 +1028,6 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
const [isDragging, setIsDragging] = React.useState(false);
// Session/config for sending comments
const setSurfaceGuard = useUIStore((state) => state.setSurfaceGuard);
const pendingFileNavigation = useUIStore((state) => state.pendingFileNavigation);
const setPendingFileNavigation = useUIStore((state) => state.setPendingFileNavigation);
const pendingFileFocusPath = useUIStore((state) => state.pendingFileFocusPath);
@@ -1098,10 +1096,9 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
React.useEffect(() => {
setLineSelection(null);
reset();
setSurfaceGuard(null);
setDraftContent('');
setIsSaving(false);
}, [selectedFile?.path, reset, setSurfaceGuard]);
}, [selectedFile?.path, reset]);
React.useEffect(() => {
setCommentSelection(lineSelection);
@@ -1711,32 +1708,6 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
}
}, [contentDetectedBinary, draftContent, fileContent, fileLoading, files, isDirty, loadedFileLineEnding, loadedFilePath, readFileStat, root, selectedFile, t]);
React.useEffect(() => {
if (!isDirty) {
setSurfaceGuard(null);
return;
}
const guard = (_nextTab: import('@/stores/useUIStore').WorkspaceSurface) => {
if (skipDirtyOnceRef.current) {
skipDirtyOnceRef.current = false;
return true;
}
setConfirmDiscardOpen(true);
pendingTabRef.current = _nextTab;
return false;
};
setSurfaceGuard(guard);
return () => {
const currentGuard = useUIStore.getState().surfaceGuard;
if (currentGuard === guard) {
setSurfaceGuard(null);
}
};
}, [isDirty, setSurfaceGuard]);
React.useEffect(() => {
if (autoSaveEnabled) {
return;
@@ -2136,11 +2107,9 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
const discardAndContinue = React.useCallback(() => {
const nextFile = pendingSelectFileRef.current;
const nextTab = pendingTabRef.current;
const closePath = pendingClosePathRef.current;
pendingSelectFileRef.current = null;
pendingTabRef.current = null;
pendingClosePathRef.current = null;
// Allow one guarded navigation (tab/file) without re-opening dialog.
@@ -2179,15 +2148,10 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
return;
}
if (nextTab) {
setSurfaceGuard(null);
useUIStore.getState().setActiveSurface(nextTab);
}
}, [displayedContent, handleSelectFile, isMobile, removeOpenPath, root, selectedFile?.path, setSurfaceGuard, setSelectedPath]);
}, [displayedContent, handleSelectFile, isMobile, removeOpenPath, root, selectedFile?.path, setSelectedPath]);
const saveAndContinue = React.useCallback(async () => {
const nextFile = pendingSelectFileRef.current;
const nextTab = pendingTabRef.current;
const closePath = pendingClosePathRef.current;
const saved = await saveDraft();
@@ -2197,7 +2161,6 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
}
pendingSelectFileRef.current = null;
pendingTabRef.current = null;
pendingClosePathRef.current = null;
// We'll proceed after saving; suppress guard reopening.
@@ -2233,11 +2196,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
return;
}
if (nextTab) {
setSurfaceGuard(null);
useUIStore.getState().setActiveSurface(nextTab);
}
}, [handleSelectFile, isMobile, removeOpenPath, root, saveDraft, selectedFile?.path, setSurfaceGuard, setSelectedPath]);
}, [handleSelectFile, isMobile, removeOpenPath, root, saveDraft, selectedFile?.path, setSelectedPath]);
const handleCloseFile = React.useCallback((path: string) => {
const isActive = selectedFile?.path === path;
@@ -168,7 +168,6 @@ export const PlanView: React.FC<PlanViewProps> = ({ targetPath = null, projectPl
const activeProjectId = useProjectsStore((state) => state.activeProjectId);
const gitDirectories = useGitStore((state) => state.directories);
const effectiveDirectory = useEffectiveDirectory() ?? '';
const setActiveSurface = useUIStore((state) => state.setActiveSurface);
const setSessionSwitcherOpen = useUIStore((state) => state.setSessionSwitcherOpen);
const runtimeApis = useRuntimeAPIs();
const { isMobile } = useDeviceInfo();
@@ -579,10 +578,9 @@ export const PlanView: React.FC<PlanViewProps> = ({ targetPath = null, projectPl
}, []);
const routeToChat = React.useCallback(() => {
setActiveSurface('chat');
setSessionSwitcherOpen(false);
onNavigatedToChat?.();
}, [onNavigatedToChat, setActiveSurface, setSessionSwitcherOpen]);
}, [onNavigatedToChat, setSessionSwitcherOpen]);
const handleConfirmPlanSend = React.useCallback(
async (execution: TodoSendExecution) => {
@@ -148,9 +148,7 @@ export const TerminalView: React.FC<TerminalViewProps> = ({ visible }) => {
terminalControllerRef.current?.focus();
}, [useTouchTerminalInput]);
const activeSurface = useUIStore((state) => state.activeSurface);
const isTerminalActive = activeSurface === 'terminal';
const isTerminalVisible = visible ?? isTerminalActive;
const isTerminalVisible = visible ?? false;
const [hasOpenedTerminalViewport, setHasOpenedTerminalViewport] = React.useState(isTerminalVisible);
React.useEffect(() => {
@@ -10,7 +10,6 @@ import { Button } from '@/components/ui/button';
import { useSessionUIStore } from '@/sync/session-ui-store';
import { useInputStore } from '@/sync/input-store';
import { useUIStore } from '@/stores/useUIStore';
import { toast } from '@/components/ui';
import { Icon } from "@/components/icon/Icon";
import { getConflictDetails, type MergeConflictDetails } from '@/lib/gitApi';
@@ -41,7 +40,6 @@ export const ConflictDialog: React.FC<ConflictDialogProps> = ({
const currentSessionId = useSessionUIStore((state) => state.currentSessionId);
const setPendingInputText = useInputStore((state) => state.setPendingInputText);
const setPendingSyntheticParts = useInputStore((state) => state.setPendingSyntheticParts);
const setActiveSurface = useUIStore((state) => state.setActiveSurface);
const [isLoading, setIsLoading] = React.useState(false);
const [conflictDetails, setConflictDetails] = React.useState<MergeConflictDetails | null>(null);
@@ -137,7 +135,6 @@ export const ConflictDialog: React.FC<ConflictDialogProps> = ({
{ text: context.payloadText, synthetic: true },
]);
setActiveSurface('chat');
onClearState?.();
onOpenChange(false);
};
@@ -159,7 +156,6 @@ export const ConflictDialog: React.FC<ConflictDialogProps> = ({
],
});
// Navigate to chat tab so user sees the new session
setActiveSurface('chat');
onClearState?.();
onOpenChange(false);
};
@@ -18,7 +18,6 @@ import { toast } from '@/components/ui';
import { Icon } from "@/components/icon/Icon";
import { useSessionUIStore } from '@/sync/session-ui-store';
import { useInputStore } from '@/sync/input-store';
import { useUIStore } from '@/stores/useUIStore';
import { rankByQuery } from '@/lib/search/fuzzySearch';
import { getGitCommitSummaries } from '@/lib/gitApi';
import { renderMagicPrompt } from '@/lib/magicPrompts';
@@ -65,7 +64,6 @@ export const IntegrateCommitsSection: React.FC<{
}) => {
const { t } = useI18n();
const currentSessionId = useSessionUIStore((s) => s.currentSessionId);
const setActiveSurface = useUIStore((s) => s.setActiveSurface);
const [branchDropdownOpen, setBranchDropdownOpen] = React.useState(false);
const [branchSearch, setBranchSearch] = React.useState('');
const searchInputRef = React.useRef<HTMLInputElement>(null);
@@ -235,8 +233,6 @@ export const IntegrateCommitsSection: React.FC<{
{ text: context.payloadText, synthetic: true },
],
});
// Navigate to chat tab so user sees the new session
setActiveSurface('chat');
return;
}
@@ -251,8 +247,7 @@ export const IntegrateCommitsSection: React.FC<{
{ text: context.instructionsText, synthetic: true },
{ text: context.payloadText, synthetic: true },
]);
setActiveSurface('chat');
}, [currentSessionId, setActiveSurface, buildConflictContext, openNewSessionDraft, setPendingInputText, setPendingSyntheticParts, t]);
}, [currentSessionId, buildConflictContext, openNewSessionDraft, setPendingInputText, setPendingSyntheticParts, t]);
const handleMove = React.useCallback(async () => {
if (ui.kind !== 'ready') return;
@@ -327,7 +327,6 @@ export const PullRequestSection: React.FC<{
const githubAuthChecked = useGitHubAuthStore((state) => state.hasChecked);
const setSettingsDialogOpen = useUIStore((state) => state.setSettingsDialogOpen);
const setSettingsPage = useUIStore((state) => state.setSettingsPage);
const setActiveSurface = useUIStore((state) => state.setActiveSurface);
const currentSessionId = useSessionUIStore((state) => state.currentSessionId);
const newSessionDraftOpen = useSessionUIStore((state) => Boolean(state.newSessionDraft?.open));
const { isMobile, hasTouchInput, screenWidth } = useDeviceInfo();
@@ -986,14 +985,13 @@ export const PullRequestSection: React.FC<{
text: '',
});
}
setActiveSurface('chat');
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
toast.error(t('gitView.pr.toast.loadChecksFailed'), { description: message });
} finally {
setIsAttachingChecks(false);
}
}, [directory, ensurePrContext, github, pr, resolveDraftTarget, setActiveSurface, status?.repo, t]);
}, [directory, ensurePrContext, github, pr, resolveDraftTarget, status?.repo, t]);
const sendCommentsToChat = React.useCallback(async () => {
if (!github?.prContext) {
@@ -1021,14 +1019,13 @@ export const PullRequestSection: React.FC<{
for (const comment of timelineComments) {
attachCommentDraft(target, comment);
}
setActiveSurface('chat');
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
toast.error(t('gitView.pr.toast.loadPrCommentsFailed'), { description: message });
} finally {
setIsAttachingComments(false);
}
}, [attachCommentDraft, directory, ensurePrContext, github, pr, resolveDraftTarget, setActiveSurface, status?.repo, t, timelineComments]);
}, [attachCommentDraft, directory, ensurePrContext, github, pr, resolveDraftTarget, status?.repo, t, timelineComments]);
const sendSingleCommentToChat = React.useCallback(async (comment: TimelineCommentItem) => {
const target = resolveDraftTarget();
@@ -1037,8 +1034,7 @@ export const PullRequestSection: React.FC<{
}
attachCommentDraft(target, comment);
setActiveSurface('chat');
}, [attachCommentDraft, resolveDraftTarget, setActiveSurface]);
}, [attachCommentDraft, resolveDraftTarget]);
const refresh = React.useCallback(async (options?: { force?: boolean; onlyExistingPr?: boolean; silent?: boolean; markInitialResolved?: boolean }) => {
await refreshPrStatus(prStatusKey, options);