mirror of
https://github.com/sst/opencode.git
synced 2025-12-23 10:11:41 +00:00
tui: add prompt history navigation with up/down arrows
This commit is contained in:
parent
5e5293d98c
commit
8f7ea1f1ae
4 changed files with 81 additions and 22 deletions
|
|
@ -1,7 +1,4 @@
|
|||
<<<<<<< HEAD
|
||||
preload = ["@opentui/solid/preload"]
|
||||
|
||||
=======
|
||||
[test]
|
||||
preload = ["./test/preload.ts"]
|
||||
>>>>>>> dev
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
3
packages/opencode/src/util/iife.ts
Normal file
3
packages/opencode/src/util/iife.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export function iife<T>(fn: () => T) {
|
||||
return fn()
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue