feat(tui): use canonical prompt attachments
This commit is contained in:
parent
91f1815732
commit
c13f06c30c
24 changed files with 611 additions and 466 deletions
|
|
@ -1,29 +1,33 @@
|
|||
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 type { SessionPromptInput } from "@opencode-ai/client/promise"
|
||||
import type { Types } from "effect"
|
||||
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<FilePart, "id" | "messageID" | "sessionID">
|
||||
| Omit<AgentPart, "id" | "messageID" | "sessionID">
|
||||
| (Omit<TextPart, "id" | "messageID" | "sessionID"> & {
|
||||
source?: {
|
||||
text: {
|
||||
start: number
|
||||
end: number
|
||||
value: string
|
||||
}
|
||||
}
|
||||
})
|
||||
)[]
|
||||
export type PastedText = {
|
||||
text: string
|
||||
source: {
|
||||
start: number
|
||||
end: number
|
||||
text: string
|
||||
}
|
||||
}
|
||||
|
||||
export type PromptInfo = Types.DeepMutable<SessionPromptInput["prompt"]> & {
|
||||
pasted: PastedText[]
|
||||
mode?: "normal" | "shell"
|
||||
}
|
||||
|
||||
export type PromptPartRef = {
|
||||
type: "file" | "agent" | "pasted"
|
||||
index: number
|
||||
}
|
||||
|
||||
export const emptyPrompt = (): PromptInfo => ({ text: "", files: [], agents: [], pasted: [] })
|
||||
|
||||
export const MAX_HISTORY_ENTRIES = 50
|
||||
|
||||
export function parsePromptHistory(text: string) {
|
||||
|
|
@ -32,7 +36,7 @@ export function parsePromptHistory(text: string) {
|
|||
.filter(Boolean)
|
||||
.map((line) => {
|
||||
try {
|
||||
return JSON.parse(line) as PromptInfo
|
||||
return parsePromptInfo(JSON.parse(line))
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
|
|
@ -46,6 +50,13 @@ export function isDuplicateEntry(previous: PromptInfo | undefined, next: PromptI
|
|||
return JSON.stringify(previous) === JSON.stringify(next)
|
||||
}
|
||||
|
||||
export function parsePromptInfo(value: unknown): PromptInfo | undefined {
|
||||
if (!value || typeof value !== "object") return
|
||||
const input = value as Record<string, unknown>
|
||||
if (typeof input.text !== "string" || !Array.isArray(input.pasted)) return
|
||||
return input as PromptInfo
|
||||
}
|
||||
|
||||
export const { use: usePromptHistory, provider: PromptHistoryProvider } = createSimpleContext({
|
||||
name: "PromptHistory",
|
||||
init: () => {
|
||||
|
|
@ -70,7 +81,7 @@ export const { use: usePromptHistory, provider: PromptHistoryProvider } = create
|
|||
if (!store.history.length) return undefined
|
||||
const current = store.history.at(store.index)
|
||||
if (!current) return undefined
|
||||
if (current.input !== input && input.length) return
|
||||
if (current.text !== input && input.length) return
|
||||
setStore(
|
||||
produce((draft) => {
|
||||
const next = store.index + direction
|
||||
|
|
@ -79,7 +90,7 @@ export const { use: usePromptHistory, provider: PromptHistoryProvider } = create
|
|||
draft.index = next
|
||||
}),
|
||||
)
|
||||
if (store.index === 0) return { input: "", parts: [] }
|
||||
if (store.index === 0) return emptyPrompt()
|
||||
return store.history.at(store.index)
|
||||
},
|
||||
append(item: PromptInfo) {
|
||||
|
|
|
|||
|
|
@ -1,24 +1,10 @@
|
|||
import { displaySlice } from "./display"
|
||||
|
||||
export function stripPromptPartIDs<Part extends { id: string; messageID: string; sessionID: string }>(part: Part) {
|
||||
const { id: _id, messageID: _messageID, sessionID: _sessionID, ...rest } = part
|
||||
return rest
|
||||
}
|
||||
|
||||
export function expandPastedTextPlaceholders(text: string, parts: readonly unknown[]) {
|
||||
return parts.reduce<string>((result, part) => {
|
||||
if (!isPastedTextPart(part)) return result
|
||||
return result.replace(part.source.text.value, part.text)
|
||||
}, text)
|
||||
}
|
||||
|
||||
function isPastedTextPart(part: unknown): part is { type: "text"; text: string; source: { text: { value: string } } } {
|
||||
if (!part || typeof part !== "object" || !("type" in part) || part.type !== "text") return false
|
||||
if (!("text" in part) || typeof part.text !== "string" || !("source" in part)) return false
|
||||
const source = part.source
|
||||
if (!source || typeof source !== "object" || !("text" in source)) return false
|
||||
const text = source.text
|
||||
return Boolean(text && typeof text === "object" && "value" in text && typeof text.value === "string")
|
||||
export function expandPastedTextPlaceholders(
|
||||
text: string,
|
||||
pasted: readonly { text: string; source: { text: string } }[],
|
||||
) {
|
||||
return pasted.reduce((result, part) => result.replace(part.source.text, part.text), text)
|
||||
}
|
||||
|
||||
export function expandTrackedPastedText(text: string, ranges: { start: number; end: number; text: string }[]) {
|
||||
|
|
|
|||
|
|
@ -4,11 +4,10 @@ import { createStore, produce, unwrap } from "solid-js/store"
|
|||
import { createSimpleContext } from "../context/helper"
|
||||
import { useTuiPaths } from "../context/runtime"
|
||||
import { appendText, readText, writeText } from "../util/persistence"
|
||||
import type { PromptInfo } from "./history"
|
||||
import { parsePromptInfo, type PromptInfo } from "./history"
|
||||
|
||||
export type StashEntry = {
|
||||
input: string
|
||||
parts: PromptInfo["parts"]
|
||||
prompt: PromptInfo
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
|
|
@ -20,7 +19,12 @@ export function parsePromptStash(text: string) {
|
|||
.filter(Boolean)
|
||||
.map((line) => {
|
||||
try {
|
||||
return JSON.parse(line) as StashEntry
|
||||
const value = JSON.parse(line) as unknown
|
||||
if (!value || typeof value !== "object") return
|
||||
const entry = value as Record<string, unknown>
|
||||
const prompt = parsePromptInfo(entry.prompt)
|
||||
if (!prompt || typeof entry.timestamp !== "number") return
|
||||
return { prompt, timestamp: entry.timestamp }
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue