From 0c65528a81c681e92f913a6ac0c1924d22361b77 Mon Sep 17 00:00:00 2001 From: Michael Latman Date: Tue, 10 Feb 2026 12:17:49 -0500 Subject: [PATCH 1/3] Add optional split chat/diff center layout (default off) --- src-tauri/src/types.rs | 11 ++++ src/App.tsx | 1 + src/features/app/components/AppLayout.tsx | 3 + .../layout/components/DesktopLayout.tsx | 66 ++++++++++++++----- .../settings/components/SettingsView.test.tsx | 24 +++++++ .../sections/SettingsDisplaySection.tsx | 21 ++++++ src/features/settings/hooks/useAppSettings.ts | 1 + src/styles/main.css | 20 ++++++ src/types.ts | 1 + 9 files changed, 131 insertions(+), 17 deletions(-) diff --git a/src-tauri/src/types.rs b/src-tauri/src/types.rs index 016e63c48..96d0088ea 100644 --- a/src-tauri/src/types.rs +++ b/src-tauri/src/types.rs @@ -586,6 +586,11 @@ pub(crate) struct AppSettings { rename = "notificationSoundsEnabled" )] pub(crate) notification_sounds_enabled: bool, + #[serde( + default = "default_split_chat_diff_view", + rename = "splitChatDiffView" + )] + pub(crate) split_chat_diff_view: bool, #[serde(default = "default_preload_git_diffs", rename = "preloadGitDiffs")] pub(crate) preload_git_diffs: bool, #[serde( @@ -923,6 +928,10 @@ fn default_system_notifications_enabled() -> bool { true } +fn default_split_chat_diff_view() -> bool { + false +} + fn default_preload_git_diffs() -> bool { true } @@ -1184,6 +1193,7 @@ impl Default for AppSettings { code_font_size: default_code_font_size(), notification_sounds_enabled: true, system_notifications_enabled: true, + split_chat_diff_view: default_split_chat_diff_view(), preload_git_diffs: default_preload_git_diffs(), git_diff_ignore_whitespace_changes: default_git_diff_ignore_whitespace_changes(), commit_message_prompt: default_commit_message_prompt(), @@ -1346,6 +1356,7 @@ mod tests { assert_eq!(settings.code_font_size, 11); assert!(settings.notification_sounds_enabled); assert!(settings.system_notifications_enabled); + assert!(!settings.split_chat_diff_view); assert!(settings.preload_git_diffs); assert!(!settings.git_diff_ignore_whitespace_changes); assert!(settings.commit_message_prompt.contains("{diff}")); diff --git a/src/App.tsx b/src/App.tsx index e0b254a04..0814757eb 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2312,6 +2312,7 @@ function MainApp() { tabletTab={tabletTab} centerMode={centerMode} preloadGitDiffs={appSettings.preloadGitDiffs} + splitChatDiffView={appSettings.splitChatDiffView} hasActivePlan={hasActivePlan} activeWorkspace={Boolean(activeWorkspace)} sidebarNode={sidebarNode} diff --git a/src/features/app/components/AppLayout.tsx b/src/features/app/components/AppLayout.tsx index 16d2ebdf1..dc887555c 100644 --- a/src/features/app/components/AppLayout.tsx +++ b/src/features/app/components/AppLayout.tsx @@ -12,6 +12,7 @@ type AppLayoutProps = { tabletTab: "codex" | "git" | "log"; centerMode: "chat" | "diff"; preloadGitDiffs: boolean; + splitChatDiffView: boolean; hasActivePlan: boolean; activeWorkspace: boolean; sidebarNode: ReactNode; @@ -48,6 +49,7 @@ export const AppLayout = memo(function AppLayout({ tabletTab, centerMode, preloadGitDiffs, + splitChatDiffView, hasActivePlan, activeWorkspace, sidebarNode, @@ -134,6 +136,7 @@ export const AppLayout = memo(function AppLayout({ topbarLeftNode={desktopTopbarLeftNode} centerMode={centerMode} preloadGitDiffs={preloadGitDiffs} + splitChatDiffView={splitChatDiffView} messagesNode={messagesNode} gitDiffViewerNode={gitDiffViewerNode} gitDiffPanelNode={gitDiffPanelNode} diff --git a/src/features/layout/components/DesktopLayout.tsx b/src/features/layout/components/DesktopLayout.tsx index 2f450f870..28b2e2109 100644 --- a/src/features/layout/components/DesktopLayout.tsx +++ b/src/features/layout/components/DesktopLayout.tsx @@ -12,6 +12,7 @@ type DesktopLayoutProps = { topbarLeftNode: ReactNode; centerMode: "chat" | "diff"; preloadGitDiffs: boolean; + splitChatDiffView: boolean; messagesNode: ReactNode; gitDiffViewerNode: ReactNode; gitDiffPanelNode: ReactNode; @@ -36,6 +37,7 @@ export function DesktopLayout({ topbarLeftNode, centerMode, preloadGitDiffs, + splitChatDiffView, messagesNode, gitDiffViewerNode, gitDiffPanelNode, @@ -50,12 +52,19 @@ export function DesktopLayout({ }: DesktopLayoutProps) { const diffLayerRef = useRef(null); const chatLayerRef = useRef(null); - const shouldRenderDiffViewer = preloadGitDiffs || centerMode === "diff"; + const shouldRenderDiffViewer = + splitChatDiffView || preloadGitDiffs || centerMode === "diff"; useEffect(() => { const diffLayer = diffLayerRef.current; const chatLayer = chatLayerRef.current; + if (splitChatDiffView) { + diffLayer?.removeAttribute("inert"); + chatLayer?.removeAttribute("inert"); + return; + } + if (diffLayer) { if (centerMode === "diff") { diffLayer.removeAttribute("inert"); @@ -81,7 +90,7 @@ export function DesktopLayout({ ) { activeElement.blur(); } - }, [centerMode]); + }, [centerMode, splitChatDiffView]); return ( <> @@ -103,21 +112,44 @@ export function DesktopLayout({ <> {approvalToastsNode} -
-
- {shouldRenderDiffViewer ? gitDiffViewerNode : null} -
-
- {messagesNode} -
+
+ {splitChatDiffView ? ( + <> +
+ {messagesNode} +
+
+ {shouldRenderDiffViewer ? gitDiffViewerNode : null} +
+ + ) : ( + <> +
+ {shouldRenderDiffViewer ? gitDiffViewerNode : null} +
+
+ {messagesNode} +
+ + )}
{ }); }); + it("toggles split chat and diff center panes", async () => { + const onUpdateAppSettings = vi.fn().mockResolvedValue(undefined); + renderDisplaySection({ onUpdateAppSettings }); + + const row = screen + .getByText("Split chat and diff center panes") + .closest(".settings-toggle-row") as HTMLElement | null; + if (!row) { + throw new Error("Expected split center panes row"); + } + const toggle = row.querySelector("button.settings-toggle") as HTMLButtonElement | null; + if (!toggle) { + throw new Error("Expected split center panes toggle"); + } + fireEvent.click(toggle); + + await waitFor(() => { + expect(onUpdateAppSettings).toHaveBeenCalledWith( + expect.objectContaining({ splitChatDiffView: true }), + ); + }); + }); + it("toggles reduce transparency", async () => { const onToggleTransparency = vi.fn(); renderDisplaySection({ onToggleTransparency, reduceTransparency: false }); diff --git a/src/features/settings/components/sections/SettingsDisplaySection.tsx b/src/features/settings/components/sections/SettingsDisplaySection.tsx index 9722759da..ac7b0757d 100644 --- a/src/features/settings/components/sections/SettingsDisplaySection.tsx +++ b/src/features/settings/components/sections/SettingsDisplaySection.tsx @@ -128,6 +128,27 @@ export function SettingsDisplaySection({
+
+
+
Split chat and diff center panes
+
+ Show chat and diff side by side instead of swapping between them. +
+
+ +
Auto-generate new thread titles
diff --git a/src/features/settings/hooks/useAppSettings.ts b/src/features/settings/hooks/useAppSettings.ts index 572acfaf7..c9b4c7112 100644 --- a/src/features/settings/hooks/useAppSettings.ts +++ b/src/features/settings/hooks/useAppSettings.ts @@ -72,6 +72,7 @@ function buildDefaultSettings(): AppSettings { codeFontSize: CODE_FONT_SIZE_DEFAULT, notificationSoundsEnabled: true, systemNotificationsEnabled: true, + splitChatDiffView: false, preloadGitDiffs: true, gitDiffIgnoreWhitespaceChanges: false, commitMessagePrompt: DEFAULT_COMMIT_MESSAGE_PROMPT, diff --git a/src/styles/main.css b/src/styles/main.css index 0b577a79c..38efd377b 100644 --- a/src/styles/main.css +++ b/src/styles/main.css @@ -851,6 +851,10 @@ position: relative; } +.content-split { + position: relative; +} + .content-layer { position: absolute; inset: 0; @@ -861,6 +865,22 @@ isolation: isolate; } +.content-layer-split { + top: var(--main-topbar-height); + bottom: 0; +} + +.content-layer-chat { + left: 0; + right: 50%; + border-right: 1px solid var(--border-subtle); +} + +.content-layer-diff { + left: 50%; + right: 0; +} + .content-layer.is-hidden { opacity: 0; pointer-events: none; diff --git a/src/types.ts b/src/types.ts index 23c20676d..31a5f38be 100644 --- a/src/types.ts +++ b/src/types.ts @@ -189,6 +189,7 @@ export type AppSettings = { codeFontSize: number; notificationSoundsEnabled: boolean; systemNotificationsEnabled: boolean; + splitChatDiffView: boolean; preloadGitDiffs: boolean; gitDiffIgnoreWhitespaceChanges: boolean; commitMessagePrompt: string; From 0f7c3058fcbd0a34f5a9f77734a2523c6d88dbe6 Mon Sep 17 00:00:00 2001 From: Michael Latman Date: Tue, 10 Feb 2026 12:21:40 -0500 Subject: [PATCH 2/3] Improve diff viewer empty state messaging --- src/features/git/components/GitDiffViewer.tsx | 23 ++++++- src/styles/diff-viewer.css | 69 +++++++++++++++++++ 2 files changed, 91 insertions(+), 1 deletion(-) diff --git a/src/features/git/components/GitDiffViewer.tsx b/src/features/git/components/GitDiffViewer.tsx index caa42448d..81957248e 100644 --- a/src/features/git/components/GitDiffViewer.tsx +++ b/src/features/git/components/GitDiffViewer.tsx @@ -5,6 +5,7 @@ import { FileDiff, WorkerPoolContextProvider } from "@pierre/diffs/react"; import type { FileDiffMetadata } from "@pierre/diffs"; import { parsePatchFiles } from "@pierre/diffs"; import RotateCcw from "lucide-react/dist/esm/icons/rotate-ccw"; +import GitCommitHorizontal from "lucide-react/dist/esm/icons/git-commit-horizontal"; import { workerFactory } from "../../../utils/diffsWorker"; import type { GitHubPullRequest, GitHubPullRequestComment } from "../../../types"; import { formatRelativeTime } from "../../../utils/time"; @@ -594,6 +595,18 @@ export function GitDiffViewer({ } rowVirtualizer.scrollToIndex(0, { align: "start" }); }, [diffs.length, rowVirtualizer]); + const emptyStateCopy = pullRequest + ? { + title: "No file changes in this pull request", + subtitle: + "The pull request loaded, but there are no diff hunks to render for this selection.", + hint: "Try switching to another pull request or commit from the Git panel.", + } + : { + title: "Working tree is clean", + subtitle: "No local changes were detected for the current workspace.", + hint: "Make an edit, stage a file, or select a commit to inspect changes here.", + }; return ( )} {!error && !isLoading && !diffs.length && ( -
No changes detected.
+
+
+ + + +

{emptyStateCopy.title}

+

{emptyStateCopy.subtitle}

+

{emptyStateCopy.hint}

+
)} {!error && diffs.length > 0 && (
Date: Tue, 10 Feb 2026 12:48:56 -0500 Subject: [PATCH 3/3] fix: load local diffs in split view without preload --- src/App.tsx | 1 + .../app/hooks/useGitPanelController.test.tsx | 14 ++++++++++++++ src/features/app/hooks/useGitPanelController.ts | 7 ++++++- 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/App.tsx b/src/App.tsx index 0814757eb..77daa3f0c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -398,6 +398,7 @@ function MainApp() { activeWorkspace, gitDiffPreloadEnabled: appSettings.preloadGitDiffs, gitDiffIgnoreWhitespaceChanges: appSettings.gitDiffIgnoreWhitespaceChanges, + splitChatDiffView: appSettings.splitChatDiffView, isCompact, isTablet, activeTab, diff --git a/src/features/app/hooks/useGitPanelController.test.tsx b/src/features/app/hooks/useGitPanelController.test.tsx index 2154261f6..3baf58c07 100644 --- a/src/features/app/hooks/useGitPanelController.test.tsx +++ b/src/features/app/hooks/useGitPanelController.test.tsx @@ -38,6 +38,7 @@ function makeProps(overrides?: Partial[ activeWorkspace: workspace, gitDiffPreloadEnabled: false, gitDiffIgnoreWhitespaceChanges: false, + splitChatDiffView: false, isCompact: false, isTablet: false, activeTab: "codex" as const, @@ -142,4 +143,17 @@ describe("useGitPanelController preload behavior", () => { const selectedEnabled = getLastEnabledArg(); expect(selectedEnabled).toBe(true); }); + + it("loads local diffs when split view is enabled and preload is disabled", () => { + renderHook(() => + useGitPanelController( + makeProps({ + splitChatDiffView: true, + }), + ), + ); + + const enabled = getLastEnabledArg(); + expect(enabled).toBe(true); + }); }); diff --git a/src/features/app/hooks/useGitPanelController.ts b/src/features/app/hooks/useGitPanelController.ts index 9d1d29f96..ebd82874f 100644 --- a/src/features/app/hooks/useGitPanelController.ts +++ b/src/features/app/hooks/useGitPanelController.ts @@ -9,6 +9,7 @@ export function useGitPanelController({ activeWorkspace, gitDiffPreloadEnabled, gitDiffIgnoreWhitespaceChanges, + splitChatDiffView, isCompact, isTablet, activeTab, @@ -21,6 +22,7 @@ export function useGitPanelController({ activeWorkspace: WorkspaceInfo | null; gitDiffPreloadEnabled: boolean; gitDiffIgnoreWhitespaceChanges: boolean; + splitChatDiffView: boolean; isCompact: boolean; isTablet: boolean; activeTab: "home" | "projects" | "codex" | "git" | "log"; @@ -104,10 +106,13 @@ export function useGitPanelController({ ); const shouldLoadSelectedLocalDiff = centerMode === "diff" && Boolean(selectedDiffPath); + const shouldLoadLocalDiffsForSplitView = splitChatDiffView && diffSource === "local"; const shouldLoadLocalDiffs = Boolean(activeWorkspace) && (shouldPreloadDiffs || - (gitDiffPreloadEnabled ? diffUiVisible : shouldLoadSelectedLocalDiff)); + (gitDiffPreloadEnabled + ? diffUiVisible + : shouldLoadSelectedLocalDiff || shouldLoadLocalDiffsForSplitView)); const shouldLoadDiffs = Boolean(activeWorkspace) && (diffSource === "local" ? shouldLoadLocalDiffs : diffUiVisible);