diff --git a/AMICODE-PATCHES.md b/AMICODE-PATCHES.md index c098e2dcf0..53d964a514 100644 --- a/AMICODE-PATCHES.md +++ b/AMICODE-PATCHES.md @@ -353,3 +353,9 @@ Rebuilt with the exact T3 recipe (`OPENCODE_VERSION=1.17.3 bun run script/build. - STILL UNVERIFIED (needs live API creds): whether the OLD legacy `thinking:{type:"enabled",budgetTokens}` form actually 400s against Opus 5 or merely degrades — i.e. whether this was "hobbled" or "unusable". Also unexercised by CI: the Grok/GLM effort variants and the opus-4-5 `budgetTokens`+`effort` combination. - FUTURE SYNC COST: this is a knowing trade — porting file end-states now makes a future clean upstream merge harder on these 4 files. Accepted. NOTE the bug class recurs every model generation (version-regex parsing of model IDs will break again at Opus 6), which argues for a standing narrow sync lane on `transform.ts` rather than one-off unfreezes. - SEPARATE RISK SPOTTED (not fixed here): the release build does a bare `fetch(models.dev/api.json)` with no fallback, so a models.dev outage hard-fails the build. Consider pinning `MODELS_DEV_API_JSON` for the hackathon build. +24. (harmonic wave indicator — de-shimmer the tool-status title, 2026-07-28) — amicode: the chat had two "working" indicators that read as identical — the fork's thinking line and the STOCK tool-group header (`packages/ui/src/components/tool-status-title.tsx`, e.g. "Working in shell" → "Worked in shell") — because both were shimmering text with no glyph. Earlier steps of this run gave the thinking line a standing-wave glyph (`AmicoWave`, `packages/ui/src/amicode/amico-wave.tsx`) and deleted the `amc-text-shimmer` CSS; this patch removes the shimmer from the last surface still asking for it. + - `tool-status-title.tsx`: all five `` render positions (swap-mode active/done, suffix-mode prefix/active/done) replaced with plain `{text}`. `TextShimmer` import removed. The now-orphaned `prefixLen` memo (it existed only to feed the shimmer's `offset` phase-alignment prop) removed too — confirmed via `oxlint` before/after (498→499→498 warnings) that leaving it in place tripped `no-unused-vars`, so dropping it was the correct call rather than "keep everything." + - Deliberately PRESERVED: the `common()` prefix-splitting logic, the width-morph animation (`animate()`/`finish()`, the `requestAnimationFrame`, the stored `width`, the `data-ready` flag), the `data-component`/`data-active`/`data-mode` attributes, the `aria-label`, and the active→done text swap itself. None of that is the shimmer — it's the genuinely good word-morph behavior. + - `text-shimmer.tsx` untouched — it has many other live callers (`basic-tool.tsx`, `message-part.tsx` ×7, `session-turn.tsx`, `v2/components/basic-tool-v2.tsx` via `text-shimmer-v2.tsx`, plus stories), confirmed via `rg -n 'TextShimmer' packages/`. Not a candidate for deletion. + - The dropped `offset` prop existed only so the shimmer's gradient phase stayed continuous across the prefix/tail split; with no shimmer there's nothing to phase-align, so it has no replacement. Verified live (Storybook, `UI/AnimatedCountList` stories) that the prefix and tail still read as one unbroken word with no seam: in the swap-mode `Playground` story, driving an active→done transition showed the mid-animation frame rendering both the active and done spans simultaneously under the animating width (`data-ready="true"`, container `style="width: 0px"` mid-transition, then settling) — the word-morph is intact. The suffix/prefix-tail mode (`data-mode="suffix"`) is currently unreachable from any real call site — both app usages (`message-part.tsx` context-tool-group and shell-group titles) and the only story pass `split={false}` — so it was verified by transiently flipping one story's `split` prop off (`Done` export, "Exploring"/"Explored"), confirming `data-mode="suffix"` renders `"Explor"` + `"ed"` as an unbroken "Explored" with no visible gap, then reverting that story edit before commit (`git status` shows only the two files below). + - Tests: ui `bun test src` → 402 pass / 0 fail (unchanged). typecheck (tsgo) clean in `packages/ui`. `oxlint packages/ui/src` → 498 warnings / 0 errors both before and after (no new warnings once `prefixLen` was dropped). diff --git a/packages/ui/src/amicode/amico-wave.stories.tsx b/packages/ui/src/amicode/amico-wave.stories.tsx new file mode 100644 index 0000000000..bba204a53a --- /dev/null +++ b/packages/ui/src/amicode/amico-wave.stories.tsx @@ -0,0 +1,313 @@ +// @ts-nocheck +// AmicoWave: the harmonic working indicator. Geometry/timing live in wave-geometry.ts and are +// unit-tested there; this file is the only place the SVG + CSS actually render, so it is also +// the only place a CSS regression (wrong ink, broken quadrature, id collisions across +// simultaneous mounts) would be visible before it ships. Nothing mounts AmicoWave in the app +// yet — see amico-wave.tsx's header comment — so these stories are the sole way to see it. +// +// ---- why Default/Schemes exist ------------------------------------------------------------- +// A review caught a critical color bug that no unit test could have caught: the component +// used to pick its dark-scheme ink via `:root:not([data-theme="light"])`, a selector copied +// from a block in v2/styles/theme.css that lives INSIDE a /* */ comment — dead CSS — so it +// matched in every state and the glyph rendered brand lemon (~1.1:1 contrast) on light +// backgrounds. It's fixed to `color: var(--v2-icon-icon-accent)`, which the active theme +// (oc-2) maps to grey-800 on light and #FFF676 on dark. Default and Schemes below exist so +// that mapping can never silently regress again. +// +// ---- the wrinkle: how these stories actually re-resolve the tokens ------------------------ +// The obvious approach — set `data-color-scheme="light"`/`"dark"` on a wrapper element, per +// theme/context.tsx (`dataset.colorScheme`) and v2/styles/theme.css's +// `[data-color-scheme="light"]` / `[data-color-scheme="dark"]` blocks — turns out to be a +// no-op INSIDE STORYBOOK. Storybook's preview only imports `@opencode-ai/ui/styles/tailwind`, +// whose theme file is the v1 `styles/theme.css`; the v2 file that defines those +// `[data-color-scheme]` blocks is only ever pulled in by the real app's +// `v2/styles/tailwind.css` chain (packages/app/src/index.css), which Storybook never loads. +// Verified empirically with Playwright against a running Storybook: a nested +// `data-color-scheme="light"` (or "dark") div's `--v2-*` custom properties were untouched by +// the attribute and simply inherited whatever the global theme toolbar had already put on +// — both a "light" and a "dark" test div resolved to the SAME `--v2-icon-icon-accent`. +// So a story built the literal way the review described would silently show the SAME color +// twice, which is exactly the failure mode it was meant to catch. +// +// Instead, each scheme pane below calls the SAME resolver the app uses at runtime — +// `resolveThemeVariantV2` over the real oc-2.json theme (theme/v2/resolve.ts, the function +// theme/context.tsx's applyThemeCss calls) — and applies the FULL resulting token set +// (188 keys: primitive ramps + semantic aliases, self-contained) as inline custom properties +// on that pane's own wrapper. That correctly re-resolves `--v2-icon-icon-accent` (and every +// other v2 token used inside) independent of whatever the global Storybook theme toggle is +// doing, because inline-set custom properties on an element always win for that subtree. This +// is arguably MORE faithful than the dead attribute would have been: it reads the live oc-2 +// mapping from its source file, so it tracks theme changes instead of a hand-copied hex. +import { createSignal, For, onMount } from "solid-js" +import { AmicoWave } from "./amico-wave" +import { MODE_WAVELENGTHS, WAVE_BOX } from "./wave-geometry" +import oc2ThemeJson from "../theme/themes/oc-2.json" +import { resolveThemeVariantV2 } from "../theme/v2/resolve" +import type { DesktopTheme } from "../theme/types" + +const oc2Theme = oc2ThemeJson as DesktopTheme + +/** The real oc-2 --v2-* token set for one color scheme, self-contained (primitive ramps + + * semantic aliases both included), so it can be applied to any wrapper and resolve correctly + * with zero dependency on ambient state — in particular, independent of the Storybook global + * theme toolbar. This is the exact function theme/context.tsx calls to paint ; here we + * scope its output to a
instead. */ +function schemeVars(scheme: "light" | "dark"): Record { + const isDark = scheme === "dark" + const tokens = resolveThemeVariantV2(isDark ? oc2Theme.dark : oc2Theme.light, isDark) + const vars: Record = {} + for (const [key, value] of Object.entries(tokens)) vars[`--${key}`] = value + return vars +} + +// Scale + freeze helpers used by Schemes/Modes/Magnified/ReducedMotion below. AmicoWave sets +// width/height as SVG presentation attributes (30x12); a plain CSS class rule already beats +// those with no !important needed, per the CSS spec's presentation-attribute priority rule. +const StoryCss = () => ( + +) + +function SchemePane(props: { scheme: "light" | "dark"; children: unknown }) { + return ( +
+
+ {props.scheme} scheme +
+ {props.children} +
+ ) +} + +// Mimics the real mount site (thinking-line.tsx, once wired): glyph, bold gerund, muted +// elapsed time — reusing the actual .amc-thinking* classes from amicode.css. +const ThinkingRow = () => ( + + + Percolating + 5m 13s + +) + +export default { + title: "Amicode/AmicoWave", + id: "amicode-amico-wave", + tags: ["autodocs"], + parameters: { + docs: { + description: { + component: `### AmicoWave + +The harmonic working indicator: a 30×12px standing wave in quadrature (a lead curve and a +fainter companion, a quarter period out of phase, so one is at full swing exactly when the +other crosses zero — what keeps the glyph from reading as a blink at 12px). It climbs through +three modes (1/2/3 full wavelengths across the box) on a slower cadence. All geometry and +timing come from wave-geometry.ts via CSS custom properties set inline by the component — +never restated here. + +Not yet mounted anywhere in the app (a later task will wire it into the thinking line and the +tool header, replacing the amc-text-shimmer treatment) — these stories are the only current +way to see it render. + +**Hard invariant, guarded by ManyInstances below:** no SVG \`\`, no \`id\` attributes. +SVG ids are document-global, and several indicators mount at once in real use (one thinking +line plus one tool header per in-flight tool call) — an id would collide across instances.`, + }, + }, + }, +} + +// --------------------------------------------------------------------------------------------- +// Default — the glyph at natural size beside its real-use text, in both schemes side by side +// so an ink regression (wrong token, or a hardcoded color that ignores scheme entirely) is +// visible without needing the Schemes story's numeric readout. +export const Default = () => ( +
+ + + + + + +
+) + +// --------------------------------------------------------------------------------------------- +// Schemes — the contrast check. getComputedStyle(el).color reads what the browser ACTUALLY +// resolved for that scheme's pane, not what the token mapping merely intends. This is the +// story that would have caught the original bug: the buggy selector lived in a dead comment, +// so the glyph's color never actually changed with scheme — it would have printed the SAME +// resolved color under both panes here. +function ContrastSwatch(props: { scheme: "light" | "dark" }) { + const [resolved, setResolved] = createSignal("…") + let wrap: HTMLDivElement | undefined + onMount(() => { + const svg = wrap?.querySelector('[data-component="amico-wave"]') + if (svg) setResolved(getComputedStyle(svg).color) + }) + return ( +
+
+ {props.scheme} +
+
+ +
+ + getComputedStyle → {resolved()} + +
+ ) +} + +export const Schemes = () => ( + <> + +
+ + +
+ +) + +// --------------------------------------------------------------------------------------------- +// Modes — each of the three standing modes, frozen and isolated, at 4x, so the shape reads. +// Wavelength is indexed by full wavelengths across the 30px box (1, 2, 3) — deliberately NOT +// the physical harmonic number; see wave-geometry.ts's MODE_WAVELENGTHS comment for why. +function FrozenMode(props: { pin: number; wavelength: number; waves: number }) { + return ( +
+
+ +
+
+ mode {props.pin} — λ={props.wavelength}px, {props.waves} full wavelength{props.waves === 1 ? "" : "s"} across + the box +
+
+ ) +} + +export const Modes = () => ( + <> + +
+ + {(wavelength, i) => } + +
+ +) + +// --------------------------------------------------------------------------------------------- +// Magnified — one live glyph at 6x so the quadrature is visible at a glance: the faint +// companion is at full swing exactly when the bold lead crosses the axis. +export const Magnified = () => ( + <> + +
+ +

