Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions web/src/components/agent-chat.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand All @@ -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 });
Expand Down
24 changes: 24 additions & 0 deletions web/src/components/ansi-output.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<ComponentProps<typeof AnsiOutput>>) {
const { container } = render(<AnsiOutput text="a very long line" {...props} />);
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.
Expand Down
7 changes: 5 additions & 2 deletions web/src/components/ansi-output.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@ type MultiBlock = Extract<Block, { kind: "multi-select" }>;
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;
Expand Down Expand Up @@ -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,
Expand Down
8 changes: 4 additions & 4 deletions web/src/hooks/use-display-prefs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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 });
});
});
9 changes: 6 additions & 3 deletions web/src/hooks/use-display-prefs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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)));
Expand Down
Loading