Closed
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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
, '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
Closed
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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
, '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
Closed
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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
, '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
Closed
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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
, '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
Closed
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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
, '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
Closed
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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
, '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
Closed
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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
, '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
Closed
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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