Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
401 changes: 319 additions & 82 deletions packages/app-bundle/manifest.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { expect, test } from "@playwright/test"
import {
assistantMessage,
completedAssistantInfo,
messageUpdated,
partUpdated,
reasoningPart,
setupTimeline,
shell,
status,
textPart,
userMessage,
} from "../performance/timeline-stability/fixture"

for (const expanded of [false, true]) {
test(`preserves shell user intent from a ${expanded ? "expanded" : "collapsed"} default`, async ({ page }) => {
const id = `prt_shell_default_${expanded}`
const timeline = await setupTimeline(page, {
messages: [userMessage(), assistantMessage([shell(id, "completed", lines(3))])],
settings: { shellToolPartsExpanded: expanded },
})
const trigger = page.locator(`[data-timeline-part-id="${id}"] [data-slot="collapsible-trigger"]`)
await expect(trigger).toHaveAttribute("aria-expanded", String(expanded))
await trigger.click()
await expect(trigger).toHaveAttribute("aria-expanded", String(!expanded))

await timeline.send(partUpdated(shell(id, "completed", lines(6))), 180)
await timeline.send(partUpdated(textPart(`prt_sibling_${expanded}`, "Sibling content")), 180)
await timeline.send(status("busy"), 100)
await timeline.send(status("idle"), 250)
await expect(trigger).toHaveAttribute("aria-expanded", String(!expanded))
})
}

test("shows and expands a running shell command without shimmering it", async ({ page }) => {
const id = "prt_shell_running_command"
const command = "sleep 10 && echo done"
await setupTimeline(page, {
messages: [userMessage(), assistantMessage([shell(id, "running", "still running", command)], { completed: false })],
settings: { shellToolPartsExpanded: false },
})

const tool = page.locator(`[data-timeline-part-id="${id}"]`)
await expect(tool.locator('[data-slot="basic-tool-tool-title"] [data-pending]')).toHaveAttribute("data-pending", "true")
await expect(tool.locator('[data-component="shell-submessage"]')).toHaveText(command)
await expect(tool.locator('[data-component="shell-submessage"] [data-pending]')).toHaveCount(0)
await tool.locator('[data-slot="collapsible-trigger"]').click()
await expect(tool.locator('[data-slot="collapsible-trigger"]')).toHaveAttribute("aria-expanded", "true")
await expect(tool.locator('[data-slot="bash-pre"]')).toContainText("still running")
})

test("transitions thinking and hidden reasoning through busy to idle", async ({ page }) => {
const reasoningID = "prt_reasoning_hidden"
const assistant = assistantMessage([reasoningPart(reasoningID, "## Inspecting stability")], { completed: false })
const timeline = await setupTimeline(page, {
messages: [userMessage(), assistant],
settings: { showReasoningSummaries: false },
cpuRate: 4,
})
await timeline.send(status("busy"), 150)

await expect(page.locator('[data-timeline-row="Thinking"]')).toBeVisible()
await expect(page.getByText("Inspecting stability", { exact: true })).toBeVisible()
await expect(page.locator(`[data-timeline-part-id="${reasoningID}"]`)).toHaveCount(0)
await timeline.send(partUpdated(shell("prt_reasoning_shell", "running")), 160)
await expect(page.locator('[data-timeline-row="Thinking"]')).toBeVisible()
await timeline.send(partUpdated(shell("prt_reasoning_shell", "completed", "done")), 180)
await timeline.send(messageUpdated(completedAssistantInfo(assistant.info)), 100)
await timeline.send(status("idle"), 300)
await expect(page.locator('[data-timeline-row="Thinking"]')).toHaveCount(0)
await expect(page.locator(`[data-timeline-part-id="${reasoningID}"]`)).toHaveCount(0)
})

test("moves busy through retry and recovery to final idle content", async ({ page }) => {
const assistant = assistantMessage([], { completed: false })
const timeline = await setupTimeline(page, {
messages: [
userMessage(undefined, {
summary: {
diffs: [
{
file: "src/retry.ts",
additions: 1,
deletions: 1,
patch: "@@ -1 +1 @@\n-export const retry = false\n+export const retry = true",
},
],
},
}),
assistant,
],
})
await timeline.send(status("busy"), 140)
await expect(page.locator('[data-timeline-row="Thinking"]')).toBeVisible()
await expect(page.locator('[data-timeline-row="DiffSummary"]')).toHaveCount(0)
await timeline.send(status("retry"), 180)
await expect(page.locator('[data-timeline-row="Retry"]')).toBeVisible()
await expect(page.locator('[data-timeline-row="Thinking"]')).toHaveCount(0)
await timeline.send(status("busy", 2), 180)
await expect(page.locator('[data-timeline-row="Thinking"]')).toBeVisible()
await timeline.send(partUpdated(textPart("prt_recovered", "Recovered response")), 140)
await timeline.send(messageUpdated(completedAssistantInfo(assistant.info)), 100)
await timeline.send(status("idle"), 350)
await expect(page.locator('[data-timeline-row="Retry"]')).toHaveCount(0)
await expect(page.locator('[data-timeline-row="Thinking"]')).toHaveCount(0)
await expect(page.locator('[data-timeline-row="DiffSummary"]')).toBeVisible()
})

function lines(count: number) {
return Array.from({ length: count }, (_, index) => `line ${index + 1}`).join("\n")
}
4 changes: 2 additions & 2 deletions packages/app-bundle/overlay/packages/app/index.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!doctype html>
<html lang="en" style="background-color: var(--v2-background-bg-deep, #fafafa)">
<html lang="en" style="background-color: var(--v2-background-bg-deep, #ffffff)">
<head>
<meta charset="utf-8" />
<meta
Expand All @@ -12,7 +12,7 @@
<link rel="shortcut icon" href="/favicon-v3.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon-v3.png" />
<link rel="manifest" href="/site.webmanifest" />
<meta name="theme-color" content="#fafafa" />
<meta name="theme-color" content="#ffffff" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
Expand Down
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
;(function () {
var key = "opencode-theme-id"
var themeId = localStorage.getItem(key) || "oc-2"
// amicode: the app HARD-PINS the brand theme (lockThemeId in app.tsx), so the
// pin is authoritative here too. Reading localStorage instead would let a
// stale id from a previous build stamp the pre-paint frame with the old
// theme, which then persists visibly until hydration corrects it.
// MUST match lockThemeId in packages/app/src/app.tsx.
var PINNED_THEME_ID = "harmoniqs"
var themeId = PINNED_THEME_ID
if (localStorage.getItem(key) !== themeId) {
localStorage.setItem(key, themeId)
localStorage.removeItem("opencode-theme-css-light")
localStorage.removeItem("opencode-theme-css-dark")
}

if (themeId === "oc-1") {
themeId = "oc-2"
Expand All @@ -26,13 +37,16 @@

document.documentElement.dataset.theme = themeId
document.documentElement.dataset.colorScheme = mode
document.documentElement.style.backgroundColor = isDark ? "#080808" : "#fafafa"
// Brand ground, tracking harmoniqs.json palette.neutral (dark #000, light #fff).
// This paints before any stylesheet, so a stock literal here shows through as
// the old brand for the first frame.
document.documentElement.style.backgroundColor = isDark ? "#0F0F0D" : "#ffffff"

// Update theme-color meta tag to match app color scheme
var metas = document.querySelectorAll("meta[name='theme-color']")
if (metas.length > 0) metas[0].setAttribute("content", isDark ? "#080808" : "#fafafa")
if (metas.length > 0) metas[0].setAttribute("content", isDark ? "#0F0F0D" : "#ffffff")

if (themeId === "oc-2") return
if (themeId === "oc-2") return // stock theme needs no cached CSS

var css = localStorage.getItem("opencode-theme-css-" + mode)
if (css) {
Expand Down
2 changes: 1 addition & 1 deletion packages/app-bundle/overlay/packages/app/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -498,7 +498,7 @@ export function AppBaseProviders(props: ParentProps<{ locale?: Locale }>) {
<MetaProvider>
<Font />
<ThemeProvider
lockThemeId="oc-2"
lockThemeId="harmoniqs"
onThemeApplied={(_, mode, scheme) => {
void window.api?.setTitlebar?.({ mode, scheme })
}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import {
hpAfterConnect,
hpClickAction,
loadSolverMode,
releaseRequestForPick,
modeAfterDisconnect,
saveSolverMode,
solverConnectionDot,
type SolverMode,
} from "@opencode-ai/ui/amicode-solver-toggle"
import { beginSolverSwitch } from "@/components/solver-switch-banner"
import {
ConnectionCard,
type ConnectionActionView,
Expand Down Expand Up @@ -46,6 +48,9 @@ export type AmicodeComputeControl = {
onSubmit: (payload: CredentialSubmitPayload) => Promise<ConnectionActionView>
onDisconnect: (id: string) => void
onRevalidate: (id: string) => void
/** opencode#78: request the durable release of the HP tier. Optional so bare
* mounts (storybook) keep the localStorage-only legacy behavior. */
onSelectPiccolo?: () => void
refetch: () => void
}

Expand Down Expand Up @@ -74,6 +79,11 @@ export function AmicodeDefaultsCapsule(props: { compute?: AmicodeComputeControl
const pick = (m: SolverMode) => {
setMode(m)
saveSolverMode(m)
// opencode#78: localStorage is the DISPLAY state; the durable half lives in
// the ops dir and only the server may write it. Releasing the tier is the
// one direction the client requests — hp arrives with a validated
// credential (submitCredential), never from a button.
if (releaseRequestForPick(m)) props.compute?.onSelectPiccolo?.()
}

const dot = createMemo(() => solverConnectionDot(props.compute?.view()))
Expand Down Expand Up @@ -103,7 +113,12 @@ export function AmicodeDefaultsCapsule(props: { compute?: AmicodeComputeControl
}
const submitCredential = async (payload: CredentialSubmitPayload) => {
const result = await props.compute!.onSubmit(payload)
if (hpAfterConnect(result)) pick("hp")
if (hpAfterConnect(result)) {
// #167 writes {mode:"hp",status:"switching"} on this same valid outcome,
// so the restart starts here — narrate it (opencode#78 follow-up).
beginSolverSwitch("hp")
pick("hp")
}
return result
}
const disconnectCompute = (id: string) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export function ConnectionBanner() {
transform: "translateX(-50%)",
"z-index": 40,
padding: "6px 14px",
"border-radius": "999px",
"border-radius": "var(--radius-full)",
"font-size": "12px",
"font-weight": 600,
border: "1px solid var(--v2-border-border-base, #3c3c3c)",
Expand Down
Loading
Loading