From 96b1d8f639991e896bc8d31afe64d6309bf3ccd2 Mon Sep 17 00:00:00 2001
From: Luke Parker <10430890+Hona@users.noreply.github.com>
Date: Fri, 13 Mar 2026 12:21:50 +1000
Subject: fix(app): stabilize todo dock e2e with composer probe (#17267)
---
.../session/composer/session-composer-region.tsx | 9 +--
.../session/composer/session-composer-state.ts | 55 +++++++++++++-
.../pages/session/composer/session-todo-dock.tsx | 21 ++++++
packages/app/src/testing/session-composer.ts | 84 ++++++++++++++++++++++
4 files changed, 163 insertions(+), 6 deletions(-)
create mode 100644 packages/app/src/testing/session-composer.ts
(limited to 'packages/app/src')
diff --git a/packages/app/src/pages/session/composer/session-composer-region.tsx b/packages/app/src/pages/session/composer/session-composer-region.tsx
index 2034fbead..84f77ea4a 100644
--- a/packages/app/src/pages/session/composer/session-composer-region.tsx
+++ b/packages/app/src/pages/session/composer/session-composer-region.tsx
@@ -44,9 +44,9 @@ export function SessionComposerRegion(props: {
}) {
const prompt = usePrompt()
const language = useLanguage()
- const { sessionKey } = useSessionKey()
+ const route = useSessionKey()
- const handoffPrompt = createMemo(() => getSessionHandoff(sessionKey())?.prompt)
+ const handoffPrompt = createMemo(() => getSessionHandoff(route.sessionKey())?.prompt)
const previewPrompt = () =>
prompt
@@ -62,7 +62,7 @@ export function SessionComposerRegion(props: {
createEffect(() => {
if (!prompt.ready()) return
- setSessionHandoff(sessionKey(), { prompt: previewPrompt() })
+ setSessionHandoff(route.sessionKey(), { prompt: previewPrompt() })
})
const [store, setStore] = createStore({
@@ -85,7 +85,7 @@ export function SessionComposerRegion(props: {
}
createEffect(() => {
- sessionKey()
+ route.sessionKey()
const ready = props.ready
const delay = 140
@@ -194,6 +194,7 @@ export function SessionComposerRegion(props: {
>
setStore("body", el)}>
return !!permissionRequest() || !!questionRequest()
})
+ const [test, setTest] = createStore({
+ on: false,
+ live: undefined as boolean | undefined,
+ todos: undefined as Todo[] | undefined,
+ })
+
+ const pull = () => {
+ const id = params.id
+ if (!id) {
+ setTest({ on: false, live: undefined, todos: undefined })
+ return
+ }
+
+ const next = composerDriver(id)
+ if (!next) {
+ setTest({ on: false, live: undefined, todos: undefined })
+ return
+ }
+
+ setTest({
+ on: true,
+ live: next.live,
+ todos: next.todos?.map((todo) => ({ ...todo })),
+ })
+ }
+
+ onMount(() => {
+ if (!composerEnabled()) return
+
+ pull()
+ createEffect(on(() => params.id, pull, { defer: true }))
+
+ const onEvent = (event: Event) => {
+ const detail = (event as CustomEvent<{ sessionID?: string }>).detail
+ if (detail?.sessionID !== params.id) return
+ pull()
+ }
+
+ window.addEventListener(composerEvent, onEvent)
+ onCleanup(() => window.removeEventListener(composerEvent, onEvent))
+ })
+
const todos = createMemo((): Todo[] => {
+ if (test.on && test.todos !== undefined) return test.todos
const id = params.id
if (!id) return []
return globalSync.data.session_todo[id] ?? []
@@ -64,7 +108,10 @@ export function createSessionComposerState(options?: { closeMs?: number | (() =>
})
const busy = createMemo(() => status().type !== "idle")
- const live = createMemo(() => busy() || blocked())
+ const live = createMemo(() => {
+ if (test.on && test.live !== undefined) return test.live
+ return busy() || blocked()
+ })
const [store, setStore] = createStore({
responding: undefined as string | undefined,
@@ -116,6 +163,10 @@ export function createSessionComposerState(options?: { closeMs?: number | (() =>
// Keep stale turn todos from reopening if the model never clears them.
const clear = () => {
+ if (test.on && test.todos !== undefined) {
+ setTest("todos", [])
+ return
+ }
const id = params.id
if (!id) return
globalSync.todo.set(id, [])
diff --git a/packages/app/src/pages/session/composer/session-todo-dock.tsx b/packages/app/src/pages/session/composer/session-todo-dock.tsx
index c7907bb54..5500de97a 100644
--- a/packages/app/src/pages/session/composer/session-todo-dock.tsx
+++ b/packages/app/src/pages/session/composer/session-todo-dock.tsx
@@ -8,6 +8,7 @@ import { TextReveal } from "@opencode-ai/ui/text-reveal"
import { TextStrikethrough } from "@opencode-ai/ui/text-strikethrough"
import { Index, createEffect, createMemo, on, onCleanup } from "solid-js"
import { createStore } from "solid-js/store"
+import { composerEnabled, composerProbe } from "@/testing/session-composer"
function dot(status: Todo["status"]) {
if (status !== "in_progress") return undefined
@@ -35,6 +36,7 @@ function dot(status: Todo["status"]) {
}
export function SessionTodoDock(props: {
+ sessionID?: string
todos: Todo[]
title: string
collapseLabel: string
@@ -69,6 +71,8 @@ export function SessionTodoDock(props: {
const off = createMemo(() => hide() > 0.98)
const turn = createMemo(() => Math.max(0, Math.min(1, value())))
const full = createMemo(() => Math.max(78, store.height))
+ const e2e = composerEnabled()
+ const probe = composerProbe(props.sessionID)
let contentRef: HTMLDivElement | undefined
createEffect(() => {
@@ -83,6 +87,23 @@ export function SessionTodoDock(props: {
onCleanup(() => observer.disconnect())
})
+ createEffect(() => {
+ if (!e2e) return
+
+ probe.set({
+ mounted: true,
+ collapsed: store.collapsed,
+ hidden: store.collapsed || off(),
+ count: props.todos.length,
+ states: props.todos.map((todo) => todo.status),
+ })
+ })
+
+ onCleanup(() => {
+ if (!e2e) return
+ probe.drop()
+ })
+
return (
>
+}
+
+export type ComposerProbeState = {
+ mounted: boolean
+ collapsed: boolean
+ hidden: boolean
+ count: number
+ states: Todo["status"][]
+}
+
+type ComposerState = {
+ driver?: ComposerDriverState
+ probe?: ComposerProbeState
+}
+
+export type ComposerWindow = Window & {
+ __opencode_e2e?: {
+ composer?: {
+ enabled?: boolean
+ sessions?: Record
+ }
+ }
+}
+
+const clone = (driver: ComposerDriverState) => ({
+ live: driver.live,
+ todos: driver.todos?.map((todo) => ({ ...todo })),
+})
+
+export const composerEnabled = () => {
+ if (typeof window === "undefined") return false
+ return (window as ComposerWindow).__opencode_e2e?.composer?.enabled === true
+}
+
+const root = () => {
+ if (!composerEnabled()) return
+ const state = (window as ComposerWindow).__opencode_e2e?.composer
+ if (!state) return
+ state.sessions ??= {}
+ return state.sessions
+}
+
+export const composerDriver = (sessionID?: string) => {
+ if (!sessionID) return
+ const state = root()?.[sessionID]?.driver
+ if (!state) return
+ return clone(state)
+}
+
+export const composerProbe = (sessionID?: string) => {
+ const set = (next: ComposerProbeState) => {
+ if (!sessionID) return
+ const sessions = root()
+ if (!sessions) return
+ const prev = sessions[sessionID] ?? {}
+ sessions[sessionID] = {
+ ...prev,
+ probe: {
+ ...next,
+ states: [...next.states],
+ },
+ }
+ }
+
+ return {
+ set,
+ drop() {
+ set({
+ mounted: false,
+ collapsed: false,
+ hidden: true,
+ count: 0,
+ states: [],
+ })
+ },
+ }
+}
--
cgit v1.2.3