feat(app): better diff/code comments (#14621)
Co-authored-by: adamelmore <2363879+adamdottv@users.noreply.github.com> Co-authored-by: David Hill <iamdavidhill@gmail.com>
This commit is contained in:
@@ -3,7 +3,7 @@ import { createEffect, on, Component, Show, onCleanup, Switch, Match, createMemo
|
||||
import { createStore } from "solid-js/store"
|
||||
import { createFocusSignal } from "@solid-primitives/active-element"
|
||||
import { useLocal } from "@/context/local"
|
||||
import { useFile } from "@/context/file"
|
||||
import { selectionFromLines, type SelectedLineRange, useFile } from "@/context/file"
|
||||
import {
|
||||
ContentPart,
|
||||
DEFAULT_PROMPT,
|
||||
@@ -43,6 +43,9 @@ import {
|
||||
canNavigateHistoryAtCursor,
|
||||
navigatePromptHistory,
|
||||
prependHistoryEntry,
|
||||
type PromptHistoryComment,
|
||||
type PromptHistoryEntry,
|
||||
type PromptHistoryStoredEntry,
|
||||
promptLength,
|
||||
} from "./prompt-input/history"
|
||||
import { createPromptSubmit } from "./prompt-input/submit"
|
||||
@@ -170,12 +173,29 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
const focus = { file: item.path, id: item.commentID }
|
||||
comments.setActive(focus)
|
||||
|
||||
const queueCommentFocus = (attempts = 6) => {
|
||||
const schedule = (left: number) => {
|
||||
requestAnimationFrame(() => {
|
||||
comments.setFocus({ ...focus })
|
||||
if (left <= 0) return
|
||||
requestAnimationFrame(() => {
|
||||
const current = comments.focus()
|
||||
if (!current) return
|
||||
if (current.file !== focus.file || current.id !== focus.id) return
|
||||
schedule(left - 1)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
schedule(attempts)
|
||||
}
|
||||
|
||||
const wantsReview = item.commentOrigin === "review" || (item.commentOrigin !== "file" && commentInReview(item.path))
|
||||
if (wantsReview) {
|
||||
if (!view().reviewPanel.opened()) view().reviewPanel.open()
|
||||
layout.fileTree.setTab("changes")
|
||||
tabs().setActive("review")
|
||||
requestAnimationFrame(() => comments.setFocus(focus))
|
||||
queueCommentFocus()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -183,8 +203,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
layout.fileTree.setTab("all")
|
||||
const tab = files.tab(item.path)
|
||||
tabs().open(tab)
|
||||
files.load(item.path)
|
||||
requestAnimationFrame(() => comments.setFocus(focus))
|
||||
tabs().setActive(tab)
|
||||
Promise.resolve(files.load(item.path)).finally(() => queueCommentFocus())
|
||||
}
|
||||
|
||||
const recent = createMemo(() => {
|
||||
@@ -219,7 +239,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
const [store, setStore] = createStore<{
|
||||
popover: "at" | "slash" | null
|
||||
historyIndex: number
|
||||
savedPrompt: Prompt | null
|
||||
savedPrompt: PromptHistoryEntry | null
|
||||
placeholder: number
|
||||
draggingType: "image" | "@mention" | null
|
||||
mode: "normal" | "shell"
|
||||
@@ -227,7 +247,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
}>({
|
||||
popover: null,
|
||||
historyIndex: -1,
|
||||
savedPrompt: null,
|
||||
savedPrompt: null as PromptHistoryEntry | null,
|
||||
placeholder: Math.floor(Math.random() * EXAMPLES.length),
|
||||
draggingType: null,
|
||||
mode: "normal",
|
||||
@@ -256,7 +276,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
const [history, setHistory] = persisted(
|
||||
Persist.global("prompt-history", ["prompt-history.v1"]),
|
||||
createStore<{
|
||||
entries: Prompt[]
|
||||
entries: PromptHistoryStoredEntry[]
|
||||
}>({
|
||||
entries: [],
|
||||
}),
|
||||
@@ -264,7 +284,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
const [shellHistory, setShellHistory] = persisted(
|
||||
Persist.global("prompt-history-shell", ["prompt-history-shell.v1"]),
|
||||
createStore<{
|
||||
entries: Prompt[]
|
||||
entries: PromptHistoryStoredEntry[]
|
||||
}>({
|
||||
entries: [],
|
||||
}),
|
||||
@@ -282,9 +302,66 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
}),
|
||||
)
|
||||
|
||||
const applyHistoryPrompt = (p: Prompt, position: "start" | "end") => {
|
||||
const historyComments = () => {
|
||||
const byID = new Map(comments.all().map((item) => [`${item.file}\n${item.id}`, item] as const))
|
||||
return prompt.context.items().flatMap((item) => {
|
||||
if (item.type !== "file") return []
|
||||
const comment = item.comment?.trim()
|
||||
if (!comment) return []
|
||||
|
||||
const selection = item.commentID ? byID.get(`${item.path}\n${item.commentID}`)?.selection : undefined
|
||||
const nextSelection =
|
||||
selection ??
|
||||
(item.selection
|
||||
? ({
|
||||
start: item.selection.startLine,
|
||||
end: item.selection.endLine,
|
||||
} satisfies SelectedLineRange)
|
||||
: undefined)
|
||||
if (!nextSelection) return []
|
||||
|
||||
return [
|
||||
{
|
||||
id: item.commentID ?? item.key,
|
||||
path: item.path,
|
||||
selection: { ...nextSelection },
|
||||
comment,
|
||||
time: item.commentID ? (byID.get(`${item.path}\n${item.commentID}`)?.time ?? Date.now()) : Date.now(),
|
||||
origin: item.commentOrigin,
|
||||
preview: item.preview,
|
||||
} satisfies PromptHistoryComment,
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
const applyHistoryComments = (items: PromptHistoryComment[]) => {
|
||||
comments.replace(
|
||||
items.map((item) => ({
|
||||
id: item.id,
|
||||
file: item.path,
|
||||
selection: { ...item.selection },
|
||||
comment: item.comment,
|
||||
time: item.time,
|
||||
})),
|
||||
)
|
||||
prompt.context.replaceComments(
|
||||
items.map((item) => ({
|
||||
type: "file" as const,
|
||||
path: item.path,
|
||||
selection: selectionFromLines(item.selection),
|
||||
comment: item.comment,
|
||||
commentID: item.id,
|
||||
commentOrigin: item.origin,
|
||||
preview: item.preview,
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
const applyHistoryPrompt = (entry: PromptHistoryEntry, position: "start" | "end") => {
|
||||
const p = entry.prompt
|
||||
const length = position === "start" ? 0 : promptLength(p)
|
||||
setStore("applyingHistory", true)
|
||||
applyHistoryComments(entry.comments)
|
||||
prompt.set(p, length)
|
||||
requestAnimationFrame(() => {
|
||||
editorRef.focus()
|
||||
@@ -846,7 +923,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
const addToHistory = (prompt: Prompt, mode: "normal" | "shell") => {
|
||||
const currentHistory = mode === "shell" ? shellHistory : history
|
||||
const setCurrentHistory = mode === "shell" ? setShellHistory : setHistory
|
||||
const next = prependHistoryEntry(currentHistory.entries, prompt)
|
||||
const next = prependHistoryEntry(currentHistory.entries, prompt, mode === "shell" ? [] : historyComments())
|
||||
if (next === currentHistory.entries) return
|
||||
setCurrentHistory("entries", next)
|
||||
}
|
||||
@@ -857,12 +934,13 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
entries: store.mode === "shell" ? shellHistory.entries : history.entries,
|
||||
historyIndex: store.historyIndex,
|
||||
currentPrompt: prompt.current(),
|
||||
currentComments: historyComments(),
|
||||
savedPrompt: store.savedPrompt,
|
||||
})
|
||||
if (!result.handled) return false
|
||||
setStore("historyIndex", result.historyIndex)
|
||||
setStore("savedPrompt", result.savedPrompt)
|
||||
applyHistoryPrompt(result.prompt, result.cursor)
|
||||
applyHistoryPrompt(result.entry, result.cursor)
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,15 @@ describe("buildRequestParts", () => {
|
||||
result.requestParts.some((part) => part.type === "file" && part.url.startsWith("file:///repo/src/foo.ts")),
|
||||
).toBe(true)
|
||||
expect(result.requestParts.some((part) => part.type === "text" && part.synthetic)).toBe(true)
|
||||
expect(
|
||||
result.requestParts.some(
|
||||
(part) =>
|
||||
part.type === "text" &&
|
||||
part.synthetic &&
|
||||
part.metadata?.opencodeComment &&
|
||||
(part.metadata.opencodeComment as { comment?: string }).comment === "check this",
|
||||
),
|
||||
).toBe(true)
|
||||
|
||||
expect(result.optimisticParts).toHaveLength(result.requestParts.length)
|
||||
expect(result.optimisticParts.every((part) => part.sessionID === "ses_1" && part.messageID === "msg_1")).toBe(true)
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { FileSelection } from "@/context/file"
|
||||
import { encodeFilePath } from "@/context/file/path"
|
||||
import type { AgentPart, FileAttachmentPart, ImageAttachmentPart, Prompt } from "@/context/prompt"
|
||||
import { Identifier } from "@/utils/id"
|
||||
import { createCommentMetadata, formatCommentNote } from "@/utils/comment-note"
|
||||
|
||||
type PromptRequestPart = (TextPartInput | FilePartInput | AgentPartInput) & { id: string }
|
||||
|
||||
@@ -41,18 +42,6 @@ const fileQuery = (selection: FileSelection | undefined) =>
|
||||
const isFileAttachment = (part: Prompt[number]): part is FileAttachmentPart => part.type === "file"
|
||||
const isAgentAttachment = (part: Prompt[number]): part is AgentPart => part.type === "agent"
|
||||
|
||||
const commentNote = (path: string, selection: FileSelection | undefined, comment: string) => {
|
||||
const start = selection ? Math.min(selection.startLine, selection.endLine) : undefined
|
||||
const end = selection ? Math.max(selection.startLine, selection.endLine) : undefined
|
||||
const range =
|
||||
start === undefined || end === undefined
|
||||
? "this file"
|
||||
: start === end
|
||||
? `line ${start}`
|
||||
: `lines ${start} through ${end}`
|
||||
return `The user made the following comment regarding ${range} of ${path}: ${comment}`
|
||||
}
|
||||
|
||||
const toOptimisticPart = (part: PromptRequestPart, sessionID: string, messageID: string): Part => {
|
||||
if (part.type === "text") {
|
||||
return {
|
||||
@@ -153,8 +142,15 @@ export function buildRequestParts(input: BuildRequestPartsInput) {
|
||||
{
|
||||
id: Identifier.ascending("part"),
|
||||
type: "text",
|
||||
text: commentNote(item.path, item.selection, comment),
|
||||
text: formatCommentNote({ path: item.path, selection: item.selection, comment }),
|
||||
synthetic: true,
|
||||
metadata: createCommentMetadata({
|
||||
path: item.path,
|
||||
selection: item.selection,
|
||||
comment,
|
||||
preview: item.preview,
|
||||
origin: item.commentOrigin,
|
||||
}),
|
||||
} satisfies PromptRequestPart,
|
||||
filePart,
|
||||
]
|
||||
|
||||
@@ -3,25 +3,42 @@ import type { Prompt } from "@/context/prompt"
|
||||
import {
|
||||
canNavigateHistoryAtCursor,
|
||||
clonePromptParts,
|
||||
normalizePromptHistoryEntry,
|
||||
navigatePromptHistory,
|
||||
prependHistoryEntry,
|
||||
promptLength,
|
||||
type PromptHistoryComment,
|
||||
} from "./history"
|
||||
|
||||
const DEFAULT_PROMPT: Prompt = [{ type: "text", content: "", start: 0, end: 0 }]
|
||||
|
||||
const text = (value: string): Prompt => [{ type: "text", content: value, start: 0, end: value.length }]
|
||||
const comment = (id: string, value = "note"): PromptHistoryComment => ({
|
||||
id,
|
||||
path: "src/a.ts",
|
||||
selection: { start: 2, end: 4 },
|
||||
comment: value,
|
||||
time: 1,
|
||||
origin: "review",
|
||||
preview: "const a = 1",
|
||||
})
|
||||
|
||||
describe("prompt-input history", () => {
|
||||
test("prependHistoryEntry skips empty prompt and deduplicates consecutive entries", () => {
|
||||
const first = prependHistoryEntry([], DEFAULT_PROMPT)
|
||||
expect(first).toEqual([])
|
||||
|
||||
const commentsOnly = prependHistoryEntry([], DEFAULT_PROMPT, [comment("c1")])
|
||||
expect(commentsOnly).toHaveLength(1)
|
||||
|
||||
const withOne = prependHistoryEntry([], text("hello"))
|
||||
expect(withOne).toHaveLength(1)
|
||||
|
||||
const deduped = prependHistoryEntry(withOne, text("hello"))
|
||||
expect(deduped).toBe(withOne)
|
||||
|
||||
const dedupedComments = prependHistoryEntry(commentsOnly, DEFAULT_PROMPT, [comment("c1")])
|
||||
expect(dedupedComments).toBe(commentsOnly)
|
||||
})
|
||||
|
||||
test("navigatePromptHistory restores saved prompt when moving down from newest", () => {
|
||||
@@ -31,24 +48,57 @@ describe("prompt-input history", () => {
|
||||
entries,
|
||||
historyIndex: -1,
|
||||
currentPrompt: text("draft"),
|
||||
currentComments: [comment("draft")],
|
||||
savedPrompt: null,
|
||||
})
|
||||
expect(up.handled).toBe(true)
|
||||
if (!up.handled) throw new Error("expected handled")
|
||||
expect(up.historyIndex).toBe(0)
|
||||
expect(up.cursor).toBe("start")
|
||||
expect(up.entry.comments).toEqual([])
|
||||
|
||||
const down = navigatePromptHistory({
|
||||
direction: "down",
|
||||
entries,
|
||||
historyIndex: up.historyIndex,
|
||||
currentPrompt: text("ignored"),
|
||||
currentComments: [],
|
||||
savedPrompt: up.savedPrompt,
|
||||
})
|
||||
expect(down.handled).toBe(true)
|
||||
if (!down.handled) throw new Error("expected handled")
|
||||
expect(down.historyIndex).toBe(-1)
|
||||
expect(down.prompt[0]?.type === "text" ? down.prompt[0].content : "").toBe("draft")
|
||||
expect(down.entry.prompt[0]?.type === "text" ? down.entry.prompt[0].content : "").toBe("draft")
|
||||
expect(down.entry.comments).toEqual([comment("draft")])
|
||||
})
|
||||
|
||||
test("navigatePromptHistory keeps entry comments when moving through history", () => {
|
||||
const entries = [
|
||||
{
|
||||
prompt: text("with comment"),
|
||||
comments: [comment("c1")],
|
||||
},
|
||||
]
|
||||
|
||||
const up = navigatePromptHistory({
|
||||
direction: "up",
|
||||
entries,
|
||||
historyIndex: -1,
|
||||
currentPrompt: text("draft"),
|
||||
currentComments: [],
|
||||
savedPrompt: null,
|
||||
})
|
||||
|
||||
expect(up.handled).toBe(true)
|
||||
if (!up.handled) throw new Error("expected handled")
|
||||
expect(up.entry.prompt[0]?.type === "text" ? up.entry.prompt[0].content : "").toBe("with comment")
|
||||
expect(up.entry.comments).toEqual([comment("c1")])
|
||||
})
|
||||
|
||||
test("normalizePromptHistoryEntry supports legacy prompt arrays", () => {
|
||||
const entry = normalizePromptHistoryEntry(text("legacy"))
|
||||
expect(entry.prompt[0]?.type === "text" ? entry.prompt[0].content : "").toBe("legacy")
|
||||
expect(entry.comments).toEqual([])
|
||||
})
|
||||
|
||||
test("helpers clone prompt and count text content length", () => {
|
||||
|
||||
@@ -1,9 +1,27 @@
|
||||
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
|
||||
@@ -25,29 +43,82 @@ export function clonePromptParts(prompt: Prompt): Prompt {
|
||||
})
|
||||
}
|
||||
|
||||
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: Prompt[], prompt: Prompt, max = MAX_HISTORY) {
|
||||
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")
|
||||
if (!text && !hasImages) return entries
|
||||
const hasComments = comments.some((comment) => !!comment.comment.trim())
|
||||
if (!text && !hasImages && !hasComments) return entries
|
||||
|
||||
const entry = clonePromptParts(prompt)
|
||||
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 isPromptEqual(promptA: Prompt, promptB: Prompt) {
|
||||
if (promptA.length !== promptB.length) return false
|
||||
for (let i = 0; i < promptA.length; i++) {
|
||||
const partA = promptA[i]
|
||||
const partB = promptB[i]
|
||||
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") {
|
||||
@@ -67,28 +138,35 @@ function isPromptEqual(promptA: Prompt, promptB: Prompt) {
|
||||
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: Prompt[]
|
||||
entries: PromptHistoryStoredEntry[]
|
||||
historyIndex: number
|
||||
currentPrompt: Prompt
|
||||
savedPrompt: Prompt | null
|
||||
currentComments: PromptHistoryComment[]
|
||||
savedPrompt: PromptHistoryEntry | null
|
||||
}
|
||||
|
||||
type HistoryNavResult =
|
||||
| {
|
||||
handled: false
|
||||
historyIndex: number
|
||||
savedPrompt: Prompt | null
|
||||
savedPrompt: PromptHistoryEntry | null
|
||||
}
|
||||
| {
|
||||
handled: true
|
||||
historyIndex: number
|
||||
savedPrompt: Prompt | null
|
||||
prompt: Prompt
|
||||
savedPrompt: PromptHistoryEntry | null
|
||||
entry: PromptHistoryEntry
|
||||
cursor: "start" | "end"
|
||||
}
|
||||
|
||||
@@ -103,22 +181,27 @@ export function navigatePromptHistory(input: HistoryNavInput): HistoryNavResult
|
||||
}
|
||||
|
||||
if (input.historyIndex === -1) {
|
||||
const entry = normalizePromptHistoryEntry(input.entries[0])
|
||||
return {
|
||||
handled: true,
|
||||
historyIndex: 0,
|
||||
savedPrompt: clonePromptParts(input.currentPrompt),
|
||||
prompt: input.entries[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,
|
||||
prompt: input.entries[next],
|
||||
entry,
|
||||
cursor: "start",
|
||||
}
|
||||
}
|
||||
@@ -132,11 +215,12 @@ export function navigatePromptHistory(input: HistoryNavInput): HistoryNavResult
|
||||
|
||||
if (input.historyIndex > 0) {
|
||||
const next = input.historyIndex - 1
|
||||
const entry = normalizePromptHistoryEntry(input.entries[next])
|
||||
return {
|
||||
handled: true,
|
||||
historyIndex: next,
|
||||
savedPrompt: input.savedPrompt,
|
||||
prompt: input.entries[next],
|
||||
entry,
|
||||
cursor: "end",
|
||||
}
|
||||
}
|
||||
@@ -147,7 +231,7 @@ export function navigatePromptHistory(input: HistoryNavInput): HistoryNavResult
|
||||
handled: true,
|
||||
historyIndex: -1,
|
||||
savedPrompt: null,
|
||||
prompt: input.savedPrompt,
|
||||
entry: input.savedPrompt,
|
||||
cursor: "end",
|
||||
}
|
||||
}
|
||||
@@ -156,7 +240,10 @@ export function navigatePromptHistory(input: HistoryNavInput): HistoryNavResult
|
||||
handled: true,
|
||||
historyIndex: -1,
|
||||
savedPrompt: null,
|
||||
prompt: DEFAULT_PROMPT,
|
||||
entry: {
|
||||
prompt: DEFAULT_PROMPT,
|
||||
comments: [],
|
||||
},
|
||||
cursor: "end",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { same } from "@/utils/same"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { Accordion } from "@opencode-ai/ui/accordion"
|
||||
import { StickyAccordionHeader } from "@opencode-ai/ui/sticky-accordion-header"
|
||||
import { Code } from "@opencode-ai/ui/code"
|
||||
import { File } from "@opencode-ai/ui/file"
|
||||
import { Markdown } from "@opencode-ai/ui/markdown"
|
||||
import { ScrollView } from "@opencode-ai/ui/scroll-view"
|
||||
import type { Message, Part, UserMessage } from "@opencode-ai/sdk/v2/client"
|
||||
@@ -47,7 +47,8 @@ function RawMessageContent(props: { message: Message; getParts: (id: string) =>
|
||||
})
|
||||
|
||||
return (
|
||||
<Code
|
||||
<File
|
||||
mode="text"
|
||||
file={file()}
|
||||
overflow="wrap"
|
||||
class="select-text"
|
||||
|
||||
Reference in New Issue
Block a user