diff options
| author | Luke Parker <[email protected]> | 2026-03-13 12:21:50 +1000 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-03-13 12:21:50 +1000 |
| commit | 96b1d8f639991e896bc8d31afe64d6309bf3ccd2 (patch) | |
| tree | d51a398a9e13046b634a5cbe517f5ecbe72db765 /packages/app/src | |
| parent | dcb17c6a678918ce0786640729fcc8cd8adb1746 (diff) | |
| download | opencode-96b1d8f639991e896bc8d31afe64d6309bf3ccd2.tar.gz opencode-96b1d8f639991e896bc8d31afe64d6309bf3ccd2.zip | |
fix(app): stabilize todo dock e2e with composer probe (#17267)
Diffstat (limited to 'packages/app/src')
4 files changed, 163 insertions, 6 deletions
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: { > <div ref={(el) => setStore("body", el)}> <SessionTodoDock + sessionID={route.params.id} todos={props.state.todos()} title={language.t("session.todo.title")} collapseLabel={language.t("session.todo.collapse")} diff --git a/packages/app/src/pages/session/composer/session-composer-state.ts b/packages/app/src/pages/session/composer/session-composer-state.ts index 525766dcf..0884f4cc6 100644 --- a/packages/app/src/pages/session/composer/session-composer-state.ts +++ b/packages/app/src/pages/session/composer/session-composer-state.ts @@ -1,4 +1,4 @@ -import { createEffect, createMemo, on, onCleanup } from "solid-js" +import { createEffect, createMemo, on, onCleanup, onMount } from "solid-js" import { createStore } from "solid-js/store" import type { PermissionRequest, QuestionRequest, Todo } from "@opencode-ai/sdk/v2" import { useParams } from "@solidjs/router" @@ -8,6 +8,7 @@ import { useLanguage } from "@/context/language" import { usePermission } from "@/context/permission" import { useSDK } from "@/context/sdk" import { useSync } from "@/context/sync" +import { composerDriver, composerEnabled, composerEvent } from "@/testing/session-composer" import { sessionPermissionRequest, sessionQuestionRequest } from "./session-request-tree" export const todoState = (input: { @@ -47,7 +48,50 @@ export function createSessionComposerState(options?: { closeMs?: number | (() => 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 ( <DockTray data-component="session-todo-dock" diff --git a/packages/app/src/testing/session-composer.ts b/packages/app/src/testing/session-composer.ts new file mode 100644 index 000000000..01c809e4c --- /dev/null +++ b/packages/app/src/testing/session-composer.ts @@ -0,0 +1,84 @@ +import type { Todo } from "@opencode-ai/sdk/v2" + +export const composerEvent = "opencode:e2e:composer" + +export type ComposerDriverState = { + live?: boolean + todos?: Array<Pick<Todo, "content" | "status" | "priority">> +} + +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<string, ComposerState> + } + } +} + +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: [], + }) + }, + } +} |
