Buckets:
| import type { Prompt } from "@/context/prompt" | |
| import type { SelectedLineRange } from "@/context/file" | |
| const DEFAULT_PROMPT: Prompt = [{ type: "text", content: "", start: 0, end: 0 }] | |
| export const MAX_HISTORY = 100 | |
| export type PromptHistoryComment = { | |
| id: string | |
| path: string | |
| selection: SelectedLineRange | |
| comment: string | |
| time: number | |
| origin?: "review" | "file" | |
| preview?: string | |
| } | |
| export type PromptHistoryEntry = { | |
| prompt: Prompt | |
| comments: PromptHistoryComment[] | |
| } | |
| export type PromptHistoryStoredEntry = Prompt | PromptHistoryEntry | |
| export function canNavigateHistoryAtCursor(direction: "up" | "down", text: string, cursor: number, inHistory = false) { | |
| const position = Math.max(0, Math.min(cursor, text.length)) | |
| const atStart = position === 0 | |
| const atEnd = position === text.length | |
| if (inHistory) return atStart || atEnd | |
| if (direction === "up") return position === 0 && text.length === 0 | |
| return position === text.length | |
| } | |
| export function clonePromptParts(prompt: Prompt): Prompt { | |
| return prompt.map((part) => { | |
| if (part.type === "text") return { ...part } | |
| if (part.type === "image") return { ...part } | |
| if (part.type === "agent") return { ...part } | |
| return { | |
| ...part, | |
| selection: part.selection ? { ...part.selection } : undefined, | |
| } | |
| }) | |
| } | |
| function cloneSelection(selection: SelectedLineRange): SelectedLineRange { | |
| return { | |
| start: selection.start, | |
| end: selection.end, | |
| ...(selection.side ? { side: selection.side } : {}), | |
| ...(selection.endSide ? { endSide: selection.endSide } : {}), | |
| } | |
| } | |
| export function clonePromptHistoryComments(comments: PromptHistoryComment[]) { | |
| return comments.map((comment) => ({ | |
| ...comment, | |
| selection: cloneSelection(comment.selection), | |
| })) | |
| } | |
| export function normalizePromptHistoryEntry(entry: PromptHistoryStoredEntry): PromptHistoryEntry { | |
| if (Array.isArray(entry)) { | |
| return { | |
| prompt: clonePromptParts(entry), | |
| comments: [], | |
| } | |
| } | |
| return { | |
| prompt: clonePromptParts(entry.prompt), | |
| comments: clonePromptHistoryComments(entry.comments), | |
| } | |
| } | |
| export function promptLength(prompt: Prompt) { | |
| return prompt.reduce((len, part) => len + ("content" in part ? part.content.length : 0), 0) | |
| } | |
| export function prependHistoryEntry( | |
| entries: PromptHistoryStoredEntry[], | |
| prompt: Prompt, | |
| comments: PromptHistoryComment[] = [], | |
| max = MAX_HISTORY, | |
| ) { | |
| const text = prompt | |
| .map((part) => ("content" in part ? part.content : "")) | |
| .join("") | |
| .trim() | |
| const hasImages = prompt.some((part) => part.type === "image") | |
| const hasComments = comments.some((comment) => !!comment.comment.trim()) | |
| if (!text && !hasImages && !hasComments) return entries | |
| const entry = { | |
| prompt: clonePromptParts(prompt), | |
| comments: clonePromptHistoryComments(comments), | |
| } satisfies PromptHistoryEntry | |
| const last = entries[0] | |
| if (last && isPromptEqual(last, entry)) return entries | |
| return [entry, ...entries].slice(0, max) | |
| } | |
| function isCommentEqual(commentA: PromptHistoryComment, commentB: PromptHistoryComment) { | |
| return ( | |
| commentA.path === commentB.path && | |
| commentA.comment === commentB.comment && | |
| commentA.origin === commentB.origin && | |
| commentA.preview === commentB.preview && | |
| commentA.selection.start === commentB.selection.start && | |
| commentA.selection.end === commentB.selection.end && | |
| commentA.selection.side === commentB.selection.side && | |
| commentA.selection.endSide === commentB.selection.endSide | |
| ) | |
| } | |
| function isPromptEqual(promptA: PromptHistoryStoredEntry, promptB: PromptHistoryStoredEntry) { | |
| const entryA = normalizePromptHistoryEntry(promptA) | |
| const entryB = normalizePromptHistoryEntry(promptB) | |
| if (entryA.prompt.length !== entryB.prompt.length) return false | |
| for (let i = 0; i < entryA.prompt.length; i++) { | |
| const partA = entryA.prompt[i] | |
| const partB = entryB.prompt[i] | |
| if (partA.type !== partB.type) return false | |
| if (partA.type === "text" && partA.content !== (partB.type === "text" ? partB.content : "")) return false | |
| if (partA.type === "file") { | |
| if (partA.path !== (partB.type === "file" ? partB.path : "")) return false | |
| const a = partA.selection | |
| const b = partB.type === "file" ? partB.selection : undefined | |
| const sameSelection = | |
| (!a && !b) || | |
| (!!a && | |
| !!b && | |
| a.startLine === b.startLine && | |
| a.startChar === b.startChar && | |
| a.endLine === b.endLine && | |
| a.endChar === b.endChar) | |
| if (!sameSelection) return false | |
| } | |
| if (partA.type === "agent" && partA.name !== (partB.type === "agent" ? partB.name : "")) return false | |
| if (partA.type === "image" && partA.id !== (partB.type === "image" ? partB.id : "")) return false | |
| } | |
| if (entryA.comments.length !== entryB.comments.length) return false | |
| for (let i = 0; i < entryA.comments.length; i++) { | |
| const commentA = entryA.comments[i] | |
| const commentB = entryB.comments[i] | |
| if (!commentA || !commentB || !isCommentEqual(commentA, commentB)) return false | |
| } | |
| return true | |
| } | |
| type HistoryNavInput = { | |
| direction: "up" | "down" | |
| entries: PromptHistoryStoredEntry[] | |
| historyIndex: number | |
| currentPrompt: Prompt | |
| currentComments: PromptHistoryComment[] | |
| savedPrompt: PromptHistoryEntry | null | |
| } | |
| type HistoryNavResult = | |
| | { | |
| handled: false | |
| historyIndex: number | |
| savedPrompt: PromptHistoryEntry | null | |
| } | |
| | { | |
| handled: true | |
| historyIndex: number | |
| savedPrompt: PromptHistoryEntry | null | |
| entry: PromptHistoryEntry | |
| cursor: "start" | "end" | |
| } | |
| export function navigatePromptHistory(input: HistoryNavInput): HistoryNavResult { | |
| if (input.direction === "up") { | |
| if (input.entries.length === 0) { | |
| return { | |
| handled: false, | |
| historyIndex: input.historyIndex, | |
| savedPrompt: input.savedPrompt, | |
| } | |
| } | |
| if (input.historyIndex === -1) { | |
| const entry = normalizePromptHistoryEntry(input.entries[0]) | |
| return { | |
| handled: true, | |
| historyIndex: 0, | |
| savedPrompt: { | |
| prompt: clonePromptParts(input.currentPrompt), | |
| comments: clonePromptHistoryComments(input.currentComments), | |
| }, | |
| entry, | |
| cursor: "start", | |
| } | |
| } | |
| if (input.historyIndex < input.entries.length - 1) { | |
| const next = input.historyIndex + 1 | |
| const entry = normalizePromptHistoryEntry(input.entries[next]) | |
| return { | |
| handled: true, | |
| historyIndex: next, | |
| savedPrompt: input.savedPrompt, | |
| entry, | |
| cursor: "start", | |
| } | |
| } | |
| return { | |
| handled: false, | |
| historyIndex: input.historyIndex, | |
| savedPrompt: input.savedPrompt, | |
| } | |
| } | |
| if (input.historyIndex > 0) { | |
| const next = input.historyIndex - 1 | |
| const entry = normalizePromptHistoryEntry(input.entries[next]) | |
| return { | |
| handled: true, | |
| historyIndex: next, | |
| savedPrompt: input.savedPrompt, | |
| entry, | |
| cursor: "end", | |
| } | |
| } | |
| if (input.historyIndex === 0) { | |
| if (input.savedPrompt) { | |
| return { | |
| handled: true, | |
| historyIndex: -1, | |
| savedPrompt: null, | |
| entry: input.savedPrompt, | |
| cursor: "end", | |
| } | |
| } | |
| return { | |
| handled: true, | |
| historyIndex: -1, | |
| savedPrompt: null, | |
| entry: { | |
| prompt: DEFAULT_PROMPT, | |
| comments: [], | |
| }, | |
| cursor: "end", | |
| } | |
| } | |
| return { | |
| handled: false, | |
| historyIndex: input.historyIndex, | |
| savedPrompt: input.savedPrompt, | |
| } | |
| } | |
Xet Storage Details
- Size:
- 7.52 kB
- Xet hash:
- 46e29d7c09ef8a9952c7d0499e4da48acc8c982fcbf33b215cf2a5be14885af0
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.