159 lines
3.8 KiB
TypeScript
159 lines
3.8 KiB
TypeScript
// Pure state machine for the prompt input.
|
|
//
|
|
// Handles history ring navigation and prompt text helpers. All functions are
|
|
// pure -- they take state in and return new state out, with no side effects.
|
|
//
|
|
// The history ring (PromptHistoryState) stores past prompts and tracks
|
|
// the current browse position. When the user arrows up at cursor offset 0,
|
|
// the current draft is saved and history begins. Arrowing past the end
|
|
// restores the draft.
|
|
export { displayCharAt, displaySlice, mentionTriggerIndex } from "../prompt/display"
|
|
import { stringWidth } from "../util/string-width"
|
|
import type { RunPrompt } from "./types"
|
|
|
|
const HISTORY_LIMIT = 200
|
|
|
|
export type PromptHistoryState = {
|
|
items: RunPrompt[]
|
|
index: number | null
|
|
draft: string
|
|
}
|
|
|
|
export type PromptMove = {
|
|
state: PromptHistoryState
|
|
text?: string
|
|
cursor?: number
|
|
apply: boolean
|
|
}
|
|
|
|
export function promptCopy(prompt: RunPrompt): RunPrompt {
|
|
return {
|
|
text: prompt.text,
|
|
parts: structuredClone(prompt.parts),
|
|
...(prompt.mode ? { mode: prompt.mode } : {}),
|
|
...(prompt.command ? { command: prompt.command } : {}),
|
|
}
|
|
}
|
|
|
|
export function promptSame(a: RunPrompt, b: RunPrompt): boolean {
|
|
return (
|
|
a.mode === b.mode &&
|
|
a.text === b.text &&
|
|
JSON.stringify(a.parts) === JSON.stringify(b.parts) &&
|
|
JSON.stringify(a.command) === JSON.stringify(b.command)
|
|
)
|
|
}
|
|
|
|
export function isExitCommand(input: string): boolean {
|
|
const text = input.trim().toLowerCase()
|
|
return text === "/exit" || text === "/quit" || text === ":q"
|
|
}
|
|
|
|
export function isNewCommand(input: string): boolean {
|
|
return input.trim().toLowerCase() === "/new"
|
|
}
|
|
|
|
export function isCompactCommand(input: string): boolean {
|
|
const text = input.trim().toLowerCase()
|
|
return text === "/compact" || text === "/summarize"
|
|
}
|
|
|
|
export function createPromptHistory(items?: RunPrompt[]): PromptHistoryState {
|
|
const list = (items ?? []).filter((item) => item.text.trim().length > 0).map(promptCopy)
|
|
const next: RunPrompt[] = []
|
|
for (const item of list) {
|
|
if (next.length > 0 && promptSame(next[next.length - 1], item)) {
|
|
continue
|
|
}
|
|
|
|
next.push(item)
|
|
}
|
|
|
|
return {
|
|
items: next.slice(-HISTORY_LIMIT),
|
|
index: null,
|
|
draft: "",
|
|
}
|
|
}
|
|
|
|
export function pushPromptHistory(state: PromptHistoryState, prompt: RunPrompt): PromptHistoryState {
|
|
if (!prompt.text.trim()) {
|
|
return state
|
|
}
|
|
|
|
const next = promptCopy(prompt)
|
|
if (state.items[state.items.length - 1] && promptSame(state.items[state.items.length - 1], next)) {
|
|
return {
|
|
...state,
|
|
index: null,
|
|
draft: "",
|
|
}
|
|
}
|
|
|
|
const items = [...state.items, next].slice(-HISTORY_LIMIT)
|
|
return {
|
|
...state,
|
|
items,
|
|
index: null,
|
|
draft: "",
|
|
}
|
|
}
|
|
|
|
export function movePromptHistory(state: PromptHistoryState, dir: -1 | 1, text: string, cursor: number): PromptMove {
|
|
if (state.items.length === 0) {
|
|
return { state, apply: false }
|
|
}
|
|
|
|
if (dir === -1 && cursor !== 0) {
|
|
return { state, apply: false }
|
|
}
|
|
|
|
if (dir === 1 && cursor !== stringWidth(text)) {
|
|
return { state, apply: false }
|
|
}
|
|
|
|
if (state.index === null) {
|
|
if (dir === 1) {
|
|
return { state, apply: false }
|
|
}
|
|
|
|
const idx = state.items.length - 1
|
|
return {
|
|
state: {
|
|
...state,
|
|
index: idx,
|
|
draft: text,
|
|
},
|
|
text: state.items[idx].text,
|
|
cursor: 0,
|
|
apply: true,
|
|
}
|
|
}
|
|
|
|
const idx = state.index + dir
|
|
if (idx < 0) {
|
|
return { state, apply: false }
|
|
}
|
|
|
|
if (idx >= state.items.length) {
|
|
return {
|
|
state: {
|
|
...state,
|
|
index: null,
|
|
},
|
|
text: state.draft,
|
|
cursor: stringWidth(state.draft),
|
|
apply: true,
|
|
}
|
|
}
|
|
|
|
return {
|
|
state: {
|
|
...state,
|
|
index: idx,
|
|
},
|
|
text: state.items[idx].text,
|
|
cursor: dir === -1 ? 0 : stringWidth(state.items[idx].text),
|
|
apply: true,
|
|
}
|
|
}
|