+ The companion (faint, 0.4 opacity) is a quarter period behind the lead — it peaks + exactly when the lead crosses zero, which is what stops the glyph reading as a blink at + the real 12px size. +

+
+ +) + +// --------------------------------------------------------------------------------------------- +// ManyInstances — the regression guard for the no-/no-id rule. SVG ids are +// document-global: if one is ever introduced (for a mask, a gradient, anything), every +// instance after the first resolves to the FIRST element's definition and visibly breaks — +// invisibly on inspection of a single instance, but obvious the moment two or more are mounted +// at once, which is the normal case in the real app (one thinking line plus one tool header +// per in-flight tool call). +export const ManyInstances = () => ( +
+

+ 12 live instances, mounted simultaneously. AmicoWave has no SVG <defs> and no id + attribute anywhere — this is the guard for that: if either is ever added, ids collide + across instances and every glyph after the first will visibly break here (even though a + single isolated instance would still look correct). +

+
+ {() => } +
+
+) + +// --------------------------------------------------------------------------------------------- +// ReducedMotion — a mockup of the CSS's own @media (prefers-reduced-motion: reduce) fallback +// (amicode.css): one static mode-1 curve, companion hidden, no animation. This is authored +// locally to DEMONSTRATE the intended fallback, not to test it live — Storybook cannot force +// the OS/browser reduced-motion setting. To see the real fallback, enable "reduce motion" at +// the OS level (or the equivalent DevTools rendering emulation) and reload; every AmicoWave in +// every story on this page will then freeze the same way. +export const ReducedMotion = () => ( + <> + +
+
+ +
+

+ Mockup only — driven by the OS/browser prefers-reduced-motion setting in + the real component, which Storybook cannot toggle. The elapsed counter in the thinking + line keeps ticking under reduced motion; only this glyph's own animation stops. +

+
+ +) diff --git a/packages/ui/src/amicode/amico-wave.tsx b/packages/ui/src/amicode/amico-wave.tsx new file mode 100644 index 0000000000..edd93baed4 --- /dev/null +++ b/packages/ui/src/amicode/amico-wave.tsx @@ -0,0 +1,73 @@ +// AMICODE: the harmonic working indicator — a standing wave in quadrature, shown while +// Amico works. Will replace the amc-text-shimmer treatment on both indicator surfaces once +// mounted — that wiring (thinking-line.tsx and the tool header) is a later task. +// +// Markup only: all geometry and timing come from ./wave-geometry, handed to the CSS as +// custom properties so there is exactly one source of truth. Two paths per mode (lead + +// companion out of phase by a quarter period); the companion is at full swing exactly when +// the lead crosses zero, which is what stops the glyph reading as a blink at 12px. +// +// NO and NO ids — several indicators mount at once (one thinking line plus one tool +// header per tool call) and SVG ids are document-global, so ids would collide and every +// instance would resolve to the first definition. If a variant ever needs masking it must +// use CSS mask-image, not an SVG . +import { For } from "solid-js" +import { + WAVE_BOX, + WAVE_LEAD_STROKE, + WAVE_COMPANION_STROKE, + WAVE_COMPANION_OPACITY, + WAVE_PERIOD_MS, + WAVE_EASING, + MODE_PATHS, + companionDelayMs, + modeCadenceMs, + modeDelaysMs, +} from "./wave-geometry" + +const DELAYS = modeDelaysMs() + +export function AmicoWave(props: { class?: string }) { + return ( + + ) +} diff --git a/packages/ui/src/amicode/amicode.css b/packages/ui/src/amicode/amicode.css index 7524aeb3a8..551dd168fe 100644 --- a/packages/ui/src/amicode/amicode.css +++ b/packages/ui/src/amicode/amicode.css @@ -54,8 +54,8 @@ /* ---- thinking line (Claude-Code-esque working indicator) ----------------- */ /* Sits after the AMICO wordmark while a reply streams (thinking-line.tsx). The - * gerund word cycles + shimmers; the meta line ticks elapsed (· tokens · esc). - * Leading dot separates it from the wordmark. */ + * gerund word cycles without shimmer — the wave glyph (amico-wave.tsx) will carry + * the motion once mounted (a later task); the meta line ticks elapsed (· tokens · esc). */ .amc-thinking { display: inline-flex; align-items: baseline; @@ -65,43 +65,11 @@ letter-spacing: 0.01em; color: var(--v2-text-text-muted); } -.amc-thinking::before { - content: ""; - align-self: center; - width: 3px; - height: 3px; - border-radius: 50%; - background: var(--v2-border-border-base); - flex-shrink: 0; -} .amc-thinking-word { font-weight: 600; white-space: nowrap; color: var(--v2-text-text-accent); } -/* The shimmer: a bright band sweeps across otherwise-dimmed accent text. - * Motion-gated — under reduced motion (or .is-still) the word is plain accent. */ -@media (prefers-reduced-motion: no-preference) { - .amc-thinking:not(.is-still) .amc-thinking-word { - color: transparent; - background-image: linear-gradient( - 100deg, - color-mix(in srgb, var(--v2-text-text-accent) 42%, transparent) 0%, - color-mix(in srgb, var(--v2-text-text-accent) 42%, transparent) 38%, - var(--v2-text-text-accent) 50%, - color-mix(in srgb, var(--v2-text-text-accent) 42%, transparent) 62%, - color-mix(in srgb, var(--v2-text-text-accent) 42%, transparent) 100% - ); - background-size: 220% 100%; - -webkit-background-clip: text; - background-clip: text; - animation: amc-text-shimmer 1.6s linear infinite; - } -} -@keyframes amc-text-shimmer { - 0% { background-position: 200% 0; } - 100% { background-position: -20% 0; } -} .amc-thinking-meta { display: inline-flex; align-items: baseline; @@ -111,6 +79,91 @@ } .amc-thinking-sep { opacity: 0.55; } .amc-thinking-hint { font-style: italic; } +/* The glyph is a block, not text — baseline alignment would sit it high. */ +.amc-thinking .amc-wave { align-self: center; } + +/* ---- an opened skill file (message-part.tsx, ToolRegistry "skill") ------- */ +/* Expanding a skill used to dump its instructions as bare markdown straight into the + * transcript, because [data-component="tool-output"] carries no surface of its own — no + * border, no background, no padding. So an opened skill read as more conversation. It is a + * FILE, so it gets a file's surface: hairline, tint, padding, and a bounded height that + * scrolls rather than pushing the transcript around. + * + * Scoped to skills — every other tool's output keeps the plain treatment. The chip ABOVE this + * stays flush left with the other Amico chips; an earlier attempt indented the whole + * activation behind an accent spine and broke the shared left edge the chips read along. + * + * Compound selector, not a bare class: `[data-component="tool-output"]` is also (0,1,0), so a + * lone `.amc-skill-file` would tie on specificity and be decided by stylesheet order. */ +[data-component="tool-output"].amc-skill-file { + margin-top: 6px; + padding: 10px 12px; + max-height: 420px; + overflow-y: auto; + border: 1px solid var(--v2-border-border-base); + border-radius: var(--radius-md); + background: var(--v2-background-bg-layer-01); + /* tool-output sets `white-space: pre` for raw command output; rendered markdown must wrap. */ + white-space: normal; +} + +/* ---- the harmonic working indicator (amico-wave.tsx) --------------------- */ +/* A standing wave in quadrature: two identical curves, the companion a quarter period out + * of phase, so one is at full swing whenever the other crosses zero. + * + * Every duration, delay and opacity here comes from wave-geometry.ts via a custom property + * set inline by the component — do NOT restate those numbers in this file. */ +.amc-wave { + display: block; + flex-shrink: 0; + /* The ink role, already resolved by the active theme: oc-2.json maps + * v2-icon-icon-accent to grey-800 on light and #FFF676 on dark. The brand lemon is a + * fill, never an ink (1.1:1 on white), and this token is what encodes that — no theme + * selector needed here. Same treatment as amico-presence.css. */ + color: var(--v2-icon-icon-accent); +} + +/* .amc-wave-ln / .amc-wave-mode are component-private — only amico-wave.tsx ever sets the + * custom properties these animation shorthands read. Both shorthands carry a var(), which + * makes the whole shorthand a pending-substitution value: if any referenced custom property + * is ever missing (e.g. someone hand-writes in a fixture without the + * component's inline style block), the ENTIRE shorthand goes invalid and silently falls back + * to no animation — a static glyph, no console warning. */ +.amc-wave-ln { + transform-box: view-box; + transform-origin: 50% 50%; + animation: amc-wave-stand var(--amc-wave-period) var(--amc-wave-ease) infinite; +} +.amc-wave-ln[data-role="companion"] { + opacity: var(--amc-wave-comp-op); + animation-delay: var(--amc-wave-quad); +} +@keyframes amc-wave-stand { + 0%, 100% { transform: scaleY(1); } + 50% { transform: scaleY(-1); } +} + +.amc-wave-mode { + animation: amc-wave-mode var(--amc-wave-cadence) steps(1) infinite; +} +/* THE ONE UNAVOIDABLE DUPLICATION. 33.3333% is MODE_VISIBLE_PCT from wave-geometry.ts, and it + * cannot be a var(): @keyframes selectors are not a property-value context, so custom + * properties are invalid there. Writing 33% instead would leave a ~23ms window each cadence + * with NO mode visible — a per-cycle flicker. A test in wave-geometry.test.ts reads this file + * and asserts this literal still matches the constant, so the two cannot drift. */ +@keyframes amc-wave-mode { + 0%, 33.3333% { opacity: 1; } + 33.3334%, 100% { opacity: 0; } +} + +/* Static mode-1 curve, companion hidden. The elapsed counter in the thinking line keeps + * ticking — it is information, not decoration. */ +@media (prefers-reduced-motion: reduce) { + .amc-wave-ln, + .amc-wave-mode { animation: none !important; } + .amc-wave-ln[data-role="companion"] { opacity: 0 !important; } + .amc-wave-mode:not(:first-child) { opacity: 0 !important; } +} /* ============================================================ THE CHIP — [data-component="amicode-card"] @@ -194,6 +247,12 @@ [data-component="amicode-card"][data-state="error"] .amc-detail { color: var(--v2-state-fg-danger); } +/* a collapsed run's count (receipt-runs.ts) — short and always-visible, so it + never loses the ellipsis race against a long diff value beside it */ +[data-component="amicode-card"] .amc-count { + flex-shrink: 0; + font-variant-numeric: tabular-nums; +} /* structured diff pieces */ [data-component="amicode-card"] .amc-diff { diff --git a/packages/ui/src/amicode/card.stories.tsx b/packages/ui/src/amicode/card.stories.tsx new file mode 100644 index 0000000000..924ff70178 --- /dev/null +++ b/packages/ui/src/amicode/card.stories.tsx @@ -0,0 +1,106 @@ +// @ts-nocheck +// Visual check for the receipt-run collapse (../receipt-runs.ts): a run of +// consecutive amicode_* receipts sharing (problem, entity, action) renders as +// ONE card with a count instead of N identical-looking ones. Real user report: +// "these repeated amico cards add a lot of clutter can we avoid this?" — see +// message-part.tsx's collapseAmicodeGroups for the wiring; this story exercises +// AmicodeToolCard directly (the count prop it now accepts) rather than the full +// message list, so the collapsed card's rendering is reviewable in isolation. +import { AmicodeToolCard } from "./card" + +export default { + title: "Amicode/ToolCard", + id: "amicode-tool-card", + tags: ["autodocs"], + parameters: { + docs: { + description: { + component: `### Collapsed receipt runs + +Four \`amicode_*\` calls that update the same entity via the same action used to +render four visually-identical cards. A run of consecutive receipts sharing +(problem, entity, action) now collapses into one card carrying a \`×N\` count, +opening the latest (highest-seq) member. A run of one renders exactly as +before — no count shown. A differing action, entity, or problem never +collapses, so a state change is never hidden.`, + }, + }, + }, +} + +const sentinel = (over: Record) => + `AMICODE_DIFF ${JSON.stringify({ problem: "x-gate", entity: "recommend", action: "proposed", seq: 1, diff: {}, ...over })}` + +const Row = (props: { label: string; children: any }) => ( +
+ + {props.label} + +
+ {props.children} +
+
+) + +// A run of 4 identical (problem, entity, action) receipts — what the bug +// report showed as four stacked cards — now collapses to one, count 4, +// opening seq 4 (the latest). +export const CollapsedRunOfFour = () => ( + + + +) + +// A run of one renders exactly as it always has — no count, no markup change. +export const SingleReceipt = () => ( + + + +) + +// Two adjacent runs whose action differs never merge with each other — each +// renders as its own card (here, each itself a collapsed run of 2). +export const AdjacentRunsDifferentActions = () => ( + + + + +) + +// Side-by-side comparison, matching the report's stacked-card shape but with +// the fix applied: the repeated "Recommend" run collapses; the differently- +// actioned "Formulation" card beside it (a run of 1) is untouched. +export const StackedTranscriptExample = () => ( +
+ + +
+) diff --git a/packages/ui/src/amicode/card.tsx b/packages/ui/src/amicode/card.tsx index 81a5190442..f2fc415a6a 100644 --- a/packages/ui/src/amicode/card.tsx +++ b/packages/ui/src/amicode/card.tsx @@ -2,7 +2,7 @@ import { For, Match, Show, Switch, createMemo } from "solid-js" import { amicodeStage } from "./stage" import { parseAskInput } from "./ask" import { AmicodeAskCard } from "./ask-card" -import { parseDiffSentinel, receiptParts } from "./receipt" +import { parseDiffSentinel, receiptParts, INLINE_KINDS } from "./receipt" import { receiptIsCurrent } from "./receipt-currency" import { systemReceiptPieces, formulationReceiptPieces } from "./facets" import { compositeChip, chipText } from "./problem" @@ -55,7 +55,7 @@ function runRefFromOutput(output: unknown): { run: string; lab?: string } | unde type DiffPiece = { key: string; from?: string; to?: string } -function Chip(props: { tool: string; status?: string; output?: string }) { +function Chip(props: { tool: string; status?: string; output?: string; count?: number }) { const stage = createMemo(() => amicodeStage(props.tool)) const running = () => props.status === "pending" || props.status === "running" const errored = () => props.status === "error" || props.status === "failed" @@ -179,6 +179,15 @@ function Chip(props: { tool: string; status?: string; output?: string }) { )} + {/* amicode: a run of N consecutive receipts sharing (problem, entity, + action) collapses to this one card (../components/message-part.tsx, + ../amicode/receipt-runs.ts) — shown only when N > 1 so a lone + receipt renders exactly as it always has. */} + 1}> + + ×{props.count} + + )} @@ -239,6 +248,63 @@ function Chip(props: { tool: string; status?: string; output?: string }) { ) } +// AMICODE: a skill activation, wearing the Amico chip. Amicode's skills ARE Amico's — its +// repertoire — so activating one is Amico acting, and it earns the same chip the domain +// receipts wear. Reads " " like every other chip: the label names the kind, +// the detail names the skill. +// +// Inert by construction. Unlike a receipt there is no entity to open, so this is the plain +// shell with no chevron rather than the clickable