From 5a94761121478236e00e246149c4a1a75091c188 Mon Sep 17 00:00:00 2001 From: imabdulazeez Date: Sun, 21 Jun 2026 16:45:17 +0530 Subject: [PATCH 1/4] add chat code block word wrap setting - adds `chatCodeBlockWordWrap` client setting (default: true) - uses persisted setting as default wrap state in code blocks - adds toggle in general settings panel with reset support --- .../settings/DesktopClientSettings.test.ts | 1 + apps/web/src/components/ChatMarkdown.tsx | 4 ++- .../components/settings/SettingsPanels.tsx | 31 +++++++++++++++++++ packages/contracts/src/settings.ts | 2 ++ 4 files changed, 37 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/settings/DesktopClientSettings.test.ts b/apps/desktop/src/settings/DesktopClientSettings.test.ts index 3584d6a21e4..a9144562aca 100644 --- a/apps/desktop/src/settings/DesktopClientSettings.test.ts +++ b/apps/desktop/src/settings/DesktopClientSettings.test.ts @@ -14,6 +14,7 @@ import * as DesktopClientSettings from "./DesktopClientSettings.ts"; const clientSettings: ClientSettings = { autoOpenPlanSidebar: false, + chatCodeBlockWordWrap: true, confirmThreadArchive: true, confirmThreadDelete: false, dismissedProviderUpdateNotificationKeys: [], diff --git a/apps/web/src/components/ChatMarkdown.tsx b/apps/web/src/components/ChatMarkdown.tsx index 711a545d90a..c72091f083b 100644 --- a/apps/web/src/components/ChatMarkdown.tsx +++ b/apps/web/src/components/ChatMarkdown.tsx @@ -54,6 +54,7 @@ import { resolveDiffThemeName, type DiffThemeName } from "../lib/diffRendering"; import { fnv1a32 } from "../lib/diffRendering"; import { LRUCache } from "../lib/lruCache"; import { useTheme } from "../hooks/useTheme"; +import { useClientSettings } from "../hooks/useSettings"; import { chatMarkdownClipboardPayload, serializeTableElementToCsv, @@ -525,7 +526,8 @@ function MarkdownCodeBlock({ children: ReactNode; }) { const [copied, setCopied] = useState(false); - const [wrapped, setWrapped] = useState(false); + const wrapDefault = useClientSettings((settings) => settings.chatCodeBlockWordWrap); + const [wrapped, setWrapped] = useState(wrapDefault); const copiedTimerRef = useRef | null>(null); const wrapLabel = wrapped ? "Disable line wrap" : "Wrap lines"; const copyLabel = copied ? "Copied" : "Copy code"; diff --git a/apps/web/src/components/settings/SettingsPanels.tsx b/apps/web/src/components/settings/SettingsPanels.tsx index 994cbb08f23..e5754a96a58 100644 --- a/apps/web/src/components/settings/SettingsPanels.tsx +++ b/apps/web/src/components/settings/SettingsPanels.tsx @@ -391,6 +391,9 @@ export function useSettingsRestore(onRestored?: () => void) { ...(settings.sidebarThreadPreviewCount !== DEFAULT_UNIFIED_SETTINGS.sidebarThreadPreviewCount ? ["Visible threads"] : []), + ...(settings.chatCodeBlockWordWrap !== DEFAULT_UNIFIED_SETTINGS.chatCodeBlockWordWrap + ? ["Wrap code blocks"] + : []), ...(settings.diffWordWrap !== DEFAULT_UNIFIED_SETTINGS.diffWordWrap ? ["Diff line wrapping"] : []), @@ -428,6 +431,7 @@ export function useSettingsRestore(onRestored?: () => void) { [ isGitWritingModelDirty, settings.autoOpenPlanSidebar, + settings.chatCodeBlockWordWrap, settings.confirmThreadArchive, settings.confirmThreadDelete, settings.addProjectBaseDirectory, @@ -456,6 +460,7 @@ export function useSettingsRestore(onRestored?: () => void) { setTheme("system"); updateSettings({ timestampFormat: DEFAULT_UNIFIED_SETTINGS.timestampFormat, + chatCodeBlockWordWrap: DEFAULT_UNIFIED_SETTINGS.chatCodeBlockWordWrap, diffWordWrap: DEFAULT_UNIFIED_SETTINGS.diffWordWrap, diffIgnoreWhitespace: DEFAULT_UNIFIED_SETTINGS.diffIgnoreWhitespace, sidebarThreadPreviewCount: DEFAULT_UNIFIED_SETTINGS.sidebarThreadPreviewCount, @@ -593,6 +598,32 @@ export function GeneralSettingsPanel() { } /> + + updateSettings({ + chatCodeBlockWordWrap: DEFAULT_UNIFIED_SETTINGS.chatCodeBlockWordWrap, + }) + } + /> + ) : null + } + control={ + + updateSettings({ chatCodeBlockWordWrap: Boolean(checked) }) + } + aria-label="Wrap code blocks by default" + /> + } + /> + Date: Mon, 22 Jun 2026 11:30:40 +0530 Subject: [PATCH 2/4] rename chatCodeBlockWordWrap to chatWordWrap - extends wrap setting to cover table cells as well as code blocks - updates setting key, UI labels, and reset logic --- .../settings/DesktopClientSettings.test.ts | 2 +- apps/web/src/components/ChatMarkdown.tsx | 5 ++-- .../components/settings/SettingsPanels.tsx | 26 +++++++++---------- packages/contracts/src/settings.ts | 4 +-- 4 files changed, 18 insertions(+), 19 deletions(-) diff --git a/apps/desktop/src/settings/DesktopClientSettings.test.ts b/apps/desktop/src/settings/DesktopClientSettings.test.ts index a9144562aca..e556008a598 100644 --- a/apps/desktop/src/settings/DesktopClientSettings.test.ts +++ b/apps/desktop/src/settings/DesktopClientSettings.test.ts @@ -14,7 +14,7 @@ import * as DesktopClientSettings from "./DesktopClientSettings.ts"; const clientSettings: ClientSettings = { autoOpenPlanSidebar: false, - chatCodeBlockWordWrap: true, + chatWordWrap: true, confirmThreadArchive: true, confirmThreadDelete: false, dismissedProviderUpdateNotificationKeys: [], diff --git a/apps/web/src/components/ChatMarkdown.tsx b/apps/web/src/components/ChatMarkdown.tsx index c72091f083b..ca8add67ca4 100644 --- a/apps/web/src/components/ChatMarkdown.tsx +++ b/apps/web/src/components/ChatMarkdown.tsx @@ -296,7 +296,8 @@ function getHighlighterPromise(language: string): Promise { function MarkdownTable({ children, ...props }: React.ComponentProps<"table">) { const containerRef = useRef(null); const tableRef = useRef(null); - const [expanded, setExpanded] = useState(false); + const wrapDefault = useClientSettings((settings) => settings.chatWordWrap); + const [expanded, setExpanded] = useState(wrapDefault); const [copied, setCopied] = useState(false); const copiedTimerRef = useRef | null>(null); const expandLabel = expanded ? "Collapse table cells" : "Expand table cells"; @@ -526,7 +527,7 @@ function MarkdownCodeBlock({ children: ReactNode; }) { const [copied, setCopied] = useState(false); - const wrapDefault = useClientSettings((settings) => settings.chatCodeBlockWordWrap); + const wrapDefault = useClientSettings((settings) => settings.chatWordWrap); const [wrapped, setWrapped] = useState(wrapDefault); const copiedTimerRef = useRef | null>(null); const wrapLabel = wrapped ? "Disable line wrap" : "Wrap lines"; diff --git a/apps/web/src/components/settings/SettingsPanels.tsx b/apps/web/src/components/settings/SettingsPanels.tsx index e5754a96a58..7edb8ec1de5 100644 --- a/apps/web/src/components/settings/SettingsPanels.tsx +++ b/apps/web/src/components/settings/SettingsPanels.tsx @@ -391,8 +391,8 @@ export function useSettingsRestore(onRestored?: () => void) { ...(settings.sidebarThreadPreviewCount !== DEFAULT_UNIFIED_SETTINGS.sidebarThreadPreviewCount ? ["Visible threads"] : []), - ...(settings.chatCodeBlockWordWrap !== DEFAULT_UNIFIED_SETTINGS.chatCodeBlockWordWrap - ? ["Wrap code blocks"] + ...(settings.chatWordWrap !== DEFAULT_UNIFIED_SETTINGS.chatWordWrap + ? ["Wrap code blocks and tables"] : []), ...(settings.diffWordWrap !== DEFAULT_UNIFIED_SETTINGS.diffWordWrap ? ["Diff line wrapping"] @@ -431,7 +431,7 @@ export function useSettingsRestore(onRestored?: () => void) { [ isGitWritingModelDirty, settings.autoOpenPlanSidebar, - settings.chatCodeBlockWordWrap, + settings.chatWordWrap, settings.confirmThreadArchive, settings.confirmThreadDelete, settings.addProjectBaseDirectory, @@ -460,7 +460,7 @@ export function useSettingsRestore(onRestored?: () => void) { setTheme("system"); updateSettings({ timestampFormat: DEFAULT_UNIFIED_SETTINGS.timestampFormat, - chatCodeBlockWordWrap: DEFAULT_UNIFIED_SETTINGS.chatCodeBlockWordWrap, + chatWordWrap: DEFAULT_UNIFIED_SETTINGS.chatWordWrap, diffWordWrap: DEFAULT_UNIFIED_SETTINGS.diffWordWrap, diffIgnoreWhitespace: DEFAULT_UNIFIED_SETTINGS.diffIgnoreWhitespace, sidebarThreadPreviewCount: DEFAULT_UNIFIED_SETTINGS.sidebarThreadPreviewCount, @@ -599,15 +599,15 @@ export function GeneralSettingsPanel() { /> updateSettings({ - chatCodeBlockWordWrap: DEFAULT_UNIFIED_SETTINGS.chatCodeBlockWordWrap, + chatWordWrap: DEFAULT_UNIFIED_SETTINGS.chatWordWrap, }) } /> @@ -615,11 +615,9 @@ export function GeneralSettingsPanel() { } control={ - updateSettings({ chatCodeBlockWordWrap: Boolean(checked) }) - } - aria-label="Wrap code blocks by default" + checked={settings.chatWordWrap} + onCheckedChange={(checked) => updateSettings({ chatWordWrap: Boolean(checked) })} + aria-label="Wrap code blocks and tables by default" /> } /> diff --git a/packages/contracts/src/settings.ts b/packages/contracts/src/settings.ts index e253ffdd576..6255eee3707 100644 --- a/packages/contracts/src/settings.ts +++ b/packages/contracts/src/settings.ts @@ -41,7 +41,7 @@ export const DEFAULT_SIDEBAR_THREAD_PREVIEW_COUNT: SidebarThreadPreviewCount = 6 export const ClientSettingsSchema = Schema.Struct({ autoOpenPlanSidebar: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), - chatCodeBlockWordWrap: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))), + chatWordWrap: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))), confirmThreadArchive: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), confirmThreadDelete: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))), dismissedProviderUpdateNotificationKeys: Schema.Array(TrimmedNonEmptyString).pipe( @@ -536,7 +536,7 @@ export type ServerSettingsPatch = typeof ServerSettingsPatch.Type; export const ClientSettingsPatch = Schema.Struct({ autoOpenPlanSidebar: Schema.optionalKey(Schema.Boolean), - chatCodeBlockWordWrap: Schema.optionalKey(Schema.Boolean), + chatWordWrap: Schema.optionalKey(Schema.Boolean), confirmThreadArchive: Schema.optionalKey(Schema.Boolean), confirmThreadDelete: Schema.optionalKey(Schema.Boolean), diffIgnoreWhitespace: Schema.optionalKey(Schema.Boolean), From 6684c336b015f369fbb73fa2b24fd60e84105156 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 22 Jun 2026 10:17:00 -0700 Subject: [PATCH 3/4] Refactor state initialization for chat word wrap --- apps/web/src/components/ChatMarkdown.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/web/src/components/ChatMarkdown.tsx b/apps/web/src/components/ChatMarkdown.tsx index ca8add67ca4..3e97ecd8350 100644 --- a/apps/web/src/components/ChatMarkdown.tsx +++ b/apps/web/src/components/ChatMarkdown.tsx @@ -296,8 +296,9 @@ function getHighlighterPromise(language: string): Promise { function MarkdownTable({ children, ...props }: React.ComponentProps<"table">) { const containerRef = useRef(null); const tableRef = useRef(null); - const wrapDefault = useClientSettings((settings) => settings.chatWordWrap); - const [expanded, setExpanded] = useState(wrapDefault); + const [expanded, setExpanded] = useState( + useClientSettings((settings) => settings.chatWordWrap) + ); const [copied, setCopied] = useState(false); const copiedTimerRef = useRef | null>(null); const expandLabel = expanded ? "Collapse table cells" : "Expand table cells"; @@ -527,8 +528,9 @@ function MarkdownCodeBlock({ children: ReactNode; }) { const [copied, setCopied] = useState(false); - const wrapDefault = useClientSettings((settings) => settings.chatWordWrap); - const [wrapped, setWrapped] = useState(wrapDefault); + const [wrapped, setWrapped] = useState( + useClientSettings((settings) => settings.chatWordWrap) + ); const copiedTimerRef = useRef | null>(null); const wrapLabel = wrapped ? "Disable line wrap" : "Wrap lines"; const copyLabel = copied ? "Copied" : "Copy code"; From cf71bb9bb513456804e932fdb3596ab2cb8fbc10 Mon Sep 17 00:00:00 2001 From: imabdulazeez Date: Mon, 22 Jun 2026 23:33:37 +0530 Subject: [PATCH 4/4] merge chatWordWrap and diffWordWrap into wordWrap - single wordWrap setting covers code blocks, tables, diffs, and file previews - obsolete chatWordWrap/diffWordWrap keys are stripped on decode --- .../settings/DesktopClientSettings.test.ts | 3 +- apps/web/src/clientPersistenceStorage.test.ts | 21 ++++++++ apps/web/src/components/ChatMarkdown.tsx | 9 ++-- apps/web/src/components/DiffPanel.tsx | 17 +++--- .../src/components/files/FilePreviewPanel.tsx | 16 ++++-- .../components/settings/SettingsPanels.tsx | 53 ++++--------------- packages/contracts/src/settings.test.ts | 27 +++++++++- packages/contracts/src/settings.ts | 6 +-- 8 files changed, 84 insertions(+), 68 deletions(-) diff --git a/apps/desktop/src/settings/DesktopClientSettings.test.ts b/apps/desktop/src/settings/DesktopClientSettings.test.ts index e556008a598..ea7ec6e1512 100644 --- a/apps/desktop/src/settings/DesktopClientSettings.test.ts +++ b/apps/desktop/src/settings/DesktopClientSettings.test.ts @@ -14,12 +14,10 @@ import * as DesktopClientSettings from "./DesktopClientSettings.ts"; const clientSettings: ClientSettings = { autoOpenPlanSidebar: false, - chatWordWrap: true, confirmThreadArchive: true, confirmThreadDelete: false, dismissedProviderUpdateNotificationKeys: [], diffIgnoreWhitespace: true, - diffWordWrap: true, favorites: [], providerModelPreferences: {}, sidebarProjectGroupingMode: "repository_path", @@ -30,6 +28,7 @@ const clientSettings: ClientSettings = { sidebarThreadSortOrder: "created_at", sidebarThreadPreviewCount: 6, timestampFormat: "24-hour", + wordWrap: true, }; const decodeClientSettingsJson = Schema.decodeEffect(Schema.fromJsonString(ClientSettingsSchema)); diff --git a/apps/web/src/clientPersistenceStorage.test.ts b/apps/web/src/clientPersistenceStorage.test.ts index ec335892bea..8f849a6e7b3 100644 --- a/apps/web/src/clientPersistenceStorage.test.ts +++ b/apps/web/src/clientPersistenceStorage.test.ts @@ -69,4 +69,25 @@ describe("clientPersistenceStorage", () => { }), ); }); + + it("defaults word wrap on and discards obsolete wrapping preferences", async () => { + const testWindow = getTestWindow(); + testWindow.localStorage.setItem( + "t3code:client-settings:v1", + JSON.stringify({ + chatWordWrap: false, + diffWordWrap: false, + }), + ); + const { readBrowserClientSettings } = await import("./clientPersistenceStorage"); + const settings = readBrowserClientSettings(); + + expect(settings).toEqual( + expect.objectContaining({ + wordWrap: true, + }), + ); + expect(settings).not.toHaveProperty("chatWordWrap"); + expect(settings).not.toHaveProperty("diffWordWrap"); + }); }); diff --git a/apps/web/src/components/ChatMarkdown.tsx b/apps/web/src/components/ChatMarkdown.tsx index 3e97ecd8350..47604d23ca2 100644 --- a/apps/web/src/components/ChatMarkdown.tsx +++ b/apps/web/src/components/ChatMarkdown.tsx @@ -296,9 +296,7 @@ function getHighlighterPromise(language: string): Promise { function MarkdownTable({ children, ...props }: React.ComponentProps<"table">) { const containerRef = useRef(null); const tableRef = useRef(null); - const [expanded, setExpanded] = useState( - useClientSettings((settings) => settings.chatWordWrap) - ); + const [expanded, setExpanded] = useState(useClientSettings((settings) => settings.wordWrap)); const [copied, setCopied] = useState(false); const copiedTimerRef = useRef | null>(null); const expandLabel = expanded ? "Collapse table cells" : "Expand table cells"; @@ -528,12 +526,11 @@ function MarkdownCodeBlock({ children: ReactNode; }) { const [copied, setCopied] = useState(false); - const [wrapped, setWrapped] = useState( - useClientSettings((settings) => settings.chatWordWrap) - ); + const [wrapped, setWrapped] = useState(useClientSettings((settings) => settings.wordWrap)); const copiedTimerRef = useRef | null>(null); const wrapLabel = wrapped ? "Disable line wrap" : "Wrap lines"; const copyLabel = copied ? "Copied" : "Copy code"; + const handleCopy = useCallback(() => { if (typeof navigator === "undefined" || navigator.clipboard == null) { return; diff --git a/apps/web/src/components/DiffPanel.tsx b/apps/web/src/components/DiffPanel.tsx index e41ba9c2440..cbcd36ce05e 100644 --- a/apps/web/src/components/DiffPanel.tsx +++ b/apps/web/src/components/DiffPanel.tsx @@ -186,7 +186,7 @@ export default function DiffPanel({ mode = "inline", composerDraftTarget }: Diff const { resolvedTheme } = useTheme(); const settings = useClientSettings(); const [diffRenderMode, setDiffRenderMode] = useState("stacked"); - const [diffWordWrap, setDiffWordWrap] = useState(settings.diffWordWrap); + const [wordWrap, setWordWrap] = useState(settings.wordWrap); const [diffIgnoreWhitespace, setDiffIgnoreWhitespace] = useState(settings.diffIgnoreWhitespace); const [baseRefQuery, setBaseRefQuery] = useState(""); const [collapsedDiffFiles, setCollapsedDiffFiles] = useState(() => ({ @@ -194,6 +194,7 @@ export default function DiffPanel({ mode = "inline", composerDraftTarget }: Diff fileKeys: EMPTY_COLLAPSED_DIFF_FILE_KEYS, })); const codeViewRef = useRef(null); + const routeThreadRef = useParams({ strict: false, select: (params) => resolveThreadRouteRef(params), @@ -695,14 +696,12 @@ export default function DiffPanel({ mode = "inline", composerDraftTarget }: Diff { - setDiffWordWrap(Boolean(pressed)); + setWordWrap(Boolean(pressed)); }} /> } @@ -710,7 +709,7 @@ export default function DiffPanel({ mode = "inline", composerDraftTarget }: Diff - {diffWordWrap ? "Disable line wrapping" : "Enable line wrapping"} + {wordWrap ? "Disable line wrapping" : "Enable line wrapping"} @@ -844,7 +843,7 @@ export default function DiffPanel({ mode = "inline", composerDraftTarget }: Diff options={{ diffStyle: diffRenderMode === "split" ? "split" : "unified", lineDiffType: "none", - overflow: diffWordWrap ? "wrap" : "scroll", + overflow: wordWrap ? "wrap" : "scroll", theme: resolveDiffThemeName(resolvedTheme), themeType: resolvedTheme as DiffThemeType, unsafeCSS: DIFF_PANEL_UNSAFE_CSS, @@ -860,7 +859,7 @@ export default function DiffPanel({ mode = "inline", composerDraftTarget }: Diff
 void;
 }
@@ -296,6 +298,7 @@ function EditableFileSurface({
   contents,
   resolvedTheme,
   revealRequestId,
+  wordWrap,
   onPostRender,
   onPendingChange,
 }: EditableFileSurfaceProps) {
@@ -516,7 +519,7 @@ function EditableFileSurface({
               onGutterUtilityClick: setSelectedRange,
               onLineSelectionChange: setSelectedRange,
               onLineSelectionEnd: handleLineSelectionEnd,
-              overflow: "scroll",
+              overflow: wordWrap ? "wrap" : "scroll",
               theme: resolveDiffThemeName(resolvedTheme),
               themeType: resolvedTheme,
               unsafeCSS: FILE_LINK_REVEAL_UNSAFE_CSS,
@@ -557,7 +560,12 @@ function RenderedMarkdownSurface({
   onPendingChange,
 }: Omit<
   EditableFileSurfaceProps,
-  "resolvedTheme" | "composerDraftTarget" | "revealLine" | "revealRequestId" | "onPostRender"
+  | "resolvedTheme"
+  | "composerDraftTarget"
+  | "revealLine"
+  | "revealRequestId"
+  | "wordWrap"
+  | "onPostRender"
 > & {
   threadRef: ScopedThreadRef;
 }) {
@@ -613,6 +621,7 @@ export default function FilePreviewPanel({
   onPendingChange,
 }: FilePreviewPanelProps) {
   const { resolvedTheme } = useTheme();
+  const wordWrap = useClientSettings((settings) => settings.wordWrap);
   const primaryEnvironmentId = usePrimaryEnvironmentId();
   const environmentHttpBaseUrl = useEnvironmentHttpBaseUrl(environmentId);
   const createAssetUrl = useAtomQueryRunner(assetEnvironment.createUrl, {
@@ -844,7 +853,7 @@ export default function FilePreviewPanel({
                   }}
                   options={{
                     disableFileHeader: true,
-                    overflow: "scroll",
+                    overflow: wordWrap ? "wrap" : "scroll",
                     theme: resolveDiffThemeName(resolvedTheme),
                     themeType: resolvedTheme,
                     unsafeCSS: FILE_LINK_REVEAL_UNSAFE_CSS,
@@ -863,6 +872,7 @@ export default function FilePreviewPanel({
                 contents={file.data.contents}
                 resolvedTheme={resolvedTheme}
                 revealRequestId={revealRequestId}
+                wordWrap={wordWrap}
                 onPostRender={onFilePostRender}
                 onPendingChange={onPendingChange}
               />
diff --git a/apps/web/src/components/settings/SettingsPanels.tsx b/apps/web/src/components/settings/SettingsPanels.tsx
index 7edb8ec1de5..40017d56314 100644
--- a/apps/web/src/components/settings/SettingsPanels.tsx
+++ b/apps/web/src/components/settings/SettingsPanels.tsx
@@ -391,12 +391,7 @@ export function useSettingsRestore(onRestored?: () => void) {
       ...(settings.sidebarThreadPreviewCount !== DEFAULT_UNIFIED_SETTINGS.sidebarThreadPreviewCount
         ? ["Visible threads"]
         : []),
-      ...(settings.chatWordWrap !== DEFAULT_UNIFIED_SETTINGS.chatWordWrap
-        ? ["Wrap code blocks and tables"]
-        : []),
-      ...(settings.diffWordWrap !== DEFAULT_UNIFIED_SETTINGS.diffWordWrap
-        ? ["Diff line wrapping"]
-        : []),
+      ...(settings.wordWrap !== DEFAULT_UNIFIED_SETTINGS.wordWrap ? ["Word wrap"] : []),
       ...(settings.diffIgnoreWhitespace !== DEFAULT_UNIFIED_SETTINGS.diffIgnoreWhitespace
         ? ["Diff whitespace changes"]
         : []),
@@ -431,18 +426,17 @@ export function useSettingsRestore(onRestored?: () => void) {
     [
       isGitWritingModelDirty,
       settings.autoOpenPlanSidebar,
-      settings.chatWordWrap,
       settings.confirmThreadArchive,
       settings.confirmThreadDelete,
       settings.addProjectBaseDirectory,
       settings.defaultThreadEnvMode,
       settings.newWorktreesStartFromOrigin,
       settings.diffIgnoreWhitespace,
-      settings.diffWordWrap,
       settings.automaticGitFetchInterval,
       settings.enableAssistantStreaming,
       settings.sidebarThreadPreviewCount,
       settings.timestampFormat,
+      settings.wordWrap,
       theme,
     ],
   );
@@ -460,8 +454,7 @@ export function useSettingsRestore(onRestored?: () => void) {
     setTheme("system");
     updateSettings({
       timestampFormat: DEFAULT_UNIFIED_SETTINGS.timestampFormat,
-      chatWordWrap: DEFAULT_UNIFIED_SETTINGS.chatWordWrap,
-      diffWordWrap: DEFAULT_UNIFIED_SETTINGS.diffWordWrap,
+      wordWrap: DEFAULT_UNIFIED_SETTINGS.wordWrap,
       diffIgnoreWhitespace: DEFAULT_UNIFIED_SETTINGS.diffIgnoreWhitespace,
       sidebarThreadPreviewCount: DEFAULT_UNIFIED_SETTINGS.sidebarThreadPreviewCount,
       autoOpenPlanSidebar: DEFAULT_UNIFIED_SETTINGS.autoOpenPlanSidebar,
@@ -599,39 +592,15 @@ export function GeneralSettingsPanel() {
         />
 
         
-                  updateSettings({
-                    chatWordWrap: DEFAULT_UNIFIED_SETTINGS.chatWordWrap,
-                  })
-                }
-              />
-            ) : null
-          }
-          control={
-             updateSettings({ chatWordWrap: Boolean(checked) })}
-              aria-label="Wrap code blocks and tables by default"
-            />
-          }
-        />
-
-        
                   updateSettings({
-                    diffWordWrap: DEFAULT_UNIFIED_SETTINGS.diffWordWrap,
+                    wordWrap: DEFAULT_UNIFIED_SETTINGS.wordWrap,
                   })
                 }
               />
@@ -639,9 +608,9 @@ export function GeneralSettingsPanel() {
           }
           control={
              updateSettings({ diffWordWrap: Boolean(checked) })}
-              aria-label="Wrap diff lines by default"
+              checked={settings.wordWrap}
+              onCheckedChange={(checked) => updateSettings({ wordWrap: Boolean(checked) })}
+              aria-label="Wrap code, tables, diffs, and file previews by default"
             />
           }
         />
diff --git a/packages/contracts/src/settings.test.ts b/packages/contracts/src/settings.test.ts
index aba97cbe205..ac2d47ca336 100644
--- a/packages/contracts/src/settings.test.ts
+++ b/packages/contracts/src/settings.test.ts
@@ -2,12 +2,35 @@ import { describe, expect, it } from "vite-plus/test";
 import * as Schema from "effect/Schema";
 
 import { ProviderInstanceId } from "./providerInstance.ts";
-import { DEFAULT_SERVER_SETTINGS, ServerSettings, ServerSettingsPatch } from "./settings.ts";
-
+import {
+  ClientSettingsSchema,
+  DEFAULT_SERVER_SETTINGS,
+  ServerSettings,
+  ServerSettingsPatch,
+} from "./settings.ts";
+
+const decodeClientSettings = Schema.decodeUnknownSync(ClientSettingsSchema);
 const decodeServerSettings = Schema.decodeUnknownSync(ServerSettings);
 const decodeServerSettingsPatch = Schema.decodeUnknownSync(ServerSettingsPatch);
 const encodeServerSettings = Schema.encodeSync(ServerSettings);
 
+describe("ClientSettings word wrap", () => {
+  it("defaults word wrap on", () => {
+    expect(decodeClientSettings({}).wordWrap).toBe(true);
+  });
+
+  it("ignores obsolete wrapping preferences", () => {
+    const decoded = decodeClientSettings({
+      chatWordWrap: false,
+      diffWordWrap: false,
+    });
+
+    expect(decoded.wordWrap).toBe(true);
+    expect(decoded).not.toHaveProperty("chatWordWrap");
+    expect(decoded).not.toHaveProperty("diffWordWrap");
+  });
+});
+
 describe("ServerSettings.providerInstances (slice-2 invariant)", () => {
   it("defaults to an empty record so legacy configs without the key still decode", () => {
     expect(DEFAULT_SERVER_SETTINGS.providerInstances).toEqual({});
diff --git a/packages/contracts/src/settings.ts b/packages/contracts/src/settings.ts
index 6255eee3707..6ccd65533dd 100644
--- a/packages/contracts/src/settings.ts
+++ b/packages/contracts/src/settings.ts
@@ -41,14 +41,12 @@ export const DEFAULT_SIDEBAR_THREAD_PREVIEW_COUNT: SidebarThreadPreviewCount = 6
 
 export const ClientSettingsSchema = Schema.Struct({
   autoOpenPlanSidebar: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),
-  chatWordWrap: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))),
   confirmThreadArchive: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),
   confirmThreadDelete: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))),
   dismissedProviderUpdateNotificationKeys: Schema.Array(TrimmedNonEmptyString).pipe(
     Schema.withDecodingDefault(Effect.succeed([])),
   ),
   diffIgnoreWhitespace: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))),
-  diffWordWrap: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),
   // Model favorites. Historically keyed by provider kind, now
   // widened to `ProviderInstanceId` so users can favorite a specific model
   // on a custom provider instance (e.g. "Codex Personal ยท gpt-5") without
@@ -93,6 +91,7 @@ export const ClientSettingsSchema = Schema.Struct({
   timestampFormat: TimestampFormat.pipe(
     Schema.withDecodingDefault(Effect.succeed(DEFAULT_TIMESTAMP_FORMAT)),
   ),
+  wordWrap: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))),
 });
 export type ClientSettings = typeof ClientSettingsSchema.Type;
 
@@ -536,11 +535,9 @@ export type ServerSettingsPatch = typeof ServerSettingsPatch.Type;
 
 export const ClientSettingsPatch = Schema.Struct({
   autoOpenPlanSidebar: Schema.optionalKey(Schema.Boolean),
-  chatWordWrap: Schema.optionalKey(Schema.Boolean),
   confirmThreadArchive: Schema.optionalKey(Schema.Boolean),
   confirmThreadDelete: Schema.optionalKey(Schema.Boolean),
   diffIgnoreWhitespace: Schema.optionalKey(Schema.Boolean),
-  diffWordWrap: Schema.optionalKey(Schema.Boolean),
   favorites: Schema.optionalKey(
     Schema.Array(
       Schema.Struct({
@@ -570,5 +567,6 @@ export const ClientSettingsPatch = Schema.Struct({
   sidebarThreadSortOrder: Schema.optionalKey(SidebarThreadSortOrder),
   sidebarThreadPreviewCount: Schema.optionalKey(SidebarThreadPreviewCount),
   timestampFormat: Schema.optionalKey(TimestampFormat),
+  wordWrap: Schema.optionalKey(Schema.Boolean),
 });
 export type ClientSettingsPatch = typeof ClientSettingsPatch.Type;