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
1 change: 1 addition & 0 deletions apps/desktop/src/settings/DesktopClientSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const clientSettings: ClientSettings = {
fontFamilySans: "",
fontFamilyTerminal: "",
fontSizeCode: 13,
fontSizeToolOutput: 11,
fontSizeInterface: 16,
fontSizePrompt: 14,
fontSizeTerminal: 12,
Expand Down
32 changes: 32 additions & 0 deletions apps/web/src/appearanceFonts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
clampCodeFontSize,
clampInterfaceFontSize,
clampPromptFontSize,
clampToolOutputFontSize,
DEFAULT_CODE_FONT_STACK,
DEFAULT_SANS_FONT_STACK,
appearanceFontStack,
Expand All @@ -13,6 +14,7 @@ import {
resolveDefaultFamilyLabel,
resolveTerminalFontPreference,
resolveTerminalFontSizePreference,
applyAppearanceFontVariables,
} from "./appearanceFonts";

describe("installed font discovery", () => {
Expand Down Expand Up @@ -155,11 +157,41 @@ describe("font size clamping", () => {
expect(clampInterfaceFontSize(96)).toBe(20);
expect(clampPromptFontSize(40)).toBe(20);
expect(clampCodeFontSize(1)).toBe(10);
expect(clampToolOutputFontSize(1)).toBe(10);
expect(clampToolOutputFontSize(99)).toBe(18);
});

it("rounds fractional values and falls back for unusable input", () => {
expect(clampCodeFontSize(13.4)).toBe(13);
expect(clampInterfaceFontSize(Number.NaN)).toBe(16);
expect(clampPromptFontSize(Number.POSITIVE_INFINITY)).toBe(14);
expect(clampToolOutputFontSize(Number.NaN)).toBe(11);
});
});

describe("appearance font variables", () => {
it("emits an independently clamped absolute tool-output size", () => {
const setProperty = vi.fn();
const root = {
style: {
fontSize: "",
removeProperty: vi.fn(),
setProperty,
},
} as unknown as HTMLElement;

applyAppearanceFontVariables(root, {
sans: "",
code: "",
composer: "",
sizeInterface: 16,
sizePrompt: 14,
sizeCode: 14,
sizeToolOutput: 8,
smoothing: true,
});

expect(setProperty).toHaveBeenCalledWith("--font-size-code", "14px");
expect(setProperty).toHaveBeenCalledWith("--font-size-tool-output", "10px");
});
});
17 changes: 17 additions & 0 deletions apps/web/src/appearanceFonts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,15 @@ import {
DEFAULT_CODE_FONT_SIZE,
DEFAULT_INTERFACE_FONT_SIZE,
DEFAULT_PROMPT_FONT_SIZE,
DEFAULT_TOOL_OUTPUT_FONT_SIZE,
MAX_CODE_FONT_SIZE,
MAX_INTERFACE_FONT_SIZE,
MAX_PROMPT_FONT_SIZE,
MAX_TOOL_OUTPUT_FONT_SIZE,
MIN_CODE_FONT_SIZE,
MIN_INTERFACE_FONT_SIZE,
MIN_PROMPT_FONT_SIZE,
MIN_TOOL_OUTPUT_FONT_SIZE,
} from "@t3tools/contracts";

export const DEFAULT_SANS_FONT_STACK =
Expand Down Expand Up @@ -84,6 +87,7 @@ export interface AppearanceFontPreferences {
readonly sizeInterface: number;
readonly sizePrompt: number;
readonly sizeCode: number;
readonly sizeToolOutput: number;
/** Grayscale `antialiased` rendering; false keeps the heavier platform default. */
readonly smoothing: boolean;
}
Expand Down Expand Up @@ -119,6 +123,10 @@ export function applyAppearanceFontVariables(
root.style.setProperty("--font-size-prompt", `${clampPromptFontSize(preferences.sizePrompt)}px`);
const code = clampCodeFontSize(preferences.sizeCode);
root.style.setProperty("--font-size-code", `${code}px`);
root.style.setProperty(
"--font-size-tool-output",
`${clampToolOutputFontSize(preferences.sizeToolOutput)}px`,
);
// The @pierre/diffs surfaces read their own hook for code text.
root.style.setProperty("--diffs-font-size", `${code}px`);

Expand Down Expand Up @@ -155,6 +163,15 @@ export function clampCodeFontSize(value: number): number {
return clampFontSize(value, MIN_CODE_FONT_SIZE, MAX_CODE_FONT_SIZE, DEFAULT_CODE_FONT_SIZE);
}

export function clampToolOutputFontSize(value: number): number {
return clampFontSize(
value,
MIN_TOOL_OUTPUT_FONT_SIZE,
MAX_TOOL_OUTPUT_FONT_SIZE,
DEFAULT_TOOL_OUTPUT_FONT_SIZE,
);
}

const FONT_PROBE_TEXT = "mmmmmmmmMMWli1O0@# fjord";
let fontProbeContext: CanvasRenderingContext2D | null | undefined;

Expand Down
18 changes: 18 additions & 0 deletions apps/web/src/components/chat/MessagesTimeline.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -626,6 +626,24 @@ describe("MessagesTimeline", () => {
expect(copyControlMarkup).not.toContain("Copy output");
});

it("sizes every expanded body block with the tool-output variable", () => {
const markup = renderToStaticMarkup(
<WorkEntryExpandedBody
body={{
blocks: [
{ kind: "output", text: "command output", isError: false, truncated: false },
{ kind: "empty" },
{ kind: "text", text: "tool detail" },
],
}}
/>,
);

const toolOutputClass = "text-[length:var(--font-size-tool-output,0.6875rem)]";
expect(markup.split(toolOutputClass)).toHaveLength(4);
expect(markup).not.toContain("--font-size-code");
});

it("renders provider no-output sentinels as the empty state", () => {
for (const sentinel of [
"(Bash completed with no output)",
Expand Down
6 changes: 3 additions & 3 deletions apps/web/src/components/chat/MessagesTimeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2308,7 +2308,7 @@ export const WorkEntryExpandedBody = memo(function WorkEntryExpandedBody(props:
{blocks.map((block) =>
block.kind === "output" ? (
<div key={`output:${block.text}`}>
<pre className="max-h-64 cursor-text overflow-auto whitespace-pre-wrap break-words font-mono text-[11px] text-foreground/80 leading-relaxed select-text">
<pre className="max-h-64 cursor-text overflow-auto whitespace-pre-wrap break-words font-mono text-[length:var(--font-size-tool-output,0.6875rem)] text-foreground/80 leading-relaxed select-text">
{block.isError ? "Error output\n" : null}
{block.text}
{block.truncated ? "\n\nOutput truncated" : null}
Expand All @@ -2317,14 +2317,14 @@ export const WorkEntryExpandedBody = memo(function WorkEntryExpandedBody(props:
) : block.kind === "empty" ? (
<p
key="empty"
className="font-mono text-[11px] italic leading-relaxed text-muted-foreground"
className="font-mono text-[length:var(--font-size-tool-output,0.6875rem)] italic leading-relaxed text-muted-foreground"
>
(No output)
</p>
) : (
<pre
key={`text:${block.text}`}
className="max-h-64 cursor-text overflow-auto whitespace-pre-wrap break-words font-mono text-secondary-label text-[11px] leading-relaxed select-text"
className="max-h-64 cursor-text overflow-auto whitespace-pre-wrap break-words font-mono text-secondary-label text-[length:var(--font-size-tool-output,0.6875rem)] leading-relaxed select-text"
>
{block.text}
</pre>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ describe("typography settings restore", () => {
...DEFAULT_UNIFIED_SETTINGS,
fontSizeInterface: 18,
fontFamilyCode: "Fira Code",
fontSizeToolOutput: 12,
}),
).toEqual(["Interface font", "Code font"]);
).toEqual(["Interface font", "Code font", "Tool output"]);
});
});

Expand Down
4 changes: 4 additions & 0 deletions apps/web/src/components/settings/SettingsPanels.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ type TypographySettings = Pick<
| "fontSizeInterface"
| "fontSizePrompt"
| "fontSizeCode"
| "fontSizeToolOutput"
| "fontSizeTerminal"
>;

Expand All @@ -101,6 +102,9 @@ export function getChangedTypographySettingLabels(settings: TypographySettings):
settings.fontSizeCode !== DEFAULT_UNIFIED_SETTINGS.fontSizeCode
? ["Code font"]
: []),
...(settings.fontSizeToolOutput !== DEFAULT_UNIFIED_SETTINGS.fontSizeToolOutput
? ["Tool output"]
: []),
...(settings.fontFamilyTerminal !== DEFAULT_UNIFIED_SETTINGS.fontFamilyTerminal ||
settings.fontSizeTerminal !== DEFAULT_UNIFIED_SETTINGS.fontSizeTerminal
? ["Terminal font"]
Expand Down
106 changes: 83 additions & 23 deletions apps/web/src/components/settings/SettingsPanels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,14 @@ import {
MAX_GLASS_OPACITY,
MAX_INTERFACE_FONT_SIZE,
MAX_PROMPT_FONT_SIZE,
MAX_TOOL_OUTPUT_FONT_SIZE,
MAX_SIDEBAR_AUTO_SETTLE_AFTER_DAYS,
MAX_TERMINAL_FONT_SIZE,
MIN_CODE_FONT_SIZE,
MIN_GLASS_OPACITY,
MIN_INTERFACE_FONT_SIZE,
MIN_PROMPT_FONT_SIZE,
MIN_TOOL_OUTPUT_FONT_SIZE,
MIN_SIDEBAR_AUTO_SETTLE_AFTER_DAYS,
MIN_TERMINAL_FONT_SIZE,
} from "@t3tools/contracts/settings";
Expand Down Expand Up @@ -544,6 +546,7 @@ export function useSettingsRestore(onRestored?: () => void) {
settings.fontFamilySans,
settings.fontFamilyTerminal,
settings.fontSizeCode,
settings.fontSizeToolOutput,
settings.fontSizeInterface,
settings.fontSizePrompt,
settings.fontSizeTerminal,
Expand Down Expand Up @@ -654,6 +657,7 @@ export function useSettingsRestore(onRestored?: () => void) {
fontSizeInterface: DEFAULT_UNIFIED_SETTINGS.fontSizeInterface,
fontSizePrompt: DEFAULT_UNIFIED_SETTINGS.fontSizePrompt,
fontSizeCode: DEFAULT_UNIFIED_SETTINGS.fontSizeCode,
fontSizeToolOutput: DEFAULT_UNIFIED_SETTINGS.fontSizeToolOutput,
fontSizeTerminal: DEFAULT_UNIFIED_SETTINGS.fontSizeTerminal,
});
onRestored?.();
Expand Down Expand Up @@ -1243,6 +1247,38 @@ function TerminalFontRow() {
);
}

function ToolOutputFontRow() {
const settings = usePrimarySettings();
const updateSettings = useUpdatePrimarySettings();
return (
<SettingsRow
{...searchableSetting("tool-output-font")}
description="Agent command and tool call output in the work log."
resetAction={
settings.fontSizeToolOutput !== DEFAULT_UNIFIED_SETTINGS.fontSizeToolOutput ? (
<SettingResetButton
label="tool output font size"
onClick={() =>
updateSettings({
fontSizeToolOutput: DEFAULT_UNIFIED_SETTINGS.fontSizeToolOutput,
})
}
/>
) : null
}
control={
<FontSizeSelect
label="Tool output font size"
min={MIN_TOOL_OUTPUT_FONT_SIZE}
max={MAX_TOOL_OUTPUT_FONT_SIZE}
value={settings.fontSizeToolOutput}
onChange={(fontSizeToolOutput) => updateSettings({ fontSizeToolOutput })}
/>
}
/>
);
}

function FontSmoothingRow() {
const settings = usePrimarySettings();
const updateSettings = useUpdatePrimarySettings();
Expand Down Expand Up @@ -1304,6 +1340,7 @@ function FontSettingsGroup() {
<InterfaceFontRow />
<PromptFontRow />
<CodeFontRow />
<ToolOutputFontRow />
<TerminalFontRow />
<FontSmoothingRow />
</>
Expand Down Expand Up @@ -1341,6 +1378,7 @@ function SimpleFontRows() {
</>
}
/>
<ToolOutputFontRow />
</>
);
}
Expand Down Expand Up @@ -1399,6 +1437,44 @@ function TypographySection() {
);
}

function FontSizeSelect({
label,
min,
max,
value,
onChange,
}: {
label: string;
min: number;
max: number;
value: number;
onChange: (value: number) => void;
}) {
return (
<Select
value={String(value)}
onValueChange={(next) => {
if (typeof next !== "string") return;
const parsed = Number(next);
if (Number.isInteger(parsed) && parsed >= min && parsed <= max) {
onChange(parsed);
}
}}
>
<SelectTrigger className="w-22 shrink-0" aria-label={label}>
<SelectValue>{value} px</SelectValue>
</SelectTrigger>
<SelectPopup align="end" alignItemWithTrigger={false}>
{Array.from({ length: max - min + 1 }, (_, index) => min + index).map((px) => (
<SelectItem hideIndicator key={px} value={String(px)}>
{px} px
</SelectItem>
))}
</SelectPopup>
</Select>
);
}

function FontFamilySettingsRow({
id,
title,
Expand Down Expand Up @@ -1455,29 +1531,13 @@ function FontFamilySettingsRow({
const control = (
<div className="flex w-full items-center gap-2 sm:w-auto">
<div className="min-w-0 flex-1 sm:w-44 sm:flex-none">{familyControl}</div>
<Select
value={String(size.value)}
onValueChange={(next) => {
if (typeof next !== "string") return;
const parsed = Number(next);
if (Number.isInteger(parsed) && parsed >= size.min && parsed <= size.max) {
size.onChange(parsed);
}
}}
>
<SelectTrigger className="w-22 shrink-0" aria-label={size.label}>
<SelectValue>{size.value} px</SelectValue>
</SelectTrigger>
<SelectPopup align="end" alignItemWithTrigger={false}>
{Array.from({ length: size.max - size.min + 1 }, (_, index) => size.min + index).map(
(px) => (
<SelectItem hideIndicator key={px} value={String(px)}>
{px} px
</SelectItem>
),
)}
</SelectPopup>
</Select>
<FontSizeSelect
label={size.label}
min={size.min}
max={size.max}
value={size.value}
onChange={size.onChange}
/>
</div>
);
return (
Expand Down
5 changes: 5 additions & 0 deletions apps/web/src/components/settings/settingsSearch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ export const SETTINGS_SEARCH_ITEMS = [
title: "Code font",
to: "/settings/appearance",
},
{
id: "tool-output-font",
title: "Tool output",
to: "/settings/appearance",
},
{
id: "terminal-font",
title: "Terminal font",
Expand Down
Loading
Loading