diff --git a/packages/opencode/bunfig.toml b/packages/opencode/bunfig.toml index 9f1e8d4cd..d7b987cbb 100644 --- a/packages/opencode/bunfig.toml +++ b/packages/opencode/bunfig.toml @@ -1,7 +1,4 @@ -<<<<<<< HEAD preload = ["@opentui/solid/preload"] -======= [test] preload = ["./test/preload.ts"] ->>>>>>> dev diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt.tsx index 4dff5cbb5..7e4782d62 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt.tsx @@ -1,6 +1,6 @@ import { InputRenderable, TextAttributes, BoxRenderable, type ParsedKey } from "@opentui/core" import { createEffect, createMemo, createResource, For, Match, onMount, Show, Switch } from "solid-js" -import { firstBy } from "remeda" +import { clone, firstBy } from "remeda" import { useLocal } from "@tui/context/local" import { Theme } from "@tui/context/theme" import { useDialog } from "@tui/ui/dialog" @@ -15,6 +15,10 @@ import fuzzysort from "fuzzysort" import { useCommandDialog } from "@tui/component/dialog-command" import { useKeybind } from "@tui/context/keybind" import { Clipboard } from "@/util/clipboard" +import path from "path" +import { Global } from "@/global" +import { appendFile } from "fs/promises" +import { iife } from "@/util/iife" export type PromptProps = { sessionID?: string @@ -26,6 +30,49 @@ type Prompt = { parts: Omit[] } +const History = iife(async () => { + const historyFile = Bun.file(path.join(Global.Path.state, "prompt-history.jsonl")) + const text = await historyFile.text().catch(() => "") + const lines = text + .split("\n") + .filter(Boolean) + .map((line) => JSON.parse(line)) + + const [store, setStore] = createStore({ + index: 0, + history: lines as Prompt[], + }) + + return { + move(direction: 1 | -1) { + 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: Prompt) { + item = clone(item) + appendFile(historyFile.name!, JSON.stringify(item) + "\n") + setStore( + produce((draft) => { + draft.history.push(item) + draft.index = 0 + }), + ) + }, + } +}) + export function Prompt(props: PromptProps) { let input: InputRenderable let anchor: BoxRenderable @@ -104,15 +151,24 @@ export function Prompt(props: PromptProps) { autocomplete.onInput(value) }} value={store.input} - onKeyDown={(e) => { + onKeyDown={async (e) => { autocomplete.onKeyDown(e) - if (e.name === "escape" && props.sessionID && !autocomplete.visible) { - sdk.session.abort({ - path: { - id: props.sessionID, - }, - }) - return + if (!autocomplete.visible) { + if (e.name === "up" || e.name === "down") { + const direction = e.name === "up" ? -1 : 1 + const item = await History.then((h) => h.move(direction)) + setStore(item) + input.cursorPosition = item.input.length + return + } + if (e.name === "escape" && props.sessionID) { + sdk.session.abort({ + path: { + id: props.sessionID, + }, + }) + return + } } const old = input.cursorPosition setTimeout(() => { @@ -169,10 +225,13 @@ export function Prompt(props: PromptProps) { return } const parts = store.parts - setStore({ - input: "", - parts: [], - }) + await History.then((h) => h.append(store)) + setStore( + produce((draft) => { + draft.input = "" + draft.parts = [] + }), + ) sdk.session.prompt({ path: { id: sessionID, diff --git a/packages/opencode/src/cli/cmd/tui/context/local.tsx b/packages/opencode/src/cli/cmd/tui/context/local.tsx index f3dc2135d..0b95d514b 100644 --- a/packages/opencode/src/cli/cmd/tui/context/local.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/local.tsx @@ -5,13 +5,13 @@ import { Theme } from "@tui/context/theme" import { uniqueBy } from "remeda" import path from "path" import { Global } from "@/global" +import { iife } from "@/util/iife" function init() { const sync = useSync() - const agents = createMemo(() => sync.data.agent.filter((x) => x.mode !== "subagent")) - - const agent = (() => { + const agent = iife(() => { + const agents = createMemo(() => sync.data.agent.filter((x) => x.mode !== "subagent")) const [store, setStore] = createStore<{ current: string }>({ @@ -45,9 +45,9 @@ function init() { return colors[index % colors.length] }, } - })() + }) - const model = (() => { + const model = iife(() => { const [store, setStore] = createStore<{ model: Record< string, @@ -123,7 +123,7 @@ function init() { }) }, } - })() + }) const result = { model, diff --git a/packages/opencode/src/util/iife.ts b/packages/opencode/src/util/iife.ts new file mode 100644 index 000000000..ca9ae6c10 --- /dev/null +++ b/packages/opencode/src/util/iife.ts @@ -0,0 +1,3 @@ +export function iife(fn: () => T) { + return fn() +}