opencode / packages /app /src /pages /session /composer /session-composer-state.ts
SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
3e05655 verified
Raw History Blame Contribute Delete
6.17 kB
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<typeof todoState>) => 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<typeof createSessionComposerController>