Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
f83c5f4
fix(app): keep timeline visible above composer dock
Astro-Han May 1, 2026
2225bb5
refactor(app): extract session scroll dock controller
Astro-Han May 1, 2026
2b5f667
fix(app): tighten session timeline bottom gap
Astro-Han May 1, 2026
036f667
refactor(app): extract session history window controller
Astro-Han May 1, 2026
6e1b2b6
refactor(app): extract session desktop context sync
Astro-Han May 1, 2026
636e0dc
refactor(app): extract session followup controller
Astro-Han May 1, 2026
29239d3
refactor(app): extract session revert controller
Astro-Han May 1, 2026
765de25
refactor(app): extract session review state controller
Astro-Han May 1, 2026
4edc30c
refactor(app): extract session comment context
Astro-Han May 1, 2026
abf7764
refactor(app): extract session review panel wiring
Astro-Han May 1, 2026
104b13e
refactor(app): extract session keyboard focus
Astro-Han May 1, 2026
5981215
refactor(app): extract session refresh effects
Astro-Han May 1, 2026
d842505
refactor(app): extract session active message controller
Astro-Han May 1, 2026
2b0dfc3
refactor(app): extract session composer region
Astro-Han May 1, 2026
531d491
refactor(app): extract session main view
Astro-Han May 1, 2026
41c17a8
refactor(app): extract session route and vcs effects
Astro-Han May 1, 2026
dd0b7f8
refactor(app): extract session timeline data
Astro-Han May 1, 2026
e6d38b8
refactor(app): extract session history backfill
Astro-Han May 1, 2026
a23fbd7
refactor(app): extract session worktree selection
Astro-Han May 1, 2026
04b5310
test(app): complete followup encode mock
Astro-Han May 1, 2026
9bed311
refactor(app): extract session timeline interaction
Astro-Han May 1, 2026
509278b
fix(app): harden session dock and revert review fixes
Astro-Han May 1, 2026
966f80f
fix(app): preserve session review navigation state
Astro-Han May 1, 2026
79fa5e9
refactor(app): tighten session helper state types
Astro-Han May 1, 2026
c5676bd
fix(app): guard session review scheduled effects
Astro-Han May 1, 2026
ec49a92
test(app): update session shell contract after split
Astro-Han May 1, 2026
1415aae
fix: keep session tail above composer
Astro-Han May 1, 2026
5dfc479
fix: address session route review feedback
Astro-Han May 1, 2026
48ee1c9
refactor: split session review panel
Astro-Han May 1, 2026
2181b43
fix: balance session timeline spacing
Astro-Han May 1, 2026
32fa04e
fix: harden session revert restore
Astro-Han May 1, 2026
7114877
fix: match session timeline edge spacing
Astro-Han May 1, 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
120 changes: 120 additions & 0 deletions packages/app/e2e/session/session-composer-dock.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { mkdir } from "node:fs/promises"
import { test, expect } from "../fixtures"
import {
composerEvent,
Expand Down Expand Up @@ -900,6 +901,125 @@ test("todo dock restarts completing delay after same-count terminal session swit
)
})

