tui: add prompt history navigation with up/down arrows
Some checks are pending
format / format (push) Waiting to run
snapshot / publish (push) Waiting to run
test / test (push) Waiting to run

This commit is contained in:
Dax Raad 2025-09-27 06:04:12 -04:00
parent 5e5293d98c
commit 8f7ea1f1ae
4 changed files with 81 additions and 22 deletions

View file

@ -1,7 +1,4 @@
<<<<<<< HEAD
preload = ["@opentui/solid/preload"]
=======
[test]
preload = ["./test/preload.ts"]
>>>>>>> dev

View file

@ -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<FilePart, "id" | "messageID" | "sessionID">[]
}
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,

View file

@ -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,

View file

@ -0,0 +1,3 @@
export function iife<T>(fn: () => T) {
return fn()
}