Co-authored-by: Brendan Allan <git@brendonovich.dev> Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com>
159 lines
5.0 KiB
TypeScript
159 lines
5.0 KiB
TypeScript
import { base64Encode } from "@opencode-ai/core/util/encode"
|
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
|
import { useParams, useSearchParams } from "@solidjs/router"
|
|
import { createMemo, createRoot, getOwner, onCleanup } from "solid-js"
|
|
import { requireServerKey } from "@/utils/session-route"
|
|
import { ServerConnection } from "./server"
|
|
import { useServerSDK } from "./server-sdk"
|
|
import { useSettings } from "./settings"
|
|
import { useSDK } from "./sdk"
|
|
import { useTabs, type Tab } from "./tabs"
|
|
import {
|
|
createPromptReady,
|
|
createPromptSession,
|
|
type ContextItem,
|
|
type FileContextItem,
|
|
type Prompt,
|
|
type PromptModel,
|
|
type PromptScope,
|
|
type PromptSession,
|
|
} from "./prompt-state"
|
|
|
|
export {
|
|
createPromptReady,
|
|
createPromptSession,
|
|
createPromptState,
|
|
DEFAULT_PROMPT,
|
|
isCommentItem,
|
|
isPromptEqual,
|
|
} from "./prompt-state"
|
|
export type {
|
|
AgentPart,
|
|
ContentPart,
|
|
ContextItem,
|
|
FileAttachmentPart,
|
|
FileContextItem,
|
|
ImageAttachmentPart,
|
|
Prompt,
|
|
PromptModel,
|
|
PromptScope,
|
|
PromptSession,
|
|
TextPart,
|
|
} from "./prompt-state"
|
|
|
|
const WORKSPACE_KEY = "__workspace__"
|
|
const MAX_PROMPT_SESSIONS = 20
|
|
|
|
export function selectPromptTab(tabs: Tab[], scope: PromptScope, server: ServerConnection.Key) {
|
|
if ("draftID" in scope) return tabs.find((tab) => tab.type === "draft" && tab.draftID === scope.draftID)
|
|
if (!scope.id) return
|
|
return (
|
|
tabs.find((tab) => tab.type === "session" && tab.server === server && tab.sessionId === scope.id) ??
|
|
({ type: "session", server, sessionId: scope.id } satisfies Tab)
|
|
)
|
|
}
|
|
|
|
function scopeKey(scope: PromptScope) {
|
|
if ("draftID" in scope) return `draft:${scope.draftID}`
|
|
return `${scope.dir}:${scope.id ?? WORKSPACE_KEY}`
|
|
}
|
|
|
|
type PromptCacheEntry = {
|
|
value: PromptSession
|
|
dispose: VoidFunction
|
|
}
|
|
|
|
export const createTabPromptState = (
|
|
tabs: ReturnType<typeof useTabs>,
|
|
tab: Tab,
|
|
...args: Parameters<typeof createPromptSession>
|
|
) => tabs.state(tab, "prompt", () => createPromptSession(...args))
|
|
|
|
export const { use: usePrompt, provider: PromptProvider } = createSimpleContext({
|
|
name: "Prompt",
|
|
gate: false,
|
|
init: () => {
|
|
const params = useParams<{ serverKey?: string; id?: string }>()
|
|
const sdk = useSDK()
|
|
const [search] = useSearchParams<{ draftId?: string }>()
|
|
const serverSDK = useServerSDK()
|
|
const tabs = useTabs()
|
|
const settings = useSettings()
|
|
const cache = new Map<string, PromptCacheEntry>()
|
|
|
|
const disposeAll = () => {
|
|
for (const entry of cache.values()) entry.dispose()
|
|
cache.clear()
|
|
}
|
|
|
|
onCleanup(disposeAll)
|
|
|
|
const prune = () => {
|
|
while (cache.size > MAX_PROMPT_SESSIONS) {
|
|
const first = cache.keys().next().value
|
|
if (!first) return
|
|
const entry = cache.get(first)
|
|
entry?.dispose()
|
|
cache.delete(first)
|
|
}
|
|
}
|
|
|
|
const owner = getOwner()
|
|
const serverKey = () =>
|
|
params.serverKey ? requireServerKey(params.serverKey) : ServerConnection.key(serverSDK().server)
|
|
const scope = (): PromptScope =>
|
|
search.draftId ? { draftID: search.draftId } : { dir: base64Encode(sdk().directory), id: params.id }
|
|
const load = (scope: PromptScope) => {
|
|
const current = settings.general.newLayoutDesigns() ? selectPromptTab(tabs.store, scope, serverKey()) : undefined
|
|
if (current) return createTabPromptState(tabs, current, serverSDK().scope, scope)
|
|
|
|
const key = scopeKey(scope)
|
|
const existing = cache.get(key)
|
|
if (existing) {
|
|
cache.delete(key)
|
|
cache.set(key, existing)
|
|
return existing.value
|
|
}
|
|
|
|
const entry = createRoot(
|
|
(dispose) => ({
|
|
value: createPromptSession(serverSDK().scope, scope),
|
|
dispose,
|
|
}),
|
|
owner,
|
|
)
|
|
|
|
cache.set(key, entry)
|
|
prune()
|
|
return entry.value
|
|
}
|
|
|
|
const session = createMemo(() => load(scope()))
|
|
const pick = (scope?: PromptScope) => (scope ? load(scope) : session())
|
|
const ready = createPromptReady(session)
|
|
|
|
return {
|
|
ready,
|
|
capture: (scope?: PromptScope) => pick(scope).capture(),
|
|
current: () => session().current(),
|
|
cursor: () => session().cursor(),
|
|
dirty: () => session().dirty(),
|
|
model: {
|
|
current: () => session().model.current(),
|
|
set: (model: PromptModel | undefined) => session().model.set(model),
|
|
},
|
|
context: {
|
|
items: () => session().context.items(),
|
|
add: (item: ContextItem) => session().context.add(item),
|
|
remove: (key: string) => session().context.remove(key),
|
|
removeComment: (path: string, commentID: string) => session().context.removeComment(path, commentID),
|
|
updateComment: (path: string, commentID: string, next: Partial<FileContextItem> & { comment?: string }) =>
|
|
session().context.updateComment(path, commentID, next),
|
|
replaceComments: (items: FileContextItem[]) => session().context.replaceComments(items),
|
|
},
|
|
set: (prompt: Prompt, cursorPosition?: number, scope?: PromptScope) => pick(scope).set(prompt, cursorPosition),
|
|
reset: (scope?: PromptScope) => pick(scope).reset(),
|
|
}
|
|
},
|
|
})
|