From dea5079713a0ce41f50f696665a5c364ebc82db4 Mon Sep 17 00:00:00 2001 From: Shantur Rathore Date: Tue, 17 Feb 2026 13:47:07 +0000 Subject: [PATCH] feat(ui): add diff toolbar toggles and word wrap Replace split/unified and context controls with icon toggles, add a word-wrap toggle (default on), and move the toolbar into the tab header to free vertical space. --- .../file-viewer/monaco-diff-viewer.tsx | 17 +++++- .../instance/shell/right-panel/RightPanel.tsx | 15 ++++- .../right-panel/components/DiffToolbar.tsx | 57 +++++++++++-------- .../shell/right-panel/tabs/ChangesTab.tsx | 24 +++++--- .../shell/right-panel/tabs/GitChangesTab.tsx | 22 ++++--- .../instance/shell/right-panel/types.ts | 2 + .../src/components/instance/shell/storage.ts | 1 + packages/ui/src/styles/panels/right-panel.css | 18 +++++- 8 files changed, 112 insertions(+), 44 deletions(-) diff --git a/packages/ui/src/components/file-viewer/monaco-diff-viewer.tsx b/packages/ui/src/components/file-viewer/monaco-diff-viewer.tsx index 6c6b71de..c6694f80 100644 --- a/packages/ui/src/components/file-viewer/monaco-diff-viewer.tsx +++ b/packages/ui/src/components/file-viewer/monaco-diff-viewer.tsx @@ -12,6 +12,7 @@ interface MonacoDiffViewerProps { after: string viewMode?: "split" | "unified" contextMode?: "expanded" | "collapsed" + wordWrap?: "on" | "off" } export function MonacoDiffViewer(props: MonacoDiffViewerProps) { @@ -54,7 +55,7 @@ export function MonacoDiffViewer(props: MonacoDiffViewerProps) { scrollBeyondLastLine: false, renderWhitespace: "selection", fontSize: 13, - wordWrap: "off", + wordWrap: props.wordWrap === "on" ? "on" : "off", glyphMargin: false, folding: false, // Keep enough gutter space so unified diffs don't overlap `+`/`-` markers. @@ -81,6 +82,7 @@ export function MonacoDiffViewer(props: MonacoDiffViewerProps) { if (!ready() || !monaco || !diffEditor) return const viewMode = props.viewMode === "unified" ? "unified" : "split" const contextMode = props.contextMode === "collapsed" ? "collapsed" : "expanded" + const wordWrap = props.wordWrap === "on" ? "on" : "off" diffEditor.updateOptions({ renderSideBySide: viewMode === "split", @@ -89,7 +91,20 @@ export function MonacoDiffViewer(props: MonacoDiffViewerProps) { contextMode === "collapsed" ? { enabled: true } : { enabled: false }, + wordWrap, }) + + try { + diffEditor.getOriginalEditor?.()?.updateOptions?.({ wordWrap }) + } catch { + // ignore + } + + try { + diffEditor.getModifiedEditor?.()?.updateOptions?.({ wordWrap }) + } catch { + // ignore + } }) createEffect(() => { diff --git a/packages/ui/src/components/instance/shell/right-panel/RightPanel.tsx b/packages/ui/src/components/instance/shell/right-panel/RightPanel.tsx index 5e240088..fc0e58aa 100644 --- a/packages/ui/src/components/instance/shell/right-panel/RightPanel.tsx +++ b/packages/ui/src/components/instance/shell/right-panel/RightPanel.tsx @@ -18,7 +18,7 @@ import type { Instance } from "../../../../types/instance" import type { BackgroundProcess } from "../../../../../../server/src/api-types" import type { Session } from "../../../../types/session" import type { DrawerViewState } from "../types" -import type { DiffContextMode, DiffViewMode, RightPanelTab } from "./types" +import type { DiffContextMode, DiffViewMode, DiffWordWrapMode, RightPanelTab } from "./types" import ChangesTab from "./tabs/ChangesTab" import FilesTab from "./tabs/FilesTab" @@ -32,6 +32,7 @@ import { useGlobalPointerDrag } from "../useGlobalPointerDrag" import { RIGHT_PANEL_CHANGES_DIFF_CONTEXT_MODE_KEY, RIGHT_PANEL_CHANGES_DIFF_VIEW_MODE_KEY, + RIGHT_PANEL_CHANGES_DIFF_WORD_WRAP_KEY, RIGHT_PANEL_CHANGES_LIST_OPEN_NONPHONE_KEY, RIGHT_PANEL_CHANGES_LIST_OPEN_PHONE_KEY, RIGHT_PANEL_CHANGES_SPLIT_WIDTH_KEY, @@ -102,6 +103,9 @@ const RightPanel: Component = (props) => { const [diffContextMode, setDiffContextMode] = createSignal( readStoredEnum(RIGHT_PANEL_CHANGES_DIFF_CONTEXT_MODE_KEY, ["expanded", "collapsed"] as const) ?? "collapsed", ) + const [diffWordWrapMode, setDiffWordWrapMode] = createSignal( + readStoredEnum(RIGHT_PANEL_CHANGES_DIFF_WORD_WRAP_KEY, ["on", "off"] as const) ?? "on", + ) const [changesSplitWidth, setChangesSplitWidth] = createSignal(320) const [filesSplitWidth, setFilesSplitWidth] = createSignal(320) @@ -195,6 +199,11 @@ const RightPanel: Component = (props) => { window.localStorage.setItem(RIGHT_PANEL_CHANGES_DIFF_CONTEXT_MODE_KEY, diffContextMode()) }) + createEffect(() => { + if (typeof window === "undefined") return + window.localStorage.setItem(RIGHT_PANEL_CHANGES_DIFF_WORD_WRAP_KEY, diffWordWrapMode()) + }) + const clampSplitWidth = (value: number) => { const min = 200 const maxByDrawer = Math.max(min, Math.floor(props.rightDrawerWidth() * 0.65)) @@ -738,8 +747,10 @@ const RightPanel: Component = (props) => { onSelectFile={handleSelectChangesFile} diffViewMode={diffViewMode} diffContextMode={diffContextMode} + diffWordWrapMode={diffWordWrapMode} onViewModeChange={setDiffViewMode} onContextModeChange={setDiffContextMode} + onWordWrapModeChange={setDiffWordWrapMode} listOpen={changesListOpen} onToggleList={toggleChangesList} splitWidth={changesSplitWidth} @@ -765,8 +776,10 @@ const RightPanel: Component = (props) => { scopeKey={gitScopeKey} diffViewMode={diffViewMode} diffContextMode={diffContextMode} + diffWordWrapMode={diffWordWrapMode} onViewModeChange={setDiffViewMode} onContextModeChange={setDiffContextMode} + onWordWrapModeChange={setDiffWordWrapMode} onOpenFile={(path) => void openGitFile(path)} onRefresh={() => void refreshGitStatus()} listOpen={gitChangesListOpen} diff --git a/packages/ui/src/components/instance/shell/right-panel/components/DiffToolbar.tsx b/packages/ui/src/components/instance/shell/right-panel/components/DiffToolbar.tsx index beb249ee..2e9e980b 100644 --- a/packages/ui/src/components/instance/shell/right-panel/components/DiffToolbar.tsx +++ b/packages/ui/src/components/instance/shell/right-panel/components/DiffToolbar.tsx @@ -1,50 +1,61 @@ import type { Component } from "solid-js" -import type { DiffContextMode, DiffViewMode } from "../types" +import { AlignJustify, FoldVertical, Split, UnfoldVertical, WrapText } from "lucide-solid" + +import type { DiffContextMode, DiffViewMode, DiffWordWrapMode } from "../types" interface DiffToolbarProps { viewMode: DiffViewMode contextMode: DiffContextMode + wordWrapMode: DiffWordWrapMode onViewModeChange: (mode: DiffViewMode) => void onContextModeChange: (mode: DiffContextMode) => void + onWordWrapModeChange: (mode: DiffWordWrapMode) => void } const DiffToolbar: Component = (props) => { + const nextViewMode = (): DiffViewMode => (props.viewMode === "split" ? "unified" : "split") + const nextContextMode = (): DiffContextMode => (props.contextMode === "collapsed" ? "expanded" : "collapsed") + const nextWordWrapMode = (): DiffWordWrapMode => (props.wordWrapMode === "on" ? "off" : "on") + + const viewModeTitle = () => (nextViewMode() === "split" ? "Switch to split view" : "Switch to unified view") + const contextModeTitle = () => + nextContextMode() === "collapsed" ? "Hide unchanged regions" : "Show full file" + const wordWrapTitle = () => (nextWordWrapMode() === "on" ? "Enable word wrap" : "Disable word wrap") + return (
+ -
) diff --git a/packages/ui/src/components/instance/shell/right-panel/tabs/ChangesTab.tsx b/packages/ui/src/components/instance/shell/right-panel/tabs/ChangesTab.tsx index 9821d3c5..a4fcd46a 100644 --- a/packages/ui/src/components/instance/shell/right-panel/tabs/ChangesTab.tsx +++ b/packages/ui/src/components/instance/shell/right-panel/tabs/ChangesTab.tsx @@ -4,7 +4,7 @@ import { MonacoDiffViewer } from "../../../../file-viewer/monaco-diff-viewer" import DiffToolbar from "../components/DiffToolbar" import SplitFilePanel from "../components/SplitFilePanel" -import type { DiffContextMode, DiffViewMode } from "../types" +import type { DiffContextMode, DiffViewMode, DiffWordWrapMode } from "../types" interface ChangesTabProps { t: (key: string, vars?: Record) => string @@ -18,8 +18,10 @@ interface ChangesTabProps { diffViewMode: Accessor diffContextMode: Accessor + diffWordWrapMode: Accessor onViewModeChange: (mode: DiffViewMode) => void onContextModeChange: (mode: DiffContextMode) => void + onWordWrapModeChange: (mode: DiffWordWrapMode) => void listOpen: Accessor onToggleList: () => void @@ -77,14 +79,6 @@ const ChangesTab: Component = (props) => { const renderViewer = () => (
-
- -
0 ? selectedFileData : null} @@ -102,6 +96,7 @@ const ChangesTab: Component = (props) => { after={String((file() as any).after || "")} viewMode={props.diffViewMode()} contextMode={props.diffContextMode()} + wordWrap={props.diffWordWrapMode()} /> )} @@ -182,6 +177,17 @@ const ChangesTab: Component = (props) => { -{totals.deletions}
+ +
+ +
} list={{ panel: renderListPanel, overlay: renderListOverlay }} diff --git a/packages/ui/src/components/instance/shell/right-panel/tabs/GitChangesTab.tsx b/packages/ui/src/components/instance/shell/right-panel/tabs/GitChangesTab.tsx index bd77d7e5..4575f1e5 100644 --- a/packages/ui/src/components/instance/shell/right-panel/tabs/GitChangesTab.tsx +++ b/packages/ui/src/components/instance/shell/right-panel/tabs/GitChangesTab.tsx @@ -7,7 +7,7 @@ import { MonacoDiffViewer } from "../../../../file-viewer/monaco-diff-viewer" import DiffToolbar from "../components/DiffToolbar" import SplitFilePanel from "../components/SplitFilePanel" -import type { DiffContextMode, DiffViewMode } from "../types" +import type { DiffContextMode, DiffViewMode, DiffWordWrapMode } from "../types" interface GitChangesTabProps { t: (key: string, vars?: Record) => string @@ -29,8 +29,10 @@ interface GitChangesTabProps { diffViewMode: Accessor diffContextMode: Accessor + diffWordWrapMode: Accessor onViewModeChange: (mode: DiffViewMode) => void onContextModeChange: (mode: DiffContextMode) => void + onWordWrapModeChange: (mode: DiffWordWrapMode) => void onOpenFile: (path: string) => void onRefresh: () => void @@ -80,14 +82,6 @@ const GitChangesTab: Component = (props) => { const renderViewer = () => (
-
- -
= (props) => { after={String((file() as any).after || "")} viewMode={props.diffViewMode()} contextMode={props.diffContextMode()} + wordWrap={props.diffWordWrapMode()} /> )} @@ -237,6 +232,15 @@ const GitChangesTab: Component = (props) => { > + + } list={{ panel: renderListPanel, overlay: renderListOverlay }} diff --git a/packages/ui/src/components/instance/shell/right-panel/types.ts b/packages/ui/src/components/instance/shell/right-panel/types.ts index e651e528..3273e5ec 100644 --- a/packages/ui/src/components/instance/shell/right-panel/types.ts +++ b/packages/ui/src/components/instance/shell/right-panel/types.ts @@ -3,3 +3,5 @@ export type RightPanelTab = "changes" | "git-changes" | "files" | "status" export type DiffViewMode = "split" | "unified" export type DiffContextMode = "expanded" | "collapsed" + +export type DiffWordWrapMode = "on" | "off" diff --git a/packages/ui/src/components/instance/shell/storage.ts b/packages/ui/src/components/instance/shell/storage.ts index 5f1d7421..b17b6743 100644 --- a/packages/ui/src/components/instance/shell/storage.ts +++ b/packages/ui/src/components/instance/shell/storage.ts @@ -23,6 +23,7 @@ export const RIGHT_PANEL_GIT_CHANGES_LIST_OPEN_NONPHONE_KEY = "opencode-session- export const RIGHT_PANEL_GIT_CHANGES_LIST_OPEN_PHONE_KEY = "opencode-session-right-panel-git-changes-list-open-phone-v1" export const RIGHT_PANEL_CHANGES_DIFF_VIEW_MODE_KEY = "opencode-session-right-panel-changes-diff-view-mode-v1" export const RIGHT_PANEL_CHANGES_DIFF_CONTEXT_MODE_KEY = "opencode-session-right-panel-changes-diff-context-mode-v1" +export const RIGHT_PANEL_CHANGES_DIFF_WORD_WRAP_KEY = "opencode-session-right-panel-changes-diff-word-wrap-v1" export const clampWidth = (value: number) => Math.min(MAX_SESSION_SIDEBAR_WIDTH, Math.max(MIN_SESSION_SIDEBAR_WIDTH, value)) diff --git a/packages/ui/src/styles/panels/right-panel.css b/packages/ui/src/styles/panels/right-panel.css index f2bfb016..edf4f565 100644 --- a/packages/ui/src/styles/panels/right-panel.css +++ b/packages/ui/src/styles/panels/right-panel.css @@ -282,7 +282,7 @@ } .file-viewer-toolbar { - @apply ml-auto flex items-center gap-1; + @apply flex items-center gap-1; } .file-viewer-toolbar-button { @@ -291,6 +291,22 @@ color: var(--text-secondary); } +.file-viewer-toolbar-icon-button { + @apply inline-flex items-center justify-center shrink-0 w-7 h-7 border border-base transition-colors; + background-color: var(--surface-base); + color: var(--text-secondary); +} + +.file-viewer-toolbar-icon-button:hover { + background-color: var(--surface-hover); + color: var(--text-primary); +} + +.file-viewer-toolbar-icon-button.active { + color: var(--text-primary); + box-shadow: inset 0 0 0 1px var(--accent-primary); +} + .file-viewer-toolbar-button:hover { background-color: var(--surface-hover); color: var(--text-primary);