Skip to content

Commit 867be49

Browse files
feat: add optional split chat/diff center layout (default off) (#397)
Co-authored-by: Thomas Ricouard <ricouard77@gmail.com>
1 parent 3f35b0f commit 867be49

13 files changed

Lines changed: 243 additions & 19 deletions

File tree

src-tauri/src/types.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -586,6 +586,11 @@ pub(crate) struct AppSettings {
586586
rename = "notificationSoundsEnabled"
587587
)]
588588
pub(crate) notification_sounds_enabled: bool,
589+
#[serde(
590+
default = "default_split_chat_diff_view",
591+
rename = "splitChatDiffView"
592+
)]
593+
pub(crate) split_chat_diff_view: bool,
589594
#[serde(default = "default_preload_git_diffs", rename = "preloadGitDiffs")]
590595
pub(crate) preload_git_diffs: bool,
591596
#[serde(
@@ -923,6 +928,10 @@ fn default_system_notifications_enabled() -> bool {
923928
true
924929
}
925930

931+
fn default_split_chat_diff_view() -> bool {
932+
false
933+
}
934+
926935
fn default_preload_git_diffs() -> bool {
927936
true
928937
}
@@ -1184,6 +1193,7 @@ impl Default for AppSettings {
11841193
code_font_size: default_code_font_size(),
11851194
notification_sounds_enabled: true,
11861195
system_notifications_enabled: true,
1196+
split_chat_diff_view: default_split_chat_diff_view(),
11871197
preload_git_diffs: default_preload_git_diffs(),
11881198
git_diff_ignore_whitespace_changes: default_git_diff_ignore_whitespace_changes(),
11891199
commit_message_prompt: default_commit_message_prompt(),
@@ -1346,6 +1356,7 @@ mod tests {
13461356
assert_eq!(settings.code_font_size, 11);
13471357
assert!(settings.notification_sounds_enabled);
13481358
assert!(settings.system_notifications_enabled);
1359+
assert!(!settings.split_chat_diff_view);
13491360
assert!(settings.preload_git_diffs);
13501361
assert!(!settings.git_diff_ignore_whitespace_changes);
13511362
assert!(settings.commit_message_prompt.contains("{diff}"));

src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,7 @@ function MainApp() {
399399
activeWorkspace,
400400
gitDiffPreloadEnabled: appSettings.preloadGitDiffs,
401401
gitDiffIgnoreWhitespaceChanges: appSettings.gitDiffIgnoreWhitespaceChanges,
402+
splitChatDiffView: appSettings.splitChatDiffView,
402403
isCompact,
403404
isTablet,
404405
activeTab,
@@ -2326,6 +2327,7 @@ function MainApp() {
23262327
tabletTab={tabletTab}
23272328
centerMode={centerMode}
23282329
preloadGitDiffs={appSettings.preloadGitDiffs}
2330+
splitChatDiffView={appSettings.splitChatDiffView}
23292331
hasActivePlan={hasActivePlan}
23302332
activeWorkspace={Boolean(activeWorkspace)}
23312333
sidebarNode={sidebarNode}

src/features/app/components/AppLayout.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ type AppLayoutProps = {
1212
tabletTab: "codex" | "git" | "log";
1313
centerMode: "chat" | "diff";
1414
preloadGitDiffs: boolean;
15+
splitChatDiffView: boolean;
1516
hasActivePlan: boolean;
1617
activeWorkspace: boolean;
1718
sidebarNode: ReactNode;
@@ -48,6 +49,7 @@ export const AppLayout = memo(function AppLayout({
4849
tabletTab,
4950
centerMode,
5051
preloadGitDiffs,
52+
splitChatDiffView,
5153
hasActivePlan,
5254
activeWorkspace,
5355
sidebarNode,
@@ -134,6 +136,7 @@ export const AppLayout = memo(function AppLayout({
134136
topbarLeftNode={desktopTopbarLeftNode}
135137
centerMode={centerMode}
136138
preloadGitDiffs={preloadGitDiffs}
139+
splitChatDiffView={splitChatDiffView}
137140
messagesNode={messagesNode}
138141
gitDiffViewerNode={gitDiffViewerNode}
139142
gitDiffPanelNode={gitDiffPanelNode}

src/features/app/hooks/useGitPanelController.test.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ function makeProps(overrides?: Partial<Parameters<typeof useGitPanelController>[
3838
activeWorkspace: workspace,
3939
gitDiffPreloadEnabled: false,
4040
gitDiffIgnoreWhitespaceChanges: false,
41+
splitChatDiffView: false,
4142
isCompact: false,
4243
isTablet: false,
4344
activeTab: "codex" as const,
@@ -142,4 +143,17 @@ describe("useGitPanelController preload behavior", () => {
142143
const selectedEnabled = getLastEnabledArg();
143144
expect(selectedEnabled).toBe(true);
144145
});
146+
147+
it("loads local diffs when split view is enabled and preload is disabled", () => {
148+
renderHook(() =>
149+
useGitPanelController(
150+
makeProps({
151+
splitChatDiffView: true,
152+
}),
153+
),
154+
);
155+
156+
const enabled = getLastEnabledArg();
157+
expect(enabled).toBe(true);
158+
});
145159
});

src/features/app/hooks/useGitPanelController.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export function useGitPanelController({
99
activeWorkspace,
1010
gitDiffPreloadEnabled,
1111
gitDiffIgnoreWhitespaceChanges,
12+
splitChatDiffView,
1213
isCompact,
1314
isTablet,
1415
activeTab,
@@ -21,6 +22,7 @@ export function useGitPanelController({
2122
activeWorkspace: WorkspaceInfo | null;
2223
gitDiffPreloadEnabled: boolean;
2324
gitDiffIgnoreWhitespaceChanges: boolean;
25+
splitChatDiffView: boolean;
2426
isCompact: boolean;
2527
isTablet: boolean;
2628
activeTab: "home" | "projects" | "codex" | "git" | "log";
@@ -104,10 +106,13 @@ export function useGitPanelController({
104106
);
105107
const shouldLoadSelectedLocalDiff =
106108
centerMode === "diff" && Boolean(selectedDiffPath);
109+
const shouldLoadLocalDiffsForSplitView = splitChatDiffView && diffSource === "local";
107110
const shouldLoadLocalDiffs =
108111
Boolean(activeWorkspace) &&
109112
(shouldPreloadDiffs ||
110-
(gitDiffPreloadEnabled ? diffUiVisible : shouldLoadSelectedLocalDiff));
113+
(gitDiffPreloadEnabled
114+
? diffUiVisible
115+
: shouldLoadSelectedLocalDiff || shouldLoadLocalDiffsForSplitView));
111116
const shouldLoadDiffs =
112117
Boolean(activeWorkspace) &&
113118
(diffSource === "local" ? shouldLoadLocalDiffs : diffUiVisible);

src/features/git/components/GitDiffViewer.tsx

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { FileDiff, WorkerPoolContextProvider } from "@pierre/diffs/react";
55
import type { FileDiffMetadata } from "@pierre/diffs";
66
import { parsePatchFiles } from "@pierre/diffs";
77
import RotateCcw from "lucide-react/dist/esm/icons/rotate-ccw";
8+
import GitCommitHorizontal from "lucide-react/dist/esm/icons/git-commit-horizontal";
89
import { workerFactory } from "../../../utils/diffsWorker";
910
import type { GitHubPullRequest, GitHubPullRequestComment } from "../../../types";
1011
import { formatRelativeTime } from "../../../utils/time";
@@ -594,6 +595,18 @@ export function GitDiffViewer({
594595
}
595596
rowVirtualizer.scrollToIndex(0, { align: "start" });
596597
}, [diffs.length, rowVirtualizer]);
598+
const emptyStateCopy = pullRequest
599+
? {
600+
title: "No file changes in this pull request",
601+
subtitle:
602+
"The pull request loaded, but there are no diff hunks to render for this selection.",
603+
hint: "Try switching to another pull request or commit from the Git panel.",
604+
}
605+
: {
606+
title: "Working tree is clean",
607+
subtitle: "No local changes were detected for the current workspace.",
608+
hint: "Make an edit, stage a file, or select a commit to inspect changes here.",
609+
};
597610

598611
return (
599612
<WorkerPoolContextProvider
@@ -654,7 +667,15 @@ export function GitDiffViewer({
654667
</div>
655668
)}
656669
{!error && !isLoading && !diffs.length && (
657-
<div className="diff-viewer-empty">No changes detected.</div>
670+
<div className="diff-viewer-empty-state" role="status" aria-live="polite">
671+
<div className="diff-viewer-empty-glow" aria-hidden />
672+
<span className="diff-viewer-empty-icon" aria-hidden>
673+
<GitCommitHorizontal size={18} />
674+
</span>
675+
<h3 className="diff-viewer-empty-title">{emptyStateCopy.title}</h3>
676+
<p className="diff-viewer-empty-subtitle">{emptyStateCopy.subtitle}</p>
677+
<p className="diff-viewer-empty-hint">{emptyStateCopy.hint}</p>
678+
</div>
658679
)}
659680
{!error && diffs.length > 0 && (
660681
<div

src/features/layout/components/DesktopLayout.tsx

Lines changed: 49 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ type DesktopLayoutProps = {
1212
topbarLeftNode: ReactNode;
1313
centerMode: "chat" | "diff";
1414
preloadGitDiffs: boolean;
15+
splitChatDiffView: boolean;
1516
messagesNode: ReactNode;
1617
gitDiffViewerNode: ReactNode;
1718
gitDiffPanelNode: ReactNode;
@@ -36,6 +37,7 @@ export function DesktopLayout({
3637
topbarLeftNode,
3738
centerMode,
3839
preloadGitDiffs,
40+
splitChatDiffView,
3941
messagesNode,
4042
gitDiffViewerNode,
4143
gitDiffPanelNode,
@@ -50,12 +52,19 @@ export function DesktopLayout({
5052
}: DesktopLayoutProps) {
5153
const diffLayerRef = useRef<HTMLDivElement | null>(null);
5254
const chatLayerRef = useRef<HTMLDivElement | null>(null);
53-
const shouldRenderDiffViewer = preloadGitDiffs || centerMode === "diff";
55+
const shouldRenderDiffViewer =
56+
splitChatDiffView || preloadGitDiffs || centerMode === "diff";
5457

5558
useEffect(() => {
5659
const diffLayer = diffLayerRef.current;
5760
const chatLayer = chatLayerRef.current;
5861

62+
if (splitChatDiffView) {
63+
diffLayer?.removeAttribute("inert");
64+
chatLayer?.removeAttribute("inert");
65+
return;
66+
}
67+
5968
if (diffLayer) {
6069
if (centerMode === "diff") {
6170
diffLayer.removeAttribute("inert");
@@ -81,7 +90,7 @@ export function DesktopLayout({
8190
) {
8291
activeElement.blur();
8392
}
84-
}, [centerMode]);
93+
}, [centerMode, splitChatDiffView]);
8594

8695
return (
8796
<>
@@ -103,21 +112,44 @@ export function DesktopLayout({
103112
<>
104113
<MainTopbar leftNode={topbarLeftNode} />
105114
{approvalToastsNode}
106-
<div className="content">
107-
<div
108-
className={`content-layer ${centerMode === "diff" ? "is-active" : "is-hidden"}`}
109-
aria-hidden={centerMode !== "diff"}
110-
ref={diffLayerRef}
111-
>
112-
{shouldRenderDiffViewer ? gitDiffViewerNode : null}
113-
</div>
114-
<div
115-
className={`content-layer ${centerMode === "chat" ? "is-active" : "is-hidden"}`}
116-
aria-hidden={centerMode !== "chat"}
117-
ref={chatLayerRef}
118-
>
119-
{messagesNode}
120-
</div>
115+
<div className={`content${splitChatDiffView ? " content-split" : ""}`}>
116+
{splitChatDiffView ? (
117+
<>
118+
<div
119+
className={`content-layer content-layer-split content-layer-chat${
120+
centerMode === "chat" ? " is-active" : ""
121+
}`}
122+
ref={chatLayerRef}
123+
>
124+
{messagesNode}
125+
</div>
126+
<div
127+
className={`content-layer content-layer-split content-layer-diff${
128+
centerMode === "diff" ? " is-active" : ""
129+
}`}
130+
ref={diffLayerRef}
131+
>
132+
{shouldRenderDiffViewer ? gitDiffViewerNode : null}
133+
</div>
134+
</>
135+
) : (
136+
<>
137+
<div
138+
className={`content-layer ${centerMode === "diff" ? "is-active" : "is-hidden"}`}
139+
aria-hidden={centerMode !== "diff"}
140+
ref={diffLayerRef}
141+
>
142+
{shouldRenderDiffViewer ? gitDiffViewerNode : null}
143+
</div>
144+
<div
145+
className={`content-layer ${centerMode === "chat" ? "is-active" : "is-hidden"}`}
146+
aria-hidden={centerMode !== "chat"}
147+
ref={chatLayerRef}
148+
>
149+
{messagesNode}
150+
</div>
151+
</>
152+
)}
121153
</div>
122154

123155
<div

src/features/settings/components/SettingsView.test.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ const baseSettings: AppSettings = {
6868
codeFontSize: 11,
6969
notificationSoundsEnabled: true,
7070
systemNotificationsEnabled: true,
71+
splitChatDiffView: false,
7172
preloadGitDiffs: true,
7273
gitDiffIgnoreWhitespaceChanges: false,
7374
commitMessagePrompt: DEFAULT_COMMIT_MESSAGE_PROMPT,
@@ -374,6 +375,29 @@ describe("SettingsView Display", () => {
374375
});
375376
});
376377

378+
it("toggles split chat and diff center panes", async () => {
379+
const onUpdateAppSettings = vi.fn().mockResolvedValue(undefined);
380+
renderDisplaySection({ onUpdateAppSettings });
381+
382+
const row = screen
383+
.getByText("Split chat and diff center panes")
384+
.closest(".settings-toggle-row") as HTMLElement | null;
385+
if (!row) {
386+
throw new Error("Expected split center panes row");
387+
}
388+
const toggle = row.querySelector("button.settings-toggle") as HTMLButtonElement | null;
389+
if (!toggle) {
390+
throw new Error("Expected split center panes toggle");
391+
}
392+
fireEvent.click(toggle);
393+
394+
await waitFor(() => {
395+
expect(onUpdateAppSettings).toHaveBeenCalledWith(
396+
expect.objectContaining({ splitChatDiffView: true }),
397+
);
398+
});
399+
});
400+
377401
it("toggles reduce transparency", async () => {
378402
const onToggleTransparency = vi.fn();
379403
renderDisplaySection({ onToggleTransparency, reduceTransparency: false });

src/features/settings/components/sections/SettingsDisplaySection.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,27 @@ export function SettingsDisplaySection({
128128
<span className="settings-toggle-knob" />
129129
</button>
130130
</div>
131+
<div className="settings-toggle-row">
132+
<div>
133+
<div className="settings-toggle-title">Split chat and diff center panes</div>
134+
<div className="settings-toggle-subtitle">
135+
Show chat and diff side by side instead of swapping between them.
136+
</div>
137+
</div>
138+
<button
139+
type="button"
140+
className={`settings-toggle ${appSettings.splitChatDiffView ? "on" : ""}`}
141+
onClick={() =>
142+
void onUpdateAppSettings({
143+
...appSettings,
144+
splitChatDiffView: !appSettings.splitChatDiffView,
145+
})
146+
}
147+
aria-pressed={appSettings.splitChatDiffView}
148+
>
149+
<span className="settings-toggle-knob" />
150+
</button>
151+
</div>
131152
<div className="settings-toggle-row">
132153
<div>
133154
<div className="settings-toggle-title">Auto-generate new thread titles</div>

src/features/settings/hooks/useAppSettings.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ function buildDefaultSettings(): AppSettings {
7272
codeFontSize: CODE_FONT_SIZE_DEFAULT,
7373
notificationSoundsEnabled: true,
7474
systemNotificationsEnabled: true,
75+
splitChatDiffView: false,
7576
preloadGitDiffs: true,
7677
gitDiffIgnoreWhitespaceChanges: false,
7778
commitMessagePrompt: DEFAULT_COMMIT_MESSAGE_PROMPT,

0 commit comments

Comments
 (0)