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 000000000..6acf0020b Binary files /dev/null and b/desktop/public/harness-logos/hermes.png differ 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",