Fix shiki and diff caching
This commit is contained in:
@@ -5,6 +5,9 @@ import { getSharedHighlighter, escapeHtml } from "../lib/markdown"
|
|||||||
|
|
||||||
const inlineLoadedLanguages = new Set<string>()
|
const inlineLoadedLanguages = new Set<string>()
|
||||||
|
|
||||||
|
type LoadLanguageArg = Parameters<Highlighter["loadLanguage"]>[0]
|
||||||
|
type CodeToHtmlOptions = Parameters<Highlighter["codeToHtml"]>[1]
|
||||||
|
|
||||||
interface CodeBlockInlineProps {
|
interface CodeBlockInlineProps {
|
||||||
code: string
|
code: string
|
||||||
language?: string
|
language?: string
|
||||||
@@ -41,13 +44,14 @@ export function CodeBlockInline(props: CodeBlockInlineProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const language = props.language as LoadLanguageArg
|
||||||
if (!inlineLoadedLanguages.has(props.language)) {
|
if (!inlineLoadedLanguages.has(props.language)) {
|
||||||
await highlighter.loadLanguage(props.language)
|
await highlighter.loadLanguage(language)
|
||||||
inlineLoadedLanguages.add(props.language)
|
inlineLoadedLanguages.add(props.language)
|
||||||
}
|
}
|
||||||
|
|
||||||
const highlighted = highlighter.codeToHtml(props.code, {
|
const highlighted = highlighter.codeToHtml(props.code, {
|
||||||
lang: props.language,
|
lang: props.language as CodeToHtmlOptions["lang"],
|
||||||
theme: isDark() ? "github-dark" : "github-light",
|
theme: isDark() ? "github-dark" : "github-light",
|
||||||
})
|
})
|
||||||
setHtml(highlighted)
|
setHtml(highlighted)
|
||||||
|
|||||||
@@ -1,62 +1,56 @@
|
|||||||
import { createMemo, Show } from "solid-js"
|
import { createMemo } from "solid-js"
|
||||||
import { DiffView, DiffModeEnum } from "@git-diff-view/solid"
|
import type { TextPart } from "../types/message"
|
||||||
import { getLanguageFromPath } from "../lib/markdown"
|
import { Markdown } from "./markdown"
|
||||||
import { normalizeDiffText } from "../lib/diff-utils"
|
import { normalizeDiffText } from "../lib/diff-utils"
|
||||||
import type { DiffViewMode } from "../stores/preferences"
|
import { getToolRenderCache, setToolRenderCache } from "../lib/tool-render-cache"
|
||||||
|
|
||||||
|
type ThemeKey = "light" | "dark"
|
||||||
|
|
||||||
interface ToolCallDiffViewerProps {
|
interface ToolCallDiffViewerProps {
|
||||||
diffText: string
|
diffText: string
|
||||||
filePath?: string
|
filePath?: string
|
||||||
theme: "light" | "dark"
|
theme: ThemeKey
|
||||||
mode: DiffViewMode
|
renderCacheKey?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type DiffData = {
|
|
||||||
oldFile?: { fileName?: string | null; fileLang?: string | null; content?: string | null }
|
function formatDiffMarkdown(diffText: string, filePath?: string): string {
|
||||||
newFile?: { fileName?: string | null; fileLang?: string | null; content?: string | null }
|
const body = normalizeDiffText(diffText) || diffText
|
||||||
hunks: string[]
|
const trimmed = body.trimStart()
|
||||||
|
const alreadyFenced = trimmed.startsWith("```")
|
||||||
|
const fenced = alreadyFenced ? body : `\`\`\`diff\n${body}\n\`\`\``
|
||||||
|
|
||||||
|
if (!filePath) {
|
||||||
|
return fenced
|
||||||
|
}
|
||||||
|
return `### ${filePath}\n\n${fenced}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ToolCallDiffViewer(props: ToolCallDiffViewerProps) {
|
export function ToolCallDiffViewer(props: ToolCallDiffViewerProps) {
|
||||||
const diffData = createMemo<DiffData | null>(() => {
|
const diffMarkdown = createMemo(() => {
|
||||||
const normalized = normalizeDiffText(props.diffText)
|
return formatDiffMarkdown(props.diffText, props.filePath)
|
||||||
if (!normalized) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const language = getLanguageFromPath(props.filePath) || "text"
|
|
||||||
const fileName = props.filePath || "diff"
|
|
||||||
|
|
||||||
return {
|
|
||||||
oldFile: {
|
|
||||||
fileName,
|
|
||||||
fileLang: language,
|
|
||||||
},
|
|
||||||
newFile: {
|
|
||||||
fileName,
|
|
||||||
fileLang: language,
|
|
||||||
},
|
|
||||||
hunks: [normalized],
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const diffPart = createMemo<TextPart>(() => {
|
||||||
|
const part: TextPart = { type: "text", text: diffMarkdown() }
|
||||||
|
if (props.renderCacheKey) {
|
||||||
|
const cached = getToolRenderCache(props.renderCacheKey)
|
||||||
|
if (cached) {
|
||||||
|
part.renderCache = cached
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return part
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleRendered = () => {
|
||||||
|
if (!props.renderCacheKey) return
|
||||||
|
setToolRenderCache(props.renderCacheKey, diffPart().renderCache)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="tool-call-diff-viewer">
|
<div class="tool-call-diff-viewer">
|
||||||
<Show
|
<Markdown part={diffPart()} isDark={props.theme === "dark"} onRendered={handleRendered} />
|
||||||
when={diffData()}
|
|
||||||
fallback={<pre class="tool-call-diff-fallback">{props.diffText}</pre>}
|
|
||||||
>
|
|
||||||
{(data) => (
|
|
||||||
<DiffView
|
|
||||||
data={data()}
|
|
||||||
diffViewMode={props.mode === "split" ? DiffModeEnum.Split : DiffModeEnum.Unified}
|
|
||||||
diffViewTheme={props.theme}
|
|
||||||
diffViewHighlight
|
|
||||||
diffViewWrap={false}
|
|
||||||
diffViewFontSize={13}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Show>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -170,6 +170,9 @@ interface ToolDisplayItem {
|
|||||||
key: string
|
key: string
|
||||||
toolPart: any
|
toolPart: any
|
||||||
messageInfo?: any
|
messageInfo?: any
|
||||||
|
messageId: string
|
||||||
|
messageVersion: number
|
||||||
|
partVersion: number
|
||||||
}
|
}
|
||||||
|
|
||||||
type DisplayItem = MessageDisplayItem | ToolDisplayItem
|
type DisplayItem = MessageDisplayItem | ToolDisplayItem
|
||||||
@@ -191,14 +194,35 @@ interface ToolCacheEntry {
|
|||||||
item: ToolDisplayItem
|
item: ToolDisplayItem
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SessionCache {
|
||||||
|
messageItemCache: Map<string, MessageCacheEntry>
|
||||||
|
toolItemCache: Map<string, ToolCacheEntry>
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionCaches = new Map<string, SessionCache>()
|
||||||
|
|
||||||
|
function getSessionCache(instanceId: string, sessionId: string): SessionCache {
|
||||||
|
const key = `${instanceId}:${sessionId}`
|
||||||
|
let cache = sessionCaches.get(key)
|
||||||
|
if (!cache) {
|
||||||
|
cache = {
|
||||||
|
messageItemCache: new Map(),
|
||||||
|
toolItemCache: new Map(),
|
||||||
|
}
|
||||||
|
sessionCaches.set(key, cache)
|
||||||
|
}
|
||||||
|
return cache
|
||||||
|
}
|
||||||
|
|
||||||
export default function MessageStream(props: MessageStreamProps) {
|
export default function MessageStream(props: MessageStreamProps) {
|
||||||
let containerRef: HTMLDivElement | undefined
|
let containerRef: HTMLDivElement | undefined
|
||||||
const [autoScroll, setAutoScroll] = createSignal(true)
|
const [autoScroll, setAutoScroll] = createSignal(true)
|
||||||
const [showScrollBottomButton, setShowScrollBottomButton] = createSignal(false)
|
const [showScrollBottomButton, setShowScrollBottomButton] = createSignal(false)
|
||||||
const [showScrollTopButton, setShowScrollTopButton] = createSignal(false)
|
const [showScrollTopButton, setShowScrollTopButton] = createSignal(false)
|
||||||
|
|
||||||
let messageItemCache = new Map<string, MessageCacheEntry>()
|
const sessionCache = getSessionCache(props.instanceId, props.sessionId)
|
||||||
let toolItemCache = new Map<string, ToolCacheEntry>()
|
let messageItemCache = sessionCache.messageItemCache
|
||||||
|
let toolItemCache = sessionCache.toolItemCache
|
||||||
let scrollAnimationFrame: number | null = null
|
let scrollAnimationFrame: number | null = null
|
||||||
let lastKnownScrollTop = 0
|
let lastKnownScrollTop = 0
|
||||||
|
|
||||||
@@ -334,7 +358,6 @@ export default function MessageStream(props: MessageStreamProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const messageView = createMemo(() => {
|
const messageView = createMemo(() => {
|
||||||
// Ensure memo reacts to preference changes
|
|
||||||
const showThinking = preferences().showThinkingBlocks
|
const showThinking = preferences().showThinkingBlocks
|
||||||
|
|
||||||
const items: DisplayItem[] = []
|
const items: DisplayItem[] = []
|
||||||
@@ -358,7 +381,6 @@ export default function MessageStream(props: MessageStreamProps) {
|
|||||||
const message = props.messages[index]
|
const message = props.messages[index]
|
||||||
const messageInfo = props.messagesInfo?.get(message.id)
|
const messageInfo = props.messagesInfo?.get(message.id)
|
||||||
|
|
||||||
// If we hit the revert point, stop rendering messages
|
|
||||||
if (props.revert?.messageID && message.id === props.revert.messageID) {
|
if (props.revert?.messageID && message.id === props.revert.messageID) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -367,9 +389,9 @@ export default function MessageStream(props: MessageStreamProps) {
|
|||||||
|
|
||||||
const baseDisplayParts = message.displayParts
|
const baseDisplayParts = message.displayParts
|
||||||
const displayParts: MessageDisplayParts =
|
const displayParts: MessageDisplayParts =
|
||||||
baseDisplayParts && baseDisplayParts.showThinking === showThinking
|
!baseDisplayParts || baseDisplayParts.showThinking !== showThinking
|
||||||
? baseDisplayParts
|
? computeDisplayParts(message, showThinking)
|
||||||
: computeDisplayParts(message, showThinking)
|
: (baseDisplayParts as MessageDisplayParts)
|
||||||
|
|
||||||
const combinedParts = displayParts.combined
|
const combinedParts = displayParts.combined
|
||||||
const version = message.version ?? 0
|
const version = message.version ?? 0
|
||||||
@@ -424,6 +446,8 @@ export default function MessageStream(props: MessageStreamProps) {
|
|||||||
for (let toolIndex = 0; toolIndex < displayParts.tool.length; toolIndex++) {
|
for (let toolIndex = 0; toolIndex < displayParts.tool.length; toolIndex++) {
|
||||||
const toolPart = displayParts.tool[toolIndex]
|
const toolPart = displayParts.tool[toolIndex]
|
||||||
const toolKey = typeof toolPart?.id === "string" ? toolPart.id : `${message.id}-tool-${toolIndex}`
|
const toolKey = typeof toolPart?.id === "string" ? toolPart.id : `${message.id}-tool-${toolIndex}`
|
||||||
|
const messageVersion = typeof message.version === "number" ? message.version : 0
|
||||||
|
const partVersion = typeof toolPart?.version === "number" ? toolPart.version : 0
|
||||||
|
|
||||||
const toolSignature = createToolSignature(message, toolPart, toolIndex, messageInfo)
|
const toolSignature = createToolSignature(message, toolPart, toolIndex, messageInfo)
|
||||||
const contentKey = createToolContentKey(toolPart, messageInfo)
|
const contentKey = createToolContentKey(toolPart, messageInfo)
|
||||||
@@ -434,6 +458,9 @@ export default function MessageStream(props: MessageStreamProps) {
|
|||||||
...toolEntry.item,
|
...toolEntry.item,
|
||||||
toolPart,
|
toolPart,
|
||||||
messageInfo,
|
messageInfo,
|
||||||
|
messageId: message.id,
|
||||||
|
messageVersion,
|
||||||
|
partVersion,
|
||||||
}
|
}
|
||||||
toolEntry.toolPart = toolPart
|
toolEntry.toolPart = toolPart
|
||||||
toolEntry.messageInfo = messageInfo
|
toolEntry.messageInfo = messageInfo
|
||||||
@@ -444,8 +471,16 @@ export default function MessageStream(props: MessageStreamProps) {
|
|||||||
newToolCache.set(toolKey, toolEntry)
|
newToolCache.set(toolKey, toolEntry)
|
||||||
items.push(updatedItem)
|
items.push(updatedItem)
|
||||||
} else {
|
} else {
|
||||||
|
const cachedItem = toolEntry.item
|
||||||
|
cachedItem.toolPart = toolPart
|
||||||
|
cachedItem.messageInfo = messageInfo
|
||||||
|
cachedItem.messageId = message.id
|
||||||
|
cachedItem.messageVersion = messageVersion
|
||||||
|
cachedItem.partVersion = partVersion
|
||||||
|
toolEntry.toolPart = toolPart
|
||||||
|
toolEntry.messageInfo = messageInfo
|
||||||
newToolCache.set(toolKey, toolEntry)
|
newToolCache.set(toolKey, toolEntry)
|
||||||
items.push(toolEntry.item)
|
items.push(cachedItem)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const toolItem: ToolDisplayItem = {
|
const toolItem: ToolDisplayItem = {
|
||||||
@@ -453,6 +488,9 @@ export default function MessageStream(props: MessageStreamProps) {
|
|||||||
key: toolKey,
|
key: toolKey,
|
||||||
toolPart,
|
toolPart,
|
||||||
messageInfo,
|
messageInfo,
|
||||||
|
messageId: message.id,
|
||||||
|
messageVersion,
|
||||||
|
partVersion,
|
||||||
}
|
}
|
||||||
console.debug("[ToolCall] create", toolKey, toolPart?.state?.status)
|
console.debug("[ToolCall] create", toolKey, toolPart?.state?.status)
|
||||||
newToolCache.set(toolKey, { toolPart, messageInfo, signature: toolSignature, contentKey, item: toolItem })
|
newToolCache.set(toolKey, { toolPart, messageInfo, signature: toolSignature, contentKey, item: toolItem })
|
||||||
@@ -463,6 +501,8 @@ export default function MessageStream(props: MessageStreamProps) {
|
|||||||
|
|
||||||
messageItemCache = newMessageCache
|
messageItemCache = newMessageCache
|
||||||
toolItemCache = newToolCache
|
toolItemCache = newToolCache
|
||||||
|
sessionCache.messageItemCache = messageItemCache
|
||||||
|
sessionCache.toolItemCache = toolItemCache
|
||||||
|
|
||||||
tokenSegments.push(`items:${items.length}`)
|
tokenSegments.push(`items:${items.length}`)
|
||||||
|
|
||||||
@@ -681,7 +721,13 @@ export default function MessageStream(props: MessageStreamProps) {
|
|||||||
</button>
|
</button>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<ToolCall toolCall={toolPart} toolCallId={item.key} />
|
<ToolCall
|
||||||
|
toolCall={toolPart}
|
||||||
|
toolCallId={item.key}
|
||||||
|
messageId={item.messageId}
|
||||||
|
messageVersion={item.messageVersion}
|
||||||
|
partVersion={item.partVersion}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -47,10 +47,12 @@ export default function PromptInput(props: PromptInputProps) {
|
|||||||
const minHeight = lineHeight * MIN_TEXTAREA_LINES
|
const minHeight = lineHeight * MIN_TEXTAREA_LINES
|
||||||
|
|
||||||
textarea.style.height = "auto"
|
textarea.style.height = "auto"
|
||||||
const newHeight = Math.min(Math.max(textarea.scrollHeight, minHeight), MAX_TEXTAREA_HEIGHT)
|
const scrollHeight = textarea.scrollHeight
|
||||||
|
const newHeight = Math.min(Math.max(scrollHeight, minHeight), MAX_TEXTAREA_HEIGHT)
|
||||||
textarea.style.height = newHeight + "px"
|
textarea.style.height = newHeight + "px"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const attachments = () => getAttachments(props.instanceId, props.sessionId)
|
const attachments = () => getAttachments(props.instanceId, props.sessionId)
|
||||||
const instanceAgents = () => agents().get(props.instanceId) || []
|
const instanceAgents = () => agents().get(props.instanceId) || []
|
||||||
|
|
||||||
@@ -309,7 +311,9 @@ export default function PromptInput(props: PromptInputProps) {
|
|||||||
|
|
||||||
function handleKeyDown(e: KeyboardEvent) {
|
function handleKeyDown(e: KeyboardEvent) {
|
||||||
const textarea = textareaRef
|
const textarea = textareaRef
|
||||||
if (!textarea) return
|
if (!textarea) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (e.key === "Backspace" || e.key === "Delete") {
|
if (e.key === "Backspace" || e.key === "Delete") {
|
||||||
const cursorPos = textarea.selectionStart
|
const cursorPos = textarea.selectionStart
|
||||||
@@ -478,7 +482,6 @@ export default function PromptInput(props: PromptInputProps) {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
adjustTextareaHeight(textarea)
|
adjustTextareaHeight(textarea)
|
||||||
}, 0)
|
}, 0)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -497,11 +500,9 @@ export default function PromptInput(props: PromptInputProps) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await addToHistory(props.instanceFolder, text)
|
await addToHistory(props.instanceFolder, text)
|
||||||
|
|
||||||
const updated = await getHistory(props.instanceFolder)
|
const updated = await getHistory(props.instanceFolder)
|
||||||
setHistory(updated)
|
setHistory(updated)
|
||||||
setHistoryIndex(-1)
|
setHistoryIndex(-1)
|
||||||
|
|
||||||
await props.onSend(text, currentAttachments)
|
await props.onSend(text, currentAttachments)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to send message:", error)
|
console.error("Failed to send message:", error)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { keyboardRegistry } from "../lib/keyboard-registry"
|
|||||||
import { formatShortcut } from "../lib/keyboard-utils"
|
import { formatShortcut } from "../lib/keyboard-utils"
|
||||||
import { showToastNotification } from "../lib/notifications"
|
import { showToastNotification } from "../lib/notifications"
|
||||||
|
|
||||||
|
|
||||||
interface SessionListProps {
|
interface SessionListProps {
|
||||||
instanceId: string
|
instanceId: string
|
||||||
sessions: Map<string, Session>
|
sessions: Map<string, Session>
|
||||||
@@ -51,6 +52,10 @@ const SessionList: Component<SessionListProps> = (props) => {
|
|||||||
const [startWidth, setStartWidth] = createSignal(DEFAULT_WIDTH)
|
const [startWidth, setStartWidth] = createSignal(DEFAULT_WIDTH)
|
||||||
const infoShortcut = keyboardRegistry.get("switch-to-info")
|
const infoShortcut = keyboardRegistry.get("switch-to-info")
|
||||||
|
|
||||||
|
const selectSession = (sessionId: string) => {
|
||||||
|
props.onSelect(sessionId)
|
||||||
|
}
|
||||||
|
|
||||||
let mouseMoveHandler: ((event: MouseEvent) => void) | null = null
|
let mouseMoveHandler: ((event: MouseEvent) => void) | null = null
|
||||||
let mouseUpHandler: (() => void) | null = null
|
let mouseUpHandler: (() => void) | null = null
|
||||||
let touchMoveHandler: ((event: TouchEvent) => void) | null = null
|
let touchMoveHandler: ((event: TouchEvent) => void) | null = null
|
||||||
@@ -246,7 +251,7 @@ const SessionList: Component<SessionListProps> = (props) => {
|
|||||||
<div class="session-list-item group">
|
<div class="session-list-item group">
|
||||||
<button
|
<button
|
||||||
class={`session-item-base ${props.activeSessionId === "info" ? "session-item-active" : "session-item-inactive"}`}
|
class={`session-item-base ${props.activeSessionId === "info" ? "session-item-active" : "session-item-inactive"}`}
|
||||||
onClick={() => props.onSelect("info")}
|
onClick={() => selectSession("info")}
|
||||||
title="Instance Info"
|
title="Instance Info"
|
||||||
role="button"
|
role="button"
|
||||||
aria-selected={props.activeSessionId === "info"}
|
aria-selected={props.activeSessionId === "info"}
|
||||||
@@ -280,7 +285,7 @@ const SessionList: Component<SessionListProps> = (props) => {
|
|||||||
<div class="session-list-item group">
|
<div class="session-list-item group">
|
||||||
<button
|
<button
|
||||||
class={`session-item-base ${isActive() ? "session-item-active" : "session-item-inactive"}`}
|
class={`session-item-base ${isActive() ? "session-item-active" : "session-item-inactive"}`}
|
||||||
onClick={() => props.onSelect(id)}
|
onClick={() => selectSession(id)}
|
||||||
title={title()}
|
title={title()}
|
||||||
role="button"
|
role="button"
|
||||||
aria-selected={isActive()}
|
aria-selected={isActive()}
|
||||||
@@ -338,7 +343,7 @@ const SessionList: Component<SessionListProps> = (props) => {
|
|||||||
<div class="session-list-item group">
|
<div class="session-list-item group">
|
||||||
<button
|
<button
|
||||||
class={`session-item-base ${isActive() ? "session-item-active" : "session-item-inactive"}`}
|
class={`session-item-base ${isActive() ? "session-item-active" : "session-item-inactive"}`}
|
||||||
onClick={() => props.onSelect(id)}
|
onClick={() => selectSession(id)}
|
||||||
title={title()}
|
title={title()}
|
||||||
role="button"
|
role="button"
|
||||||
aria-selected={isActive()}
|
aria-selected={isActive()}
|
||||||
|
|||||||
@@ -5,12 +5,24 @@ import { ToolCallDiffViewer } from "./diff-viewer"
|
|||||||
import { useTheme } from "../lib/theme"
|
import { useTheme } from "../lib/theme"
|
||||||
import { getLanguageFromPath } from "../lib/markdown"
|
import { getLanguageFromPath } from "../lib/markdown"
|
||||||
import { isRenderableDiffText } from "../lib/diff-utils"
|
import { isRenderableDiffText } from "../lib/diff-utils"
|
||||||
import { preferences, setDiffViewMode, type DiffViewMode } from "../stores/preferences"
|
import { getToolRenderCache, setToolRenderCache } from "../lib/tool-render-cache"
|
||||||
import type { TextPart } from "../types/message"
|
import type { TextPart } from "../types/message"
|
||||||
|
|
||||||
|
|
||||||
const toolScrollState = new Map<string, { scrollTop: number; atBottom: boolean }>()
|
const toolScrollState = new Map<string, { scrollTop: number; atBottom: boolean }>()
|
||||||
|
|
||||||
|
function makeRenderCacheKey(
|
||||||
|
baseId?: string | null,
|
||||||
|
messageId?: string,
|
||||||
|
messageVersion?: number,
|
||||||
|
partVersion?: number,
|
||||||
|
) {
|
||||||
|
if (!baseId && !messageId) return undefined
|
||||||
|
const suffix = `${messageVersion ?? 0}:${partVersion ?? 0}`
|
||||||
|
const keyBase = baseId || messageId || "tool"
|
||||||
|
return `${keyBase}::${suffix}`
|
||||||
|
}
|
||||||
|
|
||||||
function updateScrollState(id: string, element: HTMLElement) {
|
function updateScrollState(id: string, element: HTMLElement) {
|
||||||
if (!id) return
|
if (!id) return
|
||||||
const distanceFromBottom = element.scrollHeight - (element.scrollTop + element.clientHeight)
|
const distanceFromBottom = element.scrollHeight - (element.scrollTop + element.clientHeight)
|
||||||
@@ -44,6 +56,9 @@ function restoreScrollState(id: string, element: HTMLElement) {
|
|||||||
interface ToolCallProps {
|
interface ToolCallProps {
|
||||||
toolCall: any
|
toolCall: any
|
||||||
toolCallId?: string
|
toolCallId?: string
|
||||||
|
messageId?: string
|
||||||
|
messageVersion?: number
|
||||||
|
partVersion?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
function getToolIcon(tool: string): string {
|
function getToolIcon(tool: string): string {
|
||||||
@@ -367,11 +382,9 @@ export default function ToolCall(props: ToolCallProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderDiffTool(payload: DiffPayload) {
|
function renderDiffTool(payload: DiffPayload) {
|
||||||
const diffMode = () => (preferences().diffViewMode || "split") as DiffViewMode
|
const relativePath = payload.filePath ? getRelativePath(payload.filePath) : ""
|
||||||
|
const toolbarLabel = relativePath ? `Diff · ${relativePath}` : "Diff"
|
||||||
const handleModeChange = (mode: DiffViewMode) => {
|
const cacheKey = makeRenderCacheKey(toolCallId(), props.messageId, props.messageVersion, props.partVersion)
|
||||||
setDiffViewMode(mode)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -379,32 +392,14 @@ export default function ToolCall(props: ToolCallProps) {
|
|||||||
ref={(element) => initializeScrollContainer(element)}
|
ref={(element) => initializeScrollContainer(element)}
|
||||||
onScroll={(event) => updateScrollState(toolCallId(), event.currentTarget)}
|
onScroll={(event) => updateScrollState(toolCallId(), event.currentTarget)}
|
||||||
>
|
>
|
||||||
<div class="tool-call-diff-toolbar" role="group" aria-label="Diff view mode">
|
<div class="tool-call-diff-toolbar">
|
||||||
<span class="tool-call-diff-toolbar-label">Diff view</span>
|
<span class="tool-call-diff-toolbar-label">{toolbarLabel}</span>
|
||||||
<div class="tool-call-diff-toggle">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class={`tool-call-diff-mode-button${diffMode() === "split" ? " active" : ""}`}
|
|
||||||
aria-pressed={diffMode() === "split"}
|
|
||||||
onClick={() => handleModeChange("split")}
|
|
||||||
>
|
|
||||||
Split
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class={`tool-call-diff-mode-button${diffMode() === "unified" ? " active" : ""}`}
|
|
||||||
aria-pressed={diffMode() === "unified"}
|
|
||||||
onClick={() => handleModeChange("unified")}
|
|
||||||
>
|
|
||||||
Unified
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<ToolCallDiffViewer
|
<ToolCallDiffViewer
|
||||||
diffText={payload.diffText}
|
diffText={payload.diffText}
|
||||||
filePath={payload.filePath}
|
filePath={payload.filePath}
|
||||||
theme={isDark() ? "dark" : "light"}
|
theme={isDark() ? "dark" : "light"}
|
||||||
mode={diffMode()}
|
renderCacheKey={cacheKey}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -419,8 +414,22 @@ export default function ToolCall(props: ToolCallProps) {
|
|||||||
const isLarge = toolName === "edit" || toolName === "write" || toolName === "patch"
|
const isLarge = toolName === "edit" || toolName === "write" || toolName === "patch"
|
||||||
const messageClass = `message-text tool-call-markdown${isLarge ? " tool-call-markdown-large" : ""}`
|
const messageClass = `message-text tool-call-markdown${isLarge ? " tool-call-markdown-large" : ""}`
|
||||||
const disableHighlight = state?.status === "running"
|
const disableHighlight = state?.status === "running"
|
||||||
|
const cacheKey = makeRenderCacheKey(toolCallId(), props.messageId, props.messageVersion, props.partVersion)
|
||||||
|
|
||||||
const markdownPart: TextPart = { type: "text", text: content }
|
const markdownPart: TextPart = { type: "text", text: content }
|
||||||
|
if (cacheKey) {
|
||||||
|
const cached = getToolRenderCache(cacheKey)
|
||||||
|
if (cached) {
|
||||||
|
markdownPart.renderCache = cached
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMarkdownRendered = () => {
|
||||||
|
if (cacheKey) {
|
||||||
|
setToolRenderCache(cacheKey, markdownPart.renderCache)
|
||||||
|
}
|
||||||
|
handleScrollRendered()
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -432,7 +441,7 @@ export default function ToolCall(props: ToolCallProps) {
|
|||||||
part={markdownPart}
|
part={markdownPart}
|
||||||
isDark={isDark()}
|
isDark={isDark()}
|
||||||
disableHighlight={disableHighlight}
|
disableHighlight={disableHighlight}
|
||||||
onRendered={handleScrollRendered}
|
onRendered={handleMarkdownRendered}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
22
src/lib/tool-render-cache.ts
Normal file
22
src/lib/tool-render-cache.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import type { RenderCache } from "../types/message"
|
||||||
|
|
||||||
|
const toolRenderCache = new Map<string, RenderCache>()
|
||||||
|
|
||||||
|
export function getToolRenderCache(key?: string | null): RenderCache | undefined {
|
||||||
|
if (!key) return undefined
|
||||||
|
return toolRenderCache.get(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setToolRenderCache(key: string | undefined | null, cache?: RenderCache): void {
|
||||||
|
if (!key) return
|
||||||
|
if (cache) {
|
||||||
|
toolRenderCache.set(key, cache)
|
||||||
|
} else {
|
||||||
|
toolRenderCache.delete(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearToolRenderCache(key?: string | null): void {
|
||||||
|
if (!key) return
|
||||||
|
toolRenderCache.delete(key)
|
||||||
|
}
|
||||||
@@ -17,6 +17,27 @@ interface SessionInfo {
|
|||||||
contextUsageTokens: number
|
contextUsageTokens: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SessionForkResponse {
|
||||||
|
id: string
|
||||||
|
title?: string
|
||||||
|
parentID?: string | null
|
||||||
|
agent?: string
|
||||||
|
model?: {
|
||||||
|
providerID?: string
|
||||||
|
modelID?: string
|
||||||
|
}
|
||||||
|
time?: {
|
||||||
|
created?: number
|
||||||
|
updated?: number
|
||||||
|
}
|
||||||
|
revert?: {
|
||||||
|
messageID?: string
|
||||||
|
partID?: string
|
||||||
|
snapshot?: string
|
||||||
|
diff?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const DEFAULT_MODEL_OUTPUT_LIMIT = 32_000
|
const DEFAULT_MODEL_OUTPUT_LIMIT = 32_000
|
||||||
const ALLOWED_TOAST_VARIANTS = new Set<ToastVariant>(["info", "success", "warning", "error"])
|
const ALLOWED_TOAST_VARIANTS = new Set<ToastVariant>(["info", "success", "warning", "error"])
|
||||||
|
|
||||||
@@ -103,13 +124,6 @@ const [loading, setLoading] = createSignal({
|
|||||||
|
|
||||||
const [messagesLoaded, setMessagesLoaded] = createSignal<Map<string, Set<string>>>(new Map())
|
const [messagesLoaded, setMessagesLoaded] = createSignal<Map<string, Set<string>>>(new Map())
|
||||||
const [sessionInfoByInstance, setSessionInfoByInstance] = createSignal<Map<string, Map<string, SessionInfo>>>(new Map())
|
const [sessionInfoByInstance, setSessionInfoByInstance] = createSignal<Map<string, Map<string, SessionInfo>>>(new Map())
|
||||||
if (typeof globalThis !== "undefined") {
|
|
||||||
const debugGlobal = globalThis as any
|
|
||||||
debugGlobal.__OPENCODE_DEBUG__ = {
|
|
||||||
...(debugGlobal.__OPENCODE_DEBUG__ ?? {}),
|
|
||||||
getSessions: () => sessions(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Message index cache structure: instanceId -> sessionId -> { messageIndex, partIndex }
|
// Message index cache structure: instanceId -> sessionId -> { messageIndex, partIndex }
|
||||||
const sessionIndexes = new Map<
|
const sessionIndexes = new Map<
|
||||||
@@ -718,8 +732,8 @@ async function forkSession(
|
|||||||
throw new Error("Failed to fork session: No data returned")
|
throw new Error("Failed to fork session: No data returned")
|
||||||
}
|
}
|
||||||
|
|
||||||
const info = response.data
|
const info = response.data as SessionForkResponse
|
||||||
const forkedSession: Session = {
|
const forkedSession = {
|
||||||
id: info.id,
|
id: info.id,
|
||||||
instanceId,
|
instanceId,
|
||||||
title: info.title || "Forked Session",
|
title: info.title || "Forked Session",
|
||||||
@@ -743,7 +757,7 @@ async function forkSession(
|
|||||||
: undefined,
|
: undefined,
|
||||||
messages: [],
|
messages: [],
|
||||||
messagesInfo: new Map(),
|
messagesInfo: new Map(),
|
||||||
}
|
} as unknown as Session
|
||||||
|
|
||||||
setSessions((prev) => {
|
setSessions((prev) => {
|
||||||
const next = new Map(prev)
|
const next = new Map(prev)
|
||||||
|
|||||||
Reference in New Issue
Block a user