summaryrefslogtreecommitdiffhomepage
path: root/packages/app/src/components
diff options
context:
space:
mode:
authorAdam <[email protected]>2026-02-26 18:23:04 -0600
committerGitHub <[email protected]>2026-02-26 18:23:04 -0600
commitfc52e4b2d3a41efde772e6de8fb2e01f27821701 (patch)
treecf23af294a00a10e55f230232585344c111f0bb9 /packages/app/src/components
parent9a6bfeb782766099d4ce3a98bb9e7b4e79f8bfe6 (diff)
downloadopencode-fc52e4b2d3a41efde772e6de8fb2e01f27821701.tar.gz
opencode-fc52e4b2d3a41efde772e6de8fb2e01f27821701.zip
feat(app): better diff/code comments (#14621)
Co-authored-by: adamelmore <[email protected]> Co-authored-by: David Hill <[email protected]>
Diffstat (limited to 'packages/app/src/components')
-rw-r--r--packages/app/src/components/prompt-input.tsx100
-rw-r--r--packages/app/src/components/prompt-input/build-request-parts.test.ts9
-rw-r--r--packages/app/src/components/prompt-input/build-request-parts.ts22
-rw-r--r--packages/app/src/components/prompt-input/history.test.ts52
-rw-r--r--packages/app/src/components/prompt-input/history.ts125
-rw-r--r--packages/app/src/components/session/session-context-tab.tsx5
6 files changed, 267 insertions, 46 deletions
diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx
index 9174133ac..85aa16384 100644
--- a/packages/app/src/components/prompt-input.tsx
+++ b/packages/app/src/components/prompt-input.tsx
@@ -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
}
diff --git a/packages/app/src/components/prompt-input/build-request-parts.test.ts b/packages/app/src/components/prompt-input/build-request-parts.test.ts
index 72bdecc01..4c2e2d8be 100644
--- a/packages/app/src/components/prompt-input/build-request-parts.test.ts
+++ b/packages/app/src/components/prompt-input/build-request-parts.test.ts
@@ -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)
diff --git a/packages/app/src/components/prompt-input/build-request-parts.ts b/packages/app/src/components/prompt-input/build-request-parts.ts
index 0cc54dc2b..4146fb484 100644
--- a/packages/app/src/components/prompt-input/build-request-parts.ts
+++ b/packages/app/src/components/prompt-input/build-request-parts.ts
@@ -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,
]
diff --git a/packages/app/src/components/prompt-input/history.test.ts b/packages/app/src/components/prompt-input/history.test.ts
index b7a4f896b..37b5ce196 100644
--- a/packages/app/src/components/prompt-input/history.test.ts
+++ b/packages/app/src/components/prompt-input/history.test.ts
@@ -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", () => {
diff --git a/packages/app/src/components/prompt-input/history.ts b/packages/app/src/components/prompt-input/history.ts
index c279a3ed5..de6265321 100644
--- a/packages/app/src/components/prompt-input/history.ts
+++ b/packages/app/src/components/prompt-input/history.ts
@@ -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",
}
}
diff --git a/packages/app/src/components/session/session-context-tab.tsx b/packages/app/src/components/session/session-context-tab.tsx
index 1ea97c395..582aa3391 100644
--- a/packages/app/src/components/session/session-context-tab.tsx
+++ b/packages/app/src/components/session/session-context-tab.tsx
@@ -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"