authorgravatar for abdulazeez44@gmail.comAbdul Azeez <abdulazeez44@gmail.com> 2026-06-23 01:44:24+05:30
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-22 13:14:24-07:00
logfb1034546f989c11568ef1c4b5801e6a5372a07d
tree21d773330ffc2263970d6b76bfb94adf22875030
parentf5f98cf0ae48a14b524480a0942f9ce37190656c
signature Signed by PGP key B5690EEEBB952194

feat: add persistent word-wrap setting for chat code blocks and tables (#3480)

Co-authored-by: Julius Marminge <jmarminge@gmail.com>

8 files changed, 85 insertions(+), 32 deletions(-)

apps/desktop/src/settings/DesktopClientSettings.test.ts+1-1
...@@ -18,7 +18,6 @@ const clientSettings: ClientSettings = {...@@ -18,7 +18,6 @@ const clientSettings: ClientSettings = {
18 confirmThreadDelete: false,18 confirmThreadDelete: false,
19 dismissedProviderUpdateNotificationKeys: [],19 dismissedProviderUpdateNotificationKeys: [],
20 diffIgnoreWhitespace: true,20 diffIgnoreWhitespace: true,
21 diffWordWrap: true,
22 favorites: [],21 favorites: [],
23 providerModelPreferences: {},22 providerModelPreferences: {},
24 sidebarProjectGroupingMode: "repository_path",23 sidebarProjectGroupingMode: "repository_path",
...@@ -29,6 +28,7 @@ const clientSettings: ClientSettings = {...@@ -29,6 +28,7 @@ const clientSettings: ClientSettings = {
29 sidebarThreadSortOrder: "created_at",28 sidebarThreadSortOrder: "created_at",
30 sidebarThreadPreviewCount: 6,29 sidebarThreadPreviewCount: 6,
31 timestampFormat: "24-hour",30 timestampFormat: "24-hour",
31 wordWrap: true,
32};32};
3333
34const decodeClientSettingsJson = Schema.decodeEffect(Schema.fromJsonString(ClientSettingsSchema));34const decodeClientSettingsJson = Schema.decodeEffect(Schema.fromJsonString(ClientSettingsSchema));
apps/web/src/clientPersistenceStorage.test.ts+21
...@@ -69,4 +69,25 @@ describe("clientPersistenceStorage", () => {...@@ -69,4 +69,25 @@ describe("clientPersistenceStorage", () => {
69 }),69 }),
70 );70 );
71 });71 });
72
73 it("defaults word wrap on and discards obsolete wrapping preferences", async () => {
74 const testWindow = getTestWindow();
75 testWindow.localStorage.setItem(
76 "t3code:client-settings:v1",
77 JSON.stringify({
78 chatWordWrap: false,
79 diffWordWrap: false,
80 }),
81 );
82 const { readBrowserClientSettings } = await import("./clientPersistenceStorage");
83 const settings = readBrowserClientSettings();
84
85 expect(settings).toEqual(
86 expect.objectContaining({
87 wordWrap: true,
88 }),
89 );
90 expect(settings).not.toHaveProperty("chatWordWrap");
91 expect(settings).not.toHaveProperty("diffWordWrap");
92 });
72});93});
apps/web/src/components/ChatMarkdown.tsx+4-2
...@@ -54,6 +54,7 @@ import { resolveDiffThemeName, type DiffThemeName } from "../lib/diffRendering";...@@ -54,6 +54,7 @@ import { resolveDiffThemeName, type DiffThemeName } from "../lib/diffRendering";
54import { fnv1a32 } from "../lib/diffRendering";54import { fnv1a32 } from "../lib/diffRendering";
55import { LRUCache } from "../lib/lruCache";55import { LRUCache } from "../lib/lruCache";
56import { useTheme } from "../hooks/useTheme";56import { useTheme } from "../hooks/useTheme";
57import { useClientSettings } from "../hooks/useSettings";
57import {58import {
58 chatMarkdownClipboardPayload,59 chatMarkdownClipboardPayload,
59 serializeTableElementToCsv,60 serializeTableElementToCsv,
...@@ -295,7 +296,7 @@ function getHighlighterPromise(language: string): Promise<DiffsHighlighter> {...@@ -295,7 +296,7 @@ function getHighlighterPromise(language: string): Promise<DiffsHighlighter> {
295function MarkdownTable({ children, ...props }: React.ComponentProps<"table">) {296function MarkdownTable({ children, ...props }: React.ComponentProps<"table">) {
296 const containerRef = useRef<HTMLDivElement | null>(null);297 const containerRef = useRef<HTMLDivElement | null>(null);
297 const tableRef = useRef<HTMLTableElement | null>(null);298 const tableRef = useRef<HTMLTableElement | null>(null);
298 const [expanded, setExpanded] = useState(false);299 const [expanded, setExpanded] = useState(useClientSettings((settings) => settings.wordWrap));
299 const [copied, setCopied] = useState(false);300 const [copied, setCopied] = useState(false);
300 const copiedTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);301 const copiedTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
301 const expandLabel = expanded ? "Collapse table cells" : "Expand table cells";302 const expandLabel = expanded ? "Collapse table cells" : "Expand table cells";
...@@ -525,10 +526,11 @@ function MarkdownCodeBlock({...@@ -525,10 +526,11 @@ function MarkdownCodeBlock({
525 children: ReactNode;526 children: ReactNode;
526}) {527}) {
527 const [copied, setCopied] = useState(false);528 const [copied, setCopied] = useState(false);
528 const [wrapped, setWrapped] = useState(false);529 const [wrapped, setWrapped] = useState(useClientSettings((settings) => settings.wordWrap));
529 const copiedTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);530 const copiedTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
530 const wrapLabel = wrapped ? "Disable line wrap" : "Wrap lines";531 const wrapLabel = wrapped ? "Disable line wrap" : "Wrap lines";
531 const copyLabel = copied ? "Copied" : "Copy code";532 const copyLabel = copied ? "Copied" : "Copy code";
533
532 const handleCopy = useCallback(() => {534 const handleCopy = useCallback(() => {
533 if (typeof navigator === "undefined" || navigator.clipboard == null) {535 if (typeof navigator === "undefined" || navigator.clipboard == null) {
534 return;536 return;
apps/web/src/components/DiffPanel.tsx+8-9
...@@ -186,7 +186,7 @@ export default function DiffPanel({ mode = "inline", composerDraftTarget }: Diff...@@ -186,7 +186,7 @@ export default function DiffPanel({ mode = "inline", composerDraftTarget }: Diff
186 const { resolvedTheme } = useTheme();186 const { resolvedTheme } = useTheme();
187 const settings = useClientSettings();187 const settings = useClientSettings();
188 const [diffRenderMode, setDiffRenderMode] = useState<DiffRenderMode>("stacked");188 const [diffRenderMode, setDiffRenderMode] = useState<DiffRenderMode>("stacked");
189 const [diffWordWrap, setDiffWordWrap] = useState(settings.diffWordWrap);189 const [wordWrap, setWordWrap] = useState(settings.wordWrap);
190 const [diffIgnoreWhitespace, setDiffIgnoreWhitespace] = useState(settings.diffIgnoreWhitespace);190 const [diffIgnoreWhitespace, setDiffIgnoreWhitespace] = useState(settings.diffIgnoreWhitespace);
191 const [baseRefQuery, setBaseRefQuery] = useState("");191 const [baseRefQuery, setBaseRefQuery] = useState("");
192 const [collapsedDiffFiles, setCollapsedDiffFiles] = useState<CollapsedDiffFilesState>(() => ({192 const [collapsedDiffFiles, setCollapsedDiffFiles] = useState<CollapsedDiffFilesState>(() => ({
...@@ -194,6 +194,7 @@ export default function DiffPanel({ mode = "inline", composerDraftTarget }: Diff...@@ -194,6 +194,7 @@ export default function DiffPanel({ mode = "inline", composerDraftTarget }: Diff
194 fileKeys: EMPTY_COLLAPSED_DIFF_FILE_KEYS,194 fileKeys: EMPTY_COLLAPSED_DIFF_FILE_KEYS,
195 }));195 }));
196 const codeViewRef = useRef<AnnotatableCodeViewHandle>(null);196 const codeViewRef = useRef<AnnotatableCodeViewHandle>(null);
197
197 const routeThreadRef = useParams({198 const routeThreadRef = useParams({
198 strict: false,199 strict: false,
199 select: (params) => resolveThreadRouteRef(params),200 select: (params) => resolveThreadRouteRef(params),
...@@ -695,14 +696,12 @@ export default function DiffPanel({ mode = "inline", composerDraftTarget }: Diff...@@ -695,14 +696,12 @@ export default function DiffPanel({ mode = "inline", composerDraftTarget }: Diff
695 <TooltipTrigger696 <TooltipTrigger
696 render={697 render={
697 <Toggle698 <Toggle
698 aria-label={699 aria-label={wordWrap ? "Disable diff line wrapping" : "Enable diff line wrapping"}
699 diffWordWrap ? "Disable diff line wrapping" : "Enable diff line wrapping"
700 }
701 variant="outline"700 variant="outline"
702 size="xs"701 size="xs"
703 pressed={diffWordWrap}702 pressed={wordWrap}
704 onPressedChange={(pressed) => {703 onPressedChange={(pressed) => {
705 setDiffWordWrap(Boolean(pressed));704 setWordWrap(Boolean(pressed));
706 }}705 }}
707 />706 />
708 }707 }
...@@ -710,7 +709,7 @@ export default function DiffPanel({ mode = "inline", composerDraftTarget }: Diff...@@ -710,7 +709,7 @@ export default function DiffPanel({ mode = "inline", composerDraftTarget }: Diff
710 <TextWrapIcon className="size-3" />709 <TextWrapIcon className="size-3" />
711 </TooltipTrigger>710 </TooltipTrigger>
712 <TooltipPopup side="top">711 <TooltipPopup side="top">
713 {diffWordWrap ? "Disable line wrapping" : "Enable line wrapping"}712 {wordWrap ? "Disable line wrapping" : "Enable line wrapping"}
714 </TooltipPopup>713 </TooltipPopup>
715 </Tooltip>714 </Tooltip>
716 <Tooltip>715 <Tooltip>
...@@ -844,7 +843,7 @@ export default function DiffPanel({ mode = "inline", composerDraftTarget }: Diff...@@ -844,7 +843,7 @@ export default function DiffPanel({ mode = "inline", composerDraftTarget }: Diff
844 options={{843 options={{
845 diffStyle: diffRenderMode === "split" ? "split" : "unified",844 diffStyle: diffRenderMode === "split" ? "split" : "unified",
846 lineDiffType: "none",845 lineDiffType: "none",
847 overflow: diffWordWrap ? "wrap" : "scroll",846 overflow: wordWrap ? "wrap" : "scroll",
848 theme: resolveDiffThemeName(resolvedTheme),847 theme: resolveDiffThemeName(resolvedTheme),
849 themeType: resolvedTheme as DiffThemeType,848 themeType: resolvedTheme as DiffThemeType,
850 unsafeCSS: DIFF_PANEL_UNSAFE_CSS,849 unsafeCSS: DIFF_PANEL_UNSAFE_CSS,
...@@ -860,7 +859,7 @@ export default function DiffPanel({ mode = "inline", composerDraftTarget }: Diff...@@ -860,7 +859,7 @@ export default function DiffPanel({ mode = "inline", composerDraftTarget }: Diff
860 <pre859 <pre
861 className={cn(860 className={cn(
862 "max-h-[72vh] rounded-md border border-border/70 bg-background/70 p-3 font-mono text-[11px] leading-relaxed text-muted-foreground/90",861 "max-h-[72vh] rounded-md border border-border/70 bg-background/70 p-3 font-mono text-[11px] leading-relaxed text-muted-foreground/90",
863 diffWordWrap862 wordWrap
864 ? "overflow-auto whitespace-pre-wrap wrap-break-word"863 ? "overflow-auto whitespace-pre-wrap wrap-break-word"
865 : "overflow-auto",864 : "overflow-auto",
866 )}865 )}
apps/web/src/components/files/FilePreviewPanel.tsx+13-3
...@@ -18,6 +18,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";...@@ -18,6 +18,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
18import { isBrowserPreviewFile, openFileInPreview } from "~/browser/openFileInPreview";18import { isBrowserPreviewFile, openFileInPreview } from "~/browser/openFileInPreview";
19import ChatMarkdown from "~/components/ChatMarkdown";19import ChatMarkdown from "~/components/ChatMarkdown";
20import { OpenInPicker } from "~/components/chat/OpenInPicker";20import { OpenInPicker } from "~/components/chat/OpenInPicker";
21import { useClientSettings } from "~/hooks/useSettings";
21import { useTheme } from "~/hooks/useTheme";22import { useTheme } from "~/hooks/useTheme";
22import { getLocalStorageItem, setLocalStorageItem } from "~/hooks/useLocalStorage";23import { getLocalStorageItem, setLocalStorageItem } from "~/hooks/useLocalStorage";
23import { resolveDiffThemeName } from "~/lib/diffRendering";24import { resolveDiffThemeName } from "~/lib/diffRendering";
...@@ -248,6 +249,7 @@ interface EditableFileSurfaceProps {...@@ -248,6 +249,7 @@ interface EditableFileSurfaceProps {
248 contents: string;249 contents: string;
249 resolvedTheme: "light" | "dark";250 resolvedTheme: "light" | "dark";
250 revealRequestId: number;251 revealRequestId: number;
252 wordWrap: boolean;
251 onPostRender: FilePostRender;253 onPostRender: FilePostRender;
252 onPendingChange: (relativePath: string, pending: boolean) => void;254 onPendingChange: (relativePath: string, pending: boolean) => void;
253}255}
...@@ -296,6 +298,7 @@ function EditableFileSurface({...@@ -296,6 +298,7 @@ function EditableFileSurface({
296 contents,298 contents,
297 resolvedTheme,299 resolvedTheme,
298 revealRequestId,300 revealRequestId,
301 wordWrap,
299 onPostRender,302 onPostRender,
300 onPendingChange,303 onPendingChange,
301}: EditableFileSurfaceProps) {304}: EditableFileSurfaceProps) {
...@@ -516,7 +519,7 @@ function EditableFileSurface({...@@ -516,7 +519,7 @@ function EditableFileSurface({
516 onGutterUtilityClick: setSelectedRange,519 onGutterUtilityClick: setSelectedRange,
517 onLineSelectionChange: setSelectedRange,520 onLineSelectionChange: setSelectedRange,
518 onLineSelectionEnd: handleLineSelectionEnd,521 onLineSelectionEnd: handleLineSelectionEnd,
519 overflow: "scroll",522 overflow: wordWrap ? "wrap" : "scroll",
520 theme: resolveDiffThemeName(resolvedTheme),523 theme: resolveDiffThemeName(resolvedTheme),
521 themeType: resolvedTheme,524 themeType: resolvedTheme,
522 unsafeCSS: FILE_LINK_REVEAL_UNSAFE_CSS,525 unsafeCSS: FILE_LINK_REVEAL_UNSAFE_CSS,
...@@ -557,7 +560,12 @@ function RenderedMarkdownSurface({...@@ -557,7 +560,12 @@ function RenderedMarkdownSurface({
557 onPendingChange,560 onPendingChange,
558}: Omit<561}: Omit<
559 EditableFileSurfaceProps,562 EditableFileSurfaceProps,
560 "resolvedTheme" | "composerDraftTarget" | "revealLine" | "revealRequestId" | "onPostRender"563 | "resolvedTheme"
564 | "composerDraftTarget"
565 | "revealLine"
566 | "revealRequestId"
567 | "wordWrap"
568 | "onPostRender"
561> & {569> & {
562 threadRef: ScopedThreadRef;570 threadRef: ScopedThreadRef;
563}) {571}) {
...@@ -613,6 +621,7 @@ export default function FilePreviewPanel({...@@ -613,6 +621,7 @@ export default function FilePreviewPanel({
613 onPendingChange,621 onPendingChange,
614}: FilePreviewPanelProps) {622}: FilePreviewPanelProps) {
615 const { resolvedTheme } = useTheme();623 const { resolvedTheme } = useTheme();
624 const wordWrap = useClientSettings((settings) => settings.wordWrap);
616 const primaryEnvironmentId = usePrimaryEnvironmentId();625 const primaryEnvironmentId = usePrimaryEnvironmentId();
617 const environmentHttpBaseUrl = useEnvironmentHttpBaseUrl(environmentId);626 const environmentHttpBaseUrl = useEnvironmentHttpBaseUrl(environmentId);
618 const createAssetUrl = useAtomQueryRunner(assetEnvironment.createUrl, {627 const createAssetUrl = useAtomQueryRunner(assetEnvironment.createUrl, {
...@@ -844,7 +853,7 @@ export default function FilePreviewPanel({...@@ -844,7 +853,7 @@ export default function FilePreviewPanel({
844 }}853 }}
845 options={{854 options={{
846 disableFileHeader: true,855 disableFileHeader: true,
847 overflow: "scroll",856 overflow: wordWrap ? "wrap" : "scroll",
848 theme: resolveDiffThemeName(resolvedTheme),857 theme: resolveDiffThemeName(resolvedTheme),
849 themeType: resolvedTheme,858 themeType: resolvedTheme,
850 unsafeCSS: FILE_LINK_REVEAL_UNSAFE_CSS,859 unsafeCSS: FILE_LINK_REVEAL_UNSAFE_CSS,
...@@ -863,6 +872,7 @@ export default function FilePreviewPanel({...@@ -863,6 +872,7 @@ export default function FilePreviewPanel({
863 contents={file.data.contents}872 contents={file.data.contents}
864 resolvedTheme={resolvedTheme}873 resolvedTheme={resolvedTheme}
865 revealRequestId={revealRequestId}874 revealRequestId={revealRequestId}
875 wordWrap={wordWrap}
866 onPostRender={onFilePostRender}876 onPostRender={onFilePostRender}
867 onPendingChange={onPendingChange}877 onPendingChange={onPendingChange}
868 />878 />
apps/web/src/components/settings/SettingsPanels.tsx+11-13
...@@ -391,9 +391,7 @@ export function useSettingsRestore(onRestored?: () => void) {...@@ -391,9 +391,7 @@ export function useSettingsRestore(onRestored?: () => void) {
391 ...(settings.sidebarThreadPreviewCount !== DEFAULT_UNIFIED_SETTINGS.sidebarThreadPreviewCount391 ...(settings.sidebarThreadPreviewCount !== DEFAULT_UNIFIED_SETTINGS.sidebarThreadPreviewCount
392 ? ["Visible threads"]392 ? ["Visible threads"]
393 : []),393 : []),
394 ...(settings.diffWordWrap !== DEFAULT_UNIFIED_SETTINGS.diffWordWrap394 ...(settings.wordWrap !== DEFAULT_UNIFIED_SETTINGS.wordWrap ? ["Word wrap"] : []),
395 ? ["Diff line wrapping"]
396 : []),
397 ...(settings.diffIgnoreWhitespace !== DEFAULT_UNIFIED_SETTINGS.diffIgnoreWhitespace395 ...(settings.diffIgnoreWhitespace !== DEFAULT_UNIFIED_SETTINGS.diffIgnoreWhitespace
398 ? ["Diff whitespace changes"]396 ? ["Diff whitespace changes"]
399 : []),397 : []),
...@@ -434,11 +432,11 @@ export function useSettingsRestore(onRestored?: () => void) {...@@ -434,11 +432,11 @@ export function useSettingsRestore(onRestored?: () => void) {
434 settings.defaultThreadEnvMode,432 settings.defaultThreadEnvMode,
435 settings.newWorktreesStartFromOrigin,433 settings.newWorktreesStartFromOrigin,
436 settings.diffIgnoreWhitespace,434 settings.diffIgnoreWhitespace,
437 settings.diffWordWrap,
438 settings.automaticGitFetchInterval,435 settings.automaticGitFetchInterval,
439 settings.enableAssistantStreaming,436 settings.enableAssistantStreaming,
440 settings.sidebarThreadPreviewCount,437 settings.sidebarThreadPreviewCount,
441 settings.timestampFormat,438 settings.timestampFormat,
439 settings.wordWrap,
442 theme,440 theme,
443 ],441 ],
444 );442 );
...@@ -456,7 +454,7 @@ export function useSettingsRestore(onRestored?: () => void) {...@@ -456,7 +454,7 @@ export function useSettingsRestore(onRestored?: () => void) {
456 setTheme("system");454 setTheme("system");
457 updateSettings({455 updateSettings({
458 timestampFormat: DEFAULT_UNIFIED_SETTINGS.timestampFormat,456 timestampFormat: DEFAULT_UNIFIED_SETTINGS.timestampFormat,
459 diffWordWrap: DEFAULT_UNIFIED_SETTINGS.diffWordWrap,457 wordWrap: DEFAULT_UNIFIED_SETTINGS.wordWrap,
460 diffIgnoreWhitespace: DEFAULT_UNIFIED_SETTINGS.diffIgnoreWhitespace,458 diffIgnoreWhitespace: DEFAULT_UNIFIED_SETTINGS.diffIgnoreWhitespace,
461 sidebarThreadPreviewCount: DEFAULT_UNIFIED_SETTINGS.sidebarThreadPreviewCount,459 sidebarThreadPreviewCount: DEFAULT_UNIFIED_SETTINGS.sidebarThreadPreviewCount,
462 autoOpenPlanSidebar: DEFAULT_UNIFIED_SETTINGS.autoOpenPlanSidebar,460 autoOpenPlanSidebar: DEFAULT_UNIFIED_SETTINGS.autoOpenPlanSidebar,
...@@ -594,15 +592,15 @@ export function GeneralSettingsPanel() {...@@ -594,15 +592,15 @@ export function GeneralSettingsPanel() {
594 />592 />
595593
596 <SettingsRow594 <SettingsRow
597 title="Diff line wrapping"595 title="Word wrap"
598 description="Set the default wrap state when the diff panel opens."596 description="Wrap long lines in code blocks, tables, diffs, and file previews by default."
599 resetAction={597 resetAction={
600 settings.diffWordWrap !== DEFAULT_UNIFIED_SETTINGS.diffWordWrap ? (598 settings.wordWrap !== DEFAULT_UNIFIED_SETTINGS.wordWrap ? (
601 <SettingResetButton599 <SettingResetButton
602 label="diff line wrapping"600 label="word wrapping"
603 onClick={() =>601 onClick={() =>
604 updateSettings({602 updateSettings({
605 diffWordWrap: DEFAULT_UNIFIED_SETTINGS.diffWordWrap,603 wordWrap: DEFAULT_UNIFIED_SETTINGS.wordWrap,
606 })604 })
607 }605 }
608 />606 />
...@@ -610,9 +608,9 @@ export function GeneralSettingsPanel() {...@@ -610,9 +608,9 @@ export function GeneralSettingsPanel() {
610 }608 }
611 control={609 control={
612 <Switch610 <Switch
613 checked={settings.diffWordWrap}611 checked={settings.wordWrap}
614 onCheckedChange={(checked) => updateSettings({ diffWordWrap: Boolean(checked) })}612 onCheckedChange={(checked) => updateSettings({ wordWrap: Boolean(checked) })}
615 aria-label="Wrap diff lines by default"613 aria-label="Wrap code, tables, diffs, and file previews by default"
616 />614 />
617 }615 }
618 />616 />
packages/contracts/src/settings.test.ts+25-2
...@@ -2,12 +2,35 @@ import { describe, expect, it } from "vite-plus/test";...@@ -2,12 +2,35 @@ import { describe, expect, it } from "vite-plus/test";
2import * as Schema from "effect/Schema";2import * as Schema from "effect/Schema";
33
4import { ProviderInstanceId } from "./providerInstance.ts";4import { ProviderInstanceId } from "./providerInstance.ts";
5import { DEFAULT_SERVER_SETTINGS, ServerSettings, ServerSettingsPatch } from "./settings.ts";5import {
66 ClientSettingsSchema,
7 DEFAULT_SERVER_SETTINGS,
8 ServerSettings,
9 ServerSettingsPatch,
10} from "./settings.ts";
11
12const decodeClientSettings = Schema.decodeUnknownSync(ClientSettingsSchema);
7const decodeServerSettings = Schema.decodeUnknownSync(ServerSettings);13const decodeServerSettings = Schema.decodeUnknownSync(ServerSettings);
8const decodeServerSettingsPatch = Schema.decodeUnknownSync(ServerSettingsPatch);14const decodeServerSettingsPatch = Schema.decodeUnknownSync(ServerSettingsPatch);
9const encodeServerSettings = Schema.encodeSync(ServerSettings);15const encodeServerSettings = Schema.encodeSync(ServerSettings);
1016
17describe("ClientSettings word wrap", () => {
18 it("defaults word wrap on", () => {
19 expect(decodeClientSettings({}).wordWrap).toBe(true);
20 });
21
22 it("ignores obsolete wrapping preferences", () => {
23 const decoded = decodeClientSettings({
24 chatWordWrap: false,
25 diffWordWrap: false,
26 });
27
28 expect(decoded.wordWrap).toBe(true);
29 expect(decoded).not.toHaveProperty("chatWordWrap");
30 expect(decoded).not.toHaveProperty("diffWordWrap");
31 });
32});
33
11describe("ServerSettings.providerInstances (slice-2 invariant)", () => {34describe("ServerSettings.providerInstances (slice-2 invariant)", () => {
12 it("defaults to an empty record so legacy configs without the key still decode", () => {35 it("defaults to an empty record so legacy configs without the key still decode", () => {
13 expect(DEFAULT_SERVER_SETTINGS.providerInstances).toEqual({});36 expect(DEFAULT_SERVER_SETTINGS.providerInstances).toEqual({});
packages/contracts/src/settings.ts+2-2
...@@ -47,7 +47,6 @@ export const ClientSettingsSchema = Schema.Struct({...@@ -47,7 +47,6 @@ export const ClientSettingsSchema = Schema.Struct({
47 Schema.withDecodingDefault(Effect.succeed([])),47 Schema.withDecodingDefault(Effect.succeed([])),
48 ),48 ),
49 diffIgnoreWhitespace: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))),49 diffIgnoreWhitespace: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))),
50 diffWordWrap: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),
51 // Model favorites. Historically keyed by provider kind, now50 // Model favorites. Historically keyed by provider kind, now
52 // widened to `ProviderInstanceId` so users can favorite a specific model51 // widened to `ProviderInstanceId` so users can favorite a specific model
53 // on a custom provider instance (e.g. "Codex Personal · gpt-5") without52 // on a custom provider instance (e.g. "Codex Personal · gpt-5") without
...@@ -92,6 +91,7 @@ export const ClientSettingsSchema = Schema.Struct({...@@ -92,6 +91,7 @@ export const ClientSettingsSchema = Schema.Struct({
92 timestampFormat: TimestampFormat.pipe(91 timestampFormat: TimestampFormat.pipe(
93 Schema.withDecodingDefault(Effect.succeed(DEFAULT_TIMESTAMP_FORMAT)),92 Schema.withDecodingDefault(Effect.succeed(DEFAULT_TIMESTAMP_FORMAT)),
94 ),93 ),
94 wordWrap: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))),
95});95});
96export type ClientSettings = typeof ClientSettingsSchema.Type;96export type ClientSettings = typeof ClientSettingsSchema.Type;
9797
...@@ -538,7 +538,6 @@ export const ClientSettingsPatch = Schema.Struct({...@@ -538,7 +538,6 @@ export const ClientSettingsPatch = Schema.Struct({
538 confirmThreadArchive: Schema.optionalKey(Schema.Boolean),538 confirmThreadArchive: Schema.optionalKey(Schema.Boolean),
539 confirmThreadDelete: Schema.optionalKey(Schema.Boolean),539 confirmThreadDelete: Schema.optionalKey(Schema.Boolean),
540 diffIgnoreWhitespace: Schema.optionalKey(Schema.Boolean),540 diffIgnoreWhitespace: Schema.optionalKey(Schema.Boolean),
541 diffWordWrap: Schema.optionalKey(Schema.Boolean),
542 favorites: Schema.optionalKey(541 favorites: Schema.optionalKey(
543 Schema.Array(542 Schema.Array(
544 Schema.Struct({543 Schema.Struct({
...@@ -568,5 +567,6 @@ export const ClientSettingsPatch = Schema.Struct({...@@ -568,5 +567,6 @@ export const ClientSettingsPatch = Schema.Struct({
568 sidebarThreadSortOrder: Schema.optionalKey(SidebarThreadSortOrder),567 sidebarThreadSortOrder: Schema.optionalKey(SidebarThreadSortOrder),
569 sidebarThreadPreviewCount: Schema.optionalKey(SidebarThreadPreviewCount),568 sidebarThreadPreviewCount: Schema.optionalKey(SidebarThreadPreviewCount),
570 timestampFormat: Schema.optionalKey(TimestampFormat),569 timestampFormat: Schema.optionalKey(TimestampFormat),
570 wordWrap: Schema.optionalKey(Schema.Boolean),
571});571});
572export type ClientSettingsPatch = typeof ClientSettingsPatch.Type;572export type ClientSettingsPatch = typeof ClientSettingsPatch.Type;