Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
d992de2
feat(amicode): BrainAtmosphere wrapper + session event derivation (#59)
kateebonner Jul 25, 2026
e9bbb67
feat(amicode): mount the Brain chat-wide, absorb the inline strip (#59)
kateebonner Jul 25, 2026
3934618
feat(ui): derive two AA-floored, chat-theme-keyed glass tint tiers (#60)
kateebonner Jul 25, 2026
e04f6fc
merge: issue #59 (frontier 1 via worktree)
kateebonner Jul 25, 2026
429f11e
merge: issue #60 (frontier 1 via worktree)
kateebonner Jul 25, 2026
f4a3c3e
feat(ui): adaptive heartbeat — setActive + shared draw-gate (~8fps re…
kateebonner Jul 25, 2026
aa4fd34
feat(ui): sparse-seed boot + fixed viewport-anchored camera (#62)
kateebonner Jul 25, 2026
fa95943
feat(ui): total-population node cap with recency-window eviction (#62)
kateebonner Jul 25, 2026
d34de15
feat(ui): reduced-motion hard-pause — bounded event bursts, still at …
kateebonner Jul 25, 2026
44a5b79
feat(amicode): route the session-busy signal into the Brain heartbeat…
kateebonner Jul 25, 2026
b3f4bb8
feat(ui): float chat components on the two Glass tiers (#61)
kateebonner Jul 25, 2026
4a6c566
merge: issue #61 (frontier 2 via worktree)
kateebonner Jul 25, 2026
b1ddaa4
merge: issue #62 (frontier 2 via worktree)
kateebonner Jul 25, 2026
099fe0b
chore(ui): drop orphaned brain-data skeleton dataset
kateebonner Jul 25, 2026
bc7eabf
feat(ui): perf governor — ease motion under sustained over-budget, ne…
kateebonner Jul 25, 2026
9e5f119
feat(ui): dev force-full-tempo hook — ?brainForceActive pins tempo, d…
kateebonner Jul 25, 2026
519d7a4
test(app): headless perf-trace proxy — rAF-interval scroll trace, bot…
kateebonner Jul 25, 2026
a1a768e
test(ui): pin rest-fidelity — met budget at breathing cadence never p…
kateebonner Jul 25, 2026
19dbc87
merge: issue #63 (frontier 3 via worktree)
kateebonner Jul 25, 2026
f347ec1
feat(amicode): single-tier frost glass, narrower floating cards, glas…
kateebonner Jul 25, 2026
1f42f41
feat(amicode): glass sweep — AMICO family carries the single glass re…
kateebonner Jul 25, 2026
3c2cfa1
feat(chat): glass sweep — every composer dock floats on the glass recipe
kateebonner Jul 25, 2026
18be450
feat(chat): glass sweep — timeline surfaces stop punching the frost
kateebonner Jul 25, 2026
caa8760
feat(chat): glass sweep — floating chrome carries the single glass re…
kateebonner Jul 25, 2026
023f7fb
test(glass): the sweep IS the contract — extend glass-float to the fu…
kateebonner Jul 25, 2026
927ad5c
feat(amicode): Latent Constellation — the landing's rotating 3D at-re…
kateebonner Jul 25, 2026
65e49f3
merge: glass sweep — every chat surface on the single recipe (audit f…
kateebonner Jul 25, 2026
b2e3288
merge: Latent Constellation landing + occlusion fix
kateebonner Jul 25, 2026
69ebf7e
feat(amicode): live design tuning — max-transparent glass, bottom-doc…
kateebonner Jul 26, 2026
05fa001
feat(amicode): live-thought flares + effort-in-model-menu + session g…
kateebonner Jul 26, 2026
712e6ec
feat(amicode): flares land where you can SEE them + hug-width cards +…
kateebonner Jul 26, 2026
d4258ff
feat(amicode): centered thought camera + edge-justified messages + bo…
kateebonner Jul 26, 2026
46805c4
fix(amicode): tool-error-card hugs its content like every chat card
kateebonner Jul 26, 2026
83d070a
fix(amicode): error cards drop the accent rail — tint + ink carry sev…
kateebonner Jul 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 161 additions & 0 deletions packages/app/e2e/perf/brain-governor-trace.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import { expect, test, type Page } from "@playwright/test"
import { base64Encode } from "@opencode-ai/core/util/encode"
import { fixture, pageMessages } from "../smoke/session-timeline.fixture"
import { mockOpenCodeServer } from "../utils/mock-server"
import { expectSessionTitle } from "../utils/waits"

/* Headless-Chromium rAF-interval trace over the real app — issue #63's
PRE-GATE PROXY. It boots the Chat on the >=40-card fixture thread with the
dev force-full-tempo hook (`?brainForceActive`: Brain pinned busy, perf
governor disabled), scrolls continuously for >=10s in EACH theme, and
emits a p95 / max-frame-time / frames>33ms report.

THIS IS NOT THE RELEASE GATE. A passing trace never closes #63's perf
criterion: only a manual sign-off on the pinned reference laptop
(battery-powered, VS Code webview, both themes) does — see the perf-gate
checklist on the PR. Accordingly this spec asserts the harness RUNS and
REPORTS (shape, duration, theme coverage, forced worst case); it asserts
no numeric frame-time outcome. */

const SCROLL_MS = 11_000 // >=10s of continuous scroll per theme
const PROXY_LABEL = "pre-gate proxy — does NOT close the release gate (manual reference-laptop sign-off required)"

type ThemeTrace = {
theme: "dark" | "light"
colorScheme: string | undefined
forced: boolean
motion: string | undefined
durationMs: number
frames: number
meanMs: number
p95Ms: number
maxMs: number
over33: number
cardsSeen: number
}

test.describe("perf proxy: brain governor trace", () => {
test.setTimeout(300_000)

test("headless rAF trace over a >=40-card thread at forced full tempo, both themes", async ({ page }) => {
expect(fixture.messages[fixture.targetID].length).toBeGreaterThanOrEqual(40) // the long-thread fixture

await mockOpenCodeServer(page, {
sessions: fixture.sessions,
provider: fixture.provider,
directory: fixture.directory,
project: fixture.project,
pageMessages,
})
await seedStorage(page, fixture.directory)

const traces: ThemeTrace[] = []
for (const theme of ["dark", "light"] as const) {
await page.emulateMedia({ colorScheme: theme }) // chat colorScheme defaults to "system"
await page.goto(`/${base64Encode(fixture.directory)}/session/${fixture.targetID}?brainForceActive`)
await expectSessionTitle(page, fixture.expected.targetTitle)
await expect(page.locator("[data-timeline-row]").first()).toBeVisible()
await expect(page.locator('[data-component="brain-atmosphere"] canvas')).toBeAttached()

// the force hook must be live: Brain pinned busy, governor disabled
const stats = await page.evaluate(
() => (window as Window & { __amicoBrainStats?: () => { active: boolean; motion: string } }).__amicoBrainStats?.(),
)
expect(stats?.active).toBe(true)
expect(stats?.motion).toBe("full")

const trace = await tracedScroll(page, SCROLL_MS)
traces.push({
theme,
colorScheme: trace.colorScheme,
forced: stats?.active === true && stats?.motion === "full",
motion: stats?.motion,
...trace.report,
})
}

const report = { label: PROXY_LABEL, traces }
console.log(`[brain-perf-proxy] ${JSON.stringify(report, null, 2)}`)

// the harness ran and reported — shape only, never a numeric perf verdict
expect(report.label).toContain("pre-gate proxy")
expect(traces).toHaveLength(2)
for (const t of traces) {
expect(t.colorScheme).toBe(t.theme) // the theme actually applied
expect(t.forced).toBe(true) // un-eased worst case measured
expect(t.durationMs).toBeGreaterThanOrEqual(10_000) // >=10s continuous scroll
expect(t.frames).toBeGreaterThan(100)
expect(t.p95Ms).toBeGreaterThan(0)
expect(t.maxMs).toBeGreaterThanOrEqual(t.p95Ms)
expect(t.over33).toBeGreaterThanOrEqual(0)
expect(t.cardsSeen).toBeGreaterThanOrEqual(10) // the sweep truly moved through the thread
}
})
})

/** rAF-interval sampler: scrolls the timeline continuously (up through
history, bouncing at the ends) inside the SAME rAF loop that samples the
frame intervals — the trace measures the app under scroll, not idle. */
async function tracedScroll(page: Page, minMs: number) {
return page.evaluate(async (durationMs) => {
const scroller = [...document.querySelectorAll<HTMLElement>(".scroll-view__viewport")].find((el) =>
el.querySelector("[data-timeline-row]"),
)
if (!scroller) throw new Error("perf trace: no timeline scroller found")
const seen = new Set<string>()
const intervals: number[] = []
let dir = -1 // start at the thread's foot, sweep up through history
await new Promise<void>((resolve) => {
let last = -1
let t0 = -1
const step = (now: number) => {
if (t0 < 0) {
t0 = now
last = now
} else {
intervals.push(now - last)
last = now
}
scroller.scrollTop += dir * 90
if (scroller.scrollTop <= 2) dir = 1
else if (scroller.scrollTop + scroller.clientHeight >= scroller.scrollHeight - 2) dir = -1
if (intervals.length % 20 === 0) {
for (const el of scroller.querySelectorAll<HTMLElement>("[data-message-id]")) {
if (el.dataset.messageId) seen.add(el.dataset.messageId)
}
}
if (now - t0 >= durationMs) return resolve()
requestAnimationFrame(step)
}
requestAnimationFrame(step)
})
const sorted = intervals.slice().sort((a, b) => a - b)
const q = (p: number) => sorted[Math.min(sorted.length - 1, Math.ceil(sorted.length * p) - 1)] ?? 0
const round = (v: number) => Math.round(v * 100) / 100
return {
colorScheme: document.documentElement.dataset.colorScheme,
report: {
durationMs: Math.round(intervals.reduce((a, b) => a + b, 0)),
frames: intervals.length,
meanMs: round(intervals.reduce((a, b) => a + b, 0) / Math.max(intervals.length, 1)),
p95Ms: round(q(0.95)),
maxMs: round(sorted[sorted.length - 1] ?? 0),
over33: intervals.filter((d) => d > 33).length,
cardsSeen: seen.size,
},
}
}, minMs)
}

/** the same storage seed the smoke spec uses: a known project + settings */
async function seedStorage(page: Page, directory: string) {
await page.addInitScript((dir) => {
localStorage.setItem(
"opencode.global.dat:server",
JSON.stringify({
projects: { local: [{ worktree: dir, expanded: true }] },
lastProject: { local: dir },
}),
)
}, directory)
}
4 changes: 4 additions & 0 deletions packages/app/src/components/dialog-select-model-unpaid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { type Component, Show } from "solid-js"
import { useLocal } from "@/context/local"
import { popularProviders, useProviders } from "@/hooks/use-providers"
import { ModelTooltip } from "./model-tooltip"
import { ModelVariantRow } from "./dialog-select-model"
import { useLanguage } from "@/context/language"

type ModelState = ReturnType<typeof useLocal>["model"]
Expand Down Expand Up @@ -83,6 +84,9 @@ export const DialogSelectModelUnpaid: Component<{ model?: ModelState }> = (props
</div>
)}
</List>
{/* thinking effort folded into the model surface (Kate 2026-07-25) —
the unpaid path keeps the control the footer Select used to carry */}
<ModelVariantRow model={model} class="px-5 pb-1" />
</div>
<div class="px-1.5 pb-1.5">
<div class="w-full rounded-sm border border-border-weak-base bg-surface-raised-base">
Expand Down
61 changes: 59 additions & 2 deletions packages/app/src/components/dialog-select-model.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Popover as Kobalte } from "@kobalte/core/popover"
import { Component, ComponentProps, createMemo, JSX, Show, ValidComponent } from "solid-js"
import { Component, ComponentProps, createMemo, For, JSX, Show, ValidComponent } from "solid-js"
import { createStore } from "solid-js/store"
import { useLocal } from "@/context/local"
import { useDialog } from "@opencode-ai/ui/context/dialog"
Expand Down Expand Up @@ -85,6 +85,52 @@ const ModelList: Component<{
)
}

/** Thinking effort, INSIDE the model surface (Kate 2026-07-25): one control
for "which brain, how hard". Renders nothing unless the current model
exposes variants; picking one never dismisses the host — effort is an
attribute, not a selection. Shared by the paid popover and the unpaid
dialog so no path loses the control the standalone footer Select had. */
export const ModelVariantRow: Component<{ model?: ModelState; class?: string }> = (props) => {
const model = props.model ?? useLocal().model
const language = useLanguage()
const options = createMemo(() => {
const list = model.variant.list()
return list.length > 0 ? ["default", ...list] : []
})
return (
<Show when={options().length > 0}>
<div class={`flex shrink-0 items-center gap-2 ${props.class ?? ""}`}>
<span class="text-[13px] font-[440] leading-5 text-v2-text-text-faint">
{language.t("dialog.model.variant.label")}
</span>
<div class="ml-auto flex items-center gap-1">
<For each={options()}>
{(option) => {
const selected = () => (model.variant.current() ?? "default") === option
return (
<button
type="button"
aria-pressed={selected()}
class="h-6 rounded-md px-2 text-[12px] font-[500] capitalize transition-colors duration-120 focus-visible:outline-none"
classList={{
"bg-[var(--accent-fill-soft)] text-v2-text-text-base shadow-[inset_0_0_0_1px_var(--accent-edge)]":
selected(),
"text-v2-text-text-faint hover:bg-v2-background-bg-layer-02 hover:text-v2-text-text-base focus-visible:bg-v2-background-bg-layer-02 focus-visible:text-v2-text-text-base":
!selected(),
}}
onClick={() => model.variant.set(option === "default" ? undefined : option)}
>
{option === "default" ? language.t("common.default") : option}
</button>
)
}}
</For>
</div>
</div>
</Show>
)
}

type ModelSelectorTriggerProps = Omit<ComponentProps<typeof Kobalte.Trigger>, "as" | "ref">
type Dismiss = "escape" | "outside" | "select" | "manage" | "provider"

Expand Down Expand Up @@ -125,6 +171,11 @@ export function ModelSelectorPopover(props: {
}
const language = useLanguage()

// Kate 2026-07-25: thinking effort lives INSIDE the model menu — see
// ModelVariantRow (shared with the unpaid dialog; the footer Select is gone)
const model = props.model ?? useLocal().model
const hasVariants = createMemo(() => model.variant.list().length > 0)

return (
<Kobalte
open={store.open}
Expand All @@ -140,8 +191,10 @@ export function ModelSelectorPopover(props: {
{props.children}
</Kobalte.Trigger>
<Kobalte.Portal>
{/* glass sweep (#56): the model menu floats on the single glass recipe */}
<Kobalte.Content
class="w-72 h-80 flex flex-col p-2 rounded-md border border-border-base bg-surface-raised-stronger-non-alpha shadow-md z-50 outline-none overflow-hidden"
data-glass="standard"
class="w-72 h-80 flex flex-col p-2 rounded-md z-50 outline-none overflow-hidden"
onEscapeKeyDown={(event) => {
close("escape")
event.preventDefault()
Expand Down Expand Up @@ -190,6 +243,10 @@ export function ModelSelectorPopover(props: {
</div>
}
/>
<Show when={hasVariants()}>
<div class="mx-1 mt-1 h-px shrink-0 bg-v2-border-border-muted" />
<ModelVariantRow model={props.model} class="px-2 pb-1 pt-2" />
</Show>
</Kobalte.Content>
</Kobalte.Portal>
</Kobalte>
Expand Down
Loading
Loading