import path from "path" import { onMount } from "solid-js" import { createStore, produce, unwrap } from "solid-js/store" import type { AgentPart, FilePart, TextPart } from "@opencode-ai/sdk/v2" import { createSimpleContext } from "../context/helper" import { useTuiPaths } from "../context/runtime" import { appendText, readText, writeText } from "../util/persistence" export type PromptInfo = { input: string mode?: "normal" | "shell" parts: ( | Omit | Omit | (Omit & { source?: { text: { start: number end: number value: string } } }) )[] } export const MAX_HISTORY_ENTRIES = 50 export function parsePromptHistory(text: string) { return text .split("\n") .filter(Boolean) .map((line) => { try { return JSON.parse(line) as PromptInfo } catch { return undefined } }) .filter((line): line is PromptInfo => line !== undefined) .slice(-MAX_HISTORY_ENTRIES) } export function isDuplicateEntry(previous: PromptInfo | undefined, next: PromptInfo): boolean { if (!previous) return false return JSON.stringify(previous) === JSON.stringify(next) } export const { use: usePromptHistory, provider: PromptHistoryProvider } = createSimpleContext({ name: "PromptHistory", init: () => { const paths = useTuiPaths() const historyPath = path.join(paths.state, "prompt-history.jsonl") onMount(async () => { const lines = parsePromptHistory(await readText(historyPath).catch(() => "")) setStore("history", lines) // Rewrite valid retained entries to self-heal corruption and enforce the limit. if (lines.length > 0) writeText(historyPath, lines.map((line) => JSON.stringify(line)).join("\n") + "\n").catch(() => {}) }) const [store, setStore] = createStore({ index: 0, history: [] as PromptInfo[], }) return { move(direction: 1 | -1, input: string) { if (!store.history.length) return undefined const current = store.history.at(store.index) if (!current) return undefined if (current.input !== input && input.length) return setStore( produce((draft) => { const next = store.index + direction if (Math.abs(next) > store.history.length) return if (next > 0) return draft.index = next }), ) if (store.index === 0) return { input: "", parts: [] } return store.history.at(store.index) }, append(item: PromptInfo) { const entry = structuredClone(unwrap(item)) if (isDuplicateEntry(store.history.at(-1), entry)) { setStore("index", 0) return } let trimmed = false setStore( produce((draft) => { draft.history.push(entry) if (draft.history.length > MAX_HISTORY_ENTRIES) { draft.history = draft.history.slice(-MAX_HISTORY_ENTRIES) trimmed = true } draft.index = 0 }), ) if (trimmed) { writeText(historyPath, store.history.map((line) => JSON.stringify(line)).join("\n") + "\n").catch(() => {}) return } appendText(historyPath, JSON.stringify(entry) + "\n").catch(() => {}) }, } }, })