253 lines
8.4 KiB
TypeScript
253 lines
8.4 KiB
TypeScript
import { Show, createEffect, createMemo, onCleanup } from "solid-js"
|
|
import { createStore } from "solid-js/store"
|
|
import { useSpring } from "@opencode-ai/ui/motion-spring"
|
|
import { PromptInput } from "@/components/prompt-input"
|
|
import { useLanguage } from "@/context/language"
|
|
import { usePrompt } from "@/context/prompt"
|
|
import { getSessionHandoff, setSessionHandoff } from "@/pages/session/handoff"
|
|
import { useSessionKey } from "@/pages/session/session-layout"
|
|
import { SessionPermissionDock } from "@/pages/session/composer/session-permission-dock"
|
|
import { SessionQuestionDock } from "@/pages/session/composer/session-question-dock"
|
|
import { SessionFollowupDock } from "@/pages/session/composer/session-followup-dock"
|
|
import { SessionRevertDock } from "@/pages/session/composer/session-revert-dock"
|
|
import type { SessionComposerState } from "@/pages/session/composer/session-composer-state"
|
|
import { SessionTodoDock } from "@/pages/session/composer/session-todo-dock"
|
|
import type { FollowupDraft } from "@/components/prompt-input/submit"
|
|
import { createResizeObserver } from "@solid-primitives/resize-observer"
|
|
|
|
export function SessionComposerRegion(props: {
|
|
state: SessionComposerState
|
|
ready: boolean
|
|
centered: boolean
|
|
inputRef: (el: HTMLDivElement) => void
|
|
newSessionWorktree: string
|
|
onNewSessionWorktreeReset: () => void
|
|
onSubmit: () => void
|
|
onResponseSubmit: () => void
|
|
followup?: {
|
|
queue: () => boolean
|
|
items: { id: string; text: string }[]
|
|
sending?: string
|
|
edit?: { id: string; prompt: FollowupDraft["prompt"]; context: FollowupDraft["context"] }
|
|
onQueue: (draft: FollowupDraft) => void
|
|
onAbort: () => void
|
|
onSend: (id: string) => void
|
|
onEdit: (id: string) => void
|
|
onEditLoaded: () => void
|
|
}
|
|
revert?: {
|
|
items: { id: string; text: string }[]
|
|
restoring?: string
|
|
disabled?: boolean
|
|
onRestore: (id: string) => void
|
|
}
|
|
setPromptDockRef: (el: HTMLDivElement) => void
|
|
}) {
|
|
const prompt = usePrompt()
|
|
const language = useLanguage()
|
|
const route = useSessionKey()
|
|
|
|
const handoffPrompt = createMemo(() => getSessionHandoff(route.sessionKey())?.prompt)
|
|
|
|
const previewPrompt = () =>
|
|
prompt
|
|
.current()
|
|
.map((part) => {
|
|
if (part.type === "file") return `[file:${part.path}]`
|
|
if (part.type === "agent") return `@${part.name}`
|
|
if (part.type === "image") return `[image:${part.filename}]`
|
|
return part.content
|
|
})
|
|
.join("")
|
|
.trim()
|
|
|
|
createEffect(() => {
|
|
if (!prompt.ready()) return
|
|
setSessionHandoff(route.sessionKey(), { prompt: previewPrompt() })
|
|
})
|
|
|
|
const [store, setStore] = createStore({
|
|
ready: false,
|
|
height: 320,
|
|
body: undefined as HTMLDivElement | undefined,
|
|
})
|
|
let timer: number | undefined
|
|
let frame: number | undefined
|
|
|
|
const clear = () => {
|
|
if (timer !== undefined) {
|
|
window.clearTimeout(timer)
|
|
timer = undefined
|
|
}
|
|
if (frame !== undefined) {
|
|
cancelAnimationFrame(frame)
|
|
frame = undefined
|
|
}
|
|
}
|
|
|
|
createEffect(() => {
|
|
route.sessionKey()
|
|
const ready = props.ready
|
|
const delay = 140
|
|
|
|
clear()
|
|
setStore("ready", false)
|
|
if (!ready) return
|
|
|
|
frame = requestAnimationFrame(() => {
|
|
frame = undefined
|
|
timer = window.setTimeout(() => {
|
|
setStore("ready", true)
|
|
timer = undefined
|
|
}, delay)
|
|
})
|
|
})
|
|
|
|
onCleanup(clear)
|
|
|
|
const open = createMemo(() => store.ready && props.state.dock() && !props.state.closing())
|
|
const progress = useSpring(() => (open() ? 1 : 0), { visualDuration: 0.3, bounce: 0 })
|
|
const value = createMemo(() => Math.max(0, Math.min(1, progress())))
|
|
const dock = createMemo(() => (store.ready && props.state.dock()) || value() > 0.001)
|
|
const rolled = createMemo(() => (props.revert?.items.length ? props.revert : undefined))
|
|
const lift = createMemo(() => (rolled() ? 18 : 36 * value()))
|
|
const full = createMemo(() => Math.max(78, store.height))
|
|
|
|
createEffect(() => {
|
|
const el = store.body
|
|
if (!el) return
|
|
const update = () => setStore("height", el.getBoundingClientRect().height)
|
|
createResizeObserver(store.body, update)
|
|
update()
|
|
})
|
|
|
|
return (
|
|
<div
|
|
ref={props.setPromptDockRef}
|
|
data-component="session-prompt-dock"
|
|
class="shrink-0 w-full pb-3 flex flex-col justify-center items-center bg-background-stronger pointer-events-none"
|
|
>
|
|
<div
|
|
classList={{
|
|
"w-full px-3 pointer-events-auto": true,
|
|
"md:max-w-200 md:mx-auto 2xl:max-w-[1000px]": props.centered,
|
|
}}
|
|
>
|
|
<Show when={props.state.questionRequest()} keyed>
|
|
{(request) => (
|
|
<div>
|
|
<SessionQuestionDock request={request} onSubmit={props.onResponseSubmit} />
|
|
</div>
|
|
)}
|
|
</Show>
|
|
|
|
<Show when={props.state.permissionRequest()} keyed>
|
|
{(request) => (
|
|
<div>
|
|
<SessionPermissionDock
|
|
request={request}
|
|
responding={props.state.permissionResponding()}
|
|
onDecide={(response) => {
|
|
props.onResponseSubmit()
|
|
props.state.decide(response)
|
|
}}
|
|
/>
|
|
</div>
|
|
)}
|
|
</Show>
|
|
|
|
<Show when={!props.state.blocked()}>
|
|
<Show
|
|
when={prompt.ready()}
|
|
fallback={
|
|
<>
|
|
<Show when={rolled()} keyed>
|
|
{(revert) => (
|
|
<div class="pb-2">
|
|
<SessionRevertDock
|
|
items={revert.items}
|
|
restoring={revert.restoring}
|
|
disabled={revert.disabled}
|
|
onRestore={revert.onRestore}
|
|
/>
|
|
</div>
|
|
)}
|
|
</Show>
|
|
<div class="w-full min-h-32 md:min-h-40 rounded-md border border-border-weak-base bg-background-base/50 px-4 py-3 text-text-weak whitespace-pre-wrap pointer-events-none">
|
|
{handoffPrompt() || language.t("prompt.loading")}
|
|
</div>
|
|
</>
|
|
}
|
|
>
|
|
<Show when={dock()}>
|
|
<div
|
|
classList={{
|
|
"overflow-hidden": true,
|
|
"pointer-events-none": value() < 0.98,
|
|
}}
|
|
style={{
|
|
"max-height": `${full() * value()}px`,
|
|
}}
|
|
>
|
|
<div ref={(el) => setStore("body", el)}>
|
|
<SessionTodoDock
|
|
sessionID={route.params.id}
|
|
todos={props.state.todos()}
|
|
collapseLabel={language.t("session.todo.collapse")}
|
|
expandLabel={language.t("session.todo.expand")}
|
|
dockProgress={value()}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</Show>
|
|
<Show when={rolled()} keyed>
|
|
{(revert) => (
|
|
<div
|
|
style={{
|
|
"margin-top": `${-36 * value()}px`,
|
|
}}
|
|
>
|
|
<SessionRevertDock
|
|
items={revert.items}
|
|
restoring={revert.restoring}
|
|
disabled={revert.disabled}
|
|
onRestore={revert.onRestore}
|
|
/>
|
|
</div>
|
|
)}
|
|
</Show>
|
|
<div
|
|
classList={{
|
|
"relative z-10": true,
|
|
}}
|
|
style={{
|
|
"margin-top": `${-lift()}px`,
|
|
}}
|
|
>
|
|
<Show when={props.followup?.items.length}>
|
|
<SessionFollowupDock
|
|
items={props.followup!.items}
|
|
sending={props.followup!.sending}
|
|
onSend={props.followup!.onSend}
|
|
onEdit={props.followup!.onEdit}
|
|
/>
|
|
</Show>
|
|
<PromptInput
|
|
ref={props.inputRef}
|
|
newSessionWorktree={props.newSessionWorktree}
|
|
onNewSessionWorktreeReset={props.onNewSessionWorktreeReset}
|
|
edit={props.followup?.edit}
|
|
onEditLoaded={props.followup?.onEditLoaded}
|
|
shouldQueue={props.followup?.queue}
|
|
onQueue={props.followup?.onQueue}
|
|
onAbort={props.followup?.onAbort}
|
|
onSubmit={props.onSubmit}
|
|
/>
|
|
</div>
|
|
</Show>
|
|
</Show>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|