diff --git a/devlog/_plan/260831_aside_client_and_integrations_ux/005_remaining_marks_provenance.md b/devlog/_plan/260831_aside_client_and_integrations_ux/005_remaining_marks_provenance.md new file mode 100644 index 0000000000..3c893eec35 --- /dev/null +++ b/devlog/_plan/260831_aside_client_and_integrations_ux/005_remaining_marks_provenance.md @@ -0,0 +1,97 @@ +# The three clients still on a monogram + +Continuation of `004_brand_mark_provenance.md`, which closed six clients. After +that pass `CLIENT_MARKS` covers nine of twelve; `hermes`, `gajae` and `mcode` +still render `label.slice(0, 1)`. + +004 recorded `gajae` and `hermes` as monogram-only because neither publishes an +SVG with path geometry. That verdict stands on its own terms and is now +superseded by a wider rule: raster-to-vector conversion is authorized, so "the +vendor ships no SVG" no longer ends the search. Every mark below is traced from +the product's own raster asset rather than redrawn. + +All three were located 2026-08-31 through the `aside-jun` skill driving a +signed-in Aside browser, then re-fetched and verified locally. + +## mcode — MiniMax Code + +A genuine first-party SVG exists and 004 simply had not found it. + +- Source: `https://raw.githubusercontent.com/MiniMax-AI/MiniMax-01/main/figures/minimax.svg` +- 1255 bytes, `viewBox="0 0 490.16 411.7"`, one `` filled by a three-stop + linear gradient (`#e4177f` to `#e73562` to `#e94e4a`). +- It is the standalone symbol — the interlocking wave glyph with no wordmark + beside it. The docs-site asset (`mintcdn.com/minimax-zh/.../logo/light.svg`) + is the 129x32 horizontal lockup and was rejected for that reason: a wordmark + in a 20px square renders as unreadable letter mush. +- Publisher mark rather than product mark. MiniMax Code ships no mark of its + own and MiniMax is its publisher, so this is the closest first-party asset. +- Committed unmodified apart from dropping the Chinese-language `` and + layer-name metadata the authoring tool left behind. The gradient id is + renamed: `未命名的渐变_6` means "unnamed gradient 6", it collides across + inlined documents, and a non-ASCII id in a shared namespace is a trap. +- Multi-color, so it must NOT enter `MONOCHROME_CLIENT_MARKS`: masking would + flatten the gradient to one ink. + +## hermes — Hermes agent + +No usable SVG upstream; traced from the product's own application icon. + +- Rejected first: `website/static/img/favicon.svg` is 113 bytes and its whole + body is one `<text>` element. 004 already recorded this. +- Rejected second: `https://nousresearch.com/safari-pinned-tab.svg` (12746 + bytes, potrace output). Its first path is `M40 2560 l0 -2560 2520 0 2520 0 0 + 2560 0 2560 -2520 0 -2520 0 0 -2560z` — the full 512-unit frame. Rendered at + 20px that is a black square with a hairline hole, which is worse than a + monogram. +- Accepted: `apps/desktop/assets/icon.png` from `NousResearch/hermes-agent`, + 574273 bytes, 1024x1024 RGBA, artwork bounded at (101,108)-(924,914). This is + the icon the Hermes desktop application ships, so it is the product's own + mark, not the publisher's. +- Quantizing the opaque pixels shows two inks: a light plate (340877 px) and + black art (241765 px), with ~20k px of antialiasing between them. It is a + single-ink illustration on a rounded plate. +- Traced with `potrace -s --flat --turdsize 8 --alphamax 1.0 --opttolerance + 0.2` over the mask `alpha > 128 AND mean(rgb) < 110`, which keeps the black + art and discards the plate. One path, squared to `viewBox="0 0 823 823"` by + centering the 823x806 trace. +- `fill="currentColor"`, and it MUST join `MONOCHROME_CLIENT_MARKS`. A 20px + render on `#0d1117` confirmed the untinted mark is invisible in dark mode — + the same failure `prime`, `opencode` and `kimi` already have. + +## gajae — Gajae Code + +No SVG anywhere upstream, confirmed twice; traced from the mascot. + +- Searched and found empty: `assets/`, `public/` (404), `docs/`, plus + `assets/logo.svg`, `assets/favicon.svg`, `public/logo.svg`, + `public/favicon.svg`, `docs/logo.svg` (all 404), and every published + `@gajae-code/*` npm tarball at 0.15.6 (no SVG entries). `docs/brand-assets.md` + lists the active marks as PNG only. +- Accepted source: `assets/character.png`, 3190496 bytes, 1550x2048 RGBA, + transparent background. +- It is a vertical lockup: the mascot occupies y < 1650 and the `gajae-code` + wordmark sits below it. Rows 1650-1682 are fully transparent, which is the + seam the crop uses. Only the mascot is traced; a wordmark would not survive + 20px. +- The artwork is upscaled pixel art, so tracing at source resolution follows + every staircase and produced a 1.3 MB SVG. Downsampling to a 128px box with + Lanczos plus a 0.6px Gaussian first, then tracing, gives ~31 KB. That is + larger than any existing mark (`zcode.svg`, 11037 bytes) because this one is + an illustration rather than a glyph. +- Seven color layers, k-means++ seeded at 3 for determinism, painted + largest-area first. The committed file's fills are `#1d0a04`, `#561203`, + `#981001`, `#e3770c`, `#d32e02`, `#8f3a04` and `#02ac61` — read off + `gajae-code.svg` rather than off an earlier tuning run, whose centers differed + because it quantized at a different target size. The smallest layer is the + visor green and a fixed area floor would have dropped it, so the floor is a + fraction of the opaque area instead. +- Multi-color, so NOT in `MONOCHROME_CLIENT_MARKS`. + +## Rule this pass establishes + +A mark may be traced from the product's own raster asset when no vector exists, +provided the trace follows the source pixels rather than redrawing them, the +conversion parameters are recorded, and the result is verified by rendering at +the size it will actually be used. Tracing a wordmark into a square slot is +still refused, and so is a full-frame silhouette plate. diff --git a/devlog/_plan/260831_aside_client_and_integrations_ux/060_wp7_remaining_marks.md b/devlog/_plan/260831_aside_client_and_integrations_ux/060_wp7_remaining_marks.md new file mode 100644 index 0000000000..a585c6eb44 --- /dev/null +++ b/devlog/_plan/260831_aside_client_and_integrations_ux/060_wp7_remaining_marks.md @@ -0,0 +1,75 @@ +# wp7 — the three remaining marks + +Depends on nothing in this unit that is not already merged. Provenance and +conversion parameters are in 005; this document is the diff. + +## New assets + +`gui/public/provider-icons/minimax.svg` — the MiniMax symbol, fetched not +traced. Two edits to the upstream file: the `<title>资源 2` and the +`data-name` layer wrappers go (authoring-tool residue), and the gradient id +becomes `minimax-wave` so it cannot collide and carries no non-ASCII. + +`gui/public/provider-icons/hermes-agent.svg` — one `currentColor` path, +`viewBox="0 0 823 823"`. Named `hermes-agent` rather than `hermes` because +Hermes is also a provider name in this repo and `provider-icons/` is one flat +namespace. + +`gui/public/provider-icons/gajae-code.svg` — seven fill layers, largest area +first, `viewBox="0 0 128 128"`-class square box produced by centering the +traced bounds. + +## gui/public/provider-icons/README.md + +The "Two export clients deliberately have NO mark" section is now false and is +replaced. `gajae` and `hermes` move into the provenance list with their trace +parameters; `mcode` joins them. The section that remains explains the tracing +rule from 005 — a raster may be traced, a wordmark may not be squeezed into a +square slot, and a full-frame silhouette is rejected. + +## gui/src/components/apikeys-workspace/client-config-clients.ts + +`CLIENT_MARKS` gains three entries: + +```ts +hermes: "/provider-icons/hermes-agent.svg", +gajae: "/provider-icons/gajae-code.svg", +mcode: "/provider-icons/minimax.svg", +``` + +`MONOCHROME_CLIENT_MARKS` gains `hermes` only. `gajae` is seven inks and +`mcode` is a gradient; masking either would flatten it. + +The block comment above `CLIENT_MARKS` currently says two clients are absent on +purpose. That is now wrong in a way a reader would trust, so it is rewritten to +state the tracing rule and that every client has a mark. + +## Tests + +`gui/tests/client-marks-assets.test.ts` already covers more than the plan +originally credited it with. It asserts file existence, README provenance, the +no-``/no-``/must-have-geometry rule, that no multi-color mark is +masked, that the four known-invisible marks ARE masked, and that `dsh` is not. +Every one of those extends to the three new files without an edit, so the +"new guard: no `` element" the plan proposed would have been a duplicate +of an existing test rather than new coverage. + +What is genuinely uncovered is the completeness of the map. Nothing asserts that +every id in `CLIENTS` has a mark, so an entry dropped in a merge degrades to a +monogram silently and looks identical to a client that never had one. That guard +is new, and it was driven red by removing the `mcode` entry. + +Second new guard: a traced mark must record its raster source and its tracer +invocation in the README. A fetched mark has a URL to check; a traced one has +nothing to reproduce it from unless the parameters are written down. Driven red +by replacing the word `potrace` in the README. + +The mask-set expectations do need extending: `hermes` joins the pinned list of +marks that must be masked, while `gajae` and `mcode` are caught by the existing +multi-color assertion the moment they are added to the set by mistake. + +## Verification + +`cd gui && bun test tests/client-marks-assets.test.ts` plus the mask guard. +A 20px render of each new mark on `#ffffff` and on `#0d1117`, which is the check +that caught the Hermes dark-mode invisibility in the first place. diff --git a/devlog/_plan/260831_aside_client_and_integrations_ux/070_wp8_integration_marks.md b/devlog/_plan/260831_aside_client_and_integrations_ux/070_wp8_integration_marks.md new file mode 100644 index 0000000000..60ee9c7ca3 --- /dev/null +++ b/devlog/_plan/260831_aside_client_and_integrations_ux/070_wp8_integration_marks.md @@ -0,0 +1,121 @@ +# wp8 — one mark, every Integrations surface + +Depends on wp7: the shared component is only worth building once every client +has an asset, otherwise half the surfaces render monograms and the change looks +like a regression. + +## The problem + +`ClientConfigRow.tsx` is the only surface that draws a mark, and it owns the +img-versus-mask decision inline: + +```tsx +{mark ? monochrome ? + : + : {label.slice(0, 1)}} +``` + +Copying that ternary into three more files would put the invisibility rule in +four places, and the rule is exactly the thing that was already got wrong once. + +## New module: gui/src/components/ClientMark.tsx + +One component, one decision. Props: `markId` (the asset key, not the client id), +`label` (for the monogram letter), `size`, and `className`. + +It reads from a new shared map rather than `CLIENT_MARKS`, because the +Integrations page needs marks for four clients that are not export clients at +all — `codex`, `claude`, `claudeDesktop`, `grok`. Those are different id +namespaces that happen to overlap on strings like `claude`. + +## New module: gui/src/components/integration-marks.ts + +`INTEGRATION_MARKS: Record` mapping +every Integrations row to an asset already committed: + +- `codex` -> `/provider-icons/openai.svg` +- `claude`, `claudeDesktop` -> `/provider-icons/claude-color.svg` +- `grok` -> `/provider-icons/grok.svg` +- the twelve file clients -> the same assets `CLIENT_MARKS` uses + +None of the three non-file marks is masked, and the audit caught the plan being +wrong about two. `openai.svg` is a single fill, but that fill is `#10A37F` — +OpenAI's own green, the brand itself, which is exactly the `dsh` case +`client-config-clients.ts` already documents. Masking it would repaint a +trademark in the theme's text color. `grok.svg` is `#000000`, a genuine neutral +and therefore a real masking candidate — but it is xAI's published asset with a +literal fill, and rewriting it to `currentColor` is a change to someone else's +mark rather than a rendering decision. It stays an image and the dark-theme +contrast problem is recorded rather than papered over. + +So `MONOCHROME_INTEGRATION_MARKS` is exactly `MONOCHROME_CLIENT_MARKS`' assets +and nothing more. Keying it by ASSET PATH rather than client id is the one +improvement worth keeping from the original sketch: `kimi-color.svg` is reachable +as both a provider icon and a client mark, and a path-keyed set cannot mask it +on one surface and leave it unmasked on the other. + +## Surfaces + +`IntegrationsOverview.tsx` — `OverviewCard` puts a `` before the +`