test("e2e composer dock keeps latest turn visible when dock height changes", async ({ page, project, assistant }) => {
const title = `e2e composer scroll dock ${Date.now()}`
const longReply = [
"Here's the smoke test message counting from 1 to 100:",
"",
"```",
...Array.from({ length: 100 }, (_, index) => `${index + 1}`),
"```",
"",
"Smoke test complete! This output demonstrates:",
"",
"- 100 lines of sequential numeric output",
"- No files were created or modified",
"- Each number appears on its own line as requested",
].join("\n")

await project.open()
await withDockSession(
project.sdk,
title,
async (session) => {
const dock = await todoDock(page, session.id)
await project.gotoSession(session.id)
await assistant.reply(longReply)

await project.prompt("Write a long visible response for scroll dock testing.")

await dock.open([
{ content: "first scroll dock task", status: "pending" },
{ content: "second scroll dock task", status: "pending" },
{ content: "third scroll dock task", status: "pending" },
])

const metrics = await page.evaluate(() => {
const viewport = document.querySelector('[data-component="scroll-viewport"]')
const composer = document.querySelector('[data-component="session-prompt-dock"]')
const last = [...document.querySelectorAll("[data-message-id]")].at(-1)
if (!(viewport instanceof HTMLElement) || !(composer instanceof HTMLElement) || !(last instanceof HTMLElement)) {
return null
}
viewport.scrollTop = viewport.scrollHeight
const walker = document.createTreeWalker(last, NodeFilter.SHOW_TEXT)
let tail: Text | null = null
while (walker.nextNode()) {
const node = walker.currentNode
if (node.textContent?.includes("Each number appears on its own line as requested")) tail = node as Text
}
if (!tail) return null
const range = document.createRange()
range.selectNodeContents(tail)
const composerTop = composer.getBoundingClientRect().top
const lastBottom = last.getBoundingClientRect().bottom
const tailBottom = range.getBoundingClientRect().bottom
range.detach()
return {
scrollTop: viewport.scrollTop,
messageDistance: composerTop - lastBottom,
tailDistance: composerTop - tailBottom,
}
})

expect(metrics).not.toBeNull()
expect(metrics!.messageDistance).toBeGreaterThanOrEqual(0)
expect(metrics!.tailDistance).toBeGreaterThanOrEqual(0)

const before = metrics!.scrollTop
const viewport = page.locator('[data-component="scroll-viewport"]').first()
await viewport.hover()
await page.mouse.wheel(0, -360)

await expect
.poll(async () => {
return page.evaluate(() => {
const viewport = document.querySelector('[data-component="scroll-viewport"]')
if (!(viewport instanceof HTMLElement)) return 0
return viewport.scrollHeight - viewport.clientHeight - viewport.scrollTop
})
})
.toBeGreaterThan(120)

const distanceBeforeExpansion = await page.evaluate(() => {
const viewport = document.querySelector('[data-component="scroll-viewport"]')
if (!(viewport instanceof HTMLElement)) return 0
return viewport.scrollHeight - viewport.clientHeight - viewport.scrollTop
})

await dock.open([
{ content: "first scroll dock task", status: "pending" },
{ content: "second scroll dock task", status: "pending" },
{ content: "third scroll dock task", status: "pending" },
{ content: "fourth scroll dock task expands height", status: "pending" },
{ content: "fifth scroll dock task expands height", status: "pending" },
])

let afterUserScroll: { scrollTop: number; distanceFromBottom: number } | null = null
await expect
.poll(async () => {
afterUserScroll = await page.evaluate(() => {
const viewport = document.querySelector('[data-component="scroll-viewport"]')
if (!(viewport instanceof HTMLElement)) return null
return {
scrollTop: viewport.scrollTop,
distanceFromBottom: viewport.scrollHeight - viewport.clientHeight - viewport.scrollTop,
}
})
return afterUserScroll?.distanceFromBottom ?? -1
})
.toBeGreaterThanOrEqual(distanceBeforeExpansion - 40)

expect(afterUserScroll).not.toBeNull()
expect(afterUserScroll!.scrollTop).toBeLessThan(before)

await mkdir(".artifacts/session-scroll-dock", { recursive: true })
await page.screenshot({ path: ".artifacts/session-scroll-dock/latest-visible.png" })
},
{ trackSession: project.trackSession },
)
})

test("keyboard focus stays off prompt while blocked", async ({ page, llm, project }) => {
const questions = [
{
Expand Down
1 change: 1 addition & 0 deletions packages/app/src/components/prompt-input/submit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ beforeAll(async () => {

mock.module("@opencode-ai/util/encode", () => ({
base64Encode: (value: string) => value,
checksum: (value: string) => String(value.length),
}))

mock.module("@/context/local", () => ({
Expand Down
Loading
Loading