From 4b1922208ca863b16771b9f45c401f10a8977ba3 Mon Sep 17 00:00:00 2001 From: kate bonner Date: Thu, 9 Jul 2026 06:56:10 +0000 Subject: [PATCH 1/2] fix(amicode): remove the yellow accent rail from card surfaces MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 3px accent-colored border-left (and the onboarding wizard's 4px accent border-top) read as a heavy yellow outline on every amicode card. Drop the rail from the home dashboard hero cards, the in-chat receipt/entity/ask/run cards, the problem rail, and the onboarding wizard — each keeps its neutral 1px hairline. The H-mark, live dots, hover tints, and focus outlines (keyboard a11y) are untouched. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/ui/src/amicode/ask-card.tsx | 1 - packages/ui/src/amicode/card.tsx | 1 - packages/ui/src/amicode/entity-rail.tsx | 1 - packages/ui/src/amicode/entity-view.tsx | 1 - packages/ui/src/amicode/home-cards.tsx | 2 +- packages/ui/src/amicode/onboarding-wizard.tsx | 1 - packages/ui/src/amicode/run-window.tsx | 1 - 7 files changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/ui/src/amicode/ask-card.tsx b/packages/ui/src/amicode/ask-card.tsx index 501379d6d..abb189dcb 100644 --- a/packages/ui/src/amicode/ask-card.tsx +++ b/packages/ui/src/amicode/ask-card.tsx @@ -44,7 +44,6 @@ export function AmicodeAskCard(props: { ask: AskInput; messageID?: string; sessi gap: "8px", "min-width": "0", border: "1px solid var(--v2-border-border-base)", - "border-left": "3px solid var(--v2-icon-icon-accent)", "border-radius": "6px", background: "var(--v2-background-bg-layer-01)", padding: "8px 12px", diff --git a/packages/ui/src/amicode/card.tsx b/packages/ui/src/amicode/card.tsx index c392996a2..372e569a5 100644 --- a/packages/ui/src/amicode/card.tsx +++ b/packages/ui/src/amicode/card.tsx @@ -51,7 +51,6 @@ function Chip(props: { tool: string; status?: string; output?: string }) { gap: "8px", "min-width": "0", border: "1px solid var(--v2-border-border-base)", - "border-left": "3px solid var(--v2-icon-icon-accent)", "border-radius": "6px", background: "var(--v2-background-bg-layer-01)", padding: "4px 12px", diff --git a/packages/ui/src/amicode/entity-rail.tsx b/packages/ui/src/amicode/entity-rail.tsx index c3c4e2b86..1fd7f1f78 100644 --- a/packages/ui/src/amicode/entity-rail.tsx +++ b/packages/ui/src/amicode/entity-rail.tsx @@ -148,7 +148,6 @@ export function AmicodeEntityRail(props: { "max-height": "76px", "overflow-y": "auto", border: "1px solid var(--v2-border-border-base)", - "border-left": "3px solid var(--v2-icon-icon-accent)", "border-radius": "6px", background: "var(--v2-background-bg-layer-01)", padding: "6px 10px", diff --git a/packages/ui/src/amicode/entity-view.tsx b/packages/ui/src/amicode/entity-view.tsx index 9b1abfe2a..a5fcb93e2 100644 --- a/packages/ui/src/amicode/entity-view.tsx +++ b/packages/ui/src/amicode/entity-view.tsx @@ -56,7 +56,6 @@ export function AmicodeEntityView(props: { class="flex flex-col gap-3 py-2 pl-4 pr-3" data-component="amicode-entity-view" data-kind={props.kind} - style={{ "border-left": "3px solid var(--v2-icon-icon-accent)" }} > Date: Thu, 9 Jul 2026 07:13:59 +0000 Subject: [PATCH 2/2] feat(amicode): redesign the receipt card + entity view Fold the full AMICO receipt redesign into the rail removal: instead of just dropping the accent bar, give the whole family one visual language. - receipt card (card.tsx): the H-mark doubles as the working spinner, diffs render structured (dimmed old -> new) instead of a run-on string, a green check settles the done state, failure tints the card red, and it is a real + ) } diff --git a/packages/ui/src/amicode/entity-rail.tsx b/packages/ui/src/amicode/entity-rail.tsx index 1fd7f1f78..3af14de19 100644 --- a/packages/ui/src/amicode/entity-rail.tsx +++ b/packages/ui/src/amicode/entity-rail.tsx @@ -1,6 +1,7 @@ import { For, Show, createEffect, createMemo, createResource, createSignal, onCleanup } from "solid-js" import { hasUserReplyAfter } from "./ask" import { registerAmicodeAskBridge } from "./ask-bridge" +import { AmicoMark } from "./spinner" import { registerAmicodeUiBridge } from "./ui-bridge" import { type ProblemView, @@ -147,24 +148,15 @@ export function AmicodeEntityRail(props: { "min-width": "0", "max-height": "76px", "overflow-y": "auto", - border: "1px solid var(--v2-border-border-base)", - "border-radius": "6px", - background: "var(--v2-background-bg-layer-01)", padding: "6px 10px", "font-size": "11px", "line-height": "16px", "white-space": "nowrap", }} > - - AMICO + + + AMICO = { "\\hat H = \\tfrac{\\Omega(t)}{2}\\sum_i \\sigma_x^{(i)} - \\Delta(t)\\sum_i \\hat n_i + \\sum_{i, +): DiffPiece[] { + const { changes } = receiptParts({ problem: "", entity, action, diff: diff ?? {} }) + return changes.map((change) => + change.kind === "elision" + ? { key: "…" } + : change.kind === "set" + ? { key: humanizeKey(change.key), to: change.to } + : { key: humanizeKey(change.key), from: change.from, to: change.to }, + ) +} + export function AmicodeEntityView(props: { view: ProblemView | undefined // undefined → loading skeleton kind: string @@ -37,6 +64,41 @@ export function AmicodeEntityView(props: { ? props.anchorSeq : undefined, ) + const changedKeys = createMemo(() => { + const seq = anchored() + const set = new Set() + if (seq === undefined) return set + const event = history().find((candidate) => candidate.seq === seq) + if (event?.diff) + for (const key of Object.keys(event.diff)) { + set.add(key) + const bare = key.split(".").pop() + if (bare) set.add(bare) + } + return set + }) + const isChanged = (key: string) => { + const set = changedKeys() + return set.has(key) || set.has(key.split(".").pop() ?? key) + } + // Rows with a group-header flag when a nested group first appears (entityRows + // flattens each object's children contiguously, so a group is one run). + const fieldRows = createMemo(() => { + let prevGroup: string | undefined + return rows().map((row) => { + const group = fieldGroup(row.key) + const showGroupHeader = group !== undefined && group !== prevGroup + prevGroup = group + return { + key: row.key, + value: row.value, + name: humanizeKey(row.key), + groupLabel: group ? humanizeKey(group) : undefined, + showGroupHeader, + } + }) + }) + const latestTs = createMemo(() => history()[0]?.ts) const runTier = createMemo(() => { if (props.kind !== "run" || !props.view) return undefined const refs = props.view.runs @@ -52,174 +114,151 @@ export function AmicodeEntityView(props: { }) return ( -
+
+
+
+
+
+
+
} > {(view) => ( -
- - {view().error} - -
} > - - {(tier) => ( -
- - {tier() === "free" ? "free · unvetted" : tier()} - -
- )} + +
+ + {(tier) => ( + + {tier() === "free" ? "free · unvetted" : tier()} + + )} + + {(ts) => Updated {ts()}} +
+ {(html) => ( -
+
+
Hamiltonian
+
+
)} + 0} - fallback={ -
- — -
- } + when={fieldRows().length > 0} + fallback={
No fields recorded yet.
} > +
Details
- + {(row) => ( -
- - {row.key} - - - {row.value} - - -
+ <> + +
{row.groupLabel}
+
+
+ + {row.name} + {row.key} + + + {row.value} + + +
+ )}
+ 0}> -
- - History - -
- - {(event) => ( -
{ - if (anchored() === event.seq) queueMicrotask(() => el.scrollIntoView({ block: "nearest" })) - }} - > -
- - #{event.seq} - - - {(source) => ( - - {source()} - +
History
+
+ + {(event) => ( +
{ + if (anchored() === event.seq) queueMicrotask(() => el.scrollIntoView({ block: "nearest" })) + }} + > +
+ #{event.seq} + + {(source) => {source()}} + + {(ts) => {ts()}} +
+
+ 0} + fallback={{event.action ? humanizeKey(event.action) : "—"}} + > + + {(piece, index) => ( + <> + 0}> + + + {piece.key} + + {piece.from} + + + + {piece.to} + + )} - - - - {event.ts} - - -
- - {receiptText({ - problem: "", - entity: event.entity, - action: event.action, - diff: (event.diff ?? {}) as Record, - })} - + +
- )} -
-
+
+ )} +
+ +
+ + + Read-only. Changes are made by asking AMICO in chat — ✎ drafts the message for you. + +
)} diff --git a/packages/ui/src/amicode/problem.ts b/packages/ui/src/amicode/problem.ts index eebc7e2ec..0ae8f64c5 100644 --- a/packages/ui/src/amicode/problem.ts +++ b/packages/ui/src/amicode/problem.ts @@ -277,6 +277,22 @@ export function railState(current: ProblemView | undefined, lastGood: ProblemVie export type EntityRow = { key: string; value: string } +/** Field/label humanizer for the entity view: last dotted segment, underscores + * to spaces, sentence case — `params.drive_max` → "Drive max", `problem_type` + * → "Problem type". Presentation only; the raw key is still shown alongside. */ +export function humanizeKey(key: string): string { + const raw = (key.split(".").pop() ?? key).replaceAll("_", " ").trim() + if (!raw) return key + return raw.charAt(0).toUpperCase() + raw.slice(1) +} + +/** The dotted prefix of a flattened row key (the group it belongs to), or + * undefined for a top-level scalar. `params.drive_max` → "params". */ +export function fieldGroup(key: string): string | undefined { + const dot = key.indexOf(".") + return dot === -1 ? undefined : key.slice(0, dot) +} + /** Stable field rows: scalars in object order, one level of object nesting * flattened to dotted keys, undefined skipped. */ export function entityRows(entity: Record): EntityRow[] { diff --git a/packages/ui/src/amicode/receipt.ts b/packages/ui/src/amicode/receipt.ts index 532f13881..257041dd2 100644 --- a/packages/ui/src/amicode/receipt.ts +++ b/packages/ui/src/amicode/receipt.ts @@ -61,18 +61,39 @@ function short(value: unknown): string { return JSON.stringify(value) } +// A receipt's body, broken into typed pieces so the UI can render each change +// as a discrete unit (dimmed old value · arrow · new value) instead of a flat +// string. `receiptText` below joins these back into the exact one-line form the +// legacy chip and the tests expect — the structured and string renders never +// diverge because they come from the same source. +export type ReceiptChange = + | { kind: "elision" } + | { kind: "set"; key: string; to: string } + | { kind: "change"; key: string; from: string; to: string } +export type ReceiptParts = { label: string; changes: ReceiptChange[]; action: string } + +export function receiptParts(sentinel: DiffSentinel): ReceiptParts { + const changes: ReceiptChange[] = Object.entries(sentinel.diff).map(([key, entry]) => { + if (key === "…") return { kind: "elision" } + const bare = key.split(".").pop() ?? key + if (entry.from === null || entry.from === undefined) return { kind: "set", key: bare, to: short(entry.to) } + return { kind: "change", key: bare, from: short(entry.from), to: short(entry.to) } + }) + return { label: entityLabel(sentinel.entity), changes, action: sentinel.action } +} + /** `System · levels 3→4 · omega 4.8` — dotted diff keys render bare (last * segment); creates (from null) render value-only; the spec-A elision key * `…` renders as a bare ellipsis. Empty diff → the action. One line, always. */ export function receiptText(sentinel: DiffSentinel): string { - const parts = Object.entries(sentinel.diff).map(([key, entry]) => { - if (key === "…") return "…" - const bare = key.split(".").pop() ?? key - if (entry.from === null || entry.from === undefined) return `${bare} ${short(entry.to)}` - return `${bare} ${short(entry.from)}→${short(entry.to)}` + const { label, changes, action } = receiptParts(sentinel) + const parts = changes.map((change) => { + if (change.kind === "elision") return "…" + if (change.kind === "set") return `${change.key} ${change.to}` + return `${change.key} ${change.from}→${change.to}` }) - const body = parts.length > 0 ? parts.join(" · ") : sentinel.action - return `${entityLabel(sentinel.entity)} · ${body}` + const body = parts.length > 0 ? parts.join(" · ") : action + return `${label} · ${body}` } /** For any raw-output display path: drop a trailing sentinel line. NOTE: the diff --git a/packages/ui/src/amicode/run-window.tsx b/packages/ui/src/amicode/run-window.tsx index 52e2bf37e..c4cda0792 100644 --- a/packages/ui/src/amicode/run-window.tsx +++ b/packages/ui/src/amicode/run-window.tsx @@ -1,4 +1,5 @@ import { For, Show, createEffect, createMemo, createSignal, onCleanup, onMount } from "solid-js" +import { AmicoMark } from "./spinner" import { fetchAmicodeRunSeries, openAmicodeEntity } from "./ui-bridge" import { type RunSeries, type RunSeriesView, elapsedLabel, headlineMetric, parseRunSeriesResponse } from "./run-series" @@ -189,9 +190,6 @@ export function RunWindow(props: { run: string; lab?: string }) { "flex-direction": "column", gap: "6px", "min-width": "0", - border: "1px solid var(--v2-border-border-base)", - "border-radius": "6px", - background: "var(--v2-background-bg-layer-01)", padding: "8px 12px", "font-size": "12px", "line-height": "16px", @@ -199,9 +197,10 @@ export function RunWindow(props: { run: string; lab?: string }) { }} > {/* header: AMICO · Run · status · iter · metric · elapsed */} -
- - AMICO +
+ + + AMICO · Run diff --git a/packages/ui/src/amicode/spinner.tsx b/packages/ui/src/amicode/spinner.tsx index a836b2d1f..b807021f6 100644 --- a/packages/ui/src/amicode/spinner.tsx +++ b/packages/ui/src/amicode/spinner.tsx @@ -21,6 +21,25 @@ import { MARK_PATH } from "../components/logo" const reducedMotion = () => typeof window !== "undefined" && !!window.matchMedia?.("(prefers-reduced-motion: reduce)").matches +// Static/animated brand mark for the amicode surfaces (receipt card, entity +// view, rail, ask, run window). Same H-glyph as AmicoSpinner; color comes from +// the `.amc-mark` class (accent) via currentColor, and `running` toggles the +// pulse through `.amc-mark.is-running` (CSS owns the prefers-reduced-motion +// guard here — see amicode.css — rather than the matchMedia guard below). +export function AmicoMark(props: { class?: string; running?: boolean }) { + return ( + + ) +} + export function AmicoSpinner(props: { class?: string classList?: ComponentProps<"div">["classList"] diff --git a/packages/ui/src/styles/index.css b/packages/ui/src/styles/index.css index 1b17f6c2b..d5a8d22f5 100644 --- a/packages/ui/src/styles/index.css +++ b/packages/ui/src/styles/index.css @@ -35,6 +35,7 @@ @import "../components/logo.css" layer(components); @import "../components/markdown.css" layer(components); @import "../components/message-part.css" layer(components); +@import "../amicode/amicode.css" layer(components); @import "../components/message-nav.css" layer(components); @import "../components/popover.css" layer(components); @import "../components/progress.css" layer(components);