mirror of
https://gitea.toothfairyai.com/ToothFairyAI/tf_code.git
synced 2026-04-08 17:59:09 +00:00
fix(app): stabilize todo dock e2e with composer probe (#17267)
This commit is contained in:
@@ -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")}
|
||||
|
||||
@@ -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, [])
|
||||
|
||||
@@ -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"
|
||||
|
||||
84
packages/app/src/testing/session-composer.ts
Normal file
84
packages/app/src/testing/session-composer.ts
Normal file
@@ -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: [],
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user