diff --git a/apps/server/src/keybindings.ts b/apps/server/src/keybindings.ts index 77eb786ba..f3ea97b5e 100644 --- a/apps/server/src/keybindings.ts +++ b/apps/server/src/keybindings.ts @@ -70,7 +70,6 @@ export const DEFAULT_KEYBINDINGS: ReadonlyArray = [ { key: "mod+d", command: "terminal.split", when: "terminalFocus" }, { key: "mod+n", command: "terminal.new", when: "terminalFocus" }, { key: "mod+w", command: "terminal.close", when: "terminalFocus" }, - { key: "mod+d", command: "diff.toggle", when: "!terminalFocus" }, { key: "mod+n", command: "chat.new", when: "!terminalFocus" }, { key: "mod+shift+o", command: "chat.new", when: "!terminalFocus" }, { key: "mod+shift+n", command: "chat.newLocal", when: "!terminalFocus" }, diff --git a/apps/web/src/appSettings.ts b/apps/web/src/appSettings.ts index 051b12539..4dbd29e66 100644 --- a/apps/web/src/appSettings.ts +++ b/apps/web/src/appSettings.ts @@ -72,7 +72,6 @@ export const AppSettingsSchema = Schema.Struct({ confirmThreadDelete: Schema.Boolean.pipe(withDefaults(() => true)), autoDeleteMergedThreads: Schema.Boolean.pipe(withDefaults(() => false)), autoDeleteMergedThreadsDelayMinutes: Schema.Number.pipe(withDefaults(() => 5)), - diffWordWrap: Schema.Boolean.pipe(withDefaults(() => false)), enableAssistantStreaming: Schema.Boolean.pipe(withDefaults(() => false)), showAuthFailuresAsErrors: Schema.Boolean.pipe(withDefaults(() => true)), locale: AppLocale.pipe(withDefaults(() => DEFAULT_APP_LOCALE)), diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index ba2d09584..cd5dab47f 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -31,7 +31,7 @@ import { import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useDebouncedValue } from "@tanstack/react-pacer"; -import { useNavigate, useSearch } from "@tanstack/react-router"; +import { useNavigate } from "@tanstack/react-router"; import { gitBranchesQueryOptions, gitCreateWorktreeMutationOptions } from "~/lib/gitReactQuery"; import { projectSearchEntriesQueryOptions } from "~/lib/projectReactQuery"; import { @@ -42,7 +42,6 @@ import { import { serverConfigQueryOptions, serverQueryKeys } from "~/lib/serverReactQuery"; import { isElectron } from "../env"; import { openFileReference } from "../fileOpen"; -import { parseDiffRouteSearch, stripDiffSearchParams } from "../diffRouteSearch"; import { clampCollapsedComposerCursor, type ComposerTrigger, @@ -407,10 +406,6 @@ export default function ChatView({ threadId }: ChatViewProps) { startY: number; startSize: number; } | null>(null); - const rawSearch = useSearch({ - strict: false, - select: (params) => parseDiffRouteSearch(params), - }); const { resolvedTheme } = useTheme(); const queryClient = useQueryClient(); const createWorktreeMutation = useMutation(gitCreateWorktreeMutationOptions({ queryClient })); @@ -669,7 +664,6 @@ export default function ChatView({ threadId }: ChatViewProps) { const isServerThread = serverThread !== undefined; const isLocalDraftThread = !isServerThread && localDraftThread !== undefined; const canCheckoutPullRequestIntoThread = isLocalDraftThread; - const diffOpen = rawSearch.diff === "1"; const activeThreadId = activeThread?.id ?? null; const activeLatestTurn = activeThread?.latestTurn ?? null; const activeContextWindow = useMemo( @@ -1473,26 +1467,11 @@ export default function ChatView({ threadId }: ChatViewProps) { () => shortcutLabelForCommand(keybindings, "terminal.close"), [keybindings], ); - const diffPanelShortcutLabel = useMemo( - () => shortcutLabelForCommand(keybindings, "diff.toggle"), - [keybindings], - ); const platform = typeof navigator !== "undefined" ? navigator.platform : ""; const chatShortcutGuides = useMemo( () => buildChatShortcutGuides(keybindings, platform), [keybindings, platform], ); - const onToggleDiff = useCallback(() => { - void navigate({ - to: "/$threadId", - params: { threadId }, - replace: true, - search: (previous) => { - const rest = stripDiffSearchParams(previous); - return diffOpen ? { ...rest, diff: undefined } : { ...rest, diff: "1" }; - }, - }); - }, [diffOpen, navigate, threadId]); const pendingContext = useCodeViewerStore((state) => state.pendingContext); const clearPendingContext = useCodeViewerStore((state) => state.clearPendingContext); @@ -2775,13 +2754,6 @@ export default function ChatView({ threadId }: ChatViewProps) { return; } - if (command === "diff.toggle") { - event.preventDefault(); - event.stopPropagation(); - onToggleDiff(); - return; - } - const scriptId = projectScriptIdFromCommand(command); if (!scriptId || !activeProject) return; const script = activeProject.scripts.find((entry) => entry.id === scriptId); @@ -2803,7 +2775,6 @@ export default function ChatView({ threadId }: ChatViewProps) { runProjectScript, splitTerminal, keybindings, - onToggleDiff, toggleTerminalVisibility, ]); @@ -4481,21 +4452,6 @@ export default function ChatView({ threadId }: ChatViewProps) { setExpandedImage(preview); }, []); const expandedImageItem = expandedImage ? expandedImage.images[expandedImage.index] : null; - const onOpenTurnDiff = useCallback( - (turnId: TurnId, filePath?: string) => { - void navigate({ - to: "/$threadId", - params: { threadId }, - search: (previous) => { - const rest = stripDiffSearchParams(previous); - return filePath - ? { ...rest, diff: "1", diffTurnId: turnId, diffFilePath: filePath } - : { ...rest, diff: "1", diffTurnId: turnId }; - }, - }); - }, - [navigate, threadId], - ); const onRevertUserMessage = (messageId: MessageId) => { const targetTurnCount = revertTurnCountByUserMessageId.get(messageId); if (typeof targetTurnCount !== "number") { @@ -4646,7 +4602,6 @@ export default function ChatView({ threadId }: ChatViewProps) { activeProjectId={activeProject?.id} activeProjectName={activeProject?.name} activeProjectCwd={activeProject?.cwd} - isGitRepo={isGitRepo} isLocalDraftThread={isLocalDraftThread} threadBranch={activeThread.branch ?? null} openInCwd={gitCwd} @@ -4658,12 +4613,10 @@ export default function ChatView({ threadId }: ChatViewProps) { terminalAvailable={activeProject !== undefined} terminalOpen={terminalState.terminalOpen} terminalToggleShortcutLabel={terminalToggleShortcutLabel} - diffToggleShortcutLabel={diffPanelShortcutLabel} previewAvailable={isElectron && activeProject !== undefined} previewOpen={previewOpen} previewDock={previewDock} gitCwd={gitCwd} - diffOpen={diffOpen} clientMode={clientMode} onRenameDraftThreadTitle={(title) => { setDraftThreadTitle(activeThread.id, title); @@ -4676,7 +4629,6 @@ export default function ChatView({ threadId }: ChatViewProps) { onDeleteProjectScript={deleteProjectScript} onImportProjectScripts={importProjectScripts} onToggleTerminal={toggleTerminalVisibility} - onToggleDiff={onToggleDiff} onTogglePreview={() => activeProjectId && togglePreviewOpen(activeProjectId)} onTogglePreviewLayout={() => activeProjectId && togglePreviewLayout(activeProjectId)} /> @@ -4783,7 +4735,6 @@ export default function ChatView({ threadId }: ChatViewProps) { nowIso={nowIso} expandedWorkGroups={expandedWorkGroups} onToggleWorkGroup={onToggleWorkGroup} - onOpenTurnDiff={onOpenTurnDiff} revertTurnCountByUserMessageId={revertTurnCountByUserMessageId} onRevertUserMessage={onRevertUserMessage} isRevertingCheckpoint={isRevertingCheckpoint} diff --git a/apps/web/src/components/CodeViewerPanel.tsx b/apps/web/src/components/CodeViewerPanel.tsx index f2bc6160f..5ff1464ee 100644 --- a/apps/web/src/components/CodeViewerPanel.tsx +++ b/apps/web/src/components/CodeViewerPanel.tsx @@ -8,7 +8,7 @@ import { projectReadFileQueryOptions } from "~/lib/projectReactQuery"; import { cn, isMacPlatform } from "~/lib/utils"; import { isMarkdownPreviewFilePath } from "~/markdownPreview"; import { CodeMirrorViewer, type CodeContextSelection } from "./CodeMirrorViewer"; -import { DiffPanelLoadingState } from "./DiffPanelShell"; +import { Loader2Icon } from "lucide-react"; import { MarkdownPreview } from "./MarkdownPreview"; import { isElectron } from "~/env"; import { Button } from "./ui/button"; @@ -83,7 +83,12 @@ export const CodeViewerFileContent = memo(function CodeViewerFileContent(props: ); if (query.isLoading) { - return ; + return ( +
+ + Loading file... +
+ ); } if (query.isError) { diff --git a/apps/web/src/components/DiffPanel.tsx b/apps/web/src/components/DiffPanel.tsx deleted file mode 100644 index 6cdbe76cd..000000000 --- a/apps/web/src/components/DiffPanel.tsx +++ /dev/null @@ -1,648 +0,0 @@ -import { parsePatchFiles } from "@pierre/diffs"; -import { FileDiff, type FileDiffMetadata, Virtualizer } from "@pierre/diffs/react"; -import { useQuery } from "@tanstack/react-query"; -import { useNavigate, useParams, useSearch } from "@tanstack/react-router"; -import { ThreadId, type TurnId } from "@okcode/contracts"; -import { - ChevronLeftIcon, - ChevronRightIcon, - Columns2Icon, - Rows3Icon, - TextWrapIcon, -} from "lucide-react"; -import { - type WheelEvent as ReactWheelEvent, - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from "react"; -import { gitBranchesQueryOptions } from "~/lib/gitReactQuery"; -import { checkpointDiffQueryOptions } from "~/lib/providerReactQuery"; -import { cn } from "~/lib/utils"; -import { useCodeViewerStore } from "../codeViewerStore"; -import { parseDiffRouteSearch, stripDiffSearchParams } from "../diffRouteSearch"; -import { useTheme } from "../hooks/useTheme"; -import { buildPatchCacheKey } from "../lib/diffRendering"; -import { resolveDiffThemeName } from "../lib/diffRendering"; -import { useTurnDiffSummaries } from "../hooks/useTurnDiffSummaries"; -import { useStore } from "../store"; -import { useAppSettings } from "../appSettings"; -import { formatShortTimestamp } from "../timestampFormat"; -import { useI18nContext } from "../i18n/I18nProvider"; -import { DiffPanelLoadingState, DiffPanelShell, type DiffPanelMode } from "./DiffPanelShell"; -import { ToggleGroup, Toggle } from "./ui/toggle-group"; - -type DiffRenderMode = "stacked" | "split"; -type DiffThemeType = "light" | "dark"; - -const DIFF_PANEL_UNSAFE_CSS = ` -[data-diffs-header], -[data-diff], -[data-file], -[data-error-wrapper], -[data-virtualizer-buffer] { - --diffs-bg: color-mix(in srgb, var(--card) 90%, var(--background)) !important; - --diffs-light-bg: color-mix(in srgb, var(--card) 90%, var(--background)) !important; - --diffs-dark-bg: color-mix(in srgb, var(--card) 90%, var(--background)) !important; - --diffs-token-light-bg: transparent; - --diffs-token-dark-bg: transparent; - - --diffs-bg-context-override: color-mix(in srgb, var(--background) 97%, var(--foreground)); - --diffs-bg-hover-override: color-mix(in srgb, var(--background) 94%, var(--foreground)); - --diffs-bg-separator-override: color-mix(in srgb, var(--background) 95%, var(--foreground)); - --diffs-bg-buffer-override: color-mix(in srgb, var(--background) 90%, var(--foreground)); - - --diffs-bg-addition-override: color-mix(in srgb, var(--background) 92%, var(--success)); - --diffs-bg-addition-number-override: color-mix(in srgb, var(--background) 88%, var(--success)); - --diffs-bg-addition-hover-override: color-mix(in srgb, var(--background) 85%, var(--success)); - --diffs-bg-addition-emphasis-override: color-mix(in srgb, var(--background) 80%, var(--success)); - - --diffs-bg-deletion-override: color-mix(in srgb, var(--background) 92%, var(--destructive)); - --diffs-bg-deletion-number-override: color-mix(in srgb, var(--background) 88%, var(--destructive)); - --diffs-bg-deletion-hover-override: color-mix(in srgb, var(--background) 85%, var(--destructive)); - --diffs-bg-deletion-emphasis-override: color-mix( - in srgb, - var(--background) 80%, - var(--destructive) - ); - - background-color: var(--diffs-bg) !important; -} - -[data-file-info] { - background-color: color-mix(in srgb, var(--card) 94%, var(--foreground)) !important; - border-block-color: var(--border) !important; - color: var(--foreground) !important; -} - -[data-diffs-header] { - position: sticky !important; - top: 0; - z-index: 4; - background-color: color-mix(in srgb, var(--card) 94%, var(--foreground)) !important; - border-bottom: 1px solid var(--border) !important; -} - -[data-title] { - cursor: pointer; - transition: - color 120ms ease, - text-decoration-color 120ms ease; - text-decoration: underline; - text-decoration-color: transparent; - text-underline-offset: 2px; -} - -[data-title]:hover { - color: color-mix(in srgb, var(--foreground) 84%, var(--primary)) !important; - text-decoration-color: currentColor; -} -`; - -type RenderablePatch = - | { - kind: "files"; - files: FileDiffMetadata[]; - } - | { - kind: "raw"; - text: string; - reason: string; - }; - -function getRenderablePatch( - patch: string | undefined, - cacheScope = "diff-panel", -): RenderablePatch | null { - if (!patch) return null; - const normalizedPatch = patch.trim(); - if (normalizedPatch.length === 0) return null; - - try { - const parsedPatches = parsePatchFiles( - normalizedPatch, - buildPatchCacheKey(normalizedPatch, cacheScope), - ); - const files = parsedPatches.flatMap((parsedPatch) => parsedPatch.files); - if (files.length > 0) { - return { kind: "files", files }; - } - - return { - kind: "raw", - text: normalizedPatch, - reason: "Unsupported diff format. Showing raw patch.", - }; - } catch { - return { - kind: "raw", - text: normalizedPatch, - reason: "Failed to parse patch. Showing raw patch.", - }; - } -} - -function resolveFileDiffPath(fileDiff: FileDiffMetadata): string { - const raw = fileDiff.name ?? fileDiff.prevName ?? ""; - if (raw.startsWith("a/") || raw.startsWith("b/")) { - return raw.slice(2); - } - return raw; -} - -function buildFileDiffRenderKey(fileDiff: FileDiffMetadata): string { - return fileDiff.cacheKey ?? `${fileDiff.prevName ?? "none"}:${fileDiff.name}`; -} - -interface DiffPanelProps { - mode?: DiffPanelMode; -} - -export { DiffWorkerPoolProvider } from "./DiffWorkerPoolProvider"; - -export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { - const navigate = useNavigate(); - const { resolvedTheme } = useTheme(); - const { settings } = useAppSettings(); - const { resolvedLocale } = useI18nContext(); - const [diffRenderMode, setDiffRenderMode] = useState("stacked"); - const [diffWordWrap, setDiffWordWrap] = useState(settings.diffWordWrap); - const patchViewportRef = useRef(null); - const turnStripRef = useRef(null); - const previousDiffOpenRef = useRef(false); - const [canScrollTurnStripLeft, setCanScrollTurnStripLeft] = useState(false); - const [canScrollTurnStripRight, setCanScrollTurnStripRight] = useState(false); - const routeThreadId = useParams({ - strict: false, - select: (params) => (params.threadId ? ThreadId.makeUnsafe(params.threadId) : null), - }); - const diffSearch = useSearch({ strict: false, select: (search) => parseDiffRouteSearch(search) }); - const diffOpen = diffSearch.diff === "1"; - const activeThreadId = routeThreadId; - const activeThread = useStore((store) => - activeThreadId ? store.threads.find((thread) => thread.id === activeThreadId) : undefined, - ); - const activeProjectId = activeThread?.projectId ?? null; - const activeProject = useStore((store) => - activeProjectId ? store.projects.find((project) => project.id === activeProjectId) : undefined, - ); - const activeCwd = activeThread?.worktreePath ?? activeProject?.cwd; - const gitBranchesQuery = useQuery(gitBranchesQueryOptions(activeCwd ?? null)); - const isGitRepo = gitBranchesQuery.data?.isRepo ?? true; - const { turnDiffSummaries, inferredCheckpointTurnCountByTurnId } = - useTurnDiffSummaries(activeThread); - const orderedTurnDiffSummaries = useMemo( - () => - [...turnDiffSummaries].toSorted((left, right) => { - const leftTurnCount = - left.checkpointTurnCount ?? inferredCheckpointTurnCountByTurnId[left.turnId] ?? 0; - const rightTurnCount = - right.checkpointTurnCount ?? inferredCheckpointTurnCountByTurnId[right.turnId] ?? 0; - if (leftTurnCount !== rightTurnCount) { - return rightTurnCount - leftTurnCount; - } - return right.completedAt.localeCompare(left.completedAt); - }), - [inferredCheckpointTurnCountByTurnId, turnDiffSummaries], - ); - - const selectedTurnId = diffSearch.diffTurnId ?? null; - const selectedFilePath = selectedTurnId !== null ? (diffSearch.diffFilePath ?? null) : null; - const selectedTurn = - selectedTurnId === null - ? undefined - : (orderedTurnDiffSummaries.find((summary) => summary.turnId === selectedTurnId) ?? - orderedTurnDiffSummaries[0]); - const selectedCheckpointTurnCount = - selectedTurn && - (selectedTurn.checkpointTurnCount ?? inferredCheckpointTurnCountByTurnId[selectedTurn.turnId]); - const selectedCheckpointRange = useMemo( - () => - typeof selectedCheckpointTurnCount === "number" - ? { - fromTurnCount: Math.max(0, selectedCheckpointTurnCount - 1), - toTurnCount: selectedCheckpointTurnCount, - } - : null, - [selectedCheckpointTurnCount], - ); - const conversationCheckpointTurnCount = useMemo(() => { - const turnCounts = orderedTurnDiffSummaries - .map( - (summary) => - summary.checkpointTurnCount ?? inferredCheckpointTurnCountByTurnId[summary.turnId], - ) - .filter((value): value is number => typeof value === "number"); - if (turnCounts.length === 0) { - return undefined; - } - const latest = Math.max(...turnCounts); - return latest > 0 ? latest : undefined; - }, [inferredCheckpointTurnCountByTurnId, orderedTurnDiffSummaries]); - const conversationCheckpointRange = useMemo( - () => - !selectedTurn && typeof conversationCheckpointTurnCount === "number" - ? { - fromTurnCount: 0, - toTurnCount: conversationCheckpointTurnCount, - } - : null, - [conversationCheckpointTurnCount, selectedTurn], - ); - const activeCheckpointRange = selectedTurn - ? selectedCheckpointRange - : conversationCheckpointRange; - const conversationCacheScope = useMemo(() => { - if (selectedTurn || orderedTurnDiffSummaries.length === 0) { - return null; - } - return `conversation:${orderedTurnDiffSummaries.map((summary) => summary.turnId).join(",")}`; - }, [orderedTurnDiffSummaries, selectedTurn]); - const activeCheckpointDiffQuery = useQuery( - checkpointDiffQueryOptions({ - threadId: activeThreadId, - fromTurnCount: activeCheckpointRange?.fromTurnCount ?? null, - toTurnCount: activeCheckpointRange?.toTurnCount ?? null, - cacheScope: selectedTurn ? `turn:${selectedTurn.turnId}` : conversationCacheScope, - enabled: isGitRepo, - }), - ); - const selectedTurnCheckpointDiff = selectedTurn - ? activeCheckpointDiffQuery.data?.diff - : undefined; - const conversationCheckpointDiff = selectedTurn - ? undefined - : activeCheckpointDiffQuery.data?.diff; - const isLoadingCheckpointDiff = activeCheckpointDiffQuery.isLoading; - const checkpointDiffError = - activeCheckpointDiffQuery.error instanceof Error - ? activeCheckpointDiffQuery.error.message - : activeCheckpointDiffQuery.error - ? "Failed to load checkpoint diff." - : null; - - const selectedPatch = selectedTurn ? selectedTurnCheckpointDiff : conversationCheckpointDiff; - const hasResolvedPatch = typeof selectedPatch === "string"; - const hasNoNetChanges = hasResolvedPatch && selectedPatch.trim().length === 0; - const renderablePatch = useMemo( - () => getRenderablePatch(selectedPatch, `diff-panel:${resolvedTheme}`), - [resolvedTheme, selectedPatch], - ); - const renderableFiles = useMemo(() => { - if (!renderablePatch || renderablePatch.kind !== "files") { - return []; - } - return renderablePatch.files.toSorted((left, right) => - resolveFileDiffPath(left).localeCompare(resolveFileDiffPath(right), undefined, { - numeric: true, - sensitivity: "base", - }), - ); - }, [renderablePatch]); - - useEffect(() => { - if (diffOpen && !previousDiffOpenRef.current) { - setDiffWordWrap(settings.diffWordWrap); - } - previousDiffOpenRef.current = diffOpen; - }, [diffOpen, settings.diffWordWrap]); - - useEffect(() => { - if (!selectedFilePath || !patchViewportRef.current) { - return; - } - const target = Array.from( - patchViewportRef.current.querySelectorAll("[data-diff-file-path]"), - ).find((element) => element.dataset.diffFilePath === selectedFilePath); - target?.scrollIntoView({ block: "nearest" }); - }, [selectedFilePath, renderableFiles]); - - const openFileInCodeViewer = useCodeViewerStore((state) => state.openFile); - const openDiffFileInCodeViewer = useCallback( - (filePath: string) => { - if (!activeCwd) return; - openFileInCodeViewer(activeCwd, filePath); - }, - [activeCwd, openFileInCodeViewer], - ); - - const selectTurn = (turnId: TurnId) => { - if (!activeThread) return; - void navigate({ - to: "/$threadId", - params: { threadId: activeThread.id }, - search: (previous) => { - const rest = stripDiffSearchParams(previous); - return { ...rest, diff: "1", diffTurnId: turnId }; - }, - }); - }; - const selectWholeConversation = () => { - if (!activeThread) return; - void navigate({ - to: "/$threadId", - params: { threadId: activeThread.id }, - search: (previous) => { - const rest = stripDiffSearchParams(previous); - return { ...rest, diff: "1" }; - }, - }); - }; - const updateTurnStripScrollState = useCallback(() => { - const element = turnStripRef.current; - if (!element) { - setCanScrollTurnStripLeft(false); - setCanScrollTurnStripRight(false); - return; - } - - const maxScrollLeft = Math.max(0, element.scrollWidth - element.clientWidth); - setCanScrollTurnStripLeft(element.scrollLeft > 4); - setCanScrollTurnStripRight(element.scrollLeft < maxScrollLeft - 4); - }, []); - const scrollTurnStripBy = useCallback((offset: number) => { - const element = turnStripRef.current; - if (!element) return; - element.scrollBy({ left: offset, behavior: "smooth" }); - }, []); - const onTurnStripWheel = useCallback((event: ReactWheelEvent) => { - const element = turnStripRef.current; - if (!element) return; - if (element.scrollWidth <= element.clientWidth + 1) return; - if (Math.abs(event.deltaY) <= Math.abs(event.deltaX)) return; - - event.preventDefault(); - element.scrollBy({ left: event.deltaY, behavior: "auto" }); - }, []); - - useEffect(() => { - const element = turnStripRef.current; - if (!element) return; - - const frameId = window.requestAnimationFrame(() => updateTurnStripScrollState()); - const onScroll = () => updateTurnStripScrollState(); - - element.addEventListener("scroll", onScroll, { passive: true }); - - const resizeObserver = new ResizeObserver(() => updateTurnStripScrollState()); - resizeObserver.observe(element); - - return () => { - window.cancelAnimationFrame(frameId); - element.removeEventListener("scroll", onScroll); - resizeObserver.disconnect(); - }; - }, [updateTurnStripScrollState]); - - useEffect(() => { - const frameId = window.requestAnimationFrame(() => updateTurnStripScrollState()); - return () => { - window.cancelAnimationFrame(frameId); - }; - }, [orderedTurnDiffSummaries, selectedTurnId, updateTurnStripScrollState]); - - useEffect(() => { - const element = turnStripRef.current; - if (!element) return; - - const selectedChip = element.querySelector("[data-turn-chip-selected='true']"); - selectedChip?.scrollIntoView({ block: "nearest", inline: "nearest", behavior: "smooth" }); - }, [selectedTurn?.turnId, selectedTurnId]); - - const headerRow = ( - <> -
- {canScrollTurnStripLeft && ( -
- )} - {canScrollTurnStripRight && ( -
- )} - - -
- - {orderedTurnDiffSummaries.map((summary) => ( - - ))} -
-
-
- { - const next = value[0]; - if (next === "stacked" || next === "split") { - setDiffRenderMode(next); - } - }} - > - - - - - - - - { - setDiffWordWrap(Boolean(pressed)); - }} - > - - -
- - ); - - return ( - - {!activeThread ? ( -
- Select a thread to inspect changes. -
- ) : !isGitRepo ? ( -
- Changes are unavailable because this project is not a git repository. -
- ) : orderedTurnDiffSummaries.length === 0 ? ( -
- No captured changes yet. -
- ) : ( - <> -
- {checkpointDiffError && !renderablePatch && ( -
-

{checkpointDiffError}

-
- )} - {!renderablePatch ? ( - isLoadingCheckpointDiff ? ( - - ) : ( -
-

- {hasNoNetChanges - ? "No net changes in this selection." - : "No patch available for this selection."} -

-
- ) - ) : renderablePatch.kind === "files" ? ( - - {renderableFiles.map((fileDiff) => { - const filePath = resolveFileDiffPath(fileDiff); - const fileKey = buildFileDiffRenderKey(fileDiff); - const themedFileKey = `${fileKey}:${resolvedTheme}`; - return ( -
{ - const nativeEvent = event.nativeEvent as MouseEvent; - const composedPath = nativeEvent.composedPath?.() ?? []; - const clickedHeader = composedPath.some((node) => { - if (!(node instanceof Element)) return false; - return node.hasAttribute("data-title"); - }); - if (!clickedHeader) return; - openDiffFileInCodeViewer(filePath); - }} - > - -
- ); - })} -
- ) : ( -
-
-

{renderablePatch.reason}

-
-                    {renderablePatch.text}
-                  
-
-
- )} -
- - )} -
- ); -} diff --git a/apps/web/src/components/DiffPanelShell.tsx b/apps/web/src/components/DiffPanelShell.tsx deleted file mode 100644 index 7e1d7f563..000000000 --- a/apps/web/src/components/DiffPanelShell.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import type { ReactNode } from "react"; - -import { isElectron } from "~/env"; -import { cn } from "~/lib/utils"; - -import { Skeleton } from "./ui/skeleton"; - -export type DiffPanelMode = "inline" | "sheet" | "sidebar"; - -function getDiffPanelHeaderRowClassName(mode: DiffPanelMode) { - const shouldUseDragRegion = isElectron && mode !== "sheet"; - return cn( - "flex items-center justify-between gap-2 px-4", - shouldUseDragRegion ? "drag-region h-[52px] border-b border-border" : "h-12", - ); -} - -export function DiffPanelShell(props: { - mode: DiffPanelMode; - header: ReactNode; - children: ReactNode; -}) { - const shouldUseDragRegion = isElectron && props.mode !== "sheet"; - - return ( -
- {shouldUseDragRegion ? ( -
{props.header}
- ) : ( -
-
{props.header}
-
- )} - {props.children} -
- ); -} - -export function DiffPanelHeaderSkeleton() { - return ( - <> -
- - -
- - - -
-
-
- - -
- - ); -} - -export function DiffPanelLoadingState(props: { label: string }) { - return ( -
-
-
- - -
-
-
- - - - - -
- {props.label} -
-
-
- ); -} diff --git a/apps/web/src/components/DiffWorkerPoolProvider.tsx b/apps/web/src/components/DiffWorkerPoolProvider.tsx deleted file mode 100644 index 5babd4248..000000000 --- a/apps/web/src/components/DiffWorkerPoolProvider.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { WorkerPoolContextProvider, useWorkerPool } from "@pierre/diffs/react"; -import DiffsWorker from "@pierre/diffs/worker/worker.js?worker"; -import { useEffect, useMemo, type ReactNode } from "react"; -import { useTheme } from "../hooks/useTheme"; -import { resolveDiffThemeName, type DiffThemeName } from "../lib/diffRendering"; - -function DiffWorkerThemeSync({ themeName }: { themeName: DiffThemeName }) { - const workerPool = useWorkerPool(); - - useEffect(() => { - if (!workerPool) { - return; - } - - const current = workerPool.getDiffRenderOptions(); - if (current.theme === themeName) { - return; - } - - void workerPool - .setRenderOptions({ - ...current, - theme: themeName, - }) - .catch(() => undefined); - }, [themeName, workerPool]); - - return null; -} - -export function DiffWorkerPoolProvider({ children }: { children?: ReactNode }) { - const { resolvedTheme } = useTheme(); - const diffThemeName = resolveDiffThemeName(resolvedTheme); - const workerPoolSize = useMemo(() => { - const cores = - typeof navigator === "undefined" ? 4 : Math.max(1, navigator.hardwareConcurrency || 4); - return Math.max(2, Math.min(6, Math.floor(cores / 2))); - }, []); - - return ( - new DiffsWorker(), - poolSize: workerPoolSize, - totalASTLRUCacheSize: 240, - }} - highlighterOptions={{ - theme: diffThemeName, - tokenizeMaxLineLength: 1_000, - }} - > - - {children} - - ); -} diff --git a/apps/web/src/components/chat/ChangedFilesTree.tsx b/apps/web/src/components/chat/ChangedFilesTree.tsx index cf2734d3e..a91c377ff 100644 --- a/apps/web/src/components/chat/ChangedFilesTree.tsx +++ b/apps/web/src/components/chat/ChangedFilesTree.tsx @@ -12,7 +12,7 @@ import { DiffStatLabel, hasNonZeroStat } from "./DiffStatLabel"; import { VscodeEntryIcon } from "./VscodeEntryIcon"; import { toastManager } from "../ui/toast"; -type ChangedFileAction = "view-diff" | "open-in-editor" | "reveal-in-finder" | "copy-path"; +type ChangedFileAction = "open-in-editor" | "reveal-in-finder" | "copy-path"; type ChangedDirectoryAction = "reveal-in-finder" | "copy-path"; export const ChangedFilesTree = memo(function ChangedFilesTree(props: { @@ -21,9 +21,8 @@ export const ChangedFilesTree = memo(function ChangedFilesTree(props: { allDirectoriesExpanded: boolean; resolvedTheme: "light" | "dark"; cwd: string | undefined; - onOpenTurnDiff: (turnId: TurnId, filePath?: string) => void; }) { - const { files, allDirectoriesExpanded, onOpenTurnDiff, resolvedTheme, turnId, cwd } = props; + const { files, allDirectoriesExpanded, resolvedTheme, turnId, cwd } = props; const fileManagerName = typeof navigator !== "undefined" && isMacPlatform(navigator.platform) ? "Finder" @@ -162,16 +161,13 @@ export const ChangedFilesTree = memo(function ChangedFilesTree(props: { if (!api || !cwd) return; const clicked = await api.contextMenu.show( [ - { id: "view-diff", label: "View diff" }, { id: "open-in-editor", label: "Open in editor" }, { id: "reveal-in-finder", label: `Reveal in ${fileManagerName}` }, { id: "copy-path", label: "Copy path" }, ], { x: event.clientX, y: event.clientY }, ); - if (clicked === "view-diff") { - onOpenTurnDiff(turnId, node.path); - } else if (clicked === "open-in-editor") { + if (clicked === "open-in-editor") { openInEditor(node.path); } else if (clicked === "reveal-in-finder") { revealInFileManager(node.path); @@ -179,7 +175,7 @@ export const ChangedFilesTree = memo(function ChangedFilesTree(props: { copyPath(node.path); } }, - [cwd, fileManagerName, turnId, onOpenTurnDiff, openInEditor, revealInFileManager, copyPath], + [cwd, fileManagerName, openInEditor, revealInFileManager, copyPath], ); const renderTreeNode = (node: TurnDiffTreeNode, depth: number) => { @@ -231,7 +227,7 @@ export const ChangedFilesTree = memo(function ChangedFilesTree(props: { type="button" className="group flex w-full items-center gap-1.5 rounded-md py-1 pr-2 text-left hover:bg-background/80" style={{ paddingLeft: `${leftPadding}px` }} - onClick={() => onOpenTurnDiff(turnId, node.path)} + onClick={() => openInEditor(node.path)} onContextMenu={(event) => handleFileContextMenu(event, node)} >
diff --git a/apps/web/src/components/chat/HeaderPanelsMenu.tsx b/apps/web/src/components/chat/HeaderPanelsMenu.tsx index 1f3c301b8..577a4da3d 100644 --- a/apps/web/src/components/chat/HeaderPanelsMenu.tsx +++ b/apps/web/src/components/chat/HeaderPanelsMenu.tsx @@ -1,5 +1,5 @@ import { memo, useMemo } from "react"; -import { DiffIcon, MonitorIcon, TerminalSquareIcon } from "lucide-react"; +import { MonitorIcon, TerminalSquareIcon } from "lucide-react"; import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; import { ToggleGroup, Toggle, ToggleGroupSeparator } from "../ui/toggle-group"; @@ -9,12 +9,8 @@ interface HeaderPanelsMenuProps { terminalToggleShortcutLabel: string | null; previewAvailable: boolean; previewOpen: boolean; - diffOpen: boolean; - diffToggleShortcutLabel: string | null; - isGitRepo: boolean; onToggleTerminal: () => void; onTogglePreview: () => void; - onToggleDiff: () => void; } export const HeaderPanelsMenu = memo(function HeaderPanelsMenu({ @@ -23,20 +19,15 @@ export const HeaderPanelsMenu = memo(function HeaderPanelsMenu({ terminalToggleShortcutLabel, previewAvailable, previewOpen, - diffOpen, - diffToggleShortcutLabel, - isGitRepo, onToggleTerminal, onTogglePreview, - onToggleDiff, }: HeaderPanelsMenuProps) { const value = useMemo(() => { const v: string[] = []; if (terminalOpen) v.push("terminal"); if (previewOpen) v.push("preview"); - if (diffOpen) v.push("diff"); return v; - }, [terminalOpen, previewOpen, diffOpen]); + }, [terminalOpen, previewOpen]); return ( @@ -73,24 +64,6 @@ export const HeaderPanelsMenu = memo(function HeaderPanelsMenu({ /> Preview - - - - - - } - /> - - Diff{diffToggleShortcutLabel ? ` ${diffToggleShortcutLabel}` : ""} - - ); }); diff --git a/apps/web/src/components/chat/MessagesTimeline.test.tsx b/apps/web/src/components/chat/MessagesTimeline.test.tsx index 8237942c5..4d8125479 100644 --- a/apps/web/src/components/chat/MessagesTimeline.test.tsx +++ b/apps/web/src/components/chat/MessagesTimeline.test.tsx @@ -93,7 +93,6 @@ describe("MessagesTimeline", () => { nowIso="2026-03-17T19:12:30.000Z" expandedWorkGroups={{}} onToggleWorkGroup={() => {}} - onOpenTurnDiff={() => {}} revertTurnCountByUserMessageId={new Map()} onRevertUserMessage={() => {}} isRevertingCheckpoint={false} @@ -141,7 +140,6 @@ describe("MessagesTimeline", () => { nowIso="2026-03-17T19:12:30.000Z" expandedWorkGroups={{}} onToggleWorkGroup={() => {}} - onOpenTurnDiff={() => {}} revertTurnCountByUserMessageId={new Map()} onRevertUserMessage={() => {}} isRevertingCheckpoint={false} @@ -176,7 +174,6 @@ describe("MessagesTimeline", () => { nowIso="2026-03-17T19:12:30.000Z" expandedWorkGroups={{}} onToggleWorkGroup={() => {}} - onOpenTurnDiff={() => {}} revertTurnCountByUserMessageId={new Map()} onRevertUserMessage={() => {}} isRevertingCheckpoint={false} diff --git a/apps/web/src/components/chat/MessagesTimeline.tsx b/apps/web/src/components/chat/MessagesTimeline.tsx index 3f836e58e..3c543d486 100644 --- a/apps/web/src/components/chat/MessagesTimeline.tsx +++ b/apps/web/src/components/chat/MessagesTimeline.tsx @@ -79,7 +79,6 @@ interface MessagesTimelineProps { nowIso: string; expandedWorkGroups: Record; onToggleWorkGroup: (groupId: string) => void; - onOpenTurnDiff: (turnId: TurnId, filePath?: string) => void; revertTurnCountByUserMessageId: Map; onRevertUserMessage: (messageId: MessageId) => void; isRevertingCheckpoint: boolean; @@ -106,7 +105,6 @@ export const MessagesTimeline = memo(function MessagesTimeline({ nowIso, expandedWorkGroups, onToggleWorkGroup, - onOpenTurnDiff, revertTurnCountByUserMessageId, onRevertUserMessage, isRevertingCheckpoint, @@ -601,16 +599,6 @@ export const MessagesTimeline = memo(function MessagesTimeline({ {allDirectoriesExpanded ? "Collapse all" : "Expand all"} )} -
{!isFileSectionCollapsed && ( @@ -622,7 +610,6 @@ export const MessagesTimeline = memo(function MessagesTimeline({ allDirectoriesExpanded={allDirectoriesExpanded} resolvedTheme={resolvedTheme} cwd={markdownCwd} - onOpenTurnDiff={onOpenTurnDiff} /> )} diff --git a/apps/web/src/diffRouteSearch.test.ts b/apps/web/src/diffRouteSearch.test.ts deleted file mode 100644 index ef00874bd..000000000 --- a/apps/web/src/diffRouteSearch.test.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { parseDiffRouteSearch } from "./diffRouteSearch"; - -describe("parseDiffRouteSearch", () => { - it("parses valid diff search values", () => { - const parsed = parseDiffRouteSearch({ - diff: "1", - diffTurnId: "turn-1", - diffFilePath: "src/app.ts", - }); - - expect(parsed).toEqual({ - diff: "1", - diffTurnId: "turn-1", - diffFilePath: "src/app.ts", - }); - }); - - it("treats numeric and boolean diff toggles as open", () => { - expect( - parseDiffRouteSearch({ - diff: 1, - diffTurnId: "turn-1", - }), - ).toEqual({ - diff: "1", - diffTurnId: "turn-1", - }); - - expect( - parseDiffRouteSearch({ - diff: true, - diffTurnId: "turn-1", - }), - ).toEqual({ - diff: "1", - diffTurnId: "turn-1", - }); - }); - - it("drops turn and file values when diff is closed", () => { - const parsed = parseDiffRouteSearch({ - diff: "0", - diffTurnId: "turn-1", - diffFilePath: "src/app.ts", - }); - - expect(parsed).toEqual({}); - }); - - it("drops file value when turn is not selected", () => { - const parsed = parseDiffRouteSearch({ - diff: "1", - diffFilePath: "src/app.ts", - }); - - expect(parsed).toEqual({ - diff: "1", - }); - }); - - it("normalizes whitespace-only values", () => { - const parsed = parseDiffRouteSearch({ - diff: "1", - diffTurnId: " ", - diffFilePath: " ", - }); - - expect(parsed).toEqual({ - diff: "1", - }); - }); -}); diff --git a/apps/web/src/diffRouteSearch.ts b/apps/web/src/diffRouteSearch.ts deleted file mode 100644 index d161c0112..000000000 --- a/apps/web/src/diffRouteSearch.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { TurnId } from "@okcode/contracts"; - -export interface DiffRouteSearch { - diff?: "1" | undefined; - diffTurnId?: TurnId | undefined; - diffFilePath?: string | undefined; -} - -function isDiffOpenValue(value: unknown): boolean { - return value === "1" || value === 1 || value === true; -} - -function normalizeSearchString(value: unknown): string | undefined { - if (typeof value !== "string") { - return undefined; - } - const normalized = value.trim(); - return normalized.length > 0 ? normalized : undefined; -} - -export function stripDiffSearchParams>( - params: T, -): Omit { - const { diff: _diff, diffTurnId: _diffTurnId, diffFilePath: _diffFilePath, ...rest } = params; - return rest as Omit; -} - -export function parseDiffRouteSearch(search: Record): DiffRouteSearch { - const diff = isDiffOpenValue(search.diff) ? "1" : undefined; - const diffTurnIdRaw = diff ? normalizeSearchString(search.diffTurnId) : undefined; - const diffTurnId = diffTurnIdRaw ? TurnId.makeUnsafe(diffTurnIdRaw) : undefined; - const diffFilePath = diff && diffTurnId ? normalizeSearchString(search.diffFilePath) : undefined; - - return { - ...(diff ? { diff } : {}), - ...(diffTurnId ? { diffTurnId } : {}), - ...(diffFilePath ? { diffFilePath } : {}), - }; -} diff --git a/apps/web/src/i18n/messages/en.json b/apps/web/src/i18n/messages/en.json index 8cd5d4c28..8f6146b22 100644 --- a/apps/web/src/i18n/messages/en.json +++ b/apps/web/src/i18n/messages/en.json @@ -152,7 +152,6 @@ "settings.changed.colorTheme": "Color theme", "settings.changed.customModels": "Custom models", "settings.changed.deleteConfirmation": "Delete confirmation", - "settings.changed.diffWordWrap": "Diff line wrapping", "settings.changed.font": "Font", "settings.changed.fontFamily": "Font family", "settings.changed.gitWritingModel": "Git writing model", @@ -197,9 +196,6 @@ "settings.general.deleteConfirmation.aria": "Confirm thread deletion", "settings.general.deleteConfirmation.description": "Ask before deleting a thread and its chat history.", "settings.general.deleteConfirmation.title": "Delete confirmation", - "settings.general.diffWordWrap.aria": "Wrap diff lines by default", - "settings.general.diffWordWrap.description": "Set the default wrap state when the diff panel opens. The in-panel wrap toggle only affects the current diff session.", - "settings.general.diffWordWrap.title": "Diff line wrapping", "settings.general.font.description": "Choose the typeface for the interface.", "settings.general.font.title": "Font", "settings.general.fontFamilyOverride.aria": "Font family override", diff --git a/apps/web/src/i18n/messages/es.json b/apps/web/src/i18n/messages/es.json index 57c5daabf..6e0c1bb71 100644 --- a/apps/web/src/i18n/messages/es.json +++ b/apps/web/src/i18n/messages/es.json @@ -152,7 +152,6 @@ "settings.changed.colorTheme": "Tema de color", "settings.changed.customModels": "Modelos personalizados", "settings.changed.deleteConfirmation": "Confirmación de eliminación", - "settings.changed.diffWordWrap": "Ajuste de líneas en diff", "settings.changed.font": "Fuente", "settings.changed.fontFamily": "Familia tipográfica", "settings.changed.gitWritingModel": "Modelo de escritura de Git", @@ -197,9 +196,6 @@ "settings.general.deleteConfirmation.aria": "Confirmar eliminación del hilo", "settings.general.deleteConfirmation.description": "Pregunta antes de eliminar un hilo y su historial de chat.", "settings.general.deleteConfirmation.title": "Confirmación de eliminación", - "settings.general.diffWordWrap.aria": "Ajustar líneas del diff por defecto", - "settings.general.diffWordWrap.description": "Define el estado de ajuste predeterminado cuando se abre el panel de diff. El control dentro del panel solo afecta la sesión actual del diff.", - "settings.general.diffWordWrap.title": "Ajuste de líneas en diff", "settings.general.font.description": "Elige la tipografía de la interfaz.", "settings.general.font.title": "Fuente", "settings.general.fontFamilyOverride.aria": "Sobrescritura de familia tipográfica", diff --git a/apps/web/src/i18n/messages/fr.json b/apps/web/src/i18n/messages/fr.json index 6708fee39..94cea755d 100644 --- a/apps/web/src/i18n/messages/fr.json +++ b/apps/web/src/i18n/messages/fr.json @@ -152,7 +152,6 @@ "settings.changed.colorTheme": "Thème de couleur", "settings.changed.customModels": "Modèles personnalisés", "settings.changed.deleteConfirmation": "Confirmation de suppression", - "settings.changed.diffWordWrap": "Retour à la ligne du diff", "settings.changed.font": "Police", "settings.changed.fontFamily": "Famille de police", "settings.changed.gitWritingModel": "Modèle d'écriture Git", @@ -197,9 +196,6 @@ "settings.general.deleteConfirmation.aria": "Confirmer la suppression du fil", "settings.general.deleteConfirmation.description": "Demander confirmation avant de supprimer un fil et son historique de discussion.", "settings.general.deleteConfirmation.title": "Confirmation de suppression", - "settings.general.diffWordWrap.aria": "Activer le retour à la ligne du diff par défaut", - "settings.general.diffWordWrap.description": "Définit l'état de retour à la ligne par défaut à l'ouverture du panneau diff. Le bouton dans le panneau n'affecte que la session diff en cours.", - "settings.general.diffWordWrap.title": "Retour à la ligne du diff", "settings.general.font.description": "Choisissez la police de l'interface.", "settings.general.font.title": "Police", "settings.general.fontFamilyOverride.aria": "Remplacement de la famille de police", diff --git a/apps/web/src/i18n/messages/zh-CN.json b/apps/web/src/i18n/messages/zh-CN.json index 5a76b03cd..a8ed7e00c 100644 --- a/apps/web/src/i18n/messages/zh-CN.json +++ b/apps/web/src/i18n/messages/zh-CN.json @@ -152,7 +152,6 @@ "settings.changed.colorTheme": "配色主题", "settings.changed.customModels": "自定义模型", "settings.changed.deleteConfirmation": "删除确认", - "settings.changed.diffWordWrap": "Diff 自动换行", "settings.changed.font": "字体", "settings.changed.fontFamily": "字体族", "settings.changed.gitWritingModel": "Git 写作模型", @@ -197,9 +196,6 @@ "settings.general.deleteConfirmation.aria": "确认删除线程", "settings.general.deleteConfirmation.description": "删除线程及其聊天记录前先询问。", "settings.general.deleteConfirmation.title": "删除确认", - "settings.general.diffWordWrap.aria": "默认换行显示 diff 行", - "settings.general.diffWordWrap.description": "设置 diff 面板打开时的默认换行状态。面板内的换行开关只影响当前 diff 会话。", - "settings.general.diffWordWrap.title": "Diff 自动换行", "settings.general.font.description": "选择界面使用的字体。", "settings.general.font.title": "字体", "settings.general.fontFamilyOverride.aria": "字体族覆盖", diff --git a/apps/web/src/index.css b/apps/web/src/index.css index 73674fa1b..81b1f1c32 100644 --- a/apps/web/src/index.css +++ b/apps/web/src/index.css @@ -518,18 +518,6 @@ label:has(> select#reasoning-effort) select { margin: 1.5rem 0; } -/* Diffs theme bridge (match diff surfaces to app palette) */ -.diff-panel-viewport { - background: color-mix(in srgb, var(--background) 94%, var(--card)); -} - -.diff-render-file { - border: 1px solid var(--border); - border-radius: 0.5rem; - overflow: clip; - background: color-mix(in srgb, var(--card) 92%, var(--background)); -} - @keyframes ultrathink-rainbow { 0% { background-position: 0% 50%; diff --git a/apps/web/src/keybindings.test.ts b/apps/web/src/keybindings.test.ts index 619446a08..fe337da6e 100644 --- a/apps/web/src/keybindings.test.ts +++ b/apps/web/src/keybindings.test.ts @@ -10,7 +10,6 @@ import { formatShortcutLabel, isChatNewShortcut, isChatNewLocalShortcut, - isDiffToggleShortcut, isOpenFavoriteEditorShortcut, isTerminalClearShortcut, isTerminalCloseShortcut, @@ -104,11 +103,6 @@ const DEFAULT_BINDINGS = compile([ command: "terminal.close", whenAst: whenIdentifier("terminalFocus"), }, - { - shortcut: modShortcut("d"), - command: "diff.toggle", - whenAst: whenNot(whenIdentifier("terminalFocus")), - }, { shortcut: modShortcut("o", { shiftKey: true }), command: "chat.new" }, { shortcut: modShortcut("n", { shiftKey: true }), command: "chat.newLocal" }, { shortcut: modShortcut("o"), command: "editor.openFavorite" }, @@ -266,7 +260,6 @@ describe("shortcutLabelForCommand", () => { it("returns labels for non-terminal commands", () => { assert.strictEqual(shortcutLabelForCommand(DEFAULT_BINDINGS, "chat.new", "MacIntel"), "⇧⌘O"); - assert.strictEqual(shortcutLabelForCommand(DEFAULT_BINDINGS, "diff.toggle", "Linux"), "Ctrl+D"); assert.strictEqual( shortcutLabelForCommand(DEFAULT_BINDINGS, "editor.openFavorite", "Linux"), "Ctrl+O", @@ -323,21 +316,6 @@ describe("chat/editor shortcuts", () => { }), ); }); - - it("matches diff.toggle shortcut outside terminal focus", () => { - assert.isTrue( - isDiffToggleShortcut(event({ key: "d", metaKey: true }), DEFAULT_BINDINGS, { - platform: "MacIntel", - context: { terminalFocus: false }, - }), - ); - assert.isFalse( - isDiffToggleShortcut(event({ key: "d", metaKey: true }), DEFAULT_BINDINGS, { - platform: "MacIntel", - context: { terminalFocus: true }, - }), - ); - }); }); describe("cross-command precedence", () => { diff --git a/apps/web/src/keybindings.ts b/apps/web/src/keybindings.ts index aefe7c793..d7bf84228 100644 --- a/apps/web/src/keybindings.ts +++ b/apps/web/src/keybindings.ts @@ -213,14 +213,6 @@ export function isTerminalCloseShortcut( return matchesCommandShortcut(event, keybindings, "terminal.close", options); } -export function isDiffToggleShortcut( - event: ShortcutEventLike, - keybindings: ResolvedKeybindingsConfig, - options?: ShortcutMatchOptions, -): boolean { - return matchesCommandShortcut(event, keybindings, "diff.toggle", options); -} - export function isChatNewShortcut( event: ShortcutEventLike, keybindings: ResolvedKeybindingsConfig, diff --git a/apps/web/src/lib/diffFileReviewState.test.ts b/apps/web/src/lib/diffFileReviewState.test.ts deleted file mode 100644 index 417793ca0..000000000 --- a/apps/web/src/lib/diffFileReviewState.test.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { - acceptAllDiffFiles, - expandDiffFile, - reconcileDiffFileReviewState, - toggleDiffFileAccepted, - toggleDiffFileCollapsed, -} from "./diffFileReviewState"; - -describe("reconcileDiffFileReviewState", () => { - it("preserves existing state for known files and drops removed files", () => { - expect( - reconcileDiffFileReviewState(["src/a.ts"], { - "src/a.ts": { accepted: true, collapsed: true, contextMode: "patch" }, - "src/b.ts": { accepted: false, collapsed: true, contextMode: "full" }, - }), - ).toEqual({ - "src/a.ts": { accepted: true, collapsed: true, contextMode: "patch" }, - }); - }); - - it("initializes new files as unaccepted and collapsed", () => { - expect(reconcileDiffFileReviewState(["src/a.ts"], undefined)).toEqual({ - "src/a.ts": { accepted: false, collapsed: true, contextMode: "patch" }, - }); - }); -}); - -describe("toggleDiffFileAccepted", () => { - it("marks a file accepted and collapses it", () => { - expect(toggleDiffFileAccepted({}, "src/a.ts")).toEqual({ - "src/a.ts": { accepted: true, collapsed: true, contextMode: "patch" }, - }); - }); - - it("clears acceptance and re-expands the file", () => { - expect( - toggleDiffFileAccepted( - { - "src/a.ts": { accepted: true, collapsed: true, contextMode: "patch" }, - }, - "src/a.ts", - ), - ).toEqual({ - "src/a.ts": { accepted: false, collapsed: false, contextMode: "patch" }, - }); - }); -}); - -describe("acceptAllDiffFiles", () => { - it("marks every listed file accepted and collapsed", () => { - expect( - acceptAllDiffFiles( - { - "src/a.ts": { accepted: false, collapsed: false, contextMode: "full" }, - }, - ["src/a.ts", "src/b.ts"], - ), - ).toEqual({ - "src/a.ts": { accepted: true, collapsed: true, contextMode: "full" }, - "src/b.ts": { accepted: true, collapsed: true, contextMode: "patch" }, - }); - }); - - it("returns the same object when every listed file is already accepted", () => { - const state = { - "src/a.ts": { accepted: true, collapsed: true, contextMode: "patch" as const }, - "src/b.ts": { accepted: true, collapsed: true, contextMode: "full" as const }, - }; - - expect(acceptAllDiffFiles(state, ["src/a.ts", "src/b.ts"])).toBe(state); - }); -}); - -describe("toggleDiffFileCollapsed", () => { - it("toggles collapsed without changing acceptance", () => { - expect( - toggleDiffFileCollapsed( - { - "src/a.ts": { accepted: true, collapsed: true, contextMode: "patch" }, - }, - "src/a.ts", - ), - ).toEqual({ - "src/a.ts": { accepted: true, collapsed: false, contextMode: "patch" }, - }); - }); -}); - -describe("expandDiffFile", () => { - it("expands a collapsed file without clearing acceptance", () => { - expect( - expandDiffFile( - { - "src/a.ts": { accepted: true, collapsed: true, contextMode: "patch" }, - }, - "src/a.ts", - ), - ).toEqual({ - "src/a.ts": { accepted: true, collapsed: false, contextMode: "patch" }, - }); - }); - - it("returns the same object when the file is already expanded", () => { - const state = { - "src/a.ts": { accepted: false, collapsed: false, contextMode: "patch" }, - } satisfies Record; - // File is already expanded, so the same object reference is returned. - expect(expandDiffFile(state, "src/a.ts")).toBe(state); - }); -}); diff --git a/apps/web/src/lib/diffFileReviewState.ts b/apps/web/src/lib/diffFileReviewState.ts deleted file mode 100644 index 2a7ed526c..000000000 --- a/apps/web/src/lib/diffFileReviewState.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { type OrchestrationDiffContextMode } from "@okcode/contracts"; - -export interface DiffFileReviewState { - collapsed: boolean; - accepted: boolean; - contextMode: OrchestrationDiffContextMode; -} - -export type DiffFileReviewStateByPath = Record; - -const DEFAULT_DIFF_FILE_REVIEW_STATE: DiffFileReviewState = { - collapsed: true, - accepted: false, - contextMode: "patch", -}; - -export function reconcileDiffFileReviewState( - paths: ReadonlyArray, - current: DiffFileReviewStateByPath | undefined, -): DiffFileReviewStateByPath { - const next: DiffFileReviewStateByPath = {}; - for (const path of paths) { - next[path] = current?.[path] ?? DEFAULT_DIFF_FILE_REVIEW_STATE; - } - return next; -} - -export function toggleDiffFileAccepted( - current: DiffFileReviewStateByPath, - path: string, -): DiffFileReviewStateByPath { - const previous = current[path] ?? DEFAULT_DIFF_FILE_REVIEW_STATE; - const accepted = !previous.accepted; - return { - ...current, - [path]: { - contextMode: previous.contextMode, - accepted, - collapsed: accepted, - }, - }; -} - -export function acceptAllDiffFiles( - current: DiffFileReviewStateByPath, - paths: ReadonlyArray, -): DiffFileReviewStateByPath { - let next = current; - - for (const path of paths) { - const previous = next[path] ?? DEFAULT_DIFF_FILE_REVIEW_STATE; - if (previous.accepted && previous.collapsed) { - continue; - } - if (next === current) { - next = { ...current }; - } - next[path] = { - accepted: true, - collapsed: true, - contextMode: previous.contextMode, - }; - } - - return next; -} - -export function toggleDiffFileCollapsed( - current: DiffFileReviewStateByPath, - path: string, -): DiffFileReviewStateByPath { - const previous = current[path] ?? DEFAULT_DIFF_FILE_REVIEW_STATE; - return { - ...current, - [path]: { - ...previous, - collapsed: !previous.collapsed, - }, - }; -} - -export function expandDiffFile( - current: DiffFileReviewStateByPath, - path: string, -): DiffFileReviewStateByPath { - const previous = current[path] ?? DEFAULT_DIFF_FILE_REVIEW_STATE; - if (!previous.collapsed) { - return current; - } - return { - ...current, - [path]: { - ...previous, - collapsed: false, - }, - }; -} diff --git a/apps/web/src/mutuallyExclusivePanels.test.ts b/apps/web/src/mutuallyExclusivePanels.test.ts index cafaa463e..d66d54a97 100644 --- a/apps/web/src/mutuallyExclusivePanels.test.ts +++ b/apps/web/src/mutuallyExclusivePanels.test.ts @@ -3,50 +3,9 @@ import { describe, expect, it } from "vitest"; import { resolveExclusivePanelAction } from "./mutuallyExclusivePanels"; describe("resolveExclusivePanelAction", () => { - // ─── Diff opens while code viewer is already open ────────────────── - it("returns 'close-code-viewer' when diff transitions open while code viewer is open", () => { - const result = resolveExclusivePanelAction( - /* prevDiffOpen */ false, - /* diffOpen */ true, - /* prevCodeViewerOpen */ true, - /* codeViewerOpen */ true, - /* prevPreviewOpen */ false, - /* previewOpen */ false, - ); - expect(result).toEqual(["close-code-viewer"]); - }); - - // ─── Code viewer opens while diff is already open ────────────────── - it("returns 'close-diff' when code viewer transitions open while diff is open", () => { - const result = resolveExclusivePanelAction( - /* prevDiffOpen */ true, - /* diffOpen */ true, - /* prevCodeViewerOpen */ false, - /* codeViewerOpen */ true, - /* prevPreviewOpen */ false, - /* previewOpen */ false, - ); - expect(result).toEqual(["close-diff"]); - }); - - // ─── Preview opens while diff is already open ────────────────────── - it("returns 'close-diff' when preview transitions open while diff is open", () => { - const result = resolveExclusivePanelAction( - /* prevDiffOpen */ true, - /* diffOpen */ true, - /* prevCodeViewerOpen */ false, - /* codeViewerOpen */ false, - /* prevPreviewOpen */ false, - /* previewOpen */ true, - ); - expect(result).toEqual(["close-diff"]); - }); - // ─── Preview opens while code viewer is already open ─────────────── it("returns 'close-code-viewer' when preview transitions open while code viewer is open", () => { const result = resolveExclusivePanelAction( - /* prevDiffOpen */ false, - /* diffOpen */ false, /* prevCodeViewerOpen */ true, /* codeViewerOpen */ true, /* prevPreviewOpen */ false, @@ -55,24 +14,9 @@ describe("resolveExclusivePanelAction", () => { expect(result).toEqual(["close-code-viewer"]); }); - // ─── Diff opens while preview is already open ────────────────────── - it("returns 'close-preview' when diff transitions open while preview is open", () => { - const result = resolveExclusivePanelAction( - /* prevDiffOpen */ false, - /* diffOpen */ true, - /* prevCodeViewerOpen */ false, - /* codeViewerOpen */ false, - /* prevPreviewOpen */ true, - /* previewOpen */ true, - ); - expect(result).toEqual(["close-preview"]); - }); - // ─── Code viewer opens while preview is already open ─────────────── it("returns 'close-preview' when code viewer transitions open while preview is open", () => { const result = resolveExclusivePanelAction( - /* prevDiffOpen */ false, - /* diffOpen */ false, /* prevCodeViewerOpen */ false, /* codeViewerOpen */ true, /* prevPreviewOpen */ true, @@ -81,144 +25,88 @@ describe("resolveExclusivePanelAction", () => { expect(result).toEqual(["close-preview"]); }); - // ─── Diff opens while both code viewer and preview are open ──────── - it("closes both code viewer and preview when diff opens", () => { - const result = resolveExclusivePanelAction( - /* prevDiffOpen */ false, - /* diffOpen */ true, - /* prevCodeViewerOpen */ true, - /* codeViewerOpen */ true, - /* prevPreviewOpen */ true, - /* previewOpen */ true, - ); - expect(result).toEqual(expect.arrayContaining(["close-code-viewer", "close-preview"])); - expect(result).toHaveLength(2); - }); - // ─── No-op cases ────────────────────────────────────────────────── it("returns empty array when no panels are open", () => { - expect(resolveExclusivePanelAction(false, false, false, false, false, false)).toEqual([]); - }); - - it("returns empty array when only diff is open (no transition)", () => { - expect(resolveExclusivePanelAction(true, true, false, false, false, false)).toEqual([]); + expect(resolveExclusivePanelAction(false, false, false, false)).toEqual([]); }); it("returns empty array when only code viewer is open (no transition)", () => { - expect(resolveExclusivePanelAction(false, false, true, true, false, false)).toEqual([]); + expect(resolveExclusivePanelAction(true, true, false, false)).toEqual([]); }); it("returns empty array when only preview is open (no transition)", () => { - expect(resolveExclusivePanelAction(false, false, false, false, true, true)).toEqual([]); - }); - - it("returns empty array when diff opens but no other panels are open", () => { - expect(resolveExclusivePanelAction(false, true, false, false, false, false)).toEqual([]); + expect(resolveExclusivePanelAction(false, false, true, true)).toEqual([]); }); it("returns empty array when code viewer opens but no other panels are open", () => { - expect(resolveExclusivePanelAction(false, false, false, true, false, false)).toEqual([]); + expect(resolveExclusivePanelAction(false, true, false, false)).toEqual([]); }); it("returns empty array when preview opens but no other panels are open", () => { - expect(resolveExclusivePanelAction(false, false, false, false, false, true)).toEqual([]); - }); - - it("returns empty array when diff closes (others still closed)", () => { - expect(resolveExclusivePanelAction(true, false, false, false, false, false)).toEqual([]); + expect(resolveExclusivePanelAction(false, false, false, true)).toEqual([]); }); it("returns empty array when code viewer closes (others still closed)", () => { - expect(resolveExclusivePanelAction(false, false, true, false, false, false)).toEqual([]); + expect(resolveExclusivePanelAction(true, false, false, false)).toEqual([]); }); it("returns empty array when preview closes (others still closed)", () => { - expect(resolveExclusivePanelAction(false, false, false, false, true, false)).toEqual([]); + expect(resolveExclusivePanelAction(false, false, true, false)).toEqual([]); }); // ─── Edge: all were already open (no transition) ─────────────────── it("returns empty array when all were already open (no transition)", () => { - expect(resolveExclusivePanelAction(true, true, true, true, true, true)).toEqual([]); - }); - - // ─── Edge: both diff and code viewer transition open simultaneously ─ - it("prefers closing code viewer when diff and code viewer open simultaneously (diff wins)", () => { - const result = resolveExclusivePanelAction(false, true, false, true, false, false); - expect(result).toEqual(["close-code-viewer"]); + expect(resolveExclusivePanelAction(true, true, true, true)).toEqual([]); }); // ─── Simulation panel tests ────────────────────────────────────── - it("returns 'close-simulation' when diff transitions open while simulation is open", () => { + it("returns 'close-simulation' when code viewer transitions open while simulation is open", () => { const result = resolveExclusivePanelAction( - false, - true, - false, - false, - false, - false, + /* prevCodeViewerOpen */ false, + /* codeViewerOpen */ true, + /* prevPreviewOpen */ false, + /* previewOpen */ false, /* prevSimulationOpen */ true, /* simulationOpen */ true, ); expect(result).toEqual(["close-simulation"]); }); - it("closes diff, code viewer, and preview when simulation opens", () => { + it("closes code viewer and preview when simulation opens", () => { const result = resolveExclusivePanelAction( - true, - true, - true, - true, - true, - true, + /* prevCodeViewerOpen */ true, + /* codeViewerOpen */ true, + /* prevPreviewOpen */ true, + /* previewOpen */ true, /* prevSimulationOpen */ false, /* simulationOpen */ true, ); - expect(result).toEqual( - expect.arrayContaining(["close-diff", "close-code-viewer", "close-preview"]), - ); - expect(result).toHaveLength(3); + expect(result).toEqual(expect.arrayContaining(["close-code-viewer", "close-preview"])); + expect(result).toHaveLength(2); }); it("returns empty array when simulation opens but no other panels are open", () => { - const result = resolveExclusivePanelAction( - false, - false, - false, - false, - false, - false, - false, - true, - ); + const result = resolveExclusivePanelAction(false, false, false, false, false, true); expect(result).toEqual([]); }); it("returns empty array when only simulation is open (no transition)", () => { - const result = resolveExclusivePanelAction( - false, - false, - false, - false, - false, - false, - true, - true, - ); + const result = resolveExclusivePanelAction(false, false, false, false, true, true); expect(result).toEqual([]); }); it("closes simulation when code viewer opens", () => { - const result = resolveExclusivePanelAction(false, false, false, true, false, false, true, true); + const result = resolveExclusivePanelAction(false, true, false, false, true, true); expect(result).toEqual(["close-simulation"]); }); it("closes simulation when preview opens", () => { - const result = resolveExclusivePanelAction(false, false, false, false, false, true, true, true); + const result = resolveExclusivePanelAction(false, false, false, true, true, true); expect(result).toEqual(["close-simulation"]); }); it("backward-compatible: works without simulation args", () => { - const result = resolveExclusivePanelAction(false, true, true, true, false, false); - expect(result).toEqual(["close-code-viewer"]); + const result = resolveExclusivePanelAction(false, true, true, true); + expect(result).toEqual(["close-preview"]); }); }); diff --git a/apps/web/src/mutuallyExclusivePanels.ts b/apps/web/src/mutuallyExclusivePanels.ts index 962e825c7..6acb89de2 100644 --- a/apps/web/src/mutuallyExclusivePanels.ts +++ b/apps/web/src/mutuallyExclusivePanels.ts @@ -3,11 +3,7 @@ import { useEffect, useRef } from "react"; /** * Action types for panel mutual exclusivity enforcement. */ -export type ExclusivePanelAction = - | "close-diff" - | "close-code-viewer" - | "close-preview" - | "close-simulation"; +export type ExclusivePanelAction = "close-code-viewer" | "close-preview" | "close-simulation"; /** * Given previous and current open states for the right-side panels, @@ -18,8 +14,6 @@ export type ExclusivePanelAction = * all other open panels are closed. */ export function resolveExclusivePanelAction( - prevDiffOpen: boolean, - diffOpen: boolean, prevCodeViewerOpen: boolean, codeViewerOpen: boolean, prevPreviewOpen: boolean, @@ -27,27 +21,19 @@ export function resolveExclusivePanelAction( prevSimulationOpen: boolean = false, simulationOpen: boolean = false, ): ExclusivePanelAction[] { - const diffJustOpened = diffOpen && !prevDiffOpen; const codeViewerJustOpened = codeViewerOpen && !prevCodeViewerOpen; const previewJustOpened = previewOpen && !prevPreviewOpen; const simulationJustOpened = simulationOpen && !prevSimulationOpen; const actions: ExclusivePanelAction[] = []; - if (diffJustOpened) { - if (codeViewerOpen) actions.push("close-code-viewer"); - if (previewOpen) actions.push("close-preview"); - if (simulationOpen) actions.push("close-simulation"); - } else if (codeViewerJustOpened) { - if (diffOpen) actions.push("close-diff"); + if (codeViewerJustOpened) { if (previewOpen) actions.push("close-preview"); if (simulationOpen) actions.push("close-simulation"); } else if (previewJustOpened) { - if (diffOpen) actions.push("close-diff"); if (codeViewerOpen) actions.push("close-code-viewer"); if (simulationOpen) actions.push("close-simulation"); } else if (simulationJustOpened) { - if (diffOpen) actions.push("close-diff"); if (codeViewerOpen) actions.push("close-code-viewer"); if (previewOpen) actions.push("close-preview"); } @@ -56,39 +42,32 @@ export function resolveExclusivePanelAction( } /** - * Ensures that the diff panel, code viewer, preview panel, and simulation + * Ensures that the code viewer, preview panel, and simulation * viewer are never open simultaneously. When one panel transitions from * closed → open while another is already open, the previously-open panel(s) * are closed. */ export function useMutuallyExclusivePanels( - diffOpen: boolean, codeViewerOpen: boolean, previewOpen: boolean, - closeDiff: () => void, closeCodeViewer: () => void, closePreview: () => void, simulationOpen: boolean = false, closeSimulation?: () => void, ) { - const prevDiffOpen = useRef(diffOpen); const prevCodeViewerOpen = useRef(codeViewerOpen); const prevPreviewOpen = useRef(previewOpen); const prevSimulationOpen = useRef(simulationOpen); useEffect(() => { - const wasDiffOpen = prevDiffOpen.current; const wasCodeViewerOpen = prevCodeViewerOpen.current; const wasPreviewOpen = prevPreviewOpen.current; const wasSimulationOpen = prevSimulationOpen.current; - prevDiffOpen.current = diffOpen; prevCodeViewerOpen.current = codeViewerOpen; prevPreviewOpen.current = previewOpen; prevSimulationOpen.current = simulationOpen; const actions = resolveExclusivePanelAction( - wasDiffOpen, - diffOpen, wasCodeViewerOpen, codeViewerOpen, wasPreviewOpen, @@ -99,22 +78,11 @@ export function useMutuallyExclusivePanels( for (const action of actions) { if (action === "close-code-viewer") { closeCodeViewer(); - } else if (action === "close-diff") { - closeDiff(); } else if (action === "close-preview") { closePreview(); } else if (action === "close-simulation") { closeSimulation?.(); } } - }, [ - diffOpen, - codeViewerOpen, - previewOpen, - simulationOpen, - closeDiff, - closeCodeViewer, - closePreview, - closeSimulation, - ]); + }, [codeViewerOpen, previewOpen, simulationOpen, closeCodeViewer, closePreview, closeSimulation]); } diff --git a/apps/web/src/routes/_chat.$threadId.tsx b/apps/web/src/routes/_chat.$threadId.tsx index 4a096bbed..0ecbd87cc 100644 --- a/apps/web/src/routes/_chat.$threadId.tsx +++ b/apps/web/src/routes/_chat.$threadId.tsx @@ -1,5 +1,5 @@ import { ThreadId } from "@okcode/contracts"; -import { createFileRoute, retainSearchParams, useNavigate } from "@tanstack/react-router"; +import { createFileRoute, useNavigate } from "@tanstack/react-router"; import { Suspense, lazy, @@ -11,19 +11,7 @@ import { } from "react"; import ChatView from "../components/ChatView"; -import { DiffWorkerPoolProvider } from "../components/DiffWorkerPoolProvider"; -import { - DiffPanelHeaderSkeleton, - DiffPanelLoadingState, - DiffPanelShell, - type DiffPanelMode, -} from "../components/DiffPanelShell"; import { useComposerDraftStore } from "../composerDraftStore"; -import { - type DiffRouteSearch, - parseDiffRouteSearch, - stripDiffSearchParams, -} from "../diffRouteSearch"; import { useCodeViewerStore } from "../codeViewerStore"; import { usePreviewStateStore } from "../previewStateStore"; import { useSimulationViewerStore } from "../simulationViewerStore"; @@ -33,8 +21,8 @@ import { useClientMode } from "../hooks/useClientMode"; import { useStore } from "../store"; import { Sheet, SheetPopup } from "../components/ui/sheet"; import { Sidebar, SidebarInset, SidebarProvider, SidebarRail } from "~/components/ui/sidebar"; +import { Loader2Icon } from "lucide-react"; -const DiffPanel = lazy(() => import("../components/DiffPanel")); const CodeViewerPanel = lazy(() => import("../components/CodeViewerPanel")); const SimulationViewerLazy = lazy(() => import("../components/simulation/SimulationViewer").then((m) => ({ @@ -42,41 +30,11 @@ const SimulationViewerLazy = lazy(() => })), ); -const DIFF_INLINE_LAYOUT_MEDIA_QUERY = "(max-width: 1180px)"; -const DIFF_INLINE_SIDEBAR_WIDTH_STORAGE_KEY = "chat_diff_sidebar_width"; -const DIFF_INLINE_DEFAULT_WIDTH = "clamp(28rem,48vw,44rem)"; -const DIFF_INLINE_SIDEBAR_MIN_WIDTH = 26 * 16; const CODE_VIEWER_SIDEBAR_WIDTH_STORAGE_KEY = "chat_code_viewer_sidebar_width"; const CODE_VIEWER_DEFAULT_WIDTH = "clamp(28rem,48vw,44rem)"; const CODE_VIEWER_SIDEBAR_MIN_WIDTH = 26 * 16; const COMPOSER_COMPACT_MIN_LEFT_CONTROLS_WIDTH_PX = 208; -const DiffPanelSheet = (props: { - children: ReactNode; - diffOpen: boolean; - onCloseDiff: () => void; -}) => { - return ( - { - if (!open) { - props.onCloseDiff(); - } - }} - > - - {props.children} - - - ); -}; - const CodeViewerSheet = (props: { children: ReactNode; codeViewerOpen: boolean; @@ -103,32 +61,17 @@ const CodeViewerSheet = (props: { ); }; -const DiffLoadingFallback = (props: { mode: DiffPanelMode }) => { - return ( - }> - - - ); -}; - const CodeViewerLoadingFallback = () => { return (
- +
+ + Loading code viewer... +
); }; -const LazyDiffPanel = (props: { mode: DiffPanelMode }) => { - return ( - - }> - - - - ); -}; - const LazyCodeViewerPanel = () => { return ( }> @@ -140,7 +83,10 @@ const LazyCodeViewerPanel = () => { const SimulationLoadingFallback = () => { return (
- +
+ + Loading simulation viewer... +
); }; @@ -198,50 +144,6 @@ function useShouldAcceptInlineSidebarWidth() { }, []); } -const DiffPanelInlineSidebar = (props: { - diffOpen: boolean; - onCloseDiff: () => void; - onOpenDiff: () => void; - renderDiffContent: boolean; -}) => { - const { diffOpen, onCloseDiff, onOpenDiff, renderDiffContent } = props; - const onOpenChange = useCallback( - (open: boolean) => { - if (open) { - onOpenDiff(); - return; - } - onCloseDiff(); - }, - [onCloseDiff, onOpenDiff], - ); - const shouldAcceptInlineSidebarWidth = useShouldAcceptInlineSidebarWidth(); - - return ( - - - {renderDiffContent ? : null} - - - - ); -}; - /** Right-side sidebar panel for the code viewer — sits alongside the chat area. */ const CodeViewerInlineSidebar = (props: { codeViewerOpen: boolean; @@ -290,16 +192,12 @@ function ChatThreadRouteView() { const threadId = Route.useParams({ select: (params) => ThreadId.makeUnsafe(params.threadId), }); - const search = Route.useSearch(); const threadExists = useStore((store) => store.threads.some((thread) => thread.id === threadId)); const draftThreadExists = useComposerDraftStore((store) => Object.hasOwn(store.draftThreadsByThreadId, threadId), ); const routeThreadExists = threadExists || draftThreadExists; - const diffOpen = search.diff === "1"; const clientMode = useClientMode(); - const isNarrowDiffLayout = useMediaQuery(DIFF_INLINE_LAYOUT_MEDIA_QUERY); - const shouldUseDiffSheet = clientMode === "mobile" || isNarrowDiffLayout; const shouldUseCodeViewerSheet = clientMode === "mobile"; // Code viewer state from Zustand store @@ -322,28 +220,9 @@ function ChatThreadRouteView() { // TanStack Router keeps active route components mounted across param-only navigations // unless remountDeps are configured, so this stays warm across thread switches. - const [hasOpenedDiff, setHasOpenedDiff] = useState(diffOpen); const [hasOpenedCodeViewer, setHasOpenedCodeViewer] = useState(codeViewerOpen); const [hasOpenedSimulation, setHasOpenedSimulation] = useState(simulationOpen); - const closeDiff = useCallback(() => { - void navigate({ - to: "/$threadId", - params: { threadId }, - search: { diff: undefined }, - }); - }, [navigate, threadId]); - const openDiff = useCallback(() => { - void navigate({ - to: "/$threadId", - params: { threadId }, - search: (previous) => { - const rest = stripDiffSearchParams(previous); - return { ...rest, diff: "1" }; - }, - }); - }, [navigate, threadId]); - const closeCodeViewer = useCallback(() => { closeCodeViewerStore(); }, [closeCodeViewerStore]); @@ -358,22 +237,14 @@ function ChatThreadRouteView() { // Enforce mutual exclusivity: only one right-side panel open at a time. useMutuallyExclusivePanels( - diffOpen, codeViewerOpen, previewOpen, - closeDiff, closeCodeViewer, closePreview, simulationOpen, closeSimulation, ); - useEffect(() => { - if (diffOpen) { - setHasOpenedDiff(true); - } - }, [diffOpen]); - useEffect(() => { if (codeViewerOpen) { setHasOpenedCodeViewer(true); @@ -401,7 +272,6 @@ function ChatThreadRouteView() { return null; } - const shouldRenderDiffContent = diffOpen || hasOpenedDiff; const shouldRenderCodeViewerContent = codeViewerOpen || hasOpenedCodeViewer; const shouldRenderSimulation = simulationOpen || hasOpenedSimulation; @@ -450,7 +320,7 @@ function ChatThreadRouteView() { ) ) : null; - if (!shouldUseDiffSheet) { + if (!shouldUseCodeViewerSheet) { return ( <> @@ -461,12 +331,6 @@ function ChatThreadRouteView() { onCloseCodeViewer={closeCodeViewer} renderContent={shouldRenderCodeViewerContent} /> - {simulationNode} ); @@ -477,29 +341,14 @@ function ChatThreadRouteView() { - {shouldUseCodeViewerSheet ? ( - - {shouldRenderCodeViewerContent ? : null} - - ) : ( - - )} - - {shouldRenderDiffContent ? : null} - + + {shouldRenderCodeViewerContent ? : null} + {simulationNode} ); } export const Route = createFileRoute("/_chat/$threadId")({ - validateSearch: (search) => parseDiffRouteSearch(search), - search: { - middlewares: [retainSearchParams(["diff"])], - }, component: ChatThreadRouteView, }); diff --git a/apps/web/src/routes/_chat.settings.tsx b/apps/web/src/routes/_chat.settings.tsx index eeb95ca9b..8662b5e44 100644 --- a/apps/web/src/routes/_chat.settings.tsx +++ b/apps/web/src/routes/_chat.settings.tsx @@ -418,7 +418,6 @@ function SettingsRouteView() { ...(colorTheme !== DEFAULT_COLOR_THEME ? ["Color theme"] : []), ...(fontFamily !== "inter" ? ["Font"] : []), ...(settings.timestampFormat !== defaults.timestampFormat ? ["Time format"] : []), - ...(settings.diffWordWrap !== defaults.diffWordWrap ? ["Diff line wrapping"] : []), ...(settings.showStitchBorder !== defaults.showStitchBorder ? ["Stitch border"] : []), ...(settings.enableAssistantStreaming !== defaults.enableAssistantStreaming ? ["Assistant output"] @@ -1076,34 +1075,6 @@ function SettingsRouteView() { } /> - - updateSettings({ - diffWordWrap: defaults.diffWordWrap, - }) - } - /> - ) : null - } - control={ - - updateSettings({ - diffWordWrap: Boolean(checked), - }) - } - aria-label="Wrap diff lines by default" - /> - } - /> - }); assert.strictEqual(parsedClose.command, "terminal.close"); - const parsedDiffToggle = yield* decode(KeybindingRule, { - key: "mod+d", - command: "diff.toggle", - }); - assert.strictEqual(parsedDiffToggle.command, "diff.toggle"); - const parsedLocal = yield* decode(KeybindingRule, { key: "mod+shift+n", command: "chat.newLocal", diff --git a/packages/contracts/src/keybindings.ts b/packages/contracts/src/keybindings.ts index 48821b182..3880ddbd8 100644 --- a/packages/contracts/src/keybindings.ts +++ b/packages/contracts/src/keybindings.ts @@ -12,7 +12,6 @@ const STATIC_KEYBINDING_COMMANDS = [ "terminal.split", "terminal.new", "terminal.close", - "diff.toggle", "chat.new", "chat.newLocal", "editor.openFavorite",