`, inside `.integration-card-head`. It is `aria-hidden`: the card's +accessible name is the title button, and a mark that joined the name would read +"Claude Claude". + +`Integrations.tsx` — each tab button gets a 16px mark before its label. The tab +strip is 17 items on one row, so the mark is the thing that makes it scannable; +this is where the change pays for itself. + +`FileIntegrationPage.tsx` — a 24px mark in `.integration-client-head`, before +the `

`. + +`ClientConfigRow.tsx` — the inline ternary is deleted and replaced by +``. Behavior is identical, which the existing panel test asserts. + +## The Codex label + +`integrations.tab.codex` is `"Codex CLI"` in all nine locales. With a mark +beside it the "CLI" is redundant, and the row covers the Codex app and SDK too, +so it is also slightly wrong. It becomes `"Codex"` in every locale. `ja`, `zh`, +`zh-TW`, `ko`, `ru`, `tr`, `de`, `fr`, `en` all currently hold the literal +string `Codex CLI`, so this is one substitution per file. + +`integrations.codex.title` and `.body` are separate keys and are left alone; +they describe the routing behavior, where "CLI" is accurate. + +## CSS + +`gui/src/styles-apikeys-workspace.css` keeps `.awi-clientconfig-mark`; the new +component emits `.client-mark` plus `.client-mark--mask` / `.client-mark--img` +/ `.client-mark--monogram`, sized from a `--client-mark-size` custom property +so one rule serves 16/20/24/28px. The mask branch reuses the existing +`background: var(--text)` treatment. + +## Tests + +Extend `gui/tests/integrations-surfaces.test.tsx`: each of the three surfaces +renders a mark element per row/tab/header. Driven red by omitting the tab-strip +mark. + +New guard: every id in `INTEGRATION_MARKS` has a value, and every value resolves +to a committed file. Falsify by pointing one at a missing asset. + +New guard: `MONOCHROME_INTEGRATION_MARKS` contains no asset whose file has more +than one distinct fill. Falsify by adding `claude-color.svg`. + +New guard, from the audit: no asset whose single ink is a BRAND color may be +masked. `openai.svg` (`#10A37F`) and `deepseek-harness.svg` (`#4d6bfe`) are pinned +by name with their inks, the way the existing `dsh` test does, because no +property of the file distinguishes a brand ink from a neutral one. Falsify by +adding `openai.svg` to the mask set. + +New guard: no mark element contributes to an accessible name — each is +`aria-hidden` or has an empty `alt`. Falsify by giving the img an `alt={label}`. + +New guard: no locale contains `Codex CLI` under `integrations.tab.codex`. +Falsify by reverting one locale. + +## Verification + +`cd gui && bun test tests` focused files, plus headless Chrome screenshots of +`#integrations` at 1280 and 390 wide in both themes. The screenshots are the +only thing that proves the tab strip still wraps sanely with 17 marks in it. diff --git a/devlog/_plan/260831_aside_client_and_integrations_ux/080_wp9_conflict_overwrite.md b/devlog/_plan/260831_aside_client_and_integrations_ux/080_wp9_conflict_overwrite.md new file mode 100644 index 0000000000..c294b1ebae --- /dev/null +++ b/devlog/_plan/260831_aside_client_and_integrations_ux/080_wp9_conflict_overwrite.md @@ -0,0 +1,160 @@ +# wp9 — overwrite a conflict with defaults + +Independent of wp7/wp8 in code; ordered after them in the stack because they +touch the same page and a three-way overlap on `IntegrationsOverview.tsx` is not +worth reviewing. + +## The dead end today + +`classifyIntegration` reports `conflict` for two reasons and the writer refuses +both unconditionally (`writer.ts` apply branch): + +- `unowned-key` — our fragment paths are occupied by a value we did not write, + or there is no ownership record at all. +- `foreign-edit` — the recorded block's bytes changed, or a comment-capable + format drifted at file level. + +The GUI mirrors that: `FileIntegrationPage` sets `locked` for conflict and +`OverviewCard` disables the switch. So the only recovery is to open the file and +edit it by hand, which is precisely what a user reaching for a dashboard is +avoiding. The refusal is correct as a default — it is what stops us deleting +someone's work — but "correct default" and "only path" are different things. + +## Design: an explicit, snapshotted, journaled force + +The escape hatch is a THIRD operation, not a flag that weakens apply. + +`src/integrations/writer.ts` gains `overwriteIntegration(input)` and +`overwriteIntegrationCoordinated(input, options)`, next to apply/refresh/disable. +It differs from apply in exactly one place: where +`applyOrRefreshIntegration` returns `refuse(clientId, "conflict", ...)`, this +path continues. Everything else is shared, which is the point — the snapshot, +the atomic write, the compare-before-commit recheck, the ownership record and +the journal row all come from the same `commit()` call apply uses. + +Two properties it must NOT relax: + +- `unsafe` still refuses. A blocked container means our write would replace a + value the merge cannot reason about; forcing that is data loss with a receipt, + and 005's rule for marks applies here too — the snapshot is not a licence. +- `not_installed` and `non_loopback` still refuse. Neither is a conflict. + +The merge base is the parsed document as it stands, and the recorded fragment +paths are removed first WHEN A RECORD EXISTS — the same `removeFragments` call +the stale-refresh path makes. Without that, forcing over a drifted block leaves +orphans the new record does not cover, exactly as the refresh comment documents. + +The audit asked what happens in the other case, and it is worth writing down +because the answer looks like a bug and is not. `unowned-key` with NO record +gives `removeFragments` nothing to remove, so the merge runs against the +user's document with their values in our paths. `createdContainerPaths` +(`merge.ts`) then walks the same paths and records a container only where one +is NOT already a plain object — so every container the user already had is +correctly attributed to them, and the `createdContainers` we persist is +empty or near-empty. A later disable removes our leaves and leaves their +containers standing. That is the right outcome: we did not create those +containers, and pruning them would be the second act of destruction after the +one the user explicitly authorized. + +What the audit was right to flag is the `record!` non-null assertions in the +disable branch, whose comment records a real TypeError shipped that way. The +force path must not reuse them: it reaches the merge with a possibly-null +record by design, so it takes the apply-side code, where `record` is already +nullable, and never the disable-side code. + +New journal kind: `overwrite`. It could reuse `apply`, and that is the tempting +shortcut, but the rollback list is the one place a user goes after a mistake and +"applied" is a lie about an operation that replaced someone else's block. + +The kind string is declared in THREE independent places, none of which imports +another — the audit enumerated them and the plan originally named only two: + +- `src/integrations/journal.ts:22` — `export type OperationKind`, the persisted + vocabulary. +- `src/server/management/integration-routes.ts:73` — the route's own + `IntegrationJournalRow.kind` union, re-declared rather than imported. +- `gui/src/pages/integrations/integration-api.ts:57` — the GUI's union, also + re-declared, because the GUI does not import backend types. + +Plus `JOURNAL_KIND_KEY` in `overview-clients.ts`, which is an exhaustive +`Record` — so the compiler catches a missing +entry there, and only there. The two re-declared unions drift silently: a row +persisted as `overwrite` would fail no type check and render as an untranslated +key. A guard asserting the three declarations agree is worth more than the +feature test. + +The new i18n key goes into nine locale files. `gui/tests/locale-parity.test.ts` +already enforces key-set parity and additionally fails a zh-TW value left +identical to English unless it is allowlisted, so a placeholder translation is +not an option here. + +Undo is not special-cased: `restoreIntegration` already restores from the +snapshot by `opId` and does not care which kind produced it. The regression test +proves that end to end rather than asserting it here. + +## Route + +`src/server/management/integration-routes.ts`, the `PUT +/api/client-integrations/:client` handler. The body today is `{enabled: +boolean}`. It gains an optional `overwriteConflict?: boolean`, validated the +same way `confirmDrift` is on the restore route: present-and-not-boolean is a +400 `invalid_overwrite_conflict`. + +`enabled: false` plus `overwriteConflict: true` is a 400, not a silent ignore. +Disabling a block we do not own is the deletion this whole subsystem exists to +prevent, and a caller asking for it has misunderstood the field. + +`writerFailureResponse` needs no new branch: an `unsafe` refusal from a forced +call is still `integration_unsafe`. + +## GUI + +`FileIntegrationPage.tsx` — when `status.state === "conflict"`, a +`btn-danger` button appears beside the locked switch, opening a +`ConsequenceDialog`. The switch stays locked; the button is the only way +through. + +`IntegrationsOverview.tsx` — the same action on a conflicted card, reusing the +existing `ConsequenceDialog` and `pendingToggle` focus-restore machinery. + +`ConsequenceDialog` copy needs the file path, the sentence that the current +block is replaced by opencodex defaults, the sentence that a snapshot is taken +and the operation is undoable from the rollback list, and a confirm label that +is not "OK". Nine locales. + +The reason matters in the copy: `unowned-key` means "a block we did not write +is in the way", `foreign-edit` means "your edit to our block will be +discarded". Same operation, materially different thing being lost, so two +`changes` strings selected on `status.reason`. + +## Tests + +Backend, driven red first: + +1. force apply over `unowned-key` succeeds, writes our block, journals kind + `overwrite`, and `restore` of that op returns the original bytes exactly. + Falsify by leaving the conflict refusal in place. +2. force apply over `foreign-edit` drops the recorded fragments before merging, + so no orphan survives. Falsify by merging without `removeFragments`. +3. force apply over `unsafe` still refuses. Falsify by moving the force branch + above the unsafe check. +4. `{enabled: false, overwriteConflict: true}` is a 400. Falsify by ignoring + the combination. +5. a normal apply is unchanged — no `overwriteConflict` means the conflict + refusal still fires. Falsify by defaulting the field to true. + +GUI, driven red first: + +6. the overwrite button renders only for `conflict`, and never for `absent`, + `current`, `stale`, `unsafe` or not-installed. Falsify by widening the + condition to `unsafe`. +7. clicking it does not mutate until the dialog is confirmed. Falsify by wiring + the button straight to the mutation. +8. the dialog names the config path. Falsify by dropping the `path` var. + +## Verification + +Focused backend files run in CI, not locally (the full local suite is +forbidden). `cd gui && bun test tests` for the GUI files. A screenshot of a +conflicted client page is worth having but needs a conflicted config to exist; +the GUI test asserting the button's presence is the real gate.