From 8d11c4b0a1935ccfd8bb2069590ea658f057da03 Mon Sep 17 00:00:00 2001
From: hanafish <1106510024@qq.com>
Date: Thu, 6 Aug 2026 22:14:06 +0800
Subject: [PATCH 1/2] fix(source-control): unify diff views and focus state
---
.../DiffViewConsistency.md | 74 ++++++++++
src/engines/GitWorkflow/GitHubDiff/types.ts | 3 +-
src/features/CodeMirror/Diff/index.tsx | 3 +-
src/features/CodeViewer/GitDiffViewer.tsx | 3 +-
.../SourceControlHeaderContent.test.ts | 82 +++++++++++
.../components/SourceControlHeaderContent.tsx | 44 ++++--
.../content/GitCommitDetailContent/index.tsx | 4 +-
.../content/GitDiffContent/index.tsx | 9 +-
.../AllChangesView.tsx | 7 +-
.../SourceControlMainContent/FocusView.tsx | 15 +-
.../SourceControlMainContent/index.tsx | 1 -
.../content/SourceControlMainPane.tsx | 7 +-
.../__tests__/FocusView.emptyState.test.ts | 44 ++++++
.../content/__tests__/TEST_CASES.md | 33 +++--
.../__tests__/sourceControlMainProps.test.ts | 54 +++++++
.../content/sourceControlMainProps.ts | 74 +++++++++-
.../hooks/useSourceControlPaneActions.ts | 30 +---
.../Panels/EditorMainPane/index.tsx | 10 +-
.../CodeEditor/Panels/EditorMainPane/types.ts | 2 +
.../detail/PrChangesTab.tsx | 4 +-
.../CodeEditor/__tests__/TEST_CASES.md | 11 +-
.../sourceControlStateTransitions.test.ts | 136 ++++++++++++++++++
src/modules/WorkStation/CodeEditor/index.tsx | 3 +
.../sourceControlStateTransitions.ts | 63 ++++++++
.../CodeEditor/useSourceControlSetup.ts | 21 ++-
.../diffSessionReplay.useDetailContent.tsx | 5 +
.../WorkStation/Diff/SessionReplay/index.tsx | 32 ++++-
.../shared/DiffFileSection/index.tsx | 5 +-
.../shared/DiffSectionList/index.tsx | 4 +
.../shared/components/FileHeader/index.tsx | 3 +-
.../codeEditor/diffViewModeAtom.test.ts | 42 ++++++
.../codeEditor/diffViewModeAtom.ts | 28 ++++
src/store/workstation/codeEditor/index.ts | 3 +
src/types/git/types.ts | 3 +
34 files changed, 771 insertions(+), 91 deletions(-)
create mode 100644 docs/frontend-ui-audit-2026-08-05/DiffViewConsistency.md
create mode 100644 src/modules/WorkStation/CodeEditor/Panels/EditorMainPane/components/SourceControlHeaderContent.test.ts
create mode 100644 src/modules/WorkStation/CodeEditor/Panels/EditorMainPane/content/__tests__/FocusView.emptyState.test.ts
create mode 100644 src/modules/WorkStation/CodeEditor/__tests__/sourceControlStateTransitions.test.ts
create mode 100644 src/modules/WorkStation/CodeEditor/sourceControlStateTransitions.ts
create mode 100644 src/store/workstation/codeEditor/diffViewModeAtom.test.ts
create mode 100644 src/store/workstation/codeEditor/diffViewModeAtom.ts
diff --git a/docs/frontend-ui-audit-2026-08-05/DiffViewConsistency.md b/docs/frontend-ui-audit-2026-08-05/DiffViewConsistency.md
new file mode 100644
index 000000000..508eaa580
--- /dev/null
+++ b/docs/frontend-ui-audit-2026-08-05/DiffViewConsistency.md
@@ -0,0 +1,74 @@
+# Frontend UI Audit: Diff View Consistency
+
+**Files:**
+
+- `src/modules/WorkStation/CodeEditor/Panels/EditorMainPane/components/SourceControlHeaderContent.tsx`
+- `src/modules/WorkStation/CodeEditor/Panels/EditorMainPane/index.tsx`
+- `src/modules/WorkStation/CodeEditor/Panels/EditorMainPane/content/SourceControlMainPane.tsx`
+- `src/modules/WorkStation/CodeEditor/Panels/EditorMainPane/content/SourceControlMainContent/index.tsx`
+- `src/modules/WorkStation/CodeEditor/Panels/EditorMainPane/content/SourceControlMainContent/FocusView.tsx`
+- `src/modules/WorkStation/CodeEditor/Panels/EditorMainPane/content/SourceControlMainContent/AllChangesView.tsx`
+- `src/modules/WorkStation/CodeEditor/Panels/EditorMainPane/content/GitDiffContent/index.tsx`
+- `src/modules/WorkStation/CodeEditor/Panels/EditorMainPane/content/GitCommitDetailContent/index.tsx`
+- `src/modules/WorkStation/CodeEditor/Panels/EditorPrimarySidebar/content/PullRequestContent/detail/PrChangesTab.tsx`
+- `src/modules/WorkStation/Diff/SessionReplay/index.tsx`
+- `src/modules/WorkStation/Diff/SessionReplay/diffSessionReplay.useDetailContent.tsx`
+- `src/modules/WorkStation/shared/DiffFileSection/index.tsx`
+- `src/modules/WorkStation/shared/DiffSectionList/index.tsx`
+- `src/modules/shared/components/FileHeader/index.tsx`
+- `src/features/CodeMirror/Diff/index.tsx`
+- `src/features/CodeViewer/GitDiffViewer.tsx`
+
+**Date:** 2026-08-05
+
+**Auditor:** Codex
+
+## D1 — Design-system component usage
+
+| Line | Element | Verdict | Reason | Suggested change |
+| -------------------------------------- | ------------------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
+| `SourceControlHeaderContent.tsx:217` | Unified / split selector | keep with reason | Uses the existing `TabPill` design-system control, matching the file diff header interaction. | None. |
+| `SessionReplay/index.tsx:269` | Unified / split selector | keep with reason | Uses the same `TabPill` control and shared preference as the other diff surfaces. | None. |
+| `GitCommitDetailContent/index.tsx:377` | Collapsed file-list rail button | keep with reason | The control occupies a full-height 24 px rail; the current design-system `Button` variants do not model that geometry. | Keep the raw semantic button until the rail pattern is abstracted. |
+| `PrChangesTab.tsx:214` | Collapsed file-list rail button | keep with reason | Same full-height rail geometry as commit details. | Keep the raw semantic button until the rail pattern is abstracted. |
+| `DiffFileSection/index.tsx:418` | Sticky expandable file header | keep with reason | It is a full-width, multiline disclosure row rather than a standard action button. | Continue using a semantic button; consider a dedicated diff-section header only if the pattern gains more consumers. |
+| `FocusView.tsx:51` | No-file Focus empty state | keep with reason | Reuses the workstation `NoTabsPlaceholder` with a focused instruction instead of assembling a bespoke empty state. | None. |
+
+## D2 — Color and token usage
+
+| Line | Element | Verdict | Reason | Suggested change |
+| ---- | ----------------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------- | ---------------- |
+| — | Audited visible changes | keep with reason | New controls use existing semantic border, fill, and text tokens; no raw colors or duplicated token expressions were introduced. | None. |
+
+## D3 — Spacing and typography
+
+| Line | Element | Verdict | Reason | Suggested change |
+| ---------------------------------------- | --------------------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------- |
+| `DiffSectionList/index.tsx:51` | Virtual-list footer buffer `h-[100px]` | keep with reason | This is an exact scroll affordance rather than visual component sizing, and the spacing scale has no exact 100 px token. | None. |
+| `DiffFileSection/index.tsx:321` | Loading placeholder `h-[480px] min-h-[320px]` | fix | Both values have exact Tailwind scale equivalents and are unrelated to the current diff-mode behavior. | In a separate visual-cleanup sweep, use `h-120 min-h-80` and visually verify loading layout. |
+| `DiffFileSection/index.tsx:424` | Chevron reservation `w-[14px]` | keep with reason | The width deliberately matches the 14 px icon and has no exact standard spacing class. | None. |
+| `DiffFileSection/index.tsx:436-463` | Dense diff metadata typography | keep with reason | The 13 px filename and 11 px metadata match the established dense diff header hierarchy; the standard text scale has no exact equivalents. | Keep until diff typography is tokenized system-wide. |
+| `SourceControlHeaderContent.tsx:119-123` | Dense source-control header typography | keep with reason | The 11 px prefix and 13 px title preserve the existing editor-header hierarchy and were not introduced by this change. | Keep until editor header typography is tokenized system-wide. |
+
+## D4 — Accessibility basics
+
+| Line | Element | Verdict | Reason | Suggested change |
+| -------------------------------------- | ---------------------------------- | ---------------- | ----------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
+| `SourceControlHeaderContent.tsx:217` | Unified / split selector | keep with reason | Both options have visible translated labels and expose button semantics through `TabPill`. | None. |
+| `SessionReplay/index.tsx:269` | Unified / split selector | keep with reason | Both options have visible translated labels and expose button semantics through `TabPill`. | None. |
+| `GitCommitDetailContent/index.tsx:377` | Icon-only collapsed file-list rail | fix | It has a tooltip title but no explicit accessible label. | Add the translated “show file list” string as `aria-label` in a coordinated rail-control sweep. |
+| `PrChangesTab.tsx:214` | Icon-only collapsed file-list rail | fix | It has a tooltip title but no explicit accessible label. | Add the translated “show file list” string as `aria-label` in the same sweep. |
+| `DiffFileSection/index.tsx:418` | Expandable file header | keep with reason | The visible filename supplies an accessible name and `aria-expanded` communicates disclosure state. | None. |
+| `FocusView.tsx:51` | No-file Focus empty state | keep with reason | The translated caption explicitly tells the user to select a changed file and exposes no unrelated actions. | None. |
+
+## D5 — Duplication and abstraction candidates
+
+- **Abstract candidate:** the collapsed 24 px file-list rail appears in commit details, pull-request changes, and `GitFileDiffSplit`. A small shared rail-control component could centralize geometry, tooltip, and accessible naming. This is a multi-file sweep candidate and is intentionally not mixed into the diff-view consistency fix.
+- The new unified / split selectors intentionally reuse `TabPill` and one shared persisted atom; no new visual-pattern duplication was introduced.
+
+## Summary
+
+- **Fix candidates:** 3 (two rail-control accessible labels; one standardizable loading height)
+- **Keep with reason:** 12
+- **Abstract candidates:** 1
+- **Current change verdict:** the new controls follow the design system and introduce no new arbitrary colors, spacing, or inaccessible icon-only actions.
diff --git a/src/engines/GitWorkflow/GitHubDiff/types.ts b/src/engines/GitWorkflow/GitHubDiff/types.ts
index 52017e746..5c699e16a 100644
--- a/src/engines/GitWorkflow/GitHubDiff/types.ts
+++ b/src/engines/GitWorkflow/GitHubDiff/types.ts
@@ -3,6 +3,7 @@
*
* Type definitions for the diff viewer architecture.
*/
+import type { DiffViewMode } from "@src/types/git/types";
// ============================================
// Diff Line Types
@@ -98,7 +99,7 @@ export interface FileDiff {
// ============================================
/** The view mode for the diff */
-export type DiffViewMode = "unified" | "split";
+export type { DiffViewMode } from "@src/types/git/types";
/** Expansion type for hunks */
export type DiffHunkExpansionType = "up" | "down" | "short" | "full";
diff --git a/src/features/CodeMirror/Diff/index.tsx b/src/features/CodeMirror/Diff/index.tsx
index 626d57cc8..9d02fbd6e 100644
--- a/src/features/CodeMirror/Diff/index.tsx
+++ b/src/features/CodeMirror/Diff/index.tsx
@@ -31,6 +31,7 @@ import type { GitFileStatus } from "@src/config/gitStatus";
import { createLogger } from "@src/hooks/logger";
import { useEditorAppearanceSettings } from "@src/hooks/settings";
import { EditorService } from "@src/services/workStation/EditorService";
+import type { DiffViewMode } from "@src/types/git/types";
import { useSelectionExtension } from "../Editor/hooks/useSelectionExtension";
import type { TextSelectionInfo } from "../Editor/types";
@@ -67,7 +68,7 @@ export interface CodeMirrorDiffProps {
/** Container height */
height?: string;
/** Diff view mode: unified (inline) or split (side-by-side) */
- viewMode?: "unified" | "split";
+ viewMode?: DiffViewMode;
/** Read-only mode */
readOnly?: boolean;
/** Show merge controls (accept/reject buttons) */
diff --git a/src/features/CodeViewer/GitDiffViewer.tsx b/src/features/CodeViewer/GitDiffViewer.tsx
index d2f77b0f8..f95b651f0 100644
--- a/src/features/CodeViewer/GitDiffViewer.tsx
+++ b/src/features/CodeViewer/GitDiffViewer.tsx
@@ -7,6 +7,7 @@
import React, { useState } from "react";
import { Placeholder } from "@src/modules/shared/layouts/blocks";
+import type { DiffViewMode } from "@src/types/git/types";
import { ModernSplitDiff } from "./ModernSplitDiff";
import { VirtualizedModernDiff } from "./VirtualizedModernDiff";
@@ -15,7 +16,7 @@ import { VirtualizedModernDiff } from "./VirtualizedModernDiff";
// Types
// ============================================
-export type DiffViewMode = "unified" | "split";
+export type { DiffViewMode } from "@src/types/git/types";
export interface GitDiffViewerProps {
/** Original/old content */
diff --git a/src/modules/WorkStation/CodeEditor/Panels/EditorMainPane/components/SourceControlHeaderContent.test.ts b/src/modules/WorkStation/CodeEditor/Panels/EditorMainPane/components/SourceControlHeaderContent.test.ts
new file mode 100644
index 000000000..34dfb6b28
--- /dev/null
+++ b/src/modules/WorkStation/CodeEditor/Panels/EditorMainPane/components/SourceControlHeaderContent.test.ts
@@ -0,0 +1,82 @@
+import type { TFunction } from "i18next";
+import { createElement } from "react";
+import { renderToStaticMarkup } from "react-dom/server";
+import { describe, expect, it, vi } from "vitest";
+
+import type { WorkStationTab } from "@src/store/workstation/tabs";
+
+import { SourceControlHeaderContent } from "./SourceControlHeaderContent";
+
+vi.mock("@src/components/Button", () => ({
+ default: ({ title }: { title?: string }) =>
+ createElement("button", { "data-title": title }),
+}));
+
+vi.mock("@src/components/TabPill", () => ({
+ default: ({
+ activeTab,
+ tabs,
+ }: {
+ activeTab: string;
+ tabs: Array<{ key: string }>;
+ }) =>
+ createElement("div", {
+ "data-active-tab": activeTab,
+ "data-tabs": tabs.map((tab) => tab.key).join(","),
+ }),
+}));
+
+const t = ((key: string) => key) as TFunction;
+
+function sourceControlTab(mode: "focus" | "all-changes"): WorkStationTab {
+ return {
+ id: "source-control:changes",
+ type: "source-control",
+ title: "Review",
+ data: {
+ mode,
+ staged: false,
+ fileCount: 1,
+ focusPath: null,
+ historySelection: null,
+ },
+ } as WorkStationTab;
+}
+
+function renderHeader(mode: "focus" | "all-changes"): string {
+ return renderToStaticMarkup(
+ createElement(SourceControlHeaderContent, {
+ activeTab: sourceControlTab(mode),
+ sourceControlFilterMode: "uncommitted",
+ showSourceControlModePill: true,
+ gitReviewNavigationTotal: 0,
+ selectedIssue: null,
+ sourceControlRefreshSpinClass: undefined,
+ diffViewMode: "split",
+ t,
+ onDiffViewModeChange: vi.fn(),
+ onModeChange: vi.fn(),
+ onOpenHistoryInNewTab: vi.fn(),
+ onReviewPrevFile: vi.fn(),
+ onReviewNextFile: vi.fn(),
+ onCollapseAll: vi.fn(),
+ onRefresh: vi.fn(),
+ })
+ );
+}
+
+describe("SourceControlHeaderContent diff view controls", () => {
+ it("shows the shared unified/split control in All Changes", () => {
+ const markup = renderHeader("all-changes");
+
+ expect(markup).toContain('data-active-tab="split"');
+ expect(markup).toContain('data-tabs="unified,split"');
+ });
+
+ it("keeps the aggregate diff control out of Focus mode", () => {
+ const markup = renderHeader("focus");
+
+ expect(markup).not.toContain('data-tabs="unified,split"');
+ expect(markup).toContain('data-tabs="focus,all-changes"');
+ });
+});
diff --git a/src/modules/WorkStation/CodeEditor/Panels/EditorMainPane/components/SourceControlHeaderContent.tsx b/src/modules/WorkStation/CodeEditor/Panels/EditorMainPane/components/SourceControlHeaderContent.tsx
index c1f2fb905..3fcce3b50 100644
--- a/src/modules/WorkStation/CodeEditor/Panels/EditorMainPane/components/SourceControlHeaderContent.tsx
+++ b/src/modules/WorkStation/CodeEditor/Panels/EditorMainPane/components/SourceControlHeaderContent.tsx
@@ -28,6 +28,7 @@ import type {
SourceControlHistorySelection,
WorkStationTab,
} from "@src/store/workstation/tabs";
+import type { DiffViewMode } from "@src/types/git/types";
export interface SourceControlHeaderContentProps {
/** The active `source-control` tab (host guarantees the type). */
@@ -40,7 +41,9 @@ export interface SourceControlHeaderContentProps {
sourceControlHeaderLeadingSlot?: ReactNode;
sourceControlHeaderTrailingSlot?: ReactNode;
sourceControlRefreshSpinClass: string | undefined;
+ diffViewMode: DiffViewMode;
t: TFunction;
+ onDiffViewModeChange: (mode: DiffViewMode) => void;
onModeChange: (mode: "focus" | "all-changes") => void;
onOpenHistoryInNewTab: (selection: SourceControlHistorySelection) => void;
onReviewPrevFile: () => void;
@@ -60,7 +63,9 @@ export const SourceControlHeaderContent: React.FC<
sourceControlHeaderLeadingSlot,
sourceControlHeaderTrailingSlot,
sourceControlRefreshSpinClass,
+ diffViewMode,
t,
+ onDiffViewModeChange,
onModeChange,
onOpenHistoryInNewTab,
onReviewPrevFile,
@@ -197,16 +202,35 @@ export const SourceControlHeaderContent: React.FC<
)}
{showCollapseAll && (
- }
- />
+ <>
+ onDiffViewModeChange(key as DiffViewMode)}
+ variant="pill"
+ color="fill"
+ fillWidth={false}
+ size="small"
+ />
+
+ }
+ />
+ >
)}