import { createEffect, createMemo, on, onCleanup } from "solid-js" import { createStore } from "solid-js/store" import type { PermissionRequest, QuestionRequest, Todo } from "@opencode-ai/sdk/v2" import { useParams } from "@solidjs/router" import { showToast } from "@/utils/toast" import { useServerSync } from "@/context/server-sync" import { useLanguage } from "@/context/language" import { usePermission } from "@/context/permission" import { useSDK } from "@/context/sdk" import { useSync } from "@/context/sync" import { sessionPermissionRequest, sessionQuestionRequest } from "./session-request-tree" export const todoState = (input: { count: number done: boolean live: boolean }): "hide" | "clear" | "open" | "close" => { if (input.count === 0) return "hide" if (!input.live) return "clear" if (!input.done) return "open" return "close" } export const todoDockAtBoundary = (state: ReturnType) => state === "open" const idle = { type: "idle" as const } export function createSessionComposerController(options?: { closeMs?: number | (() => number) }) { const params = useParams() const sdk = useSDK() const sync = useSync() const serverSync = useServerSync() const language = useLanguage() const permission = usePermission() const questionRequest = createMemo((): QuestionRequest | undefined => { return sessionQuestionRequest(sync().data.session, sync().data.question, params.id) }) const permissionRequest = createMemo((): PermissionRequest | undefined => { return sessionPermissionRequest(sync().data.session, sync().data.permission, params.id, (item) => { return !permission.autoResponds(item, sdk().directory) }) }) const blocked = createMemo(() => { const id = params.id if (!id) return false return !!permissionRequest() || !!questionRequest() }) const todos = createMemo((): Todo[] => { const id = params.id if (!id) return [] return serverSync().session.data.todo[id] ?? [] }) const done = createMemo( () => todos().length > 0 && todos().every((todo) => todo.status === "completed" || todo.status === "cancelled"), ) const live = createMemo(() => sync().data.session_working(params.id ?? "") || blocked()) const [store, setStore] = createStore({ sessionID: params.id, responding: undefined as string | undefined, dock: todos().length > 0 && !done() && live(), closing: false, opening: false, }) const permissionResponding = createMemo(() => { const perm = permissionRequest() if (!perm) return false return store.responding === perm.id }) const decide = (response: "once" | "always" | "reject") => { const perm = permissionRequest() if (!perm) return if (store.responding === perm.id) return setStore("responding", perm.id) sdk() .api.permission.reply({ sessionID: perm.sessionID, requestID: perm.id, reply: response }) .catch((err: unknown) => { const description = err instanceof Error ? err.message : String(err) showToast({ title: language.t("common.requestFailed"), description }) }) .finally(() => { setStore("responding", (id) => (id === perm.id ? undefined : id)) }) } let timer: number | undefined let raf: number | undefined const closeMs = () => { const value = options?.closeMs if (typeof value === "function") return Math.max(0, value()) if (typeof value === "number") return Math.max(0, value) return 400 } const scheduleClose = () => { if (timer) window.clearTimeout(timer) timer = window.setTimeout(() => { setStore({ dock: false, closing: false }) timer = undefined }, closeMs()) } // Keep stale turn todos from reopening if the model never clears them. const clear = () => { const id = params.id if (!id) return sync().set("todo", id, []) } createEffect( on( () => [params.id, todos().length, done(), live()] as const, ([id, count, complete, active], previous) => { if (raf) cancelAnimationFrame(raf) raf = undefined const next = todoState({ count, done: complete, live: active, }) if (!previous || previous[0] !== id) { if (timer) window.clearTimeout(timer) timer = undefined setStore({ sessionID: id, dock: todoDockAtBoundary(next), closing: false, opening: false }) if (next === "clear") clear() return } if (next === "hide") { if (timer) window.clearTimeout(timer) timer = undefined setStore({ dock: false, closing: false, opening: false }) return } if (next === "clear") { if (timer) window.clearTimeout(timer) timer = undefined clear() return } if (next === "open") { if (timer) window.clearTimeout(timer) timer = undefined const hidden = !store.dock || store.closing setStore({ dock: true, closing: false }) if (hidden) { setStore("opening", true) raf = requestAnimationFrame(() => { setStore("opening", false) raf = undefined }) return } setStore("opening", false) return } setStore({ dock: true, opening: false, closing: true }) if (!timer) scheduleClose() }, ), ) onCleanup(() => { if (!timer) return window.clearTimeout(timer) }) onCleanup(() => { if (!raf) return cancelAnimationFrame(raf) }) return { blocked, questionRequest, permissionRequest, permissionResponding, decide, todos, dock: () => store.sessionID === params.id ? store.dock : todoDockAtBoundary(todoState({ count: todos().length, done: done(), live: live() })), closing: () => store.sessionID === params.id && store.closing, opening: () => store.sessionID === params.id && store.opening, } } export type SessionComposerController = ReturnType