From 22218cc034f6aad5d02fdaf25470f77b092a525a Mon Sep 17 00:00:00 2001 From: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com> Date: Sat, 25 Jul 2026 09:03:46 -0400 Subject: [PATCH] feat(desktop): ship Hermes + OpenClaw tier-2 logos with a coverage guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PRESET_HARNESSES ships 8 tier-2 presets; PRESET_LOGOS and desktop/public/harness-logos/ covered only 6. `hermes` and `openclaw` fell through to the generic TerminalSquare glyph beside six siblings showing real brand marks, and RuntimeIcon's onError fallback made a missing file indistinguishable from an unmapped id at runtime. Add both marks from their MIT-licensed upstreams: - hermes.png from NousResearch/hermes-agent@6ad632b website/static/img/logo.png — cropped the baked-in border frame (a dark box once downscaled), padded square, 64x64, 16-colour palette. 1.7 KB, in line with the existing 1.0-3.4 KB siblings. - openclaw.svg from openclaw/openclaw@b06f40a ui/public/favicon.svg — upstream animates the mascot with SMIL specifically so it plays inside -loaded SVGs, which is exactly how RuntimeIcon renders it. Stripped the animation elements to hold the rest pose (verified pixel-identical to the upstream t=0 frame at 256px) so a settings list doesn't bob and blink. Provenance and modifications are recorded in a new CREDITS.md next to the assets, and the contributor guide's step 4 now requires that row. The guard: the two sides of this mapping are a Rust slice and a TS record, so nothing typechecks them against each other — which is why the gap went unnoticed through review and screenshots. presetLogos.test.mjs parses PRESET_HARNESSES out of discovery.rs and asserts every preset id has a PRESET_LOGOS entry whose file exists on disk, plus the reverse direction for stale entries. It follows the cross-language fixture precedent in effortTable.fixture.test.mjs. Verified failing on each of the three drift modes it claims to catch, not just passing green. Importing RuntimeIcon.tsx from a test needed the node test loader to tolerate Vite asset imports (`./claude.png?inline`), which it resolved as a bare package and threw on; assets now load as an inert stub, the same treatment CSS already gets. Co-authored-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com> Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com> --- crates/buzz-acp/README.md | 2 +- desktop/public/harness-logos/CREDITS.md | 16 ++++ desktop/public/harness-logos/hermes.png | Bin 0 -> 1698 bytes desktop/public/harness-logos/openclaw.svg | 16 ++++ .../features/onboarding/ui/RuntimeIcon.tsx | 4 +- .../onboarding/ui/presetLogos.test.mjs | 75 ++++++++++++++++++ desktop/test-loader-hooks.mjs | 21 +++++ 7 files changed, 132 insertions(+), 2 deletions(-) create mode 100644 desktop/public/harness-logos/CREDITS.md create mode 100644 desktop/public/harness-logos/hermes.png create mode 100644 desktop/public/harness-logos/openclaw.svg create mode 100644 desktop/src/features/onboarding/ui/presetLogos.test.mjs diff --git a/crates/buzz-acp/README.md b/crates/buzz-acp/README.md index db38c32ae..957b0d824 100644 --- a/crates/buzz-acp/README.md +++ b/crates/buzz-acp/README.md @@ -306,7 +306,7 @@ To add a new runtime to the tier-2 gallery: 1. **Verify the ACP entrypoint** from the vendor's own documentation — do not rely on a PR description alone. Test with the actual binary. 2. **Add a `HarnessDefinition` entry** to the `PRESET_HARNESSES` slice in `desktop/src-tauri/src/managed_agents/discovery.rs`. Fill `id`, `label`, `command`, `args`, `install_instructions_url`, `install_hint`. Leave `env` empty unless the harness requires a specific env var to enable ACP mode. 3. **Add the preset id to `BUILTIN_IDS`** in `desktop/src-tauri/src/managed_agents/custom_harnesses.rs` so custom JSON files cannot shadow it. -4. **Add a bundled logo** (64×64 PNG or optimised SVG) to `desktop/public/harness-logos/.png` and add a corresponding entry to `PRESET_LOGOS` in `desktop/src/features/onboarding/ui/RuntimeIcon.tsx`. +4. **Add a bundled logo** (64×64 PNG or optimised SVG) to `desktop/public/harness-logos/.png` and add a corresponding entry to `PRESET_LOGOS` in `desktop/src/features/onboarding/ui/RuntimeIcon.tsx`. Record the source and license in `desktop/public/harness-logos/CREDITS.md`. Only bundle a mark whose upstream license permits redistribution; skipping this step is caught by `presetLogos.test.mjs`, which asserts every `PRESET_HARNESSES` id has a mapped logo that exists on disk. 5. Run `cargo test --lib` and `just desktop-typecheck` to verify everything compiles. The built-in `BUILTIN_IDS` set (`goose`, `claude`, `codex`, `buzz-agent`, and all current preset ids) is the reserved namespace; every other id is available for custom harnesses. diff --git a/desktop/public/harness-logos/CREDITS.md b/desktop/public/harness-logos/CREDITS.md new file mode 100644 index 000000000..f3a53a13b --- /dev/null +++ b/desktop/public/harness-logos/CREDITS.md @@ -0,0 +1,16 @@ +# Preset harness logos — provenance + +Third-party marks bundled to identify tier-2 preset harnesses in the runtime +gallery (`PRESET_LOGOS` in `desktop/src/features/onboarding/ui/RuntimeIcon.tsx`). +Nominative use only — each mark identifies its own vendor's harness. + +Add a row here when adding a preset logo; only bundle marks whose upstream +license permits redistribution. + +| File | Upstream | Commit | License | Source path | Modifications | +|---|---|---|---|---|---| +| `hermes.png` | [NousResearch/hermes-agent](https://github.com/NousResearch/hermes-agent) | `6ad632b` | MIT © 2025 Nous Research | `website/static/img/logo.png` | Cropped the baked-in border frame, padded to square, resized to 64×64, quantised to a 16-colour palette | +| `openclaw.svg` | [openclaw/openclaw](https://github.com/openclaw/openclaw) | `b06f40a` | MIT © 2026 OpenClaw Foundation | `ui/public/favicon.svg` | Removed the SMIL animation elements (renders the upstream rest pose statically — verified pixel-identical to the upstream frame at t=0); minified paths | + +`amp.png`, `cursor.png`, `grok.png`, `kimi.png`, `omp.png`, and `opencode.svg` +predate this file; their provenance was not recorded when they were added. diff --git a/desktop/public/harness-logos/hermes.png b/desktop/public/harness-logos/hermes.png new file mode 100644 index 0000000000000000000000000000000000000000..6acf0020b1d2d756db6353f086f030f936e96ed6 GIT binary patch literal 1698 zcmV;T23`4yP)^Yin~&CQaMl4E0IKtMnn8yf}&1_J{F0RaI4000000RR91%-`68000Ip zNklluFyw9mh>&G3Y>*i!62qf{r92H8XK;X)cKpK` zryXjiPG#&A>vWjbKAdV%3aC|3B%KhghS1mmB4phgH&_Qq*aU(SNcQX|frNeB{*eu# zkMrmGeeU=7z27;%b3`#COGvsq<`KPT3)j8nAn0?01RVAM0O1L(Fp9lSv~92MSN{tL zx4lfFCaojkJzGkQLe|oK^~>_1 z>+1Sb6T2`c*G}3m?@pmB(p*e&60v(|Rso)Z5{y@j=(*0X5u0%#UgV|B7|je$yG2t0&(Kd1rM^ z@aB8Y3@f-jU6_M=+E+Kdt&Q$I@W%^%vwQ#aqNj9MQ3hx!JIb~hqc?aL_9(qs)|Rxk zZhg;;&maB^(Ys+@!J-G&RNk(txITXUoX+-lygvw5escZJ;6TQtt7OWyaA~L&a#ti#pT9cGCW^yn*?&&_PzV4t-0DRj+PPs>y z$?QnRWS+O*0%Vqd$X3J5fjv)EQ#5rrNf|FEnZj4}H=7iMBXjaRDASsEH)-v^Ro68!*kn_>e+3z;|ChywqJ5NH4e0 z0UQFiWX5E#gvEOrvYUD;;UABfR;U^@6SR*1*W?SWYm*K+vA4j?m8 zfA#zKCO)x#{K@YWJ^8hfJvYDb-8cLC7uGYjuL00-X73)T_>o};TIJpfm4a|vv>p^> zw`V;3YX5Gp|1Z&=xP1 z-kzrYMqd+)7vH?)G8Uaw&@F)UxMT zz+O;J%F$9*>9!FCbH=710697qkVa3Z8nnBB^!Al~UjXDZ*zXQ~oZr5 zB9zLEOjPqpGz7#5s|l|;08-A04Amv+Q4%!Vh%541K}TR{JDH9E4nS751WlZuCWTp8 z7K;L+Mar}d*dXylN@8f93Qe)Z|GfypFc8x9{yG~X0*R@NRNTEC)Q!!T(lJZeEOykb)X${%>K6oih(O+?&sVMMH%!b;82#mrTY{*&PC z=XO#W0Ub?ZuW~VZ%l=)B5sPO?)s)BW-ow)7~?i=0Ql{d^>6 z`o=;F3*hQvFkRxttXHbX$Q$}{NVRNQ>ss_dNhYb@rAZTVf8scIDxFu8j^j9q!Cj$H z(s3NeaS{VhojX15CD(nTs^a3f1_h8j*7@saweZlP6Ly90uT*-$tp&#}{-Xc0`|?36 z{_P~9i%5Nu7aBkQW_j29?haQhT$?+oXGyIBKC17XaOlIJQ|1c?lD5xQR~NTXY*jgq z^UK#lj*~b&=7k^gq~jbqdgs-n2`5=sHK6N8*SW8{>kj_80G#XHQwnMyy!LU2>%`aL z%LCJuG<3`59Om%_z`(e-jz88pv9SoqD*WR08o$cC sn+DTOpvSW(Fz?E^NE|@u4}GrS-xEWL^_Of4r2qf`07*qoM6N<$g1-(-FaQ7m literal 0 HcmV?d00001 diff --git a/desktop/public/harness-logos/openclaw.svg b/desktop/public/harness-logos/openclaw.svg new file mode 100644 index 000000000..8c1496d62 --- /dev/null +++ b/desktop/public/harness-logos/openclaw.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/desktop/src/features/onboarding/ui/RuntimeIcon.tsx b/desktop/src/features/onboarding/ui/RuntimeIcon.tsx index 11b4be8a8..3cee0be9a 100644 --- a/desktop/src/features/onboarding/ui/RuntimeIcon.tsx +++ b/desktop/src/features/onboarding/ui/RuntimeIcon.tsx @@ -18,13 +18,15 @@ const RUNTIME_LOGOS: Record = { // Public-path logos for bundled presets. Served from /harness-logos/ at runtime. // Keys match the preset `id` values emitted by the backend PRESET_HARNESSES. -const PRESET_LOGOS: Record = { +export const PRESET_LOGOS: Record = { cursor: "/harness-logos/cursor.png", omp: "/harness-logos/omp.png", grok: "/harness-logos/grok.png", opencode: "/harness-logos/opencode.svg", kimi: "/harness-logos/kimi.png", amp: "/harness-logos/amp.png", + hermes: "/harness-logos/hermes.png", + openclaw: "/harness-logos/openclaw.svg", }; function isBuzzRuntime(runtime: AcpRuntimeCatalogEntry): boolean { diff --git a/desktop/src/features/onboarding/ui/presetLogos.test.mjs b/desktop/src/features/onboarding/ui/presetLogos.test.mjs new file mode 100644 index 000000000..81b22ec8b --- /dev/null +++ b/desktop/src/features/onboarding/ui/presetLogos.test.mjs @@ -0,0 +1,75 @@ +/** + * Preset-logo coverage guard. + * + * Every tier-2 preset the backend emits must have a bundled logo, or it renders + * as the generic TerminalSquare fallback next to siblings that show real marks. + * The two sides live in different languages — Rust `PRESET_HARNESSES` vs the TS + * `PRESET_LOGOS` record — so no compiler catches drift, and `RuntimeIcon`'s + * `onError` fallback hides a missing file at runtime. This test reads the Rust + * source as text (the same trick `motion.test.mjs` uses for CSS) and asserts + * both directions plus on-disk existence of every mapped file. + */ + +import assert from "node:assert/strict"; +import { existsSync, readFileSync } from "node:fs"; +import path from "node:path"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; + +import { PRESET_LOGOS } from "./RuntimeIcon.tsx"; + +const desktopRoot = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + "../../../..", +); + +const discoveryRs = readFileSync( + path.join(desktopRoot, "src-tauri/src/managed_agents/discovery.rs"), + "utf8", +); + +const presetBlock = discoveryRs.match( + /const PRESET_HARNESSES: &\[PresetHarness\] = &\[([\s\S]*?)\n\];/, +); +assert.ok(presetBlock, "could not locate PRESET_HARNESSES in discovery.rs"); + +const presetIds = [...presetBlock[1].matchAll(/^\s{8}id: "([^"]+)",$/gm)].map( + (match) => match[1], +); + +test("PRESET_HARNESSES parse found the preset ids", () => { + // Guards the regex itself: a struct-field rename would otherwise silently + // yield zero ids and make every assertion below vacuously pass. + assert.ok( + presetIds.length >= 8, + `expected at least 8 preset ids, parsed ${presetIds.length}`, + ); +}); + +for (const id of presetIds) { + test(`preset "${id}" has a bundled logo`, () => { + const logoPath = PRESET_LOGOS[id]; + assert.ok( + logoPath, + `preset "${id}" has no PRESET_LOGOS entry — it renders the generic ` + + `TerminalSquare fallback. Add desktop/public${logoPath ?? `/harness-logos/${id}.png`} ` + + `and map it in RuntimeIcon.tsx.`, + ); + assert.ok( + existsSync(path.join(desktopRoot, "public", logoPath)), + `PRESET_LOGOS["${id}"] points at ${logoPath}, which is missing from ` + + `desktop/public — RuntimeIcon's onError would silently fall back.`, + ); + }); +} + +test("PRESET_LOGOS has no entries for unknown presets", () => { + const unknown = Object.keys(PRESET_LOGOS).filter( + (id) => !presetIds.includes(id), + ); + assert.deepEqual( + unknown, + [], + `PRESET_LOGOS maps ids the backend does not emit as presets: ${unknown.join(", ")}`, + ); +}); diff --git a/desktop/test-loader-hooks.mjs b/desktop/test-loader-hooks.mjs index c71cc1859..dd6f34327 100644 --- a/desktop/test-loader-hooks.mjs +++ b/desktop/test-loader-hooks.mjs @@ -54,7 +54,20 @@ const stubModules = new Map([ const STUB_URL_PREFIX = "buzz-test-stub:"; +// Vite resolves asset imports (`./logo.png`, `./logo.png?inline`) to a URL or +// base64 string at bundle time; node's ESM resolver has no such loader and +// throws on the query suffix. Serve an inert string so components that embed +// assets stay unit-testable. +const ASSET_SPECIFIER = /\.(?:png|jpe?g|gif|svg|webp|avif|ico)(?:\?[^/]*)?$/; +const ASSET_URL_PREFIX = "buzz-test-asset:"; + export function resolve(specifier, context, nextResolve) { + if (ASSET_SPECIFIER.test(specifier)) { + return { + shortCircuit: true, + url: `${ASSET_URL_PREFIX}${specifier}`, + }; + } if (stubModules.has(specifier)) { return { shortCircuit: true, @@ -98,6 +111,14 @@ export function resolve(specifier, context, nextResolve) { } export async function load(url, context, nextLoad) { + if (url.startsWith(ASSET_URL_PREFIX)) { + return { + format: "module", + shortCircuit: true, + source: 'export default "test-asset";\n', + }; + } + if (url.startsWith(STUB_URL_PREFIX)) { return { format: "module",