diff --git a/web/src/components/agent-chat.test.tsx b/web/src/components/agent-chat.test.tsx index 00c0197..58db070 100644 --- a/web/src/components/agent-chat.test.tsx +++ b/web/src/components/agent-chat.test.tsx @@ -164,7 +164,7 @@ describe("AgentChat — raw-terminal escape hatch", () => { it("shows the plain mirror (no buttons, menu as raw text) when raw terminal is on", () => { localStorage.setItem( - "collie:display-prefs:v3", + "collie:display-prefs:v4", JSON.stringify({ wrap: true, fontSize: 11, rawTerminal: true }), ); renderChat({ text: MENU_TEXT }); @@ -184,7 +184,7 @@ describe("AgentChat — raw-terminal escape hatch", () => { it("raw terminal bypasses the wizard too — the dialog shows verbatim, keys-pad drivable", () => { localStorage.setItem( - "collie:display-prefs:v3", + "collie:display-prefs:v4", JSON.stringify({ wrap: true, fontSize: 11, rawTerminal: true }), ); renderChat({ text: WIZARD_TEXT }); diff --git a/web/src/components/ansi-output.test.tsx b/web/src/components/ansi-output.test.tsx index 16bd92c..f8c1b11 100644 --- a/web/src/components/ansi-output.test.tsx +++ b/web/src/components/ansi-output.test.tsx @@ -47,6 +47,30 @@ describe("terminal mirror colour space", () => { }); }); +// Wrap defaults ON (#53): the mirror is mostly agent prose and a phone shows far fewer columns than +// the desktop width panes are spawned at. The no-wrap branch is still the right rendering for TUI +// tables and box drawing, but it is now reachable ONLY through the View toggle — so it is exactly +// the kind of code a later refactor can drop without any test noticing. +describe("mirror line wrapping", () => { + function preFor(props: Partial>) { + const { container } = render(); + return container.querySelector("pre")!; + } + + it("wraps by default rather than making the block a horizontal panner", () => { + const cls = preFor({}).className; + expect(cls).toContain("whitespace-pre-wrap"); + expect(cls).not.toContain("overflow-x-auto"); + }); + + it("still pans, column-faithful, when wrap is turned off", () => { + const cls = preFor({ wrap: false }).className; + expect(cls).toContain("whitespace-pre"); + expect(cls).toContain("overflow-x-auto"); + expect(cls).not.toContain("whitespace-pre-wrap"); + }); +}); + // URLs printed by an agent are plain characters — the mirror finds them and wraps those ranges in // anchors. The invariants worth guarding are the ones a refactor would silently break: the text is // still exactly what the terminal printed, and nothing but http(s) ever becomes an href. diff --git a/web/src/components/ansi-output.tsx b/web/src/components/ansi-output.tsx index f71bdc9..83dde2a 100644 --- a/web/src/components/ansi-output.tsx +++ b/web/src/components/ansi-output.tsx @@ -36,7 +36,10 @@ type MultiBlock = Extract; export interface AnsiOutputProps { text: string; className?: string; - /** false = no wrap; the block scrolls horizontally, preserving column alignment. Default false — enable Wrap in View for prose. */ + /** true = wrap; the block breaks at the viewport width instead of scrolling horizontally. Default + * true — the mirror is mostly agent prose, and a phone shows far fewer columns than the desktop + * width panes are spawned at, so panning was the common case. Disable Wrap in View for + * column-faithful TUI tables. */ wrap?: boolean; /** Monospace font size in px. Default 11. */ fontSize?: number; @@ -154,7 +157,7 @@ function preClass(wrap: boolean, className?: string): string { export const AnsiOutput = memo(function AnsiOutput({ text, className, - wrap = false, + wrap = true, fontSize = 11, query = "", currentMatch = -1, diff --git a/web/src/hooks/use-display-prefs.test.ts b/web/src/hooks/use-display-prefs.test.ts index 82343f8..daee3bc 100644 --- a/web/src/hooks/use-display-prefs.test.ts +++ b/web/src/hooks/use-display-prefs.test.ts @@ -2,14 +2,14 @@ import { renderHook, act } from "@testing-library/react"; import { useDisplayPrefs } from "./use-display-prefs"; // Minimal localStorage stub — Vitest/jsdom includes a real one but this ensures it's clean per test. -const STORAGE_KEY = "collie:display-prefs:v3"; +const STORAGE_KEY = "collie:display-prefs:v4"; describe("useDisplayPrefs", () => { beforeEach(() => localStorage.clear()); it("returns defaults when localStorage is empty", () => { const { result } = renderHook(() => useDisplayPrefs()); - expect(result.current.prefs).toEqual({ wrap: false, fontSize: 12, rawTerminal: false }); + expect(result.current.prefs).toEqual({ wrap: true, fontSize: 12, rawTerminal: false }); }); it("persists wrap=true and reloads it on mount", () => { @@ -75,12 +75,12 @@ describe("useDisplayPrefs", () => { it("falls back to defaults on malformed JSON", () => { localStorage.setItem(STORAGE_KEY, "not-json{{{"); const { result } = renderHook(() => useDisplayPrefs()); - expect(result.current.prefs).toEqual({ wrap: false, fontSize: 12, rawTerminal: false }); + expect(result.current.prefs).toEqual({ wrap: true, fontSize: 12, rawTerminal: false }); }); it("falls back to defaults when stored value is not an object", () => { localStorage.setItem(STORAGE_KEY, JSON.stringify(42)); const { result } = renderHook(() => useDisplayPrefs()); - expect(result.current.prefs).toEqual({ wrap: false, fontSize: 12, rawTerminal: false }); + expect(result.current.prefs).toEqual({ wrap: true, fontSize: 12, rawTerminal: false }); }); }); diff --git a/web/src/hooks/use-display-prefs.ts b/web/src/hooks/use-display-prefs.ts index 1119070..dd3e1d4 100644 --- a/web/src/hooks/use-display-prefs.ts +++ b/web/src/hooks/use-display-prefs.ts @@ -4,7 +4,10 @@ import { useCallback, useState } from "react"; // Safe to call in SSR contexts (localStorage guarded throughout). export interface DisplayPrefs { - /** Whether the mirror wraps long lines (default: false — preserves column alignment like desktop Herdr; enable Wrap in View for prose). */ + /** Whether the mirror wraps long lines (default: true). The mirror is mostly agent prose, and a + * phone shows ~45-50 columns against panes herdr spawns at desktop width (190 in one reporter's + * session), so panning was the common case, not the exception. Column-faithful no-wrap for TUI + * tables stays one tap away in View. */ wrap: boolean; /** Font size in px for the mirror pre (default: 12, range: 9–16). */ fontSize: number; @@ -17,10 +20,10 @@ export interface DisplayPrefs { rawTerminal: boolean; } -const STORAGE_KEY = "collie:display-prefs:v3"; +const STORAGE_KEY = "collie:display-prefs:v4"; const FONT_MIN = 9; const FONT_MAX = 16; -const DEFAULTS: DisplayPrefs = { wrap: false, fontSize: 12, rawTerminal: false }; +const DEFAULTS: DisplayPrefs = { wrap: true, fontSize: 12, rawTerminal: false }; function clampFont(n: number): number { return Math.max(FONT_MIN, Math.min(FONT_MAX, Math.round(n)));