feat(control-plane): a logo for every coding agent, not just five (#3101)

#3079 resolved `metadata.harness` to a logo, but registered only the five ids
hindsight-coding-agents emitted at the time. That integration (#2522) now ships
ten, so the majority of harnesses fell back to a raw `harness=<id>` metadata chip
— the exact thing the logo was introduced to replace.

Register the full emitted set, taken from both places that define an id:
`src/harness/hook-lifecycle.ts` (one HookSpec per hook-driven agent) and the
persistent-plugin entrypoints in `src/harness/registry.ts`, whose id is their
`createPluginEntry(...)` argument. New: `antigravity-cli`, `cline-cli`,
`copilot-cli`, `devin-cli`, `grok-build`, `kilo`.

Icons come from `hindsight-docs/static/img/icons/` where the docs site already
carries the brand (Cline, GitHub Copilot, Devin, Grok). It carries none for
Antigravity or Kilo, so those are the vendors' own marks; the registry comment
and CLAUDE.md now say that is allowed rather than implying the docs dir is the
only source.

`gemini` stays registered even though the integration replaced that harness with
`antigravity-cli` and nothing emits it any more: documents retained while it did
are still in people's banks and should keep their logo. The test that pins the
registry to the emitted set now carries that as an explicit RETIRED list, so a
speculative id still can't sneak in.

Monochrome dark-on-transparent marks (Cline, Copilot, Devin) get `dark:invert`.
Grok deliberately does not — it is a filled black tile with a white glyph, so it
reads on dark already and inverting would burn a white square into the row.
Verified all eleven at 16px against both themes.
This commit is contained in:
Nicolò Boschi
2026-07-31 15:03:53 +02:00
committed by GitHub
parent a90f922376
commit c3b98998b7
9 changed files with 89 additions and 16 deletions
+10 -5
View File
@@ -296,18 +296,23 @@ document it retains, so the control plane can show its logo instead of another
- tag `harness:<id>` — the same value, so the documents list can filter on it
The ids are defined by that integration's HookSpecs
(`src/harness/hook-lifecycle.ts`) and `src/harness/registry.ts` — currently
`claude-code`, `codex`, `cursor-cli`, `gemini`, `opencode`.
(`src/harness/hook-lifecycle.ts`) plus the persistent-plugin entrypoints
registered in `src/harness/registry.ts`, whose id is their
`createPluginEntry(...)` argument — currently `antigravity-cli`, `claude-code`,
`cline-cli`, `codex`, `copilot-cli`, `cursor-cli`, `devin-cli`, `grok-build`,
`kilo`, `opencode`.
The control plane resolves the value in
`hindsight-control-plane/src/lib/harness-logo.ts` (metadata wins over the tag) and
renders it with `components/ui/harness-logo.tsx` in the documents table and the
document detail dialog. **Adding a harness to the integration means adding it to
that registry in the same change**: copy its icon from
`hindsight-docs/static/img/icons/` into
`hindsight-docs/static/img/icons/` (or take it from the agent's own brand assets
when the docs site carries none) into
`hindsight-control-plane/public/img/harness/` and add one entry. Don't register
ids nothing writes — a test asserts the registry matches the emitted set. An
unregistered harness is not an error: it renders no logo and still shows as
ids nothing writes — a test asserts the registry matches the emitted set, plus an
explicit list of retired ids kept so already-retained documents keep their logo.
An unregistered harness is not an error: it renders no logo and still shows as
ordinary metadata.
### Adding New Integrations
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

@@ -0,0 +1 @@
<svg viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2"><path d="M363.414 85.141c58.666 0 106.24 47.787 106.24 106.731v35.563l30.933 61.781c3.05 6.101 3.05 13.312-.043 19.392l-30.89 61.099v35.584c0 58.923-47.574 106.71-106.24 106.71H150.912c-58.688 0-106.24-47.787-106.24-106.71v-35.584L13.099 308.8a21.57 21.57 0 01-.064-19.776l31.616-61.59v-35.562c0-58.944 47.573-106.73 106.24-106.73h212.523zM176.32 204.801c-26.603 0-48.512 21.908-48.512 48.511v86.23c.555 26.218 22.272 47.488 48.512 47.488 26.219 0 47.936-21.27 48.49-47.488v-86.23c0-26.603-21.887-48.49-48.49-48.512zm156.288 0c-26.603 0-48.512 21.908-48.512 48.511v86.23c0 26.602 21.91 48.512 48.512 48.512 26.603 0 48.512-21.91 48.512-48.512v-86.23c0-26.603-21.91-48.512-48.512-48.512h-.021.021z" fill="#383e45"/><path d="M257.152 118.57c32.533 0 59.285-26.751 59.285-59.285C316.437 26.752 289.685 0 257.152 0s-59.285 26.752-59.285 59.285c0 32.534 26.752 59.286 59.285 59.286z" fill="#383e45"/></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M23.922 16.992c-.861 1.495-5.859 5.023-11.922 5.023-6.063 0-11.061-3.528-11.922-5.023A.641.641 0 0 1 0 16.736v-2.869a.841.841 0 0 1 .053-.22c.372-.935 1.347-2.292 2.605-2.656.167-.429.414-1.055.644-1.517a10.195 10.195 0 0 1-.052-1.086c0-1.331.282-2.499 1.132-3.368.397-.406.89-.717 1.474-.952 1.399-1.136 3.392-2.093 6.122-2.093 2.731 0 4.767.957 6.166 2.093.584.235 1.077.546 1.474.952.85.869 1.132 2.037 1.132 3.368 0 .368-.014.733-.052 1.086.23.462.477 1.088.644 1.517 1.258.364 2.233 1.721 2.605 2.656a.832.832 0 0 1 .053.22v2.869a.641.641 0 0 1-.078.256ZM12.172 11h-.344a4.323 4.323 0 0 1-.355.508C10.703 12.455 9.555 13 7.965 13c-1.725 0-2.989-.359-3.782-1.259a2.005 2.005 0 0 1-.085-.104L4 11.741v6.585c1.435.779 4.514 2.179 8 2.179 3.486 0 6.565-1.4 8-2.179v-6.585l-.098-.104s-.033.045-.085.104c-.793.9-2.057 1.259-3.782 1.259-1.59 0-2.738-.545-3.508-1.492a4.323 4.323 0 0 1-.355-.508h-.016.016Zm.641-2.935c.136 1.057.403 1.913.878 2.497.442.544 1.134.938 2.344.938 1.573 0 2.292-.337 2.657-.751.384-.435.558-1.15.558-2.361 0-1.14-.243-1.847-.705-2.319-.477-.488-1.319-.862-2.824-1.025-1.487-.161-2.192.138-2.533.529-.269.307-.437.808-.438 1.578v.021c0 .265.021.562.063.893Zm-1.626 0c.042-.331.063-.628.063-.894v-.02c-.001-.77-.169-1.271-.438-1.578-.341-.391-1.046-.69-2.533-.529-1.505.163-2.347.537-2.824 1.025-.462.472-.705 1.179-.705 2.319 0 1.211.175 1.926.558 2.361.365.414 1.084.751 2.657.751 1.21 0 1.902-.394 2.344-.938.475-.584.742-1.44.878-2.497Z"/><path d="M14.5 14.25a1 1 0 0 1 1 1v2a1 1 0 0 1-2 0v-2a1 1 0 0 1 1-1Zm-5 0a1 1 0 0 1 1 1v2a1 1 0 0 1-2 0v-2a1 1 0 0 1 1-1Z"/></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

@@ -0,0 +1 @@
<svg role="img" width="24" height="24" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><title>Devin</title><path d="M2.033 9.867l2.554 1.483a.589.589 0 00.592 0l2.554-1.483.01-.008a.608.608 0 00.11-.084l.013-.015a.631.631 0 00.076-.1c.003-.005.008-.01.01-.016a.558.558 0 00.052-.125l.007-.028a.611.611 0 00.019-.14V7.868c0-.572.307-1.105.8-1.392a1.595 1.595 0 011.598 0l1.277.742a.54.54 0 00.129.053l.028.01c.044.01.088.015.133.016h.006l.013-.002a.587.587 0 00.27-.074l.011-.004 2.554-1.483a.596.596 0 00.297-.516V2.253a.595.595 0 00-.297-.516L12.293.257a.587.587 0 00-.591 0L9.148 1.737l-.01.01a.609.609 0 00-.109.083l-.014.015a.632.632 0 00-.076.1c-.003.005-.008.01-.01.016a.57.57 0 00-.052.124l-.007.028a.612.612 0 00-.018.14v1.483c0 .572-.307 1.105-.8 1.393a1.597 1.597 0 01-1.599 0l-1.276-.742a.603.603 0 00-.13-.053l-.028-.008a.658.658 0 00-.133-.018h-.02a.57.57 0 00-.269.074c-.003.002-.008.002-.012.005L2.033 5.872a.596.596 0 00-.297.515v2.966c0 .213.113.41.297.515z"></path><path d="M15.943 10.607a1.596 1.596 0 011.599 0l1.276.74c.041.025.085.04.13.055l.028.008c.043.01.088.016.133.018h.005c.005 0 .01-.002.014-.003a.474.474 0 00.122-.016l.021-.005a.616.616 0 00.126-.052c.004-.002.009-.002.013-.005l2.554-1.482a.597.597 0 00.297-.516V6.383a.596.596 0 00-.297-.515l-2.552-1.483a.587.587 0 00-.592 0l-2.553 1.482-.011.008a.61.61 0 00-.108.084l-.014.016a.637.637 0 00-.076.1c-.003.005-.008.01-.01.016a.57.57 0 00-.052.124l-.007.029a.612.612 0 00-.018.14v1.482c0 .572-.307 1.105-.8 1.393a1.597 1.597 0 01-1.599 0l-1.276-.742a.584.584 0 00-.13-.053l-.028-.008a.62.62 0 00-.133-.018h-.02a.587.587 0 00-.269.074l-.012.004L9.15 10a.596.596 0 00-.296.516v2.966c0 .212.112.409.296.515l2.554 1.483s.008.002.012.005c.04.022.082.04.126.052l.02.004a.57.57 0 00.123.017l.014.002h.006c.054 0 .108-.01.16-.025a.587.587 0 00.13-.054l1.277-.741a1.597 1.597 0 012.398 1.392v1.482c0 .049.007.095.019.14l.007.028a.619.619 0 00.051.125c.004.006.008.01.01.016a.6.6 0 00.076.1l.014.015c.033.032.069.06.108.084.004.002.006.006.011.008l2.554 1.483a.59.59 0 00.593 0l2.554-1.483a.597.597 0 00.296-.516v-2.965a.595.595 0 00-.296-.516l-2.554-1.483s-.008-.002-.012-.005a.54.54 0 00-.126-.051c-.007-.003-.013-.003-.02-.005a.635.635 0 00-.125-.017h-.018a.557.557 0 00-.16.026.588.588 0 00-.13.053l-1.276.742a1.595 1.595 0 01-1.598 0 1.615 1.615 0 010-2.785l-.005-.001z"></path><path d="M14.848 18.265l-2.554-1.482-.012-.005a.526.526 0 00-.126-.052c-.007-.002-.014-.002-.02-.005a.64.64 0 00-.124-.017h-.02a.56.56 0 00-.16.026.588.588 0 00-.13.053l-1.276.742a1.594 1.594 0 01-1.598 0c-.493-.286-.8-.82-.8-1.393V14.65a.563.563 0 00-.018-.14l-.008-.028a.604.604 0 00-.051-.124l-.01-.017a.603.603 0 00-.076-.1l-.014-.015a.596.596 0 00-.109-.084c-.003-.002-.005-.006-.01-.008L5.178 12.65a.587.587 0 00-.591 0l-2.554 1.483a.596.596 0 00-.297.516v2.965c0 .213.113.41.297.516l2.554 1.483.012.004a.618.618 0 00.267.074l.016.002h.007a.55.55 0 00.16-.026.584.584 0 00.129-.053l1.277-.742a1.597 1.597 0 012.398 1.393v1.482c0 .05.007.095.019.14l.007.028c.013.044.03.085.051.125l.01.016c.022.036.047.07.076.1l.014.015c.032.032.069.06.109.084l.01.008 2.554 1.483a.587.587 0 00.593 0l2.554-1.483a.596.596 0 00.296-.515v-2.966a.596.596 0 00-.296-.516h-.002z"></path></svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" image-rendering="optimizeQuality" fill-rule="evenodd" clip-rule="evenodd" viewBox="0 0 512 509.641"><path d="M115.612 0h280.776C459.975 0 512 52.026 512 115.612v278.416c0 63.587-52.025 115.613-115.612 115.613H115.612C52.026 509.641 0 457.615 0 394.028V115.612C0 52.026 52.026 0 115.612 0z"/><path fill="#fff" d="M213.235 306.019l178.976-180.002v.169l51.695-51.763c-.924 1.32-1.86 2.605-2.785 3.89-39.281 54.164-58.46 80.649-43.07 146.922l-.09-.101c10.61 45.11-.744 95.137-37.398 131.836-46.216 46.306-120.167 56.611-181.063 14.928l42.462-19.675c38.863 15.278 81.392 8.57 111.947-22.03 30.566-30.6 37.432-75.159 22.065-112.252-2.92-7.025-11.67-8.795-17.792-4.263l-124.947 92.341zm-25.786 22.437l-.033.034L68.094 435.217c7.565-10.429 16.957-20.294 26.327-30.149 26.428-27.803 52.653-55.359 36.654-94.302-21.422-52.112-8.952-113.177 30.724-152.898 41.243-41.254 101.98-51.661 152.706-30.758 11.23 4.172 21.016 10.114 28.638 15.639l-42.359 19.584c-39.44-16.563-84.629-5.299-112.207 22.313-37.298 37.308-44.84 102.003-1.128 143.81z"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 32 32">
<rect width="32" height="32" fill="oklch(95% 0.15 108)"/>
<g fill="#000000">
<path d="M23,26v-2h3v-5l-2-2h-4v2h-3v5l2,2h4ZM20,20h3v3h-3v-3Z"/>
<rect x="12" y="17" width="3" height="3"/>
<polygon points="26 12 23 12 23 9 20 6 17 6 17 9 20 9 20 12 17 12 17 15 26 15 26 12"/>
<path d="M0,0v32h32V0H0ZM29,29H3V3h26v26Z"/>
<polygon points="15 26 15 23 9 23 9 17 6 17 6 23.1875 8.8125 26 15 26"/>
<rect x="12" y="6" width="3" height="3"/>
<polygon points="9 12 12 12 12 15 15 15 15 12 12 9 9 9 9 6 6 6 6 15 9 15 9 12"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 686 B

@@ -11,13 +11,15 @@
* hand in the UI.
*
* The ids below are exactly the ones that integration emits — see its
* `src/harness/hook-lifecycle.ts` (one HookSpec per agent) and
* `src/harness/registry.ts`. Do not add entries for agents that cannot appear
* yet: an id nothing writes is a logo nothing renders. When that integration
* gains a harness, add it here in the same change — drop its icon in
* `public/img/harness/` (copied from `hindsight-docs/static/img/icons/`; the
* docs site and the control plane are separate packages and cannot share a
* static dir) and add one entry.
* `src/harness/hook-lifecycle.ts` (one HookSpec per hook-driven agent) and
* `src/harness/registry.ts` (the persistent-plugin agents, whose id is the
* `createPluginEntry(...)` argument of their entrypoint). Do not add entries for
* agents that cannot appear yet: an id nothing writes is a logo nothing renders.
* When that integration gains a harness, add it here in the same change — drop
* its icon in `public/img/harness/` (copied from
* `hindsight-docs/static/img/icons/`, or taken from the agent's own brand assets
* when the docs site carries none; the docs site and the control plane are
* separate packages and cannot share a static dir) and add one entry.
*
* An unknown harness is not an error: it renders no logo, and the value still
* shows as an ordinary metadata chip.
@@ -40,15 +42,46 @@ export interface HarnessLogoEntry {
/** Exported for the test that checks every entry's asset is actually shipped. */
export const HARNESS_LOGO_REGISTRY: Record<string, HarnessLogoEntry> = {
"antigravity-cli": {
id: "antigravity-cli",
label: "Antigravity CLI",
src: "/img/harness/antigravity-cli.png",
},
"claude-code": { id: "claude-code", label: "Claude Code", src: "/img/harness/claude-code.png" },
"cline-cli": {
id: "cline-cli",
label: "Cline CLI",
src: "/img/harness/cline-cli.svg",
invertOnDark: true,
},
codex: { id: "codex", label: "Codex", src: "/img/harness/codex.svg", invertOnDark: true },
"copilot-cli": {
id: "copilot-cli",
label: "GitHub Copilot CLI",
src: "/img/harness/copilot-cli.svg",
invertOnDark: true,
},
"cursor-cli": {
id: "cursor-cli",
label: "Cursor CLI",
src: "/img/harness/cursor-cli.svg",
invertOnDark: true,
},
"devin-cli": {
id: "devin-cli",
label: "Devin CLI",
src: "/img/harness/devin-cli.svg",
invertOnDark: true,
},
// Retired: the Gemini CLI harness was replaced by `antigravity-cli`, so nothing
// emits this id any more. The entry stays because documents retained while it
// did are still in people's banks, and they should keep their logo.
gemini: { id: "gemini", label: "Gemini", src: "/img/harness/gemini.svg" },
// Not inverted: unlike the other monochrome marks this one is a filled black
// tile with a white glyph, so it stays legible on dark — inverting it would
// burn a white square into the row.
"grok-build": { id: "grok-build", label: "Grok Build", src: "/img/harness/grok-build.svg" },
kilo: { id: "kilo", label: "Kilo CLI", src: "/img/harness/kilo.svg" },
opencode: { id: "opencode", label: "OpenCode", src: "/img/harness/opencode.png" },
};
@@ -34,9 +34,25 @@ describe("documentHarness", () => {
});
describe("resolveHarnessLogo", () => {
// The exact set hindsight-coding-agents emits (src/harness/hook-lifecycle.ts
// + registry.ts). The registry must cover these and nothing speculative.
const EMITTED_HARNESSES = ["claude-code", "codex", "cursor-cli", "gemini", "opencode"];
// The exact set hindsight-coding-agents emits: one per HookSpec in
// src/harness/hook-lifecycle.ts, plus the persistent-plugin harnesses whose id
// is their entrypoint's createPluginEntry(...) argument (opencode, kilo,
// cline-cli). The registry must cover these and nothing speculative.
const EMITTED_HARNESSES = [
"antigravity-cli",
"claude-code",
"cline-cli",
"codex",
"copilot-cli",
"cursor-cli",
"devin-cli",
"grok-build",
"kilo",
"opencode",
];
// Ids the integration used to emit. Kept so documents already retained under
// them keep their logo; a new id never belongs here.
const RETIRED_HARNESSES = ["gemini"];
it("resolves every id the coding-agent integration emits", () => {
for (const id of EMITTED_HARNESSES) {
@@ -45,7 +61,9 @@ describe("resolveHarnessLogo", () => {
});
it("registers no harness that nothing writes", () => {
expect(Object.keys(HARNESS_LOGO_REGISTRY).sort()).toEqual([...EMITTED_HARNESSES].sort());
expect(Object.keys(HARNESS_LOGO_REGISTRY).sort()).toEqual(
[...EMITTED_HARNESSES, ...RETIRED_HARNESSES].sort()
);
});
it("normalizes case, spacing and underscores", () => {