| author | |
| committer | |
| log | fb1034546f989c11568ef1c4b5801e6a5372a07d |
| tree | 21d773330ffc2263970d6b76bfb94adf22875030 |
| parent | f5f98cf0ae48a14b524480a0942f9ce37190656c |
| signature | Signed by PGP key B5690EEEBB952194 |
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 | }; |
| 33 | 33 | ||
| 34 | const decodeClientSettingsJson = Schema.decodeEffect(Schema.fromJsonString(ClientSettingsSchema)); | 34 | const 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"; |
| 54 | import { fnv1a32 } from "../lib/diffRendering"; | 54 | import { fnv1a32 } from "../lib/diffRendering"; |
| 55 | import { LRUCache } from "../lib/lruCache"; | 55 | import { LRUCache } from "../lib/lruCache"; |
| 56 | import { useTheme } from "../hooks/useTheme"; | 56 | import { useTheme } from "../hooks/useTheme"; |
| 57 | import { useClientSettings } from "../hooks/useSettings"; | ||
| 57 | import { | 58 | import { |
| 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> { |
| 295 | function MarkdownTable({ children, ...props }: React.ComponentProps<"table">) { | 296 | function 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 | <TooltipTrigger | 696 | <TooltipTrigger |
| 696 | render={ | 697 | render={ |
| 697 | <Toggle | 698 | <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 | <pre | 859 | <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 | diffWordWrap | 862 | 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"; |
| 18 | import { isBrowserPreviewFile, openFileInPreview } from "~/browser/openFileInPreview"; | 18 | import { isBrowserPreviewFile, openFileInPreview } from "~/browser/openFileInPreview"; |
| 19 | import ChatMarkdown from "~/components/ChatMarkdown"; | 19 | import ChatMarkdown from "~/components/ChatMarkdown"; |
| 20 | import { OpenInPicker } from "~/components/chat/OpenInPicker"; | 20 | import { OpenInPicker } from "~/components/chat/OpenInPicker"; |
| 21 | import { useClientSettings } from "~/hooks/useSettings"; | ||
| 21 | import { useTheme } from "~/hooks/useTheme"; | 22 | import { useTheme } from "~/hooks/useTheme"; |
| 22 | import { getLocalStorageItem, setLocalStorageItem } from "~/hooks/useLocalStorage"; | 23 | import { getLocalStorageItem, setLocalStorageItem } from "~/hooks/useLocalStorage"; |
| 23 | import { resolveDiffThemeName } from "~/lib/diffRendering"; | 24 | import { 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.sidebarThreadPreviewCount | 391 | ...(settings.sidebarThreadPreviewCount !== DEFAULT_UNIFIED_SETTINGS.sidebarThreadPreviewCount |
| 392 | ? ["Visible threads"] | 392 | ? ["Visible threads"] |
| 393 | : []), | 393 | : []), |
| 394 | ...(settings.diffWordWrap !== DEFAULT_UNIFIED_SETTINGS.diffWordWrap | 394 | ...(settings.wordWrap !== DEFAULT_UNIFIED_SETTINGS.wordWrap ? ["Word wrap"] : []), |
| 395 | ? ["Diff line wrapping"] | ||
| 396 | : []), | ||
| 397 | ...(settings.diffIgnoreWhitespace !== DEFAULT_UNIFIED_SETTINGS.diffIgnoreWhitespace | 395 | ...(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 | /> |
| 595 | 593 | ||
| 596 | <SettingsRow | 594 | <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 | <SettingResetButton | 599 | <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 | <Switch | 610 | <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"; |
| 2 | import * as Schema from "effect/Schema"; | 2 | import * as Schema from "effect/Schema"; |
| 3 | 3 | ||
| 4 | import { ProviderInstanceId } from "./providerInstance.ts"; | 4 | import { ProviderInstanceId } from "./providerInstance.ts"; |
| 5 | import { DEFAULT_SERVER_SETTINGS, ServerSettings, ServerSettingsPatch } from "./settings.ts"; | 5 | import { |
| 6 | 6 | ClientSettingsSchema, | |
| 7 | DEFAULT_SERVER_SETTINGS, | ||
| 8 | ServerSettings, | ||
| 9 | ServerSettingsPatch, | ||
| 10 | } from "./settings.ts"; | ||
| 11 | |||
| 12 | const decodeClientSettings = Schema.decodeUnknownSync(ClientSettingsSchema); | ||
| 7 | const decodeServerSettings = Schema.decodeUnknownSync(ServerSettings); | 13 | const decodeServerSettings = Schema.decodeUnknownSync(ServerSettings); |
| 8 | const decodeServerSettingsPatch = Schema.decodeUnknownSync(ServerSettingsPatch); | 14 | const decodeServerSettingsPatch = Schema.decodeUnknownSync(ServerSettingsPatch); |
| 9 | const encodeServerSettings = Schema.encodeSync(ServerSettings); | 15 | const encodeServerSettings = Schema.encodeSync(ServerSettings); |
| 10 | 16 | ||
| 17 | describe("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 | |||
| 11 | describe("ServerSettings.providerInstances (slice-2 invariant)", () => { | 34 | describe("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, now | 50 | // Model favorites. Historically keyed by provider kind, now |
| 52 | // widened to `ProviderInstanceId` so users can favorite a specific model | 51 | // widened to `ProviderInstanceId` so users can favorite a specific model |
| 53 | // on a custom provider instance (e.g. "Codex Personal · gpt-5") without | 52 | // 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 | }); |
| 96 | export type ClientSettings = typeof ClientSettingsSchema.Type; | 96 | export type ClientSettings = typeof ClientSettingsSchema.Type; |
| 97 | 97 | ||
| ... | @@ -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 | }); |
| 572 | export type ClientSettingsPatch = typeof ClientSettingsPatch.Type; | 572 | export type ClientSettingsPatch = typeof ClientSettingsPatch.Type; |