Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
110 commits
Select commit Hold shift + click to select a range
ba238a6
ci: Add Android build and publish workflow with APK artifact upload
wallentx Mar 16, 2026
e9db0cf
ci: Allow publish workflow to run with android_only input
wallentx Mar 16, 2026
ebeef27
ci: Use github.token instead of committer output in publish workflow
wallentx Mar 16, 2026
3a066b7
feat: Add aarch64-linux-android target to sidecar binaries
wallentx Mar 16, 2026
583ff76
ci: Simplify publish workflow and fix Android build conditions
wallentx Mar 16, 2026
a891a57
feat: Refactor app initialization and add WSL path conversion
wallentx Mar 16, 2026
a7b0147
ci: Fix Android-only workflow condition and update dependencies
wallentx Mar 16, 2026
1e73b76
sync release versions for v1.17.13
Jul 1, 2026
9495e3e
chore: bump gitlab-ai-provider to 6.10.0 (#34756)
vglafirov Jul 1, 2026
7de5aa0
chore: update nix node_modules hashes
opencode-agent[bot] Jul 1, 2026
74e5644
feat(stats): redesign lab hero
adamdotdevin Jul 1, 2026
262ef4f
feat(stats): add lab overview
adamdotdevin Jul 1, 2026
91ca75c
feat(stats): redesign lab usage
adamdotdevin Jul 1, 2026
85e8ac3
feat(data): another redesigned lab section
adamdotdevin Jul 1, 2026
3706b33
feat(stats): use catalog descriptions
adamdotdevin Jul 1, 2026
5668d36
chore: generate
opencode-agent[bot] Jul 1, 2026
beb586a
fix(stats): refine lab page tooltips
adamdotdevin Jul 1, 2026
fc29f99
fix(stats): include locale header in vary (#34789)
adamdotdevin Jul 1, 2026
3df5556
zen: new inference
fwang Jul 1, 2026
f266e82
fix(desktop): keep window tabs across app close (#34807)
Hona Jul 1, 2026
3cf7180
fix(app): stabilize session timeline layout continuity (#34533)
Hona Jul 2, 2026
917eca4
feat(desktop): refine session tab preview popover (#34792)
usrnk1 Jul 2, 2026
4de7e90
fix(ui): raise v2 tooltip layer (#34455)
opencode-agent[bot] Jul 2, 2026
b393499
chore: update nix node_modules hashes
opencode-agent[bot] Jul 2, 2026
6ca60d9
fix(opencode): update Cerebras SDK reasoning replay (#34826)
opencode-agent[bot] Jul 2, 2026
a39db9c
chore: update nix node_modules hashes
opencode-agent[bot] Jul 2, 2026
39dfbb5
fix(app): resolve target session lineage outside router transition (#…
Hona Jul 2, 2026
d6b8971
chore: generate
opencode-agent[bot] Jul 2, 2026
a0d64b1
fix(opencode): filter session list by resolved instance directory (#3…
Hona Jul 2, 2026
5fecf7a
fix(app): only allow \(...\) syntax for inline latex (#34850)
Brendonovich Jul 2, 2026
27c9b13
test(opencode): cover Windows session list directory spellings (#34857)
Hona Jul 2, 2026
1fb2ecc
fix(app): scope session tab indicators to the tab's server (#34861)
Hona Jul 2, 2026
403164a
chore: generate
opencode-agent[bot] Jul 2, 2026
4023359
fix(app): clear original prompt after retarget (#34863)
Brendonovich Jul 2, 2026
fbb95a6
fix(app): seed session status before warming session info (#34864)
Hona Jul 2, 2026
7d26186
feat(app): v2 review panel overhaul (#31882)
arvsrn Jul 2, 2026
4a42cae
fix(app): keep terminal mounted when switching session tabs in a work…
Hona Jul 2, 2026
f52424e
fix(ui): resolve toast icon inside the toast component (#34874)
Hona Jul 2, 2026
373cd08
fix(copilot): honor advertised model endpoints (#34958)
rekram1-node Jul 2, 2026
7457139
fix(stats): polish lab pages
adamdotdevin Jul 2, 2026
299daa2
feat(stats): redesign model hero
adamdotdevin Jul 2, 2026
5ecd19d
chore: generate
opencode-agent[bot] Jul 2, 2026
3adfb97
feat(tui): add debug info dialog with copy to clipboard (#35004)
StarpTech Jul 2, 2026
cb54824
zen: new inference
fwang Jul 3, 2026
20b090e
zen: new inference
fwang Jul 3, 2026
c07ac0d
feat(app): navigate tabs on mousedown in new layout (#35042)
Hona Jul 3, 2026
6dba0cc
zen: new inference
fwang Jul 3, 2026
5455fed
zen: new inference
fwang Jul 3, 2026
04d236c
fix(app): keep v2 review pane mounted across session tab switches (#3…
Hona Jul 3, 2026
cb93114
feat: experimental codemode (#34677)
rekram1-node Jul 3, 2026
83c638e
chore: generate
opencode-agent[bot] Jul 3, 2026
2ef1a59
fix(ci): setup Node 24 before bun install for node-gyp (#35076)
Hona Jul 3, 2026
379adee
revert: back out experimental codemode (#35077)
rekram1-node Jul 3, 2026
2409c7a
feat(codemode): add confined execution package (#35079)
rekram1-node Jul 3, 2026
3f0fc22
chore: generate
opencode-agent[bot] Jul 3, 2026
7d48a22
chore: update nix node_modules hashes
opencode-agent[bot] Jul 3, 2026
eb34766
chore: remove root node-gyp pin after Node 24 setup (#35092)
rekram1-node Jul 3, 2026
bf58fae
feat(desktop): add recently closed projects to home (#34926)
usrnk1 Jul 3, 2026
4c6e2a9
feat(app): align subagent UI with v2 (#34931)
arvsrn Jul 3, 2026
caedf36
chore: generate
opencode-agent[bot] Jul 3, 2026
d46c02b
feat(desktop): papercut fixes (#34939)
usrnk1 Jul 3, 2026
458ec7b
refactor(opencode): expose MCP tools in native shape from the service…
rekram1-node Jul 3, 2026
a9144ec
feat(app): improvements to model search (#34954)
arvsrn Jul 3, 2026
30936a9
chore: generate
opencode-agent[bot] Jul 3, 2026
abaab29
feat(opencode): add code-mode MCP adapter (#35085)
rekram1-node Jul 3, 2026
72845e0
chore: generate
opencode-agent[bot] Jul 3, 2026
41a3cfc
chore: update nix node_modules hashes
opencode-agent[bot] Jul 3, 2026
a4fed69
feat(app): dropdown search fix (#34961)
arvsrn Jul 3, 2026
9aab24e
fix(opencode): provide Observability beneath all route service graphs…
kitlangton Jul 3, 2026
eba0bd0
chore: upgrade turbo (#35173)
jlongster Jul 3, 2026
7d84cb0
feat(stats): add model momentum section
adamdotdevin Jul 3, 2026
2152624
chore: generate
opencode-agent[bot] Jul 3, 2026
33ed95f
fix(core): rewrite replacements while hoisting layers (#35175)
jlongster Jul 3, 2026
1f47bbf
fix(stats): link market share labs
adamdotdevin Jul 3, 2026
1681547
chore: artifacts
adamdotdevin Jul 3, 2026
a09447b
chore: generate
opencode-agent[bot] Jul 3, 2026
39c6dd1
feat(tui): render code-mode execute tool with child calls (#35113)
rekram1-node Jul 3, 2026
27e8e2e
refactor(opencode): fail the execute tool on program failure (#35180)
rekram1-node Jul 3, 2026
911fb70
fix(tui): align execute child calls with task indentation (#35190)
rekram1-node Jul 3, 2026
96d53c6
refactor(core): move path resolve into fs service (#35202)
jlongster Jul 3, 2026
ed6dc87
feat(opencode): gate execute tool behind code mode flag (#35185)
rekram1-node Jul 3, 2026
436cd39
chore: generate
opencode-agent[bot] Jul 3, 2026
b44bc0a
cleanup: tweak compaction prompt (#35220)
opencode-agent[bot] Jul 3, 2026
933dbfd
fix(app): restore unfocused composer typing (#35249)
Hona Jul 3, 2026
8b6a2b1
fix(app): delay initial tab preview (#35266)
opencode-agent[bot] Jul 4, 2026
f63a451
fix(desktop): stabilize esm shim injection (#35270)
Hona Jul 4, 2026
a226767
fix(app): match new session panel corners (#35257)
Hona Jul 4, 2026
7a8e7c8
tui: preserve spinner registration (#35292)
simonklee Jul 4, 2026
ba810ed
feat: Add aarch64-linux-android target to sidecar binaries
wallentx Mar 16, 2026
c7138f6
chore(ci): Improve workflow portability and add Android build support
wallentx Jul 5, 2026
76d0bef
chore: merge origin termux
wallentx Jul 5, 2026
faebe15
feat(ci): Add Android artifact build workflow and guidance
wallentx Jul 5, 2026
68c9ea3
fix(opencode): use NDK sysroot for Android artifact build
wallentx Jul 5, 2026
17bd854
fix(opencode): pass Android NDK libc paths to Zig
wallentx Jul 5, 2026
5db3d87
fix(opencode): patch OpenTUI Android system libs
wallentx Jul 5, 2026
d83b710
fix(opencode): rely on NDK libc config for Android
wallentx Jul 5, 2026
ff3049d
fix(opencode): resolve Android OpenTUI loader relatively
wallentx Jul 5, 2026
ac2df9c
chore(ci): add Android Bun compile smoke target
wallentx Jul 5, 2026
d7b3039
fix(opencode): relocate Android Bun payload pointer
wallentx Jul 5, 2026
bbebd26
chore(ci): build Android Bun runtime from source
wallentx Jul 5, 2026
c848b4b
fix(ci): install LLVM for Android Bun source build
wallentx Jul 5, 2026
c417a27
fix(ci): bootstrap LLVM 21 for Android Bun build
wallentx Jul 5, 2026
8bce446
fix(ci): keep Bun Android build inside source tree
wallentx Jul 5, 2026
778b204
fix(ci): link Android NDK runtimes for Bun build
wallentx Jul 5, 2026
153cb15
fix(ci): use Android strip for Bun runtime build
wallentx Jul 5, 2026
6f1c51e
chore(termux): add release mirror workflows
wallentx Jul 6, 2026
51c4eea
chore(termux): align app token variables
wallentx Jul 6, 2026
6c143cb
chore(termux): use app client id input
wallentx Jul 6, 2026
68371ac
chore(termux): fall back to github token
wallentx Jul 6, 2026
b93298e
chore(termux): prepare v1.17.13
github-actions[bot] Jul 6, 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
Prev Previous commit
Next Next commit
fix(app): stabilize session timeline layout continuity (anomalyco#34533)
  • Loading branch information
Hona authored Jul 2, 2026
commit 3cf71808c4c6cef68b57cd04cd02ae5d644582e5
1 change: 1 addition & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

61 changes: 61 additions & 0 deletions packages/app/e2e/performance/timeline-stability/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Timeline Layout Continuity

Run from `packages/app`:

```sh
bun run test:stability
```

The suite runs a production build in one Chromium worker. Selected scenarios use deterministic 4x CPU stress after application readiness. This is a stress profile, not emulation of a specific device.

## What It Proves

The continuity probe samples DOM-derived layout and visibility state across browser render opportunities. Tests declare explicit contracts such as:

- Preserve a visible semantic anchor while the user is away from the bottom.
- Preserve end anchoring while active content grows or new content appears.
- Keep adjacent visible rows ordered without material overlap.
- Keep user-selected disclosure state through updates and virtualization.
- Avoid a sampled blank interval while one visible surface replaces another.
- Preserve logical row and control identity where local state or focus depends on it.
- Keep keyboard, wheel, and nested-scroll ownership consistent during remeasurement.

The suite exercises real browser reducer, projection, component, virtualizer, layout, focus, and interaction code. The backend and event producer are controlled fixtures.

## What It Does Not Prove

The pass/fail oracle does not inspect every compositor-presented pixel. A sample taken after `requestAnimationFrame` is a DOM/layout observation, not proof that every sampled state was displayed or that every displayed frame was sampled.

The suite does not provide complete coverage for:

- Compositor-only or raster-only glitches.
- Color, contrast, canvas, WebGL, masks, irregular clips, or arbitrary occlusion.
- Physical display refresh rates, native OS scaling, or a named low-end device.
- TCP packetization, proxy buffering, or the complete real server/provider pipeline.

Playwright video, trace, screenshots, and observation JSON are diagnostic evidence. They are not pixel baselines and do not participate in normal pass/fail decisions.

For optional before/violation/after screenshots, set `OPENCODE_STABILITY_CAPTURE=1`. Capture is opt-in because compositor readback can perturb timing.

## Test Layers

- **Projection:** admitted rows, grouping, labels, and final visible states.
- **Local state:** disclosure state, identity, duplicate delivery, and virtualization restoration.
- **Interaction:** wheel, keyboard, nested scrolling, actionability, and focus behavior.
- **Layout continuity:** anchoring, adjacency, responsive reflow, and visible surface handoffs.
- **Reducer hardening:** validly shaped but intentionally reordered, duplicated, removed, or replaced events.
- **Oracle contract:** pure analyzer and browser sampler calibration tests.

Production-lifecycle fixtures should model states emitted by the current producer. Impossible or reordered sequences belong in reducer-hardening tests and must not be described as normal provider behavior.

## Diagnostics

Failures retain:

- `video.webm`
- `trace.zip`
- failure screenshot
- sampled DOM/layout trace JSON
- event markers and summarized violations

The analyzer records both unclipped layout bounds and ancestor-clipped visible intersections. Scrollbar and raw `scrollTop` changes alone do not fail continuity checks; user-visible semantic anchor movement does.
250 changes: 250 additions & 0 deletions packages/app/e2e/performance/timeline-stability/adverse.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
import { expect, test } from "@playwright/test"
import {
defineVisualRegions,
reportVisualStability,
startVisualProbe,
stopVisualProbe,
visualPlan,
} from "../../utils/visual-stability"
import {
assistantMessage,
partUpdated,
setupTimeline,
shell,
textPart,
toolPart,
userMessage,
waitForVisualSettle,
type TimelineMessage,
} from "./fixture"

test.describe("timeline adverse visual stability", () => {
test("does not pull a scrolled-away user while an active shell grows", async ({ page }, testInfo) => {
const activeShellID = "prt_adverse_01_shell"
const messages = [
...history(24),
userMessage(),
assistantMessage([shell(activeShellID, "running")], { completed: false }),
]
const timeline = await setupTimeline(page, {
messages,
settings: { shellToolPartsExpanded: true },
cpuRate: 4,
eventRetry: 30,
})
const scroller = page.locator(".scroll-view__viewport", {
has: page.locator('[data-timeline-row="AssistantPart"]'),
})
await scroller.evaluate((element) => {
element.dispatchEvent(new WheelEvent("wheel", { bubbles: true, cancelable: true, deltaY: -450 }))
element.scrollTop = Math.max(0, element.scrollHeight - element.clientHeight - 450)
})
await page.waitForTimeout(150)
await expect
.poll(() => scroller.evaluate((element) => element.scrollHeight - element.clientHeight - element.scrollTop))
.toBeGreaterThan(100)
const anchor = await scroller.evaluate((element) => {
const view = element.getBoundingClientRect()
return [...element.querySelectorAll<HTMLElement>("[data-timeline-key]")].find((row) => {
const rect = row.getBoundingClientRect()
return rect.top >= view.top + 40 && rect.bottom <= view.bottom - 40
})?.dataset.timelineKey
})
expect(anchor).toBeTruthy()
await waitForVisualSettle(page, [`[data-timeline-key="${anchor}"]`])

const regions = defineVisualRegions({
anchor: { selector: `[data-timeline-key="${anchor}"]` },
})
await startVisualProbe(page, regions)
await timeline.send(partUpdated(shell(activeShellID, "running", lines(1))), 180)
await timeline.send(partUpdated(shell(activeShellID, "running", lines(10))), 90)
await timeline.send(partUpdated(shell(activeShellID, "running", lines(50))), 350)
await timeline.send(partUpdated(shell(activeShellID, "completed", lines(50))), 500)
const trace = await stopVisualProbe<keyof typeof regions>(page)
await reportVisualStability(
testInfo,
"scrolled-away-shell",
trace,
visualPlan(regions, [
{ type: "required", regions: ["anchor"] },
{ type: "unique", regions: ["anchor"] },
{ type: "stable", regions: ["anchor"] },
{ type: "fixed", regions: ["anchor"] },
{ type: "opacity", regions: "all" },
{ type: "continuity", regions: "all" },
{ type: "motion", regions: "all", maxPositionReversals: 0 },
{ type: "label-stability", regions: "all" },
]),
)
})

test("preserves an explicit shell state across virtualization", async ({ page }) => {
const targetID = "prt_virtual_shell"
const messages = [
userMessage(undefined, { id: "msg_0000_virtual_user", created: 1700000000000 }),
assistantMessage([shell(targetID, "completed", lines(20))], {
id: "msg_0001_virtual_assistant",
parentID: "msg_0000_virtual_user",
created: 1700000001000,
}),
...history(35, 10),
]
await setupTimeline(page, { messages, settings: { shellToolPartsExpanded: false } })
const scroller = page.locator(".scroll-view__viewport", { has: page.locator("[data-timeline-row]") })
await scroller.evaluate((element) => {
element.dispatchEvent(new WheelEvent("wheel", { bubbles: true, cancelable: true, deltaY: -1_000 }))
element.scrollTop = 0
})
await page.waitForTimeout(300)
const trigger = page.locator(`[data-timeline-part-id="${targetID}"] [data-slot="collapsible-trigger"]`)
await expect(trigger).toBeVisible()
await trigger.click()
await expect(trigger).toHaveAttribute("aria-expanded", "true")

await scroller.evaluate((element) => (element.scrollTop = element.scrollHeight))
await expect(page.locator(`[data-timeline-part-id="${targetID}"]`)).toHaveCount(0)
await scroller.evaluate((element) => (element.scrollTop = 0))
await expect(trigger).toBeVisible()
await expect(trigger).toHaveAttribute("aria-expanded", "true")
})

test("keeps narrow viewport rows ordered during long shell growth", async ({ page }, testInfo) => {
const shellID = "prt_narrow_01_shell"
const followingID = "prt_narrow_02_following"
const timeline = await setupTimeline(page, {
messages: [
userMessage(),
assistantMessage(
[shell(shellID, "running"), textPart(followingID, "A narrow following row that wraps across lines.")],
{
completed: false,
},
),
],
settings: { shellToolPartsExpanded: true },
viewport: { width: 430, height: 800 },
cpuRate: 4,
})
await waitForVisualSettle(page, [
`[data-timeline-part-id="${shellID}"]`,
`[data-timeline-part-id="${followingID}"]`,
])
const regions = defineVisualRegions({
shell: { selector: `[data-timeline-part-id="${shellID}"]`, closest: '[data-timeline-row="AssistantPart"]' },
following: {
selector: `[data-timeline-part-id="${followingID}"]`,
closest: '[data-timeline-row="AssistantPart"]',
},
})
await startVisualProbe(page, regions)
await timeline.send(partUpdated(shell(shellID, "running", wideLines(10))), 100)
await timeline.send(partUpdated(shell(shellID, "running", wideLines(50))), 300)
await timeline.send(partUpdated(shell(shellID, "completed", wideLines(50))), 500)
const trace = await stopVisualProbe<keyof typeof regions>(page)
await reportVisualStability(
testInfo,
"narrow-shell",
trace,
visualPlan(
regions,
[
{ type: "required", regions: ["shell", "following"] },
{ type: "unique", regions: ["shell", "following"] },
{ type: "stable", regions: ["shell", "following"] },
{ type: "opacity", regions: "all" },
{ type: "continuity", regions: "all" },
{ type: "motion", regions: "all", maxPositionReversals: 0 },
{ type: "label-stability", regions: "all" },
{ type: "preserve-bottom-anchor" },
{ type: "flow", regions: ["shell", "following"] },
],
{ perMarker: true },
),
)
})

test("keeps visible rows ordered while resizing desktop to narrow and back", async ({ page }, testInfo) => {
const shellID = "prt_resize_01_shell"
const contextIDs = ["prt_resize_02_read", "prt_resize_03_glob"]
const followingID = "prt_resize_04_following"
await setupTimeline(page, {
messages: [
userMessage(),
assistantMessage([
shell(shellID, "completed", wideLines(15)),
toolPart(contextIDs[0]!, "read", "completed", { filePath: "src/a.ts" }),
toolPart(contextIDs[1]!, "glob", "completed", { path: ".", pattern: "**/*.ts" }),
textPart(followingID, "Following responsive timeline content that wraps on narrow screens."),
]),
],
settings: { shellToolPartsExpanded: true },
cpuRate: 4,
seedHistory: true,
})
const group = `[data-timeline-part-ids="${contextIDs.join(",")}"]`
const regions = defineVisualRegions({
shell: { selector: `[data-timeline-part-id="${shellID}"]`, closest: '[data-timeline-row="AssistantPart"]' },
context: { selector: group, closest: '[data-timeline-row="AssistantPart"]' },
following: {
selector: `[data-timeline-part-id="${followingID}"]`,
closest: '[data-timeline-row="AssistantPart"]',
},
})
await startVisualProbe(page, regions)
await page.setViewportSize({ width: 430, height: 800 })
await page.waitForTimeout(500)
await page.setViewportSize({ width: 900, height: 800 })
await page.waitForTimeout(500)
await page.setViewportSize({ width: 1400, height: 900 })
await page.waitForTimeout(500)
const trace = await stopVisualProbe<keyof typeof regions>(page)
await reportVisualStability(
testInfo,
"responsive-resize",
trace,
visualPlan(regions, [
{ type: "required", regions: ["shell", "context", "following"] },
{ type: "unique", regions: ["shell", "context", "following"] },
{ type: "stable", regions: ["shell", "context", "following"] },
{ type: "opacity", regions: "all" },
{ type: "continuity", regions: "all" },
{ type: "motion", regions: "all", maxPositionReversals: 4, maxReversals: 4 },
{ type: "label-stability", regions: "all" },
{ type: "flow", regions: ["shell", "context", "following"] },
]),
)
})
})

function history(count: number, offset = 0): TimelineMessage[] {
return Array.from({ length: count }, (_, index) => {
const value = index + offset
const prefix = `msg_0${String(value).padStart(3, "0")}_history`
const userID = `${prefix}_a_user`
return [
userMessage(undefined, { id: userID, created: 1699990000000 + value * 10_000 }),
assistantMessage(
[
textPart(
`prt_history_${String(value).padStart(3, "0")}`,
`Historical response ${value}. ${"Stable history content. ".repeat(8)}`,
),
],
{
id: `${prefix}_b_assistant`,
parentID: userID,
created: 1699990001000 + value * 10_000,
},
),
]
}).flat()
}

function lines(count: number) {
return Array.from({ length: count }, (_, index) => `line ${index + 1}`).join("\n")
}

function wideLines(count: number) {
return Array.from({ length: count }, (_, index) => `line ${index + 1} ${"wide-output-".repeat(20)}`).join("\n")
}
Loading