diff --git a/packages/webui/src/client/assets/fonts/brand/Nunito-latin.woff2 b/packages/webui/src/client/assets/fonts/brand/Nunito-latin.woff2 new file mode 100644 index 000000000..e1188184c Binary files /dev/null and b/packages/webui/src/client/assets/fonts/brand/Nunito-latin.woff2 differ diff --git a/packages/webui/src/client/assets/fonts/brand/OFL.txt b/packages/webui/src/client/assets/fonts/brand/OFL.txt new file mode 100644 index 000000000..c8210f08c --- /dev/null +++ b/packages/webui/src/client/assets/fonts/brand/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2014 The Nunito Project Authors (https://github.com/googlefonts/nunito) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/packages/webui/src/client/features/workspace/ui/workspace.css b/packages/webui/src/client/features/workspace/ui/workspace.css index d50f8a131..525b1ec8d 100644 --- a/packages/webui/src/client/features/workspace/ui/workspace.css +++ b/packages/webui/src/client/features/workspace/ui/workspace.css @@ -266,7 +266,11 @@ .webui-workspace-launcher { display: flex; min-width: 0; min-height: 0; flex: 1 1 auto; flex-direction: column; align-items: center; justify-content: center; container: webui-workspace-launcher / size; padding: var(--spacing_24); background: var(--bg_default_primary); color: var(--text_default_primary); } .webui-workspace-launcher-content { display: flex; width: 100%; flex-direction: column; align-items: center; transform: translateY(-24px); } .webui-workspace-launcher-brand { display: flex; min-height: clamp(72px, 10vw, 112px); flex: 0 0 auto; align-items: center; justify-content: center; margin-bottom: clamp(24px, 4vw, 48px); } - .webui-workspace-launcher-wordmark { font-family: "Century Gothic", "Arial Rounded MT Bold", "Segoe UI", sans-serif; font-size: clamp(60px, 7vw, 88px); font-weight: 700; letter-spacing: 0.02em; line-height: 1; white-space: nowrap; } + /* The mark is drawn with the font the client carries, so every platform + * draws the same shapes. The faces after it are the fallbacks for a build + * where the font file did not make it into the artifact: the mark is still + * drawn, just not in its own face. */ + .webui-workspace-launcher-wordmark { font-family: "MiniMax Wordmark", "Century Gothic", "Arial Rounded MT Bold", "Segoe UI", sans-serif; font-size: clamp(60px, 7vw, 88px); font-weight: 700; letter-spacing: 0.02em; line-height: 1; white-space: nowrap; } .webui-workspace-launcher-wordmark span { display: inline-block; } .webui-workspace-launcher-wordmark .webui-workspace-launcher-wordmark-mini { font-size: 0.88em; } .webui-workspace-launcher-wordmark .webui-workspace-launcher-wordmark-max { font-size: 1.08em; } diff --git a/packages/webui/src/client/styles/brand-font.css b/packages/webui/src/client/styles/brand-font.css new file mode 100644 index 000000000..9159fb132 --- /dev/null +++ b/packages/webui/src/client/styles/brand-font.css @@ -0,0 +1,50 @@ +/* + * The wordmark's own font, shipped with the client. + * + * The wordmark used to name a family that only exists on some machines: + * "Century Gothic" is a Microsoft font, so a Mac without Office, a Linux box + * and a Windows box each fell through to whatever they did have, and the same + * mark came out in a different face on every platform. That is not a cosmetic + * difference — the recorded baseline for one platform stopped matching the + * other two, and a person comparing the client against the recorded + * screenshot saw a difference that no code change had caused. + * + * So the mark is drawn with a font the client carries. Nunito is a rounded + * sans under the SIL Open Font License 1.1 — the licence text ships beside + * the font file — and it is the closest licensed face to the rounded one the + * mark used to land on: a Mac without Office fell through to Arial Rounded + * MT Bold, which is a Monotype font the client may not redistribute. Nunito + * keeps that roundness while being free to ship. Because it is a web font, + * the browser draws the same outlines everywhere instead of asking the host + * what it happens to have installed. + * + * The family is called "MiniMax Wordmark" rather than "Nunito" on purpose: + * a name no host happens to have installed is a name only this file can + * answer, so a machine that already has Nunito installed cannot answer + * first with a different version of it. + * + * font-display: block keeps the mark from being painted in a fallback face + * first: the mark is the one thing on that page that must not flicker + * between two shapes. + * + * One file covers the mark. Nunito is a variable face — its weight axis runs + * from 200 to 1000 — so the file below is instanced by the browser at each of + * the two weights the mark uses: the wordmark is set at 700 and its lower + * line at 600. Two rules, one file, and no second face to keep in step. + */ + +@font-face { + font-family: "MiniMax Wordmark"; + font-style: normal; + font-weight: 600; + font-display: block; + src: url("/assets/fonts/brand/Nunito-latin.woff2") format("woff2"); +} + +@font-face { + font-family: "MiniMax Wordmark"; + font-style: normal; + font-weight: 700; + font-display: block; + src: url("/assets/fonts/brand/Nunito-latin.woff2") format("woff2"); +} diff --git a/packages/webui/src/client/styles/index.css b/packages/webui/src/client/styles/index.css index c99d362c1..6e35dbbc9 100644 --- a/packages/webui/src/client/styles/index.css +++ b/packages/webui/src/client/styles/index.css @@ -15,6 +15,10 @@ */ @import "./tokens.css"; +/* The wordmark's own font, carried by the client. It comes after the tokens + * because a font declaration is not part of the layered composition: it says + * which face answers a family name, not how the shell is built. */ +@import "./brand-font.css"; /* Motion foundations (ADR 0028 § Shared contracts): the four named families and * the reduced-motion policy. It comes right after the tokens, because every * surface below it moves by reading one of these values. */ diff --git a/packages/webui/test/browser/harness.mjs b/packages/webui/test/browser/harness.mjs index 2b69ead1a..234cab3bb 100644 --- a/packages/webui/test/browser/harness.mjs +++ b/packages/webui/test/browser/harness.mjs @@ -119,11 +119,19 @@ export async function openApp(page, hash = "#session=A", options = {}) { const faces = await Promise.all([ document.fonts.load('14px "HarmonyOS Sans"'), document.fonts.load('14px "Hack"'), + // The wordmark is a web font the client carries, and its font-display + // is block: until that face is ready the mark is not painted at all. + // A screenshot taken inside that window records a blank mark, so the + // face has to be settled before the client is measured. + document.fonts.load('14px "MiniMax Wordmark"'), ]); await document.fonts.ready; return faces.map((fonts) => fonts.length); }); - expect(loadedFonts, "both pinned browser fonts must load before measuring the client").toEqual([1, 1]); + expect( + loadedFonts, + "the pinned browser fonts and the bundled wordmark face must load before measuring the client", + ).toEqual([1, 1, 1]); // Register the frame wait before advancing a deliberately frozen test clock. await page.evaluate(() => { window.__webuiInitialLayoutReady = new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve))); diff --git a/packages/webui/test/browser/responsive-baselines.spec.mjs-snapshots/workspace-panel-at-1024-darwin.png b/packages/webui/test/browser/responsive-baselines.spec.mjs-snapshots/workspace-panel-at-1024-darwin.png index c19f47d09..01ba7d0bb 100644 Binary files a/packages/webui/test/browser/responsive-baselines.spec.mjs-snapshots/workspace-panel-at-1024-darwin.png and b/packages/webui/test/browser/responsive-baselines.spec.mjs-snapshots/workspace-panel-at-1024-darwin.png differ diff --git a/packages/webui/test/browser/responsive-baselines.spec.mjs-snapshots/workspace-panel-at-1024-linux.png b/packages/webui/test/browser/responsive-baselines.spec.mjs-snapshots/workspace-panel-at-1024-linux.png index e4bc7630c..c72b95805 100644 Binary files a/packages/webui/test/browser/responsive-baselines.spec.mjs-snapshots/workspace-panel-at-1024-linux.png and b/packages/webui/test/browser/responsive-baselines.spec.mjs-snapshots/workspace-panel-at-1024-linux.png differ diff --git a/packages/webui/test/browser/responsive-baselines.spec.mjs-snapshots/workspace-panel-at-1024-win32.png b/packages/webui/test/browser/responsive-baselines.spec.mjs-snapshots/workspace-panel-at-1024-win32.png index 4d8df6438..1b6092517 100644 Binary files a/packages/webui/test/browser/responsive-baselines.spec.mjs-snapshots/workspace-panel-at-1024-win32.png and b/packages/webui/test/browser/responsive-baselines.spec.mjs-snapshots/workspace-panel-at-1024-win32.png differ diff --git a/packages/webui/test/browser/responsive-baselines.spec.mjs-snapshots/workspace-panel-at-1440-darwin.png b/packages/webui/test/browser/responsive-baselines.spec.mjs-snapshots/workspace-panel-at-1440-darwin.png index 1767917a6..c3c12ebbf 100644 Binary files a/packages/webui/test/browser/responsive-baselines.spec.mjs-snapshots/workspace-panel-at-1440-darwin.png and b/packages/webui/test/browser/responsive-baselines.spec.mjs-snapshots/workspace-panel-at-1440-darwin.png differ diff --git a/packages/webui/test/browser/responsive-baselines.spec.mjs-snapshots/workspace-panel-at-1440-linux.png b/packages/webui/test/browser/responsive-baselines.spec.mjs-snapshots/workspace-panel-at-1440-linux.png index b6040a065..4cd9b7d10 100644 Binary files a/packages/webui/test/browser/responsive-baselines.spec.mjs-snapshots/workspace-panel-at-1440-linux.png and b/packages/webui/test/browser/responsive-baselines.spec.mjs-snapshots/workspace-panel-at-1440-linux.png differ diff --git a/packages/webui/test/browser/responsive-baselines.spec.mjs-snapshots/workspace-panel-at-1440-win32.png b/packages/webui/test/browser/responsive-baselines.spec.mjs-snapshots/workspace-panel-at-1440-win32.png index e52cf5b77..b07a13a9d 100644 Binary files a/packages/webui/test/browser/responsive-baselines.spec.mjs-snapshots/workspace-panel-at-1440-win32.png and b/packages/webui/test/browser/responsive-baselines.spec.mjs-snapshots/workspace-panel-at-1440-win32.png differ diff --git a/packages/webui/test/browser/responsive-baselines.spec.mjs-snapshots/workspace-panel-at-768-darwin.png b/packages/webui/test/browser/responsive-baselines.spec.mjs-snapshots/workspace-panel-at-768-darwin.png index de13497ee..846d84f7f 100644 Binary files a/packages/webui/test/browser/responsive-baselines.spec.mjs-snapshots/workspace-panel-at-768-darwin.png and b/packages/webui/test/browser/responsive-baselines.spec.mjs-snapshots/workspace-panel-at-768-darwin.png differ diff --git a/packages/webui/test/browser/responsive-baselines.spec.mjs-snapshots/workspace-panel-at-768-linux.png b/packages/webui/test/browser/responsive-baselines.spec.mjs-snapshots/workspace-panel-at-768-linux.png index fd89e1461..becbae4cc 100644 Binary files a/packages/webui/test/browser/responsive-baselines.spec.mjs-snapshots/workspace-panel-at-768-linux.png and b/packages/webui/test/browser/responsive-baselines.spec.mjs-snapshots/workspace-panel-at-768-linux.png differ diff --git a/packages/webui/test/browser/responsive-baselines.spec.mjs-snapshots/workspace-panel-at-768-win32.png b/packages/webui/test/browser/responsive-baselines.spec.mjs-snapshots/workspace-panel-at-768-win32.png index 1e77dbfd5..14446369c 100644 Binary files a/packages/webui/test/browser/responsive-baselines.spec.mjs-snapshots/workspace-panel-at-768-win32.png and b/packages/webui/test/browser/responsive-baselines.spec.mjs-snapshots/workspace-panel-at-768-win32.png differ diff --git a/packages/webui/test/browser/visual-baselines.spec.mjs-snapshots/embedded-browser-panel-darwin.png b/packages/webui/test/browser/visual-baselines.spec.mjs-snapshots/embedded-browser-panel-darwin.png index 74ff52556..a74ae9687 100644 Binary files a/packages/webui/test/browser/visual-baselines.spec.mjs-snapshots/embedded-browser-panel-darwin.png and b/packages/webui/test/browser/visual-baselines.spec.mjs-snapshots/embedded-browser-panel-darwin.png differ diff --git a/packages/webui/test/browser/visual-baselines.spec.mjs-snapshots/embedded-browser-panel-linux.png b/packages/webui/test/browser/visual-baselines.spec.mjs-snapshots/embedded-browser-panel-linux.png index f59892fdc..3d1d31000 100644 Binary files a/packages/webui/test/browser/visual-baselines.spec.mjs-snapshots/embedded-browser-panel-linux.png and b/packages/webui/test/browser/visual-baselines.spec.mjs-snapshots/embedded-browser-panel-linux.png differ diff --git a/packages/webui/test/browser/visual-baselines.spec.mjs-snapshots/embedded-browser-panel-win32.png b/packages/webui/test/browser/visual-baselines.spec.mjs-snapshots/embedded-browser-panel-win32.png index c9ba0835d..d57f12b4c 100644 Binary files a/packages/webui/test/browser/visual-baselines.spec.mjs-snapshots/embedded-browser-panel-win32.png and b/packages/webui/test/browser/visual-baselines.spec.mjs-snapshots/embedded-browser-panel-win32.png differ diff --git a/packages/webui/test/gates/fixtures/path-metadata-f00.json b/packages/webui/test/gates/fixtures/path-metadata-f00.json index bbd6655d3..e21eb54c2 100644 --- a/packages/webui/test/gates/fixtures/path-metadata-f00.json +++ b/packages/webui/test/gates/fixtures/path-metadata-f00.json @@ -1,7 +1,7 @@ { "baseline": "f00b1268c28ad15054a8882620c3fb08beb86d58", "sourceSha256": "46b86b658adc1611f3531d8dc9844612f7e9ee3de0eec4a532aeec44b5de0a12", - "declaredDelta": "Public marker preserves the original layer and adds only the published- role prefix; publication legality has separate positive/negative fixtures. Post-baseline additions: conversation/pagination remains a mechanism, platform/transport-origin remains infrastructure, runtime/ssh/authentication remains outside the client, bindings/use-webui-shutdown remains a binding, platform/webui-shutdown remains infrastructure, and server/http/shutdown remains outside the client.", + "declaredDelta": "Public marker preserves the original layer and adds only the published- role prefix; publication legality has separate positive/negative fixtures. Post-baseline additions: conversation/pagination remains a mechanism, platform/transport-origin remains infrastructure, runtime/ssh/authentication remains outside the client, bindings/use-webui-shutdown remains a binding, platform/webui-shutdown remains infrastructure, server/http/shutdown remains outside the client, and client/styles/brand-font remains a stylesheet.", "cases": [ { "path": "runtime/native-pty.ts", @@ -6405,6 +6405,13 @@ "role": "outside", "business": null, "home": "outside" + }, + { + "path": "client/styles/brand-font.css", + "layers": ["unknown"], + "role": "styles", + "business": null, + "home": "current" } ], "directions": [ diff --git a/packages/webui/test/unit/webui-w0-css-structure.test.ts b/packages/webui/test/unit/webui-w0-css-structure.test.ts index 02cd8338f..3ab0ed9e1 100644 --- a/packages/webui/test/unit/webui-w0-css-structure.test.ts +++ b/packages/webui/test/unit/webui-w0-css-structure.test.ts @@ -17,7 +17,7 @@ // does not depend on `pnpm build:styles` having run. import { describe, it, expect, beforeAll } from "vitest"; -import { readFileSync, readdirSync } from "node:fs"; +import { existsSync, readFileSync, readdirSync } from "node:fs"; import { SKELETON_CONTENT_MAX_WIDTH_PX } from "../../src/client/features/transcript/ui/TranscriptSkeletons.js"; import { WEBUI_LAYOUT_THRESHOLDS } from "../../src/client/ui/layout-thresholds.js"; import path from "node:path"; @@ -446,6 +446,10 @@ describe("W0 · stylesheet composition", () => { // the split harmless (every file wraps its rules in `@layer components`). expect(imports).toEqual([ "./tokens.css", + // The wordmark's own font, carried by the client so the mark is drawn + // the same way on every machine. A font declaration is not part of the + // layered composition, so it sits next to the tokens. + "./brand-font.css", // The shared contracts (ADR 0028): the motion foundations every surface // animates with, the field's own arrangement, the feedback family's // states, and the shell's own rules. @@ -524,6 +528,43 @@ describe("W0 · stylesheet composition", () => { }); }); +describe("W0 · the wordmark's own font", () => { + it("names the font files the client actually carries", () => { + const fontCss = readFileSync(path.join(stylesDir, "brand-font.css"), "utf8"); + const urls = [...fontCss.matchAll(/url\("([^"]+)"\)/gu)].map((match) => match[1]); + // Two faces: the mark is set at 700 and its lower line at 600. + expect(urls).toHaveLength(2); + for (const url of urls) { + // The path is absolute: one stylesheet serves every surface, so a + // relative path would resolve against the page rather than the font. + expect(url.startsWith("/assets/fonts/brand/")).toBe(true); + // The file has to be there. Nothing else in the gate checks that a URL + // in a stylesheet resolves — the build copies what the script tells it + // to, and the browser asks for the font and is answered with a 404, + // which is a blank mark rather than a failure. + expect(existsSync(path.join(clientDir, url))).toBe(true); + } + }); + + it("asks for the wordmark's own face first and keeps the old faces behind it", () => { + const workspaceCss = readFileSync( + path.join(clientDir, "features/workspace/ui/workspace.css"), + "utf8", + ); + const rule = workspaceCss.match(/\.webui-workspace-launcher-wordmark\s*\{([^}]*)\}/u); + expect(rule).not.toBeNull(); + const stack = rule?.[1].match(/font-family:\s*([^;]+);/u)?.[1] ?? ""; + const families = stack.split(",").map((family) => family.trim()); + // The bundled face comes first: it is the one that answers on every + // machine, so the mark does not depend on what the host happens to have. + expect(families[0]).toBe('"MiniMax Wordmark"'); + // The faces that came before are still behind it: if the font file does + // not make it into an artifact, the mark is still drawn — just not in + // its own face. + expect(families).toContain('"Century Gothic"'); + }); +}); + describe("W0 · structural declarations W5 must preserve", () => { it("keeps the rail at its fixed width", () => { // The rail's width is the shell skeleton's, and it is declared once: the diff --git a/release/public-source.json b/release/public-source.json index 2129bfcd0..ec8c73b5d 100644 --- a/release/public-source.json +++ b/release/public-source.json @@ -3867,6 +3867,8 @@ "packages/webui/src/client/application/task-session-workflows.ts", "packages/webui/src/client/application/version-cell.ts", "packages/webui/src/client/assets/assets-manifest.txt", + "packages/webui/src/client/assets/fonts/brand/Nunito-latin.woff2", + "packages/webui/src/client/assets/fonts/brand/OFL.txt", "packages/webui/src/client/assets/fonts/katex/KaTeX_AMS-Regular.ttf", "packages/webui/src/client/assets/fonts/katex/KaTeX_AMS-Regular.woff", "packages/webui/src/client/assets/fonts/katex/KaTeX_AMS-Regular.woff2", @@ -4396,6 +4398,7 @@ "packages/webui/src/client/shell/use-shell-activity-wiring.ts", "packages/webui/src/client/shell/use-workspace-panel-session.ts", "packages/webui/src/client/styles/agent-avatar.css", + "packages/webui/src/client/styles/brand-font.css", "packages/webui/src/client/styles/button.css", "packages/webui/src/client/styles/card.css", "packages/webui/src/client/styles/dialog.css", diff --git a/scripts/build-webui.mjs b/scripts/build-webui.mjs index 052e707b6..b96553596 100644 --- a/scripts/build-webui.mjs +++ b/scripts/build-webui.mjs @@ -165,6 +165,13 @@ cpSync( path.join(outdir, "client/fonts"), { recursive: true }, ); +// The wordmark's own font travels with the client. It is the one face that +// has to look the same on every machine, so it cannot be left to the host. +cpSync( + path.join(packageDir, "src/client/assets/fonts/brand"), + path.join(outdir, "client/assets/fonts/brand"), + { recursive: true }, +); // The WebUI server owns its own mcode-tools process boundary. Keep the // embedded entry and the command launcher beside the server bundle so the diff --git a/scripts/package-webui-npm.mjs b/scripts/package-webui-npm.mjs index c7a33f5b0..a8e8657a7 100644 --- a/scripts/package-webui-npm.mjs +++ b/scripts/package-webui-npm.mjs @@ -125,6 +125,9 @@ const client = await build({ cpSync(path.join(webuiDir, "src/client/index.html"), path.join(outdir, "client/index.html")); cpSync(path.join(webuiDir, "src/client/assets/img"), path.join(outdir, "client/assets/img"), { recursive: true }); cpSync(path.join(webuiDir, "src/client/assets/fonts/katex"), path.join(outdir, "client/fonts"), { recursive: true }); +// The wordmark's own font travels with the client: it is the one face that +// has to look the same on every machine, so it cannot be left to the host. +cpSync(path.join(webuiDir, "src/client/assets/fonts/brand"), path.join(outdir, "client/assets/fonts/brand"), { recursive: true }); copyLocalRuntimeAssets({ repositoryRoot: root, outputDir: outdir, filter: () => true }); // The bundled server resolves its agent and skill assets relative to its own // directory (import.meta.url), so it looks for /server/assets. Mirror