diff --git a/apps/desktop/src/settings/DesktopClientSettings.test.ts b/apps/desktop/src/settings/DesktopClientSettings.test.ts index 23a75eb3f79d..d361873d19cc 100644 --- a/apps/desktop/src/settings/DesktopClientSettings.test.ts +++ b/apps/desktop/src/settings/DesktopClientSettings.test.ts @@ -25,6 +25,7 @@ const clientSettings: ClientSettings = { fontFamilySans: "", fontFamilyTerminal: "", fontSizeCode: 13, + fontSizeToolOutput: 11, fontSizeInterface: 16, fontSizePrompt: 14, fontSizeTerminal: 12, diff --git a/apps/web/src/appearanceFonts.test.ts b/apps/web/src/appearanceFonts.test.ts index c82c908207a3..3fbc579e4bc1 100644 --- a/apps/web/src/appearanceFonts.test.ts +++ b/apps/web/src/appearanceFonts.test.ts @@ -5,6 +5,7 @@ import { clampCodeFontSize, clampInterfaceFontSize, clampPromptFontSize, + clampToolOutputFontSize, DEFAULT_CODE_FONT_STACK, DEFAULT_SANS_FONT_STACK, appearanceFontStack, @@ -13,6 +14,7 @@ import { resolveDefaultFamilyLabel, resolveTerminalFontPreference, resolveTerminalFontSizePreference, + applyAppearanceFontVariables, } from "./appearanceFonts"; describe("installed font discovery", () => { @@ -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"); }); }); diff --git a/apps/web/src/appearanceFonts.ts b/apps/web/src/appearanceFonts.ts index 9eda4e3df97a..f21b8491e36a 100644 --- a/apps/web/src/appearanceFonts.ts +++ b/apps/web/src/appearanceFonts.ts @@ -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 = @@ -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; } @@ -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`); @@ -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; diff --git a/apps/web/src/components/chat/MessagesTimeline.test.tsx b/apps/web/src/components/chat/MessagesTimeline.test.tsx index a9e95c2b0f22..a8501e68166d 100644 --- a/apps/web/src/components/chat/MessagesTimeline.test.tsx +++ b/apps/web/src/components/chat/MessagesTimeline.test.tsx @@ -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( + , + ); + + 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)", diff --git a/apps/web/src/components/chat/MessagesTimeline.tsx b/apps/web/src/components/chat/MessagesTimeline.tsx index 00bbfd5690e3..90f97be892c1 100644 --- a/apps/web/src/components/chat/MessagesTimeline.tsx +++ b/apps/web/src/components/chat/MessagesTimeline.tsx @@ -2308,7 +2308,7 @@ export const WorkEntryExpandedBody = memo(function WorkEntryExpandedBody(props: {blocks.map((block) => block.kind === "output" ? (
-
+            
               {block.isError ? "Error output\n" : null}
               {block.text}
               {block.truncated ? "\n\nOutput truncated" : null}
@@ -2317,14 +2317,14 @@ export const WorkEntryExpandedBody = memo(function WorkEntryExpandedBody(props:
         ) : block.kind === "empty" ? (
           

(No output)

) : (
             {block.text}
           
diff --git a/apps/web/src/components/settings/SettingsPanels.logic.test.ts b/apps/web/src/components/settings/SettingsPanels.logic.test.ts index ec4ad4ff5875..a757b340923d 100644 --- a/apps/web/src/components/settings/SettingsPanels.logic.test.ts +++ b/apps/web/src/components/settings/SettingsPanels.logic.test.ts @@ -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"]); }); }); diff --git a/apps/web/src/components/settings/SettingsPanels.logic.ts b/apps/web/src/components/settings/SettingsPanels.logic.ts index 39f4f3cdafd6..b3e9df6a7a8c 100644 --- a/apps/web/src/components/settings/SettingsPanels.logic.ts +++ b/apps/web/src/components/settings/SettingsPanels.logic.ts @@ -83,6 +83,7 @@ type TypographySettings = Pick< | "fontSizeInterface" | "fontSizePrompt" | "fontSizeCode" + | "fontSizeToolOutput" | "fontSizeTerminal" >; @@ -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"] diff --git a/apps/web/src/components/settings/SettingsPanels.tsx b/apps/web/src/components/settings/SettingsPanels.tsx index 6f90e6a02553..08ab356ba01d 100644 --- a/apps/web/src/components/settings/SettingsPanels.tsx +++ b/apps/web/src/components/settings/SettingsPanels.tsx @@ -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"; @@ -544,6 +546,7 @@ export function useSettingsRestore(onRestored?: () => void) { settings.fontFamilySans, settings.fontFamilyTerminal, settings.fontSizeCode, + settings.fontSizeToolOutput, settings.fontSizeInterface, settings.fontSizePrompt, settings.fontSizeTerminal, @@ -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?.(); @@ -1243,6 +1247,38 @@ function TerminalFontRow() { ); } +function ToolOutputFontRow() { + const settings = usePrimarySettings(); + const updateSettings = useUpdatePrimarySettings(); + return ( + + updateSettings({ + fontSizeToolOutput: DEFAULT_UNIFIED_SETTINGS.fontSizeToolOutput, + }) + } + /> + ) : null + } + control={ + updateSettings({ fontSizeToolOutput })} + /> + } + /> + ); +} + function FontSmoothingRow() { const settings = usePrimarySettings(); const updateSettings = useUpdatePrimarySettings(); @@ -1304,6 +1340,7 @@ function FontSettingsGroup() { + @@ -1341,6 +1378,7 @@ function SimpleFontRows() { } /> + ); } @@ -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 ( + + ); +} + function FontFamilySettingsRow({ id, title, @@ -1455,29 +1531,13 @@ function FontFamilySettingsRow({ const control = (
{familyControl}
- +
); return ( diff --git a/apps/web/src/components/settings/settingsSearch.ts b/apps/web/src/components/settings/settingsSearch.ts index e3aef6705665..2c8bbfd08230 100644 --- a/apps/web/src/components/settings/settingsSearch.ts +++ b/apps/web/src/components/settings/settingsSearch.ts @@ -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", diff --git a/apps/web/src/routes/__root.tsx b/apps/web/src/routes/__root.tsx index bc8f8507b814..99be179d81bd 100644 --- a/apps/web/src/routes/__root.tsx +++ b/apps/web/src/routes/__root.tsx @@ -168,6 +168,7 @@ function FontAppearanceSync() { const fontSizeInterface = useClientSettings((settings) => settings.fontSizeInterface); const fontSizePrompt = useClientSettings((settings) => settings.fontSizePrompt); const fontSizeCode = useClientSettings((settings) => settings.fontSizeCode); + const fontSizeToolOutput = useClientSettings((settings) => settings.fontSizeToolOutput); const fontSmoothing = useClientSettings((settings) => settings.fontSmoothing); useEffect(() => { @@ -178,6 +179,7 @@ function FontAppearanceSync() { sizeInterface: fontSizeInterface, sizePrompt: fontSizePrompt, sizeCode: fontSizeCode, + sizeToolOutput: fontSizeToolOutput, smoothing: fontSmoothing, }); for (const family of new Set([fontFamilySans, fontFamilyCode, fontFamilyComposer])) { @@ -195,6 +197,7 @@ function FontAppearanceSync() { fontFamilyComposer, fontFamilySans, fontSizeCode, + fontSizeToolOutput, fontSizeInterface, fontSizePrompt, fontSmoothing, diff --git a/packages/contracts/src/settings.test.ts b/packages/contracts/src/settings.test.ts index 570157292b54..f578f7ef7834 100644 --- a/packages/contracts/src/settings.test.ts +++ b/packages/contracts/src/settings.test.ts @@ -49,6 +49,22 @@ describe("ClientSettings glass opacity", () => { }); }); +describe("ClientSettings tool output font size", () => { + it("defaults to 11px", () => { + expect(decodeClientSettings({}).fontSizeToolOutput).toBe(11); + }); + + it.each([9, 19, 11.5])("rejects a size outside the supported integer range: %s", (value) => { + expect(() => decodeClientSettings({ fontSizeToolOutput: value })).toThrow(); + expect(() => decodeClientSettingsPatch({ fontSizeToolOutput: value })).toThrow(); + }); + + it.each([10, 11, 18])("accepts a size within the supported range: %s", (value) => { + expect(decodeClientSettings({ fontSizeToolOutput: value }).fontSizeToolOutput).toBe(value); + expect(decodeClientSettingsPatch({ fontSizeToolOutput: value }).fontSizeToolOutput).toBe(value); + }); +}); + describe("ClientSettings environment identification", () => { it("defaults to artwork and accepts each presentation mode", () => { expect(decodeClientSettings({}).environmentIdentificationMode).toBe("artwork"); diff --git a/packages/contracts/src/settings.ts b/packages/contracts/src/settings.ts index 9306e430b6e1..8a60280a13f8 100644 --- a/packages/contracts/src/settings.ts +++ b/packages/contracts/src/settings.ts @@ -92,6 +92,14 @@ export const CodeFontSize = Schema.Int.check( export type CodeFontSize = typeof CodeFontSize.Type; export const DEFAULT_CODE_FONT_SIZE: CodeFontSize = 13; +export const MIN_TOOL_OUTPUT_FONT_SIZE = 10; +export const MAX_TOOL_OUTPUT_FONT_SIZE = 18; +export const ToolOutputFontSize = Schema.Int.check( + Schema.isBetween({ minimum: MIN_TOOL_OUTPUT_FONT_SIZE, maximum: MAX_TOOL_OUTPUT_FONT_SIZE }), +); +export type ToolOutputFontSize = typeof ToolOutputFontSize.Type; +export const DEFAULT_TOOL_OUTPUT_FONT_SIZE: ToolOutputFontSize = 11; + export const MIN_TERMINAL_FONT_SIZE = 8; export const MAX_TERMINAL_FONT_SIZE = 20; export const TerminalFontSize = Schema.Int.check( @@ -136,6 +144,9 @@ export const ClientSettingsSchema = Schema.Struct({ fontSizeCode: CodeFontSize.pipe( Schema.withDecodingDefault(Effect.succeed(DEFAULT_CODE_FONT_SIZE)), ), + fontSizeToolOutput: ToolOutputFontSize.pipe( + Schema.withDecodingDefault(Effect.succeed(DEFAULT_TOOL_OUTPUT_FONT_SIZE)), + ), fontSizeTerminal: TerminalFontSize.pipe( Schema.withDecodingDefault(Effect.succeed(DEFAULT_TERMINAL_FONT_SIZE)), ), @@ -778,6 +789,7 @@ export const ClientSettingsPatch = Schema.Struct({ fontSizeInterface: Schema.optionalKey(InterfaceFontSize), fontSizePrompt: Schema.optionalKey(PromptFontSize), fontSizeCode: Schema.optionalKey(CodeFontSize), + fontSizeToolOutput: Schema.optionalKey(ToolOutputFontSize), fontSizeTerminal: Schema.optionalKey(TerminalFontSize), fontFamilyCode: Schema.optionalKey(FontFamilyPreference), fontFamilyComposer: Schema.optionalKey(FontFamilyPreference),