Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src-tauri/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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}"));
Expand Down
2 changes: 2 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,7 @@ function MainApp() {
activeWorkspace,
gitDiffPreloadEnabled: appSettings.preloadGitDiffs,
gitDiffIgnoreWhitespaceChanges: appSettings.gitDiffIgnoreWhitespaceChanges,
splitChatDiffView: appSettings.splitChatDiffView,
isCompact,
isTablet,
activeTab,
Expand Down Expand Up @@ -2326,6 +2327,7 @@ function MainApp() {
tabletTab={tabletTab}
centerMode={centerMode}
preloadGitDiffs={appSettings.preloadGitDiffs}
splitChatDiffView={appSettings.splitChatDiffView}
hasActivePlan={hasActivePlan}
activeWorkspace={Boolean(activeWorkspace)}
sidebarNode={sidebarNode}
Expand Down
3 changes: 3 additions & 0 deletions src/features/app/components/AppLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ type AppLayoutProps = {
tabletTab: "codex" | "git" | "log";
centerMode: "chat" | "diff";
preloadGitDiffs: boolean;
splitChatDiffView: boolean;
hasActivePlan: boolean;
activeWorkspace: boolean;
sidebarNode: ReactNode;
Expand Down Expand Up @@ -48,6 +49,7 @@ export const AppLayout = memo(function AppLayout({
tabletTab,
centerMode,
preloadGitDiffs,
splitChatDiffView,
hasActivePlan,
activeWorkspace,
sidebarNode,
Expand Down Expand Up @@ -134,6 +136,7 @@ export const AppLayout = memo(function AppLayout({
topbarLeftNode={desktopTopbarLeftNode}
centerMode={centerMode}
preloadGitDiffs={preloadGitDiffs}
splitChatDiffView={splitChatDiffView}
messagesNode={messagesNode}
gitDiffViewerNode={gitDiffViewerNode}
gitDiffPanelNode={gitDiffPanelNode}
Expand Down
14 changes: 14 additions & 0 deletions src/features/app/hooks/useGitPanelController.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ function makeProps(overrides?: Partial<Parameters<typeof useGitPanelController>[
activeWorkspace: workspace,
gitDiffPreloadEnabled: false,
gitDiffIgnoreWhitespaceChanges: false,
splitChatDiffView: false,
isCompact: false,
isTablet: false,
activeTab: "codex" as const,
Expand Down Expand Up @@ -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);
});
});
7 changes: 6 additions & 1 deletion src/features/app/hooks/useGitPanelController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export function useGitPanelController({
activeWorkspace,
gitDiffPreloadEnabled,
gitDiffIgnoreWhitespaceChanges,
splitChatDiffView,
isCompact,
isTablet,
activeTab,
Expand All @@ -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";
Expand Down Expand Up @@ -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);
Expand Down
23 changes: 22 additions & 1 deletion src/features/git/components/GitDiffViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 (
<WorkerPoolContextProvider
Expand Down Expand Up @@ -654,7 +667,15 @@ export function GitDiffViewer({
</div>
)}
{!error && !isLoading && !diffs.length && (
<div className="diff-viewer-empty">No changes detected.</div>
<div className="diff-viewer-empty-state" role="status" aria-live="polite">
<div className="diff-viewer-empty-glow" aria-hidden />
<span className="diff-viewer-empty-icon" aria-hidden>
<GitCommitHorizontal size={18} />
</span>
<h3 className="diff-viewer-empty-title">{emptyStateCopy.title}</h3>
<p className="diff-viewer-empty-subtitle">{emptyStateCopy.subtitle}</p>
<p className="diff-viewer-empty-hint">{emptyStateCopy.hint}</p>
</div>
)}
{!error && diffs.length > 0 && (
<div
Expand Down
66 changes: 49 additions & 17 deletions src/features/layout/components/DesktopLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ type DesktopLayoutProps = {
topbarLeftNode: ReactNode;
centerMode: "chat" | "diff";
preloadGitDiffs: boolean;
splitChatDiffView: boolean;
messagesNode: ReactNode;
gitDiffViewerNode: ReactNode;
gitDiffPanelNode: ReactNode;
Expand All @@ -36,6 +37,7 @@ export function DesktopLayout({
topbarLeftNode,
centerMode,
preloadGitDiffs,
splitChatDiffView,
messagesNode,
gitDiffViewerNode,
gitDiffPanelNode,
Expand All @@ -50,12 +52,19 @@ export function DesktopLayout({
}: DesktopLayoutProps) {
const diffLayerRef = useRef<HTMLDivElement | null>(null);
const chatLayerRef = useRef<HTMLDivElement | null>(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");
Expand All @@ -81,7 +90,7 @@ export function DesktopLayout({
) {
activeElement.blur();
}
}, [centerMode]);
}, [centerMode, splitChatDiffView]);

return (
<>
Expand All @@ -103,21 +112,44 @@ export function DesktopLayout({
<>
<MainTopbar leftNode={topbarLeftNode} />
{approvalToastsNode}
<div className="content">
<div
className={`content-layer ${centerMode === "diff" ? "is-active" : "is-hidden"}`}
aria-hidden={centerMode !== "diff"}
ref={diffLayerRef}
>
{shouldRenderDiffViewer ? gitDiffViewerNode : null}
</div>
<div
className={`content-layer ${centerMode === "chat" ? "is-active" : "is-hidden"}`}
aria-hidden={centerMode !== "chat"}
ref={chatLayerRef}
>
{messagesNode}
</div>
<div className={`content${splitChatDiffView ? " content-split" : ""}`}>
{splitChatDiffView ? (
<>
<div
className={`content-layer content-layer-split content-layer-chat${
centerMode === "chat" ? " is-active" : ""
}`}
ref={chatLayerRef}
>
{messagesNode}
</div>
<div
className={`content-layer content-layer-split content-layer-diff${
centerMode === "diff" ? " is-active" : ""
}`}
ref={diffLayerRef}
>
{shouldRenderDiffViewer ? gitDiffViewerNode : null}
</div>
</>
) : (
<>
<div
className={`content-layer ${centerMode === "diff" ? "is-active" : "is-hidden"}`}
aria-hidden={centerMode !== "diff"}
ref={diffLayerRef}
>
{shouldRenderDiffViewer ? gitDiffViewerNode : null}
</div>
<div
className={`content-layer ${centerMode === "chat" ? "is-active" : "is-hidden"}`}
aria-hidden={centerMode !== "chat"}
ref={chatLayerRef}
>
{messagesNode}
</div>
</>
)}
</div>

<div
Expand Down
24 changes: 24 additions & 0 deletions src/features/settings/components/SettingsView.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ const baseSettings: AppSettings = {
codeFontSize: 11,
notificationSoundsEnabled: true,
systemNotificationsEnabled: true,
splitChatDiffView: false,
preloadGitDiffs: true,
gitDiffIgnoreWhitespaceChanges: false,
commitMessagePrompt: DEFAULT_COMMIT_MESSAGE_PROMPT,
Expand Down Expand Up @@ -374,6 +375,29 @@ describe("SettingsView Display", () => {
});
});

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 });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,27 @@ export function SettingsDisplaySection({
<span className="settings-toggle-knob" />
</button>
</div>
<div className="settings-toggle-row">
<div>
<div className="settings-toggle-title">Split chat and diff center panes</div>
<div className="settings-toggle-subtitle">
Show chat and diff side by side instead of swapping between them.
</div>
</div>
<button
type="button"
className={`settings-toggle ${appSettings.splitChatDiffView ? "on" : ""}`}
onClick={() =>
void onUpdateAppSettings({
...appSettings,
splitChatDiffView: !appSettings.splitChatDiffView,
})
}
aria-pressed={appSettings.splitChatDiffView}
>
<span className="settings-toggle-knob" />
</button>
</div>
<div className="settings-toggle-row">
<div>
<div className="settings-toggle-title">Auto-generate new thread titles</div>
Expand Down
1 change: 1 addition & 0 deletions src/features/settings/hooks/useAppSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading