feat(amicode): the harmonic wave working indicator, and two transcript cleanups - #100

Merged
aarontrowbridge merged 13 commits into
local/amicodefrom
pr/amico-working-indicator
Aug 1, 2026
Merged

feat(amicode): the harmonic wave working indicator, and two transcript cleanups#100
aarontrowbridge merged 13 commits into
local/amicodefrom
pr/amico-working-indicator

Conversation

@aarontrowbridge

Copy link
Copy Markdown
Member

The chat had two "working" indicators that looked identical — the thinking line and the tool-group header — because both were shimmering text with no glyph. This replaces that with one brand motif, and fixes two transcript problems found while verifying it in the dev host.

The harmonic wave

A standing wave in quadrature: two identical sine curves, the companion a quarter period out of phase, so one is at full swing whenever the other crosses zero. That is what stops the glyph reading as a blink at 12px — a single standing line passes through zero amplitude twice per cycle. Three curves cross-fade so the harmonic index climbs.

Geometry and timing live in wave-geometry.ts, pure and DOM-free, following the thinking.ts / run-series.ts convention. All animation is CSS — two keyframes, transform and opacity only, no rAF — since this sits on screen for minutes at a time.

Two correctness traps are written down in the code because both are invisible on inspection:

  • Per-mode animation-delay magnitudes must descend for the visible sequence to ascend. The intuitive -1×/-2× ordering silently plays mode 1 → 3 → 2. The delays are therefore computed by a tested function and passed as inline style, so the CSS cannot reintroduce it. There is a regression test that fails if the ordering inverts.
  • No SVG <defs> and no id attributes. Several indicators mount at once (one per in-flight tool call) and SVG ids are document-global, so ids would collide and every instance would resolve to the first definition. A story mounts 8 at once as the guard.

Repeated receipt cards collapse

Four amicode_* calls against one entity rendered four identical cards. Runs of consecutive receipts sharing (problem, entity, action) now render as one card with a count, opening the highest seq. Conservative by design — a differing action, entity or problem, an interrupting part, or an unparseable sentinel all prevent merging, so no state change can hide. INLINE_KINDS entities are excluded entirely: they can resolve to a live InlineEntityView via a reactive currency check the pure module cannot see, so collapsing them risked dropping a render path.

Skill activations

ui.tool.skill ("Skill") was reachable only as a fallback for a missing input.name, which never happens — so an activated skill rendered as a bare word, indistinguishable from any other tool. Skills now say what they are and wear the Amico chip, since amicode's skills are Amico's repertoire. An opened skill file gets a bounded, tinted panel instead of dumping markdown into the transcript, because [data-component="tool-output"] carries no surface of its own.

Notes for review

  • tool-status-title.tsx is a stock opencode component. The change is logged in AMICODE-PATCHES.md. Only the shimmer went; the active→done word morph is deliberately preserved.
  • --v2-icon-icon-accent is the ink role, resolved by oc-2.json to grey-800 on light and #FFF676 on dark. Do not add a scheme selector for it. In particular, :root:not([data-theme="light"]) is dead CSS — the block containing it in v2/styles/theme.css is inside a /* */ comment, and data-theme holds a theme id rather than a scheme. An earlier revision built on it and shipped a lemon-on-white glyph at ~1.1:1.
  • Storybook cannot exercise the scheme switch by setting data-color-scheme on a wrapper; its preview loads only the v1 theme bundle. amico-wave.stories.tsx resolves tokens through resolveThemeVariantV2() instead, which is the only reason the contrast bug above was caught before a user saw it.
  • The H-mark in .amc-lane's head is now static, since two animated brand marks side by side compete with each other.

Testing

bun test src in packages/ui: 432 pass / 0 fail. bun turbo typecheck clean. oxlint packages/ui/src at 505 warnings / 0 errors — identical to the base, no new warnings. Verified in the extension dev host on both light and dark themes.

Not included

A transcript rail (a vertical spine bracketing Amico's stretch of a turn) was built and reverted. The design held up, but it stalled on a dev-host webview serving a stale content-hashed document, which meant no CSS or JS change reached the screen at all and looked exactly like a broken feature. Worth knowing independently of this PR: a new vendored binary needs a full dev-host restart, not Cmd+R — the webview can stay pinned to the old asset hash.

aarontrowbridgeand others added 13 commits July 29, 2026 06:34
…c number
The three curves are physically n=2,4,6 for a fixed-end string (lambda_n = 2L/n
with L=30), labelled 1/2/3 because the index counts full wavelengths across the
box. The consecutive set (lambda 60/30/20) was built and compared; the current
set won on looks. Written down because this codebase's readers are quantum-
control physicists who will reach for lambda_n = 2L/n and find the labels off
by a factor of two.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 4691a0b)
…ment
Adds the SolidJS component (amico-wave.tsx) that renders the standing-wave
glyph purely from amico-wave.ts's geometry/timing constants via CSS custom
properties, plus the matching CSS block and an anti-drift test guarding the
one unavoidable duplicated literal (the @Keyframes percentage). Removes the
now-superseded amc-text-shimmer treatment (leading dot + shimmer keyframes)
from the thinking line.
Also flips allowImportingTsExtensions on for packages/ui: this directory now
has both amico-wave.ts (pure module) and amico-wave.tsx (component) sharing a
stem, and under "bundler" resolution an extensionless "./amico-wave" import
resolves to the .tsx sibling instead of the .ts module (confirmed empirically
with a standalone repro) — the component would have silently self-imported.
Pinning the import with an explicit ".ts" extension fixes it, and this flag
is TypeScript's sanctioned way to permit that syntax under noEmit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 7a7bcff)
…the .ts/.tsx stem collision
amico-wave.ts and amico-wave.tsx shared a stem, and bun resolves an extensionless
import of that stem to the .tsx — so the component self-imported. Fixed by renaming
rather than by relaxing tsconfig: the directory convention (thinking.ts /
thinking-line.tsx) already avoids this by construction. Reverts
allowImportingTsExtensions and adds a test that fails if any same-stem pair
reappears in this directory.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 3b7b3b4)
… drift guard
The :root:not([data-theme="light"]) selector was derived from CSS that is inside a
block comment in v2/styles/theme.css. data-theme carries a theme id, not a scheme,
so the selector matched every reachable state and the wave rendered lemon at ~1.1:1
on light themes. oc-2.json already encodes the ink role in v2-icon-icon-accent
(grey-800 light, #FFF676 dark), which is what amico-presence.css uses, so no theme
selector is needed at all.
Also tightens the amc-wave-mode CSS/TS drift guard: the old toContain() check passed
under mutations that break the animation (ON edge dragged to 50%, OFF edge dragged to
40%) because the literal could still appear elsewhere in a loosely-bounded slice. Now
asserts each breakpoint against its own selector+brace, and the previously-unguarded
OFF literal is exported as MODE_OFF_PCT and checked too. Adds a MODE_HOLD_MS %
WAVE_PERIOD_MS invariant test, and rewords two comments that described the component
as already mounted (it isn't yet — a later task wires it in).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit fdab7d9)
…with a count
Four amicode_* calls against the same entity rendered four identical cards. Runs
of consecutive receipts sharing (problem, entity, action) now render as one card
with a count, opening the latest seq. Conservative by design: a differing action,
entity or problem, an interrupting part, or an unparseable sentinel all prevent
merging, so no state change can be hidden.
(cherry picked from commit 5176a69)
An activated skill rendered as a bare name ('brainstorming'), indistinguishable
from any other tool call: ui.tool.skill ('Skill') was reachable only as a
fallback for a missing input.name, which never happens. The kind now always
renders in front of the name, in both the expandable trigger and the compact
title map.
Also removes the last TextShimmer in the skill renderer. The previous commit
retired the shimmer from tool-status-title but missed this site, leaving the two
inconsistent.
Not made an amicode receipt card, despite the temptation: those carry an
AMICODE_DIFF sentinel and open an entity view. A skill has neither, and spending
the H-mark card on generic agent mechanics would dilute what the mark means.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit b6981f6)
Amicode's skills are Amico's own repertoire, so activating one is Amico acting and
earns the same chip the domain receipts wear: H-mark, rule, label naming the kind,
detail naming the skill. Replaces the plain text label from the previous commit.
New AmicoSkillChip in card.tsx rather than a new mode on Chip -- Chip is
sentinel-driven and a skill has no AMICODE_DIFF, so threading it through would
have meant special-casing the parse path. Inert shell, no chevron: there is no
entity to open.
It nests inside BasicTool's trigger so the expandable instruction body is kept.
That is only safe because BasicTool declares an `icon` prop and never renders it
-- verified, nothing in basic-tool.tsx reads props.icon -- so the H-mark is the
row's only glyph and no stock component needed changing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 17c6755)
Expanding a skill dumped its instructions as bare markdown into the transcript,
because [data-component="tool-output"] has no surface of its own -- no border, no
background, no padding, and white-space:pre. So an opened skill read as more
conversation. It is a file, so it now gets a file's surface: hairline, tint,
padding, and a bounded scrolling height instead of pushing the transcript around.
Scoped to skills; other tools' output is unchanged. Compound selector because a
bare .amc-skill-file would tie with [data-component="tool-output"] on specificity
and be settled by stylesheet order.
Follows a revert of the aside-lane attempt, which indented the whole activation
behind an accent spine and broke the shared left edge that Model / Skill /
Recommend chips read along. The chip belongs flush with its siblings; it was only
ever the opened body that needed separating.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 8e11411)
@aarontrowbridge
aarontrowbridge merged commit b61d294 into local/amicodeAug 1, 2026
2 of 4 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@aarontrowbridge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat(amicode): the harmonic wave working indicator, and two transcript cleanups - #100

Merged
aarontrowbridge merged 13 commits into
local/amicodefrom
pr/amico-working-indicator
Aug 1, 2026
Merged

feat(amicode): the harmonic wave working indicator, and two transcript cleanups#100
aarontrowbridge merged 13 commits into
local/amicodefrom
pr/amico-working-indicator

Conversation

@aarontrowbridge

Copy link
Copy Markdown
Member

The chat had two "working" indicators that looked identical — the thinking line and the tool-group header — because both were shimmering text with no glyph. This replaces that with one brand motif, and fixes two transcript problems found while verifying it in the dev host.

The harmonic wave

A standing wave in quadrature: two identical sine curves, the companion a quarter period out of phase, so one is at full swing whenever the other crosses zero. That is what stops the glyph reading as a blink at 12px — a single standing line passes through zero amplitude twice per cycle. Three curves cross-fade so the harmonic index climbs.

Geometry and timing live in wave-geometry.ts, pure and DOM-free, following the thinking.ts / run-series.ts convention. All animation is CSS — two keyframes, transform and opacity only, no rAF — since this sits on screen for minutes at a time.

Two correctness traps are written down in the code because both are invisible on inspection:

  • Per-mode animation-delay magnitudes must descend for the visible sequence to ascend. The intuitive -1×/-2× ordering silently plays mode 1 → 3 → 2. The delays are therefore computed by a tested function and passed as inline style, so the CSS cannot reintroduce it. There is a regression test that fails if the ordering inverts.
  • No SVG <defs> and no id attributes. Several indicators mount at once (one per in-flight tool call) and SVG ids are document-global, so ids would collide and every instance would resolve to the first definition. A story mounts 8 at once as the guard.

Repeated receipt cards collapse

Four amicode_* calls against one entity rendered four identical cards. Runs of consecutive receipts sharing (problem, entity, action) now render as one card with a count, opening the highest seq. Conservative by design — a differing action, entity or problem, an interrupting part, or an unparseable sentinel all prevent merging, so no state change can hide. INLINE_KINDS entities are excluded entirely: they can resolve to a live InlineEntityView via a reactive currency check the pure module cannot see, so collapsing them risked dropping a render path.

Skill activations

ui.tool.skill ("Skill") was reachable only as a fallback for a missing input.name, which never happens — so an activated skill rendered as a bare word, indistinguishable from any other tool. Skills now say what they are and wear the Amico chip, since amicode's skills are Amico's repertoire. An opened skill file gets a bounded, tinted panel instead of dumping markdown into the transcript, because [data-component="tool-output"] carries no surface of its own.

Notes for review

  • tool-status-title.tsx is a stock opencode component. The change is logged in AMICODE-PATCHES.md. Only the shimmer went; the active→done word morph is deliberately preserved.
  • --v2-icon-icon-accent is the ink role, resolved by oc-2.json to grey-800 on light and #FFF676 on dark. Do not add a scheme selector for it. In particular, :root:not([data-theme="light"]) is dead CSS — the block containing it in v2/styles/theme.css is inside a /* */ comment, and data-theme holds a theme id rather than a scheme. An earlier revision built on it and shipped a lemon-on-white glyph at ~1.1:1.
  • Storybook cannot exercise the scheme switch by setting data-color-scheme on a wrapper; its preview loads only the v1 theme bundle. amico-wave.stories.tsx resolves tokens through resolveThemeVariantV2() instead, which is the only reason the contrast bug above was caught before a user saw it.
  • The H-mark in .amc-lane's head is now static, since two animated brand marks side by side compete with each other.

Testing

bun test src in packages/ui: 432 pass / 0 fail. bun turbo typecheck clean. oxlint packages/ui/src at 505 warnings / 0 errors — identical to the base, no new warnings. Verified in the extension dev host on both light and dark themes.

Not included

A transcript rail (a vertical spine bracketing Amico's stretch of a turn) was built and reverted. The design held up, but it stalled on a dev-host webview serving a stale content-hashed document, which meant no CSS or JS change reached the screen at all and looked exactly like a broken feature. Worth knowing independently of this PR: a new vendored binary needs a full dev-host restart, not Cmd+R — the webview can stay pinned to the old asset hash.

aarontrowbridgeand others added 13 commits July 29, 2026 06:34
…c number
The three curves are physically n=2,4,6 for a fixed-end string (lambda_n = 2L/n
with L=30), labelled 1/2/3 because the index counts full wavelengths across the
box. The consecutive set (lambda 60/30/20) was built and compared; the current
set won on looks. Written down because this codebase's readers are quantum-
control physicists who will reach for lambda_n = 2L/n and find the labels off
by a factor of two.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 4691a0b)
…ment
Adds the SolidJS component (amico-wave.tsx) that renders the standing-wave
glyph purely from amico-wave.ts's geometry/timing constants via CSS custom
properties, plus the matching CSS block and an anti-drift test guarding the
one unavoidable duplicated literal (the @Keyframes percentage). Removes the
now-superseded amc-text-shimmer treatment (leading dot + shimmer keyframes)
from the thinking line.
Also flips allowImportingTsExtensions on for packages/ui: this directory now
has both amico-wave.ts (pure module) and amico-wave.tsx (component) sharing a
stem, and under "bundler" resolution an extensionless "./amico-wave" import
resolves to the .tsx sibling instead of the .ts module (confirmed empirically
with a standalone repro) — the component would have silently self-imported.
Pinning the import with an explicit ".ts" extension fixes it, and this flag
is TypeScript's sanctioned way to permit that syntax under noEmit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 7a7bcff)
…the .ts/.tsx stem collision
amico-wave.ts and amico-wave.tsx shared a stem, and bun resolves an extensionless
import of that stem to the .tsx — so the component self-imported. Fixed by renaming
rather than by relaxing tsconfig: the directory convention (thinking.ts /
thinking-line.tsx) already avoids this by construction. Reverts
allowImportingTsExtensions and adds a test that fails if any same-stem pair
reappears in this directory.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 3b7b3b4)
… drift guard
The :root:not([data-theme="light"]) selector was derived from CSS that is inside a
block comment in v2/styles/theme.css. data-theme carries a theme id, not a scheme,
so the selector matched every reachable state and the wave rendered lemon at ~1.1:1
on light themes. oc-2.json already encodes the ink role in v2-icon-icon-accent
(grey-800 light, #FFF676 dark), which is what amico-presence.css uses, so no theme
selector is needed at all.
Also tightens the amc-wave-mode CSS/TS drift guard: the old toContain() check passed
under mutations that break the animation (ON edge dragged to 50%, OFF edge dragged to
40%) because the literal could still appear elsewhere in a loosely-bounded slice. Now
asserts each breakpoint against its own selector+brace, and the previously-unguarded
OFF literal is exported as MODE_OFF_PCT and checked too. Adds a MODE_HOLD_MS %
WAVE_PERIOD_MS invariant test, and rewords two comments that described the component
as already mounted (it isn't yet — a later task wires it in).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit fdab7d9)
…with a count
Four amicode_* calls against the same entity rendered four identical cards. Runs
of consecutive receipts sharing (problem, entity, action) now render as one card
with a count, opening the latest seq. Conservative by design: a differing action,
entity or problem, an interrupting part, or an unparseable sentinel all prevent
merging, so no state change can be hidden.
(cherry picked from commit 5176a69)
An activated skill rendered as a bare name ('brainstorming'), indistinguishable
from any other tool call: ui.tool.skill ('Skill') was reachable only as a
fallback for a missing input.name, which never happens. The kind now always
renders in front of the name, in both the expandable trigger and the compact
title map.
Also removes the last TextShimmer in the skill renderer. The previous commit
retired the shimmer from tool-status-title but missed this site, leaving the two
inconsistent.
Not made an amicode receipt card, despite the temptation: those carry an
AMICODE_DIFF sentinel and open an entity view. A skill has neither, and spending
the H-mark card on generic agent mechanics would dilute what the mark means.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit b6981f6)
Amicode's skills are Amico's own repertoire, so activating one is Amico acting and
earns the same chip the domain receipts wear: H-mark, rule, label naming the kind,
detail naming the skill. Replaces the plain text label from the previous commit.
New AmicoSkillChip in card.tsx rather than a new mode on Chip -- Chip is
sentinel-driven and a skill has no AMICODE_DIFF, so threading it through would
have meant special-casing the parse path. Inert shell, no chevron: there is no
entity to open.
It nests inside BasicTool's trigger so the expandable instruction body is kept.
That is only safe because BasicTool declares an `icon` prop and never renders it
-- verified, nothing in basic-tool.tsx reads props.icon -- so the H-mark is the
row's only glyph and no stock component needed changing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 17c6755)
Expanding a skill dumped its instructions as bare markdown into the transcript,
because [data-component="tool-output"] has no surface of its own -- no border, no
background, no padding, and white-space:pre. So an opened skill read as more
conversation. It is a file, so it now gets a file's surface: hairline, tint,
padding, and a bounded scrolling height instead of pushing the transcript around.
Scoped to skills; other tools' output is unchanged. Compound selector because a
bare .amc-skill-file would tie with [data-component="tool-output"] on specificity
and be settled by stylesheet order.
Follows a revert of the aside-lane attempt, which indented the whole activation
behind an accent spine and broke the shared left edge that Model / Skill /
Recommend chips read along. The chip belongs flush with its siblings; it was only
ever the opened body that needed separating.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 8e11411)
@aarontrowbridge
aarontrowbridge merged commit b61d294 into local/amicodeAug 1, 2026
2 of 4 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@aarontrowbridge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(amicode): the harmonic wave working indicator, and two transcript cleanups - #100

Merged
aarontrowbridge merged 13 commits into
local/amicodefrom
pr/amico-working-indicator
Aug 1, 2026
Merged

feat(amicode): the harmonic wave working indicator, and two transcript cleanups#100
aarontrowbridge merged 13 commits into
local/amicodefrom
pr/amico-working-indicator

Conversation

@aarontrowbridge

Copy link
Copy Markdown
Member

The chat had two "working" indicators that looked identical — the thinking line and the tool-group header — because both were shimmering text with no glyph. This replaces that with one brand motif, and fixes two transcript problems found while verifying it in the dev host.

The harmonic wave

A standing wave in quadrature: two identical sine curves, the companion a quarter period out of phase, so one is at full swing whenever the other crosses zero. That is what stops the glyph reading as a blink at 12px — a single standing line passes through zero amplitude twice per cycle. Three curves cross-fade so the harmonic index climbs.

Geometry and timing live in wave-geometry.ts, pure and DOM-free, following the thinking.ts / run-series.ts convention. All animation is CSS — two keyframes, transform and opacity only, no rAF — since this sits on screen for minutes at a time.

Two correctness traps are written down in the code because both are invisible on inspection:

  • Per-mode animation-delay magnitudes must descend for the visible sequence to ascend. The intuitive -1×/-2× ordering silently plays mode 1 → 3 → 2. The delays are therefore computed by a tested function and passed as inline style, so the CSS cannot reintroduce it. There is a regression test that fails if the ordering inverts.
  • No SVG <defs> and no id attributes. Several indicators mount at once (one per in-flight tool call) and SVG ids are document-global, so ids would collide and every instance would resolve to the first definition. A story mounts 8 at once as the guard.

Repeated receipt cards collapse

Four amicode_* calls against one entity rendered four identical cards. Runs of consecutive receipts sharing (problem, entity, action) now render as one card with a count, opening the highest seq. Conservative by design — a differing action, entity or problem, an interrupting part, or an unparseable sentinel all prevent merging, so no state change can hide. INLINE_KINDS entities are excluded entirely: they can resolve to a live InlineEntityView via a reactive currency check the pure module cannot see, so collapsing them risked dropping a render path.

Skill activations

ui.tool.skill ("Skill") was reachable only as a fallback for a missing input.name, which never happens — so an activated skill rendered as a bare word, indistinguishable from any other tool. Skills now say what they are and wear the Amico chip, since amicode's skills are Amico's repertoire. An opened skill file gets a bounded, tinted panel instead of dumping markdown into the transcript, because [data-component="tool-output"] carries no surface of its own.

Notes for review

  • tool-status-title.tsx is a stock opencode component. The change is logged in AMICODE-PATCHES.md. Only the shimmer went; the active→done word morph is deliberately preserved.
  • --v2-icon-icon-accent is the ink role, resolved by oc-2.json to grey-800 on light and #FFF676 on dark. Do not add a scheme selector for it. In particular, :root:not([data-theme="light"]) is dead CSS — the block containing it in v2/styles/theme.css is inside a /* */ comment, and data-theme holds a theme id rather than a scheme. An earlier revision built on it and shipped a lemon-on-white glyph at ~1.1:1.
  • Storybook cannot exercise the scheme switch by setting data-color-scheme on a wrapper; its preview loads only the v1 theme bundle. amico-wave.stories.tsx resolves tokens through resolveThemeVariantV2() instead, which is the only reason the contrast bug above was caught before a user saw it.
  • The H-mark in .amc-lane's head is now static, since two animated brand marks side by side compete with each other.

Testing

bun test src in packages/ui: 432 pass / 0 fail. bun turbo typecheck clean. oxlint packages/ui/src at 505 warnings / 0 errors — identical to the base, no new warnings. Verified in the extension dev host on both light and dark themes.

Not included

A transcript rail (a vertical spine bracketing Amico's stretch of a turn) was built and reverted. The design held up, but it stalled on a dev-host webview serving a stale content-hashed document, which meant no CSS or JS change reached the screen at all and looked exactly like a broken feature. Worth knowing independently of this PR: a new vendored binary needs a full dev-host restart, not Cmd+R — the webview can stay pinned to the old asset hash.

aarontrowbridgeand others added 13 commits July 29, 2026 06:34
…c number
The three curves are physically n=2,4,6 for a fixed-end string (lambda_n = 2L/n
with L=30), labelled 1/2/3 because the index counts full wavelengths across the
box. The consecutive set (lambda 60/30/20) was built and compared; the current
set won on looks. Written down because this codebase's readers are quantum-
control physicists who will reach for lambda_n = 2L/n and find the labels off
by a factor of two.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 4691a0b)
…ment
Adds the SolidJS component (amico-wave.tsx) that renders the standing-wave
glyph purely from amico-wave.ts's geometry/timing constants via CSS custom
properties, plus the matching CSS block and an anti-drift test guarding the
one unavoidable duplicated literal (the @Keyframes percentage). Removes the
now-superseded amc-text-shimmer treatment (leading dot + shimmer keyframes)
from the thinking line.
Also flips allowImportingTsExtensions on for packages/ui: this directory now
has both amico-wave.ts (pure module) and amico-wave.tsx (component) sharing a
stem, and under "bundler" resolution an extensionless "./amico-wave" import
resolves to the .tsx sibling instead of the .ts module (confirmed empirically
with a standalone repro) — the component would have silently self-imported.
Pinning the import with an explicit ".ts" extension fixes it, and this flag
is TypeScript's sanctioned way to permit that syntax under noEmit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 7a7bcff)
…the .ts/.tsx stem collision
amico-wave.ts and amico-wave.tsx shared a stem, and bun resolves an extensionless
import of that stem to the .tsx — so the component self-imported. Fixed by renaming
rather than by relaxing tsconfig: the directory convention (thinking.ts /
thinking-line.tsx) already avoids this by construction. Reverts
allowImportingTsExtensions and adds a test that fails if any same-stem pair
reappears in this directory.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 3b7b3b4)
… drift guard
The :root:not([data-theme="light"]) selector was derived from CSS that is inside a
block comment in v2/styles/theme.css. data-theme carries a theme id, not a scheme,
so the selector matched every reachable state and the wave rendered lemon at ~1.1:1
on light themes. oc-2.json already encodes the ink role in v2-icon-icon-accent
(grey-800 light, #FFF676 dark), which is what amico-presence.css uses, so no theme
selector is needed at all.
Also tightens the amc-wave-mode CSS/TS drift guard: the old toContain() check passed
under mutations that break the animation (ON edge dragged to 50%, OFF edge dragged to
40%) because the literal could still appear elsewhere in a loosely-bounded slice. Now
asserts each breakpoint against its own selector+brace, and the previously-unguarded
OFF literal is exported as MODE_OFF_PCT and checked too. Adds a MODE_HOLD_MS %
WAVE_PERIOD_MS invariant test, and rewords two comments that described the component
as already mounted (it isn't yet — a later task wires it in).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit fdab7d9)
…with a count
Four amicode_* calls against the same entity rendered four identical cards. Runs
of consecutive receipts sharing (problem, entity, action) now render as one card
with a count, opening the latest seq. Conservative by design: a differing action,
entity or problem, an interrupting part, or an unparseable sentinel all prevent
merging, so no state change can be hidden.
(cherry picked from commit 5176a69)
An activated skill rendered as a bare name ('brainstorming'), indistinguishable
from any other tool call: ui.tool.skill ('Skill') was reachable only as a
fallback for a missing input.name, which never happens. The kind now always
renders in front of the name, in both the expandable trigger and the compact
title map.
Also removes the last TextShimmer in the skill renderer. The previous commit
retired the shimmer from tool-status-title but missed this site, leaving the two
inconsistent.
Not made an amicode receipt card, despite the temptation: those carry an
AMICODE_DIFF sentinel and open an entity view. A skill has neither, and spending
the H-mark card on generic agent mechanics would dilute what the mark means.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit b6981f6)
Amicode's skills are Amico's own repertoire, so activating one is Amico acting and
earns the same chip the domain receipts wear: H-mark, rule, label naming the kind,
detail naming the skill. Replaces the plain text label from the previous commit.
New AmicoSkillChip in card.tsx rather than a new mode on Chip -- Chip is
sentinel-driven and a skill has no AMICODE_DIFF, so threading it through would
have meant special-casing the parse path. Inert shell, no chevron: there is no
entity to open.
It nests inside BasicTool's trigger so the expandable instruction body is kept.
That is only safe because BasicTool declares an `icon` prop and never renders it
-- verified, nothing in basic-tool.tsx reads props.icon -- so the H-mark is the
row's only glyph and no stock component needed changing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 17c6755)
Expanding a skill dumped its instructions as bare markdown into the transcript,
because [data-component="tool-output"] has no surface of its own -- no border, no
background, no padding, and white-space:pre. So an opened skill read as more
conversation. It is a file, so it now gets a file's surface: hairline, tint,
padding, and a bounded scrolling height instead of pushing the transcript around.
Scoped to skills; other tools' output is unchanged. Compound selector because a
bare .amc-skill-file would tie with [data-component="tool-output"] on specificity
and be settled by stylesheet order.
Follows a revert of the aside-lane attempt, which indented the whole activation
behind an accent spine and broke the shared left edge that Model / Skill /
Recommend chips read along. The chip belongs flush with its siblings; it was only
ever the opened body that needed separating.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 8e11411)
@aarontrowbridge
aarontrowbridge merged commit b61d294 into local/amicodeAug 1, 2026
2 of 4 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@aarontrowbridge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(amicode): the harmonic wave working indicator, and two transcript cleanups - #100

Merged
aarontrowbridge merged 13 commits into
local/amicodefrom
pr/amico-working-indicator
Aug 1, 2026
Merged

feat(amicode): the harmonic wave working indicator, and two transcript cleanups#100
aarontrowbridge merged 13 commits into
local/amicodefrom
pr/amico-working-indicator

Conversation

@aarontrowbridge

Copy link
Copy Markdown
Member

The chat had two "working" indicators that looked identical — the thinking line and the tool-group header — because both were shimmering text with no glyph. This replaces that with one brand motif, and fixes two transcript problems found while verifying it in the dev host.

The harmonic wave

A standing wave in quadrature: two identical sine curves, the companion a quarter period out of phase, so one is at full swing whenever the other crosses zero. That is what stops the glyph reading as a blink at 12px — a single standing line passes through zero amplitude twice per cycle. Three curves cross-fade so the harmonic index climbs.

Geometry and timing live in wave-geometry.ts, pure and DOM-free, following the thinking.ts / run-series.ts convention. All animation is CSS — two keyframes, transform and opacity only, no rAF — since this sits on screen for minutes at a time.

Two correctness traps are written down in the code because both are invisible on inspection:

  • Per-mode animation-delay magnitudes must descend for the visible sequence to ascend. The intuitive -1×/-2× ordering silently plays mode 1 → 3 → 2. The delays are therefore computed by a tested function and passed as inline style, so the CSS cannot reintroduce it. There is a regression test that fails if the ordering inverts.
  • No SVG <defs> and no id attributes. Several indicators mount at once (one per in-flight tool call) and SVG ids are document-global, so ids would collide and every instance would resolve to the first definition. A story mounts 8 at once as the guard.

Repeated receipt cards collapse

Four amicode_* calls against one entity rendered four identical cards. Runs of consecutive receipts sharing (problem, entity, action) now render as one card with a count, opening the highest seq. Conservative by design — a differing action, entity or problem, an interrupting part, or an unparseable sentinel all prevent merging, so no state change can hide. INLINE_KINDS entities are excluded entirely: they can resolve to a live InlineEntityView via a reactive currency check the pure module cannot see, so collapsing them risked dropping a render path.

Skill activations

ui.tool.skill ("Skill") was reachable only as a fallback for a missing input.name, which never happens — so an activated skill rendered as a bare word, indistinguishable from any other tool. Skills now say what they are and wear the Amico chip, since amicode's skills are Amico's repertoire. An opened skill file gets a bounded, tinted panel instead of dumping markdown into the transcript, because [data-component="tool-output"] carries no surface of its own.

Notes for review

  • tool-status-title.tsx is a stock opencode component. The change is logged in AMICODE-PATCHES.md. Only the shimmer went; the active→done word morph is deliberately preserved.
  • --v2-icon-icon-accent is the ink role, resolved by oc-2.json to grey-800 on light and #FFF676 on dark. Do not add a scheme selector for it. In particular, :root:not([data-theme="light"]) is dead CSS — the block containing it in v2/styles/theme.css is inside a /* */ comment, and data-theme holds a theme id rather than a scheme. An earlier revision built on it and shipped a lemon-on-white glyph at ~1.1:1.
  • Storybook cannot exercise the scheme switch by setting data-color-scheme on a wrapper; its preview loads only the v1 theme bundle. amico-wave.stories.tsx resolves tokens through resolveThemeVariantV2() instead, which is the only reason the contrast bug above was caught before a user saw it.
  • The H-mark in .amc-lane's head is now static, since two animated brand marks side by side compete with each other.

Testing

bun test src in packages/ui: 432 pass / 0 fail. bun turbo typecheck clean. oxlint packages/ui/src at 505 warnings / 0 errors — identical to the base, no new warnings. Verified in the extension dev host on both light and dark themes.

Not included

A transcript rail (a vertical spine bracketing Amico's stretch of a turn) was built and reverted. The design held up, but it stalled on a dev-host webview serving a stale content-hashed document, which meant no CSS or JS change reached the screen at all and looked exactly like a broken feature. Worth knowing independently of this PR: a new vendored binary needs a full dev-host restart, not Cmd+R — the webview can stay pinned to the old asset hash.

aarontrowbridgeand others added 13 commits July 29, 2026 06:34
…c number
The three curves are physically n=2,4,6 for a fixed-end string (lambda_n = 2L/n
with L=30), labelled 1/2/3 because the index counts full wavelengths across the
box. The consecutive set (lambda 60/30/20) was built and compared; the current
set won on looks. Written down because this codebase's readers are quantum-
control physicists who will reach for lambda_n = 2L/n and find the labels off
by a factor of two.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 4691a0b)
…ment
Adds the SolidJS component (amico-wave.tsx) that renders the standing-wave
glyph purely from amico-wave.ts's geometry/timing constants via CSS custom
properties, plus the matching CSS block and an anti-drift test guarding the
one unavoidable duplicated literal (the @Keyframes percentage). Removes the
now-superseded amc-text-shimmer treatment (leading dot + shimmer keyframes)
from the thinking line.
Also flips allowImportingTsExtensions on for packages/ui: this directory now
has both amico-wave.ts (pure module) and amico-wave.tsx (component) sharing a
stem, and under "bundler" resolution an extensionless "./amico-wave" import
resolves to the .tsx sibling instead of the .ts module (confirmed empirically
with a standalone repro) — the component would have silently self-imported.
Pinning the import with an explicit ".ts" extension fixes it, and this flag
is TypeScript's sanctioned way to permit that syntax under noEmit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 7a7bcff)
…the .ts/.tsx stem collision
amico-wave.ts and amico-wave.tsx shared a stem, and bun resolves an extensionless
import of that stem to the .tsx — so the component self-imported. Fixed by renaming
rather than by relaxing tsconfig: the directory convention (thinking.ts /
thinking-line.tsx) already avoids this by construction. Reverts
allowImportingTsExtensions and adds a test that fails if any same-stem pair
reappears in this directory.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 3b7b3b4)
… drift guard
The :root:not([data-theme="light"]) selector was derived from CSS that is inside a
block comment in v2/styles/theme.css. data-theme carries a theme id, not a scheme,
so the selector matched every reachable state and the wave rendered lemon at ~1.1:1
on light themes. oc-2.json already encodes the ink role in v2-icon-icon-accent
(grey-800 light, #FFF676 dark), which is what amico-presence.css uses, so no theme
selector is needed at all.
Also tightens the amc-wave-mode CSS/TS drift guard: the old toContain() check passed
under mutations that break the animation (ON edge dragged to 50%, OFF edge dragged to
40%) because the literal could still appear elsewhere in a loosely-bounded slice. Now
asserts each breakpoint against its own selector+brace, and the previously-unguarded
OFF literal is exported as MODE_OFF_PCT and checked too. Adds a MODE_HOLD_MS %
WAVE_PERIOD_MS invariant test, and rewords two comments that described the component
as already mounted (it isn't yet — a later task wires it in).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit fdab7d9)
…with a count
Four amicode_* calls against the same entity rendered four identical cards. Runs
of consecutive receipts sharing (problem, entity, action) now render as one card
with a count, opening the latest seq. Conservative by design: a differing action,
entity or problem, an interrupting part, or an unparseable sentinel all prevent
merging, so no state change can be hidden.
(cherry picked from commit 5176a69)
An activated skill rendered as a bare name ('brainstorming'), indistinguishable
from any other tool call: ui.tool.skill ('Skill') was reachable only as a
fallback for a missing input.name, which never happens. The kind now always
renders in front of the name, in both the expandable trigger and the compact
title map.
Also removes the last TextShimmer in the skill renderer. The previous commit
retired the shimmer from tool-status-title but missed this site, leaving the two
inconsistent.
Not made an amicode receipt card, despite the temptation: those carry an
AMICODE_DIFF sentinel and open an entity view. A skill has neither, and spending
the H-mark card on generic agent mechanics would dilute what the mark means.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit b6981f6)
Amicode's skills are Amico's own repertoire, so activating one is Amico acting and
earns the same chip the domain receipts wear: H-mark, rule, label naming the kind,
detail naming the skill. Replaces the plain text label from the previous commit.
New AmicoSkillChip in card.tsx rather than a new mode on Chip -- Chip is
sentinel-driven and a skill has no AMICODE_DIFF, so threading it through would
have meant special-casing the parse path. Inert shell, no chevron: there is no
entity to open.
It nests inside BasicTool's trigger so the expandable instruction body is kept.
That is only safe because BasicTool declares an `icon` prop and never renders it
-- verified, nothing in basic-tool.tsx reads props.icon -- so the H-mark is the
row's only glyph and no stock component needed changing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 17c6755)
Expanding a skill dumped its instructions as bare markdown into the transcript,
because [data-component="tool-output"] has no surface of its own -- no border, no
background, no padding, and white-space:pre. So an opened skill read as more
conversation. It is a file, so it now gets a file's surface: hairline, tint,
padding, and a bounded scrolling height instead of pushing the transcript around.
Scoped to skills; other tools' output is unchanged. Compound selector because a
bare .amc-skill-file would tie with [data-component="tool-output"] on specificity
and be settled by stylesheet order.
Follows a revert of the aside-lane attempt, which indented the whole activation
behind an accent spine and broke the shared left edge that Model / Skill /
Recommend chips read along. The chip belongs flush with its siblings; it was only
ever the opened body that needed separating.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 8e11411)
@aarontrowbridge
aarontrowbridge merged commit b61d294 into local/amicodeAug 1, 2026
2 of 4 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@aarontrowbridge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat(amicode): the harmonic wave working indicator, and two transcript cleanups - #100

Merged
aarontrowbridge merged 13 commits into
local/amicodefrom
pr/amico-working-indicator
Aug 1, 2026
Merged

feat(amicode): the harmonic wave working indicator, and two transcript cleanups#100
aarontrowbridge merged 13 commits into
local/amicodefrom
pr/amico-working-indicator

Conversation

@aarontrowbridge

Copy link
Copy Markdown
Member

The chat had two "working" indicators that looked identical — the thinking line and the tool-group header — because both were shimmering text with no glyph. This replaces that with one brand motif, and fixes two transcript problems found while verifying it in the dev host.

The harmonic wave

A standing wave in quadrature: two identical sine curves, the companion a quarter period out of phase, so one is at full swing whenever the other crosses zero. That is what stops the glyph reading as a blink at 12px — a single standing line passes through zero amplitude twice per cycle. Three curves cross-fade so the harmonic index climbs.

Geometry and timing live in wave-geometry.ts, pure and DOM-free, following the thinking.ts / run-series.ts convention. All animation is CSS — two keyframes, transform and opacity only, no rAF — since this sits on screen for minutes at a time.

Two correctness traps are written down in the code because both are invisible on inspection:

  • Per-mode animation-delay magnitudes must descend for the visible sequence to ascend. The intuitive -1×/-2× ordering silently plays mode 1 → 3 → 2. The delays are therefore computed by a tested function and passed as inline style, so the CSS cannot reintroduce it. There is a regression test that fails if the ordering inverts.
  • No SVG <defs> and no id attributes. Several indicators mount at once (one per in-flight tool call) and SVG ids are document-global, so ids would collide and every instance would resolve to the first definition. A story mounts 8 at once as the guard.

Repeated receipt cards collapse

Four amicode_* calls against one entity rendered four identical cards. Runs of consecutive receipts sharing (problem, entity, action) now render as one card with a count, opening the highest seq. Conservative by design — a differing action, entity or problem, an interrupting part, or an unparseable sentinel all prevent merging, so no state change can hide. INLINE_KINDS entities are excluded entirely: they can resolve to a live InlineEntityView via a reactive currency check the pure module cannot see, so collapsing them risked dropping a render path.

Skill activations

ui.tool.skill ("Skill") was reachable only as a fallback for a missing input.name, which never happens — so an activated skill rendered as a bare word, indistinguishable from any other tool. Skills now say what they are and wear the Amico chip, since amicode's skills are Amico's repertoire. An opened skill file gets a bounded, tinted panel instead of dumping markdown into the transcript, because [data-component="tool-output"] carries no surface of its own.

Notes for review

  • tool-status-title.tsx is a stock opencode component. The change is logged in AMICODE-PATCHES.md. Only the shimmer went; the active→done word morph is deliberately preserved.
  • --v2-icon-icon-accent is the ink role, resolved by oc-2.json to grey-800 on light and #FFF676 on dark. Do not add a scheme selector for it. In particular, :root:not([data-theme="light"]) is dead CSS — the block containing it in v2/styles/theme.css is inside a /* */ comment, and data-theme holds a theme id rather than a scheme. An earlier revision built on it and shipped a lemon-on-white glyph at ~1.1:1.
  • Storybook cannot exercise the scheme switch by setting data-color-scheme on a wrapper; its preview loads only the v1 theme bundle. amico-wave.stories.tsx resolves tokens through resolveThemeVariantV2() instead, which is the only reason the contrast bug above was caught before a user saw it.
  • The H-mark in .amc-lane's head is now static, since two animated brand marks side by side compete with each other.

Testing

bun test src in packages/ui: 432 pass / 0 fail. bun turbo typecheck clean. oxlint packages/ui/src at 505 warnings / 0 errors — identical to the base, no new warnings. Verified in the extension dev host on both light and dark themes.

Not included

A transcript rail (a vertical spine bracketing Amico's stretch of a turn) was built and reverted. The design held up, but it stalled on a dev-host webview serving a stale content-hashed document, which meant no CSS or JS change reached the screen at all and looked exactly like a broken feature. Worth knowing independently of this PR: a new vendored binary needs a full dev-host restart, not Cmd+R — the webview can stay pinned to the old asset hash.

aarontrowbridgeand others added 13 commits July 29, 2026 06:34
…c number
The three curves are physically n=2,4,6 for a fixed-end string (lambda_n = 2L/n
with L=30), labelled 1/2/3 because the index counts full wavelengths across the
box. The consecutive set (lambda 60/30/20) was built and compared; the current
set won on looks. Written down because this codebase's readers are quantum-
control physicists who will reach for lambda_n = 2L/n and find the labels off
by a factor of two.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 4691a0b)
…ment
Adds the SolidJS component (amico-wave.tsx) that renders the standing-wave
glyph purely from amico-wave.ts's geometry/timing constants via CSS custom
properties, plus the matching CSS block and an anti-drift test guarding the
one unavoidable duplicated literal (the @Keyframes percentage). Removes the
now-superseded amc-text-shimmer treatment (leading dot + shimmer keyframes)
from the thinking line.
Also flips allowImportingTsExtensions on for packages/ui: this directory now
has both amico-wave.ts (pure module) and amico-wave.tsx (component) sharing a
stem, and under "bundler" resolution an extensionless "./amico-wave" import
resolves to the .tsx sibling instead of the .ts module (confirmed empirically
with a standalone repro) — the component would have silently self-imported.
Pinning the import with an explicit ".ts" extension fixes it, and this flag
is TypeScript's sanctioned way to permit that syntax under noEmit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 7a7bcff)
…the .ts/.tsx stem collision
amico-wave.ts and amico-wave.tsx shared a stem, and bun resolves an extensionless
import of that stem to the .tsx — so the component self-imported. Fixed by renaming
rather than by relaxing tsconfig: the directory convention (thinking.ts /
thinking-line.tsx) already avoids this by construction. Reverts
allowImportingTsExtensions and adds a test that fails if any same-stem pair
reappears in this directory.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 3b7b3b4)
… drift guard
The :root:not([data-theme="light"]) selector was derived from CSS that is inside a
block comment in v2/styles/theme.css. data-theme carries a theme id, not a scheme,
so the selector matched every reachable state and the wave rendered lemon at ~1.1:1
on light themes. oc-2.json already encodes the ink role in v2-icon-icon-accent
(grey-800 light, #FFF676 dark), which is what amico-presence.css uses, so no theme
selector is needed at all.
Also tightens the amc-wave-mode CSS/TS drift guard: the old toContain() check passed
under mutations that break the animation (ON edge dragged to 50%, OFF edge dragged to
40%) because the literal could still appear elsewhere in a loosely-bounded slice. Now
asserts each breakpoint against its own selector+brace, and the previously-unguarded
OFF literal is exported as MODE_OFF_PCT and checked too. Adds a MODE_HOLD_MS %
WAVE_PERIOD_MS invariant test, and rewords two comments that described the component
as already mounted (it isn't yet — a later task wires it in).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit fdab7d9)
…with a count
Four amicode_* calls against the same entity rendered four identical cards. Runs
of consecutive receipts sharing (problem, entity, action) now render as one card
with a count, opening the latest seq. Conservative by design: a differing action,
entity or problem, an interrupting part, or an unparseable sentinel all prevent
merging, so no state change can be hidden.
(cherry picked from commit 5176a69)
An activated skill rendered as a bare name ('brainstorming'), indistinguishable
from any other tool call: ui.tool.skill ('Skill') was reachable only as a
fallback for a missing input.name, which never happens. The kind now always
renders in front of the name, in both the expandable trigger and the compact
title map.
Also removes the last TextShimmer in the skill renderer. The previous commit
retired the shimmer from tool-status-title but missed this site, leaving the two
inconsistent.
Not made an amicode receipt card, despite the temptation: those carry an
AMICODE_DIFF sentinel and open an entity view. A skill has neither, and spending
the H-mark card on generic agent mechanics would dilute what the mark means.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit b6981f6)
Amicode's skills are Amico's own repertoire, so activating one is Amico acting and
earns the same chip the domain receipts wear: H-mark, rule, label naming the kind,
detail naming the skill. Replaces the plain text label from the previous commit.
New AmicoSkillChip in card.tsx rather than a new mode on Chip -- Chip is
sentinel-driven and a skill has no AMICODE_DIFF, so threading it through would
have meant special-casing the parse path. Inert shell, no chevron: there is no
entity to open.
It nests inside BasicTool's trigger so the expandable instruction body is kept.
That is only safe because BasicTool declares an `icon` prop and never renders it
-- verified, nothing in basic-tool.tsx reads props.icon -- so the H-mark is the
row's only glyph and no stock component needed changing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 17c6755)
Expanding a skill dumped its instructions as bare markdown into the transcript,
because [data-component="tool-output"] has no surface of its own -- no border, no
background, no padding, and white-space:pre. So an opened skill read as more
conversation. It is a file, so it now gets a file's surface: hairline, tint,
padding, and a bounded scrolling height instead of pushing the transcript around.
Scoped to skills; other tools' output is unchanged. Compound selector because a
bare .amc-skill-file would tie with [data-component="tool-output"] on specificity
and be settled by stylesheet order.
Follows a revert of the aside-lane attempt, which indented the whole activation
behind an accent spine and broke the shared left edge that Model / Skill /
Recommend chips read along. The chip belongs flush with its siblings; it was only
ever the opened body that needed separating.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 8e11411)
@aarontrowbridge
aarontrowbridge merged commit b61d294 into local/amicodeAug 1, 2026
2 of 4 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@aarontrowbridge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(amicode): the harmonic wave working indicator, and two transcript cleanups - #100

Merged
aarontrowbridge merged 13 commits into
local/amicodefrom
pr/amico-working-indicator
Aug 1, 2026
Merged

feat(amicode): the harmonic wave working indicator, and two transcript cleanups#100
aarontrowbridge merged 13 commits into
local/amicodefrom
pr/amico-working-indicator

Conversation

@aarontrowbridge

Copy link
Copy Markdown
Member

The chat had two "working" indicators that looked identical — the thinking line and the tool-group header — because both were shimmering text with no glyph. This replaces that with one brand motif, and fixes two transcript problems found while verifying it in the dev host.

The harmonic wave

A standing wave in quadrature: two identical sine curves, the companion a quarter period out of phase, so one is at full swing whenever the other crosses zero. That is what stops the glyph reading as a blink at 12px — a single standing line passes through zero amplitude twice per cycle. Three curves cross-fade so the harmonic index climbs.

Geometry and timing live in wave-geometry.ts, pure and DOM-free, following the thinking.ts / run-series.ts convention. All animation is CSS — two keyframes, transform and opacity only, no rAF — since this sits on screen for minutes at a time.

Two correctness traps are written down in the code because both are invisible on inspection:

  • Per-mode animation-delay magnitudes must descend for the visible sequence to ascend. The intuitive -1×/-2× ordering silently plays mode 1 → 3 → 2. The delays are therefore computed by a tested function and passed as inline style, so the CSS cannot reintroduce it. There is a regression test that fails if the ordering inverts.
  • No SVG <defs> and no id attributes. Several indicators mount at once (one per in-flight tool call) and SVG ids are document-global, so ids would collide and every instance would resolve to the first definition. A story mounts 8 at once as the guard.

Repeated receipt cards collapse

Four amicode_* calls against one entity rendered four identical cards. Runs of consecutive receipts sharing (problem, entity, action) now render as one card with a count, opening the highest seq. Conservative by design — a differing action, entity or problem, an interrupting part, or an unparseable sentinel all prevent merging, so no state change can hide. INLINE_KINDS entities are excluded entirely: they can resolve to a live InlineEntityView via a reactive currency check the pure module cannot see, so collapsing them risked dropping a render path.

Skill activations

ui.tool.skill ("Skill") was reachable only as a fallback for a missing input.name, which never happens — so an activated skill rendered as a bare word, indistinguishable from any other tool. Skills now say what they are and wear the Amico chip, since amicode's skills are Amico's repertoire. An opened skill file gets a bounded, tinted panel instead of dumping markdown into the transcript, because [data-component="tool-output"] carries no surface of its own.

Notes for review

  • tool-status-title.tsx is a stock opencode component. The change is logged in AMICODE-PATCHES.md. Only the shimmer went; the active→done word morph is deliberately preserved.
  • --v2-icon-icon-accent is the ink role, resolved by oc-2.json to grey-800 on light and #FFF676 on dark. Do not add a scheme selector for it. In particular, :root:not([data-theme="light"]) is dead CSS — the block containing it in v2/styles/theme.css is inside a /* */ comment, and data-theme holds a theme id rather than a scheme. An earlier revision built on it and shipped a lemon-on-white glyph at ~1.1:1.
  • Storybook cannot exercise the scheme switch by setting data-color-scheme on a wrapper; its preview loads only the v1 theme bundle. amico-wave.stories.tsx resolves tokens through resolveThemeVariantV2() instead, which is the only reason the contrast bug above was caught before a user saw it.
  • The H-mark in .amc-lane's head is now static, since two animated brand marks side by side compete with each other.

Testing

bun test src in packages/ui: 432 pass / 0 fail. bun turbo typecheck clean. oxlint packages/ui/src at 505 warnings / 0 errors — identical to the base, no new warnings. Verified in the extension dev host on both light and dark themes.

Not included

A transcript rail (a vertical spine bracketing Amico's stretch of a turn) was built and reverted. The design held up, but it stalled on a dev-host webview serving a stale content-hashed document, which meant no CSS or JS change reached the screen at all and looked exactly like a broken feature. Worth knowing independently of this PR: a new vendored binary needs a full dev-host restart, not Cmd+R — the webview can stay pinned to the old asset hash.

aarontrowbridgeand others added 13 commits July 29, 2026 06:34
…c number
The three curves are physically n=2,4,6 for a fixed-end string (lambda_n = 2L/n
with L=30), labelled 1/2/3 because the index counts full wavelengths across the
box. The consecutive set (lambda 60/30/20) was built and compared; the current
set won on looks. Written down because this codebase's readers are quantum-
control physicists who will reach for lambda_n = 2L/n and find the labels off
by a factor of two.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 4691a0b)
…ment
Adds the SolidJS component (amico-wave.tsx) that renders the standing-wave
glyph purely from amico-wave.ts's geometry/timing constants via CSS custom
properties, plus the matching CSS block and an anti-drift test guarding the
one unavoidable duplicated literal (the @Keyframes percentage). Removes the
now-superseded amc-text-shimmer treatment (leading dot + shimmer keyframes)
from the thinking line.
Also flips allowImportingTsExtensions on for packages/ui: this directory now
has both amico-wave.ts (pure module) and amico-wave.tsx (component) sharing a
stem, and under "bundler" resolution an extensionless "./amico-wave" import
resolves to the .tsx sibling instead of the .ts module (confirmed empirically
with a standalone repro) — the component would have silently self-imported.
Pinning the import with an explicit ".ts" extension fixes it, and this flag
is TypeScript's sanctioned way to permit that syntax under noEmit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 7a7bcff)
…the .ts/.tsx stem collision
amico-wave.ts and amico-wave.tsx shared a stem, and bun resolves an extensionless
import of that stem to the .tsx — so the component self-imported. Fixed by renaming
rather than by relaxing tsconfig: the directory convention (thinking.ts /
thinking-line.tsx) already avoids this by construction. Reverts
allowImportingTsExtensions and adds a test that fails if any same-stem pair
reappears in this directory.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 3b7b3b4)
… drift guard
The :root:not([data-theme="light"]) selector was derived from CSS that is inside a
block comment in v2/styles/theme.css. data-theme carries a theme id, not a scheme,
so the selector matched every reachable state and the wave rendered lemon at ~1.1:1
on light themes. oc-2.json already encodes the ink role in v2-icon-icon-accent
(grey-800 light, #FFF676 dark), which is what amico-presence.css uses, so no theme
selector is needed at all.
Also tightens the amc-wave-mode CSS/TS drift guard: the old toContain() check passed
under mutations that break the animation (ON edge dragged to 50%, OFF edge dragged to
40%) because the literal could still appear elsewhere in a loosely-bounded slice. Now
asserts each breakpoint against its own selector+brace, and the previously-unguarded
OFF literal is exported as MODE_OFF_PCT and checked too. Adds a MODE_HOLD_MS %
WAVE_PERIOD_MS invariant test, and rewords two comments that described the component
as already mounted (it isn't yet — a later task wires it in).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit fdab7d9)
…with a count
Four amicode_* calls against the same entity rendered four identical cards. Runs
of consecutive receipts sharing (problem, entity, action) now render as one card
with a count, opening the latest seq. Conservative by design: a differing action,
entity or problem, an interrupting part, or an unparseable sentinel all prevent
merging, so no state change can be hidden.
(cherry picked from commit 5176a69)
An activated skill rendered as a bare name ('brainstorming'), indistinguishable
from any other tool call: ui.tool.skill ('Skill') was reachable only as a
fallback for a missing input.name, which never happens. The kind now always
renders in front of the name, in both the expandable trigger and the compact
title map.
Also removes the last TextShimmer in the skill renderer. The previous commit
retired the shimmer from tool-status-title but missed this site, leaving the two
inconsistent.
Not made an amicode receipt card, despite the temptation: those carry an
AMICODE_DIFF sentinel and open an entity view. A skill has neither, and spending
the H-mark card on generic agent mechanics would dilute what the mark means.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit b6981f6)
Amicode's skills are Amico's own repertoire, so activating one is Amico acting and
earns the same chip the domain receipts wear: H-mark, rule, label naming the kind,
detail naming the skill. Replaces the plain text label from the previous commit.
New AmicoSkillChip in card.tsx rather than a new mode on Chip -- Chip is
sentinel-driven and a skill has no AMICODE_DIFF, so threading it through would
have meant special-casing the parse path. Inert shell, no chevron: there is no
entity to open.
It nests inside BasicTool's trigger so the expandable instruction body is kept.
That is only safe because BasicTool declares an `icon` prop and never renders it
-- verified, nothing in basic-tool.tsx reads props.icon -- so the H-mark is the
row's only glyph and no stock component needed changing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 17c6755)
Expanding a skill dumped its instructions as bare markdown into the transcript,
because [data-component="tool-output"] has no surface of its own -- no border, no
background, no padding, and white-space:pre. So an opened skill read as more
conversation. It is a file, so it now gets a file's surface: hairline, tint,
padding, and a bounded scrolling height instead of pushing the transcript around.
Scoped to skills; other tools' output is unchanged. Compound selector because a
bare .amc-skill-file would tie with [data-component="tool-output"] on specificity
and be settled by stylesheet order.
Follows a revert of the aside-lane attempt, which indented the whole activation
behind an accent spine and broke the shared left edge that Model / Skill /
Recommend chips read along. The chip belongs flush with its siblings; it was only
ever the opened body that needed separating.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 8e11411)
@aarontrowbridge
aarontrowbridge merged commit b61d294 into local/amicodeAug 1, 2026
2 of 4 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@aarontrowbridge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(amicode): the harmonic wave working indicator, and two transcript cleanups - #100

Merged
aarontrowbridge merged 13 commits into
local/amicodefrom
pr/amico-working-indicator
Aug 1, 2026
Merged

feat(amicode): the harmonic wave working indicator, and two transcript cleanups#100
aarontrowbridge merged 13 commits into
local/amicodefrom
pr/amico-working-indicator

Conversation

@aarontrowbridge

Copy link
Copy Markdown
Member

The chat had two "working" indicators that looked identical — the thinking line and the tool-group header — because both were shimmering text with no glyph. This replaces that with one brand motif, and fixes two transcript problems found while verifying it in the dev host.

The harmonic wave

A standing wave in quadrature: two identical sine curves, the companion a quarter period out of phase, so one is at full swing whenever the other crosses zero. That is what stops the glyph reading as a blink at 12px — a single standing line passes through zero amplitude twice per cycle. Three curves cross-fade so the harmonic index climbs.

Geometry and timing live in wave-geometry.ts, pure and DOM-free, following the thinking.ts / run-series.ts convention. All animation is CSS — two keyframes, transform and opacity only, no rAF — since this sits on screen for minutes at a time.

Two correctness traps are written down in the code because both are invisible on inspection:

  • Per-mode animation-delay magnitudes must descend for the visible sequence to ascend. The intuitive -1×/-2× ordering silently plays mode 1 → 3 → 2. The delays are therefore computed by a tested function and passed as inline style, so the CSS cannot reintroduce it. There is a regression test that fails if the ordering inverts.
  • No SVG <defs> and no id attributes. Several indicators mount at once (one per in-flight tool call) and SVG ids are document-global, so ids would collide and every instance would resolve to the first definition. A story mounts 8 at once as the guard.

Repeated receipt cards collapse

Four amicode_* calls against one entity rendered four identical cards. Runs of consecutive receipts sharing (problem, entity, action) now render as one card with a count, opening the highest seq. Conservative by design — a differing action, entity or problem, an interrupting part, or an unparseable sentinel all prevent merging, so no state change can hide. INLINE_KINDS entities are excluded entirely: they can resolve to a live InlineEntityView via a reactive currency check the pure module cannot see, so collapsing them risked dropping a render path.

Skill activations

ui.tool.skill ("Skill") was reachable only as a fallback for a missing input.name, which never happens — so an activated skill rendered as a bare word, indistinguishable from any other tool. Skills now say what they are and wear the Amico chip, since amicode's skills are Amico's repertoire. An opened skill file gets a bounded, tinted panel instead of dumping markdown into the transcript, because [data-component="tool-output"] carries no surface of its own.

Notes for review

  • tool-status-title.tsx is a stock opencode component. The change is logged in AMICODE-PATCHES.md. Only the shimmer went; the active→done word morph is deliberately preserved.
  • --v2-icon-icon-accent is the ink role, resolved by oc-2.json to grey-800 on light and #FFF676 on dark. Do not add a scheme selector for it. In particular, :root:not([data-theme="light"]) is dead CSS — the block containing it in v2/styles/theme.css is inside a /* */ comment, and data-theme holds a theme id rather than a scheme. An earlier revision built on it and shipped a lemon-on-white glyph at ~1.1:1.
  • Storybook cannot exercise the scheme switch by setting data-color-scheme on a wrapper; its preview loads only the v1 theme bundle. amico-wave.stories.tsx resolves tokens through resolveThemeVariantV2() instead, which is the only reason the contrast bug above was caught before a user saw it.
  • The H-mark in .amc-lane's head is now static, since two animated brand marks side by side compete with each other.

Testing

bun test src in packages/ui: 432 pass / 0 fail. bun turbo typecheck clean. oxlint packages/ui/src at 505 warnings / 0 errors — identical to the base, no new warnings. Verified in the extension dev host on both light and dark themes.

Not included

A transcript rail (a vertical spine bracketing Amico's stretch of a turn) was built and reverted. The design held up, but it stalled on a dev-host webview serving a stale content-hashed document, which meant no CSS or JS change reached the screen at all and looked exactly like a broken feature. Worth knowing independently of this PR: a new vendored binary needs a full dev-host restart, not Cmd+R — the webview can stay pinned to the old asset hash.

aarontrowbridgeand others added 13 commits July 29, 2026 06:34
…c number
The three curves are physically n=2,4,6 for a fixed-end string (lambda_n = 2L/n
with L=30), labelled 1/2/3 because the index counts full wavelengths across the
box. The consecutive set (lambda 60/30/20) was built and compared; the current
set won on looks. Written down because this codebase's readers are quantum-
control physicists who will reach for lambda_n = 2L/n and find the labels off
by a factor of two.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 4691a0b)
…ment
Adds the SolidJS component (amico-wave.tsx) that renders the standing-wave
glyph purely from amico-wave.ts's geometry/timing constants via CSS custom
properties, plus the matching CSS block and an anti-drift test guarding the
one unavoidable duplicated literal (the @Keyframes percentage). Removes the
now-superseded amc-text-shimmer treatment (leading dot + shimmer keyframes)
from the thinking line.
Also flips allowImportingTsExtensions on for packages/ui: this directory now
has both amico-wave.ts (pure module) and amico-wave.tsx (component) sharing a
stem, and under "bundler" resolution an extensionless "./amico-wave" import
resolves to the .tsx sibling instead of the .ts module (confirmed empirically
with a standalone repro) — the component would have silently self-imported.
Pinning the import with an explicit ".ts" extension fixes it, and this flag
is TypeScript's sanctioned way to permit that syntax under noEmit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 7a7bcff)
…the .ts/.tsx stem collision
amico-wave.ts and amico-wave.tsx shared a stem, and bun resolves an extensionless
import of that stem to the .tsx — so the component self-imported. Fixed by renaming
rather than by relaxing tsconfig: the directory convention (thinking.ts /
thinking-line.tsx) already avoids this by construction. Reverts
allowImportingTsExtensions and adds a test that fails if any same-stem pair
reappears in this directory.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 3b7b3b4)
… drift guard
The :root:not([data-theme="light"]) selector was derived from CSS that is inside a
block comment in v2/styles/theme.css. data-theme carries a theme id, not a scheme,
so the selector matched every reachable state and the wave rendered lemon at ~1.1:1
on light themes. oc-2.json already encodes the ink role in v2-icon-icon-accent
(grey-800 light, #FFF676 dark), which is what amico-presence.css uses, so no theme
selector is needed at all.
Also tightens the amc-wave-mode CSS/TS drift guard: the old toContain() check passed
under mutations that break the animation (ON edge dragged to 50%, OFF edge dragged to
40%) because the literal could still appear elsewhere in a loosely-bounded slice. Now
asserts each breakpoint against its own selector+brace, and the previously-unguarded
OFF literal is exported as MODE_OFF_PCT and checked too. Adds a MODE_HOLD_MS %
WAVE_PERIOD_MS invariant test, and rewords two comments that described the component
as already mounted (it isn't yet — a later task wires it in).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit fdab7d9)
…with a count
Four amicode_* calls against the same entity rendered four identical cards. Runs
of consecutive receipts sharing (problem, entity, action) now render as one card
with a count, opening the latest seq. Conservative by design: a differing action,
entity or problem, an interrupting part, or an unparseable sentinel all prevent
merging, so no state change can be hidden.
(cherry picked from commit 5176a69)
An activated skill rendered as a bare name ('brainstorming'), indistinguishable
from any other tool call: ui.tool.skill ('Skill') was reachable only as a
fallback for a missing input.name, which never happens. The kind now always
renders in front of the name, in both the expandable trigger and the compact
title map.
Also removes the last TextShimmer in the skill renderer. The previous commit
retired the shimmer from tool-status-title but missed this site, leaving the two
inconsistent.
Not made an amicode receipt card, despite the temptation: those carry an
AMICODE_DIFF sentinel and open an entity view. A skill has neither, and spending
the H-mark card on generic agent mechanics would dilute what the mark means.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit b6981f6)
Amicode's skills are Amico's own repertoire, so activating one is Amico acting and
earns the same chip the domain receipts wear: H-mark, rule, label naming the kind,
detail naming the skill. Replaces the plain text label from the previous commit.
New AmicoSkillChip in card.tsx rather than a new mode on Chip -- Chip is
sentinel-driven and a skill has no AMICODE_DIFF, so threading it through would
have meant special-casing the parse path. Inert shell, no chevron: there is no
entity to open.
It nests inside BasicTool's trigger so the expandable instruction body is kept.
That is only safe because BasicTool declares an `icon` prop and never renders it
-- verified, nothing in basic-tool.tsx reads props.icon -- so the H-mark is the
row's only glyph and no stock component needed changing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 17c6755)
Expanding a skill dumped its instructions as bare markdown into the transcript,
because [data-component="tool-output"] has no surface of its own -- no border, no
background, no padding, and white-space:pre. So an opened skill read as more
conversation. It is a file, so it now gets a file's surface: hairline, tint,
padding, and a bounded scrolling height instead of pushing the transcript around.
Scoped to skills; other tools' output is unchanged. Compound selector because a
bare .amc-skill-file would tie with [data-component="tool-output"] on specificity
and be settled by stylesheet order.
Follows a revert of the aside-lane attempt, which indented the whole activation
behind an accent spine and broke the shared left edge that Model / Skill /
Recommend chips read along. The chip belongs flush with its siblings; it was only
ever the opened body that needed separating.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 8e11411)
@aarontrowbridge
aarontrowbridge merged commit b61d294 into local/amicodeAug 1, 2026
2 of 4 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@aarontrowbridge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat(amicode): the harmonic wave working indicator, and two transcript cleanups - #100

Merged
aarontrowbridge merged 13 commits into
local/amicodefrom
pr/amico-working-indicator
Aug 1, 2026
Merged

feat(amicode): the harmonic wave working indicator, and two transcript cleanups#100
aarontrowbridge merged 13 commits into
local/amicodefrom
pr/amico-working-indicator

Conversation

@aarontrowbridge

Copy link
Copy Markdown
Member

The chat had two "working" indicators that looked identical — the thinking line and the tool-group header — because both were shimmering text with no glyph. This replaces that with one brand motif, and fixes two transcript problems found while verifying it in the dev host.

The harmonic wave

A standing wave in quadrature: two identical sine curves, the companion a quarter period out of phase, so one is at full swing whenever the other crosses zero. That is what stops the glyph reading as a blink at 12px — a single standing line passes through zero amplitude twice per cycle. Three curves cross-fade so the harmonic index climbs.

Geometry and timing live in wave-geometry.ts, pure and DOM-free, following the thinking.ts / run-series.ts convention. All animation is CSS — two keyframes, transform and opacity only, no rAF — since this sits on screen for minutes at a time.

Two correctness traps are written down in the code because both are invisible on inspection:

  • Per-mode animation-delay magnitudes must descend for the visible sequence to ascend. The intuitive -1×/-2× ordering silently plays mode 1 → 3 → 2. The delays are therefore computed by a tested function and passed as inline style, so the CSS cannot reintroduce it. There is a regression test that fails if the ordering inverts.
  • No SVG <defs> and no id attributes. Several indicators mount at once (one per in-flight tool call) and SVG ids are document-global, so ids would collide and every instance would resolve to the first definition. A story mounts 8 at once as the guard.

Repeated receipt cards collapse

Four amicode_* calls against one entity rendered four identical cards. Runs of consecutive receipts sharing (problem, entity, action) now render as one card with a count, opening the highest seq. Conservative by design — a differing action, entity or problem, an interrupting part, or an unparseable sentinel all prevent merging, so no state change can hide. INLINE_KINDS entities are excluded entirely: they can resolve to a live InlineEntityView via a reactive currency check the pure module cannot see, so collapsing them risked dropping a render path.

Skill activations

ui.tool.skill ("Skill") was reachable only as a fallback for a missing input.name, which never happens — so an activated skill rendered as a bare word, indistinguishable from any other tool. Skills now say what they are and wear the Amico chip, since amicode's skills are Amico's repertoire. An opened skill file gets a bounded, tinted panel instead of dumping markdown into the transcript, because [data-component="tool-output"] carries no surface of its own.

Notes for review

  • tool-status-title.tsx is a stock opencode component. The change is logged in AMICODE-PATCHES.md. Only the shimmer went; the active→done word morph is deliberately preserved.
  • --v2-icon-icon-accent is the ink role, resolved by oc-2.json to grey-800 on light and #FFF676 on dark. Do not add a scheme selector for it. In particular, :root:not([data-theme="light"]) is dead CSS — the block containing it in v2/styles/theme.css is inside a /* */ comment, and data-theme holds a theme id rather than a scheme. An earlier revision built on it and shipped a lemon-on-white glyph at ~1.1:1.
  • Storybook cannot exercise the scheme switch by setting data-color-scheme on a wrapper; its preview loads only the v1 theme bundle. amico-wave.stories.tsx resolves tokens through resolveThemeVariantV2() instead, which is the only reason the contrast bug above was caught before a user saw it.
  • The H-mark in .amc-lane's head is now static, since two animated brand marks side by side compete with each other.

Testing

bun test src in packages/ui: 432 pass / 0 fail. bun turbo typecheck clean. oxlint packages/ui/src at 505 warnings / 0 errors — identical to the base, no new warnings. Verified in the extension dev host on both light and dark themes.

Not included

A transcript rail (a vertical spine bracketing Amico's stretch of a turn) was built and reverted. The design held up, but it stalled on a dev-host webview serving a stale content-hashed document, which meant no CSS or JS change reached the screen at all and looked exactly like a broken feature. Worth knowing independently of this PR: a new vendored binary needs a full dev-host restart, not Cmd+R — the webview can stay pinned to the old asset hash.

aarontrowbridgeand others added 13 commits July 29, 2026 06:34
…c number
The three curves are physically n=2,4,6 for a fixed-end string (lambda_n = 2L/n
with L=30), labelled 1/2/3 because the index counts full wavelengths across the
box. The consecutive set (lambda 60/30/20) was built and compared; the current
set won on looks. Written down because this codebase's readers are quantum-
control physicists who will reach for lambda_n = 2L/n and find the labels off
by a factor of two.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 4691a0b)
…ment
Adds the SolidJS component (amico-wave.tsx) that renders the standing-wave
glyph purely from amico-wave.ts's geometry/timing constants via CSS custom
properties, plus the matching CSS block and an anti-drift test guarding the
one unavoidable duplicated literal (the @Keyframes percentage). Removes the
now-superseded amc-text-shimmer treatment (leading dot + shimmer keyframes)
from the thinking line.
Also flips allowImportingTsExtensions on for packages/ui: this directory now
has both amico-wave.ts (pure module) and amico-wave.tsx (component) sharing a
stem, and under "bundler" resolution an extensionless "./amico-wave" import
resolves to the .tsx sibling instead of the .ts module (confirmed empirically
with a standalone repro) — the component would have silently self-imported.
Pinning the import with an explicit ".ts" extension fixes it, and this flag
is TypeScript's sanctioned way to permit that syntax under noEmit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 7a7bcff)
…the .ts/.tsx stem collision
amico-wave.ts and amico-wave.tsx shared a stem, and bun resolves an extensionless
import of that stem to the .tsx — so the component self-imported. Fixed by renaming
rather than by relaxing tsconfig: the directory convention (thinking.ts /
thinking-line.tsx) already avoids this by construction. Reverts
allowImportingTsExtensions and adds a test that fails if any same-stem pair
reappears in this directory.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 3b7b3b4)
… drift guard
The :root:not([data-theme="light"]) selector was derived from CSS that is inside a
block comment in v2/styles/theme.css. data-theme carries a theme id, not a scheme,
so the selector matched every reachable state and the wave rendered lemon at ~1.1:1
on light themes. oc-2.json already encodes the ink role in v2-icon-icon-accent
(grey-800 light, #FFF676 dark), which is what amico-presence.css uses, so no theme
selector is needed at all.
Also tightens the amc-wave-mode CSS/TS drift guard: the old toContain() check passed
under mutations that break the animation (ON edge dragged to 50%, OFF edge dragged to
40%) because the literal could still appear elsewhere in a loosely-bounded slice. Now
asserts each breakpoint against its own selector+brace, and the previously-unguarded
OFF literal is exported as MODE_OFF_PCT and checked too. Adds a MODE_HOLD_MS %
WAVE_PERIOD_MS invariant test, and rewords two comments that described the component
as already mounted (it isn't yet — a later task wires it in).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit fdab7d9)
…with a count
Four amicode_* calls against the same entity rendered four identical cards. Runs
of consecutive receipts sharing (problem, entity, action) now render as one card
with a count, opening the latest seq. Conservative by design: a differing action,
entity or problem, an interrupting part, or an unparseable sentinel all prevent
merging, so no state change can be hidden.
(cherry picked from commit 5176a69)
An activated skill rendered as a bare name ('brainstorming'), indistinguishable
from any other tool call: ui.tool.skill ('Skill') was reachable only as a
fallback for a missing input.name, which never happens. The kind now always
renders in front of the name, in both the expandable trigger and the compact
title map.
Also removes the last TextShimmer in the skill renderer. The previous commit
retired the shimmer from tool-status-title but missed this site, leaving the two
inconsistent.
Not made an amicode receipt card, despite the temptation: those carry an
AMICODE_DIFF sentinel and open an entity view. A skill has neither, and spending
the H-mark card on generic agent mechanics would dilute what the mark means.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit b6981f6)
Amicode's skills are Amico's own repertoire, so activating one is Amico acting and
earns the same chip the domain receipts wear: H-mark, rule, label naming the kind,
detail naming the skill. Replaces the plain text label from the previous commit.
New AmicoSkillChip in card.tsx rather than a new mode on Chip -- Chip is
sentinel-driven and a skill has no AMICODE_DIFF, so threading it through would
have meant special-casing the parse path. Inert shell, no chevron: there is no
entity to open.
It nests inside BasicTool's trigger so the expandable instruction body is kept.
That is only safe because BasicTool declares an `icon` prop and never renders it
-- verified, nothing in basic-tool.tsx reads props.icon -- so the H-mark is the
row's only glyph and no stock component needed changing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 17c6755)
Expanding a skill dumped its instructions as bare markdown into the transcript,
because [data-component="tool-output"] has no surface of its own -- no border, no
background, no padding, and white-space:pre. So an opened skill read as more
conversation. It is a file, so it now gets a file's surface: hairline, tint,
padding, and a bounded scrolling height instead of pushing the transcript around.
Scoped to skills; other tools' output is unchanged. Compound selector because a
bare .amc-skill-file would tie with [data-component="tool-output"] on specificity
and be settled by stylesheet order.
Follows a revert of the aside-lane attempt, which indented the whole activation
behind an accent spine and broke the shared left edge that Model / Skill /
Recommend chips read along. The chip belongs flush with its siblings; it was only
ever the opened body that needed separating.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 8e11411)
@aarontrowbridge
aarontrowbridge merged commit b61d294 into local/amicodeAug 1, 2026
2 of 4 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@aarontrowbridge