Fix shiki and diff caching

This commit is contained in:
Shantur Rathore
2025-11-10 21:54:01 +00:00
parent 910249ff25
commit d277d50ed7
8 changed files with 197 additions and 102 deletions

View File

@@ -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)

View File

@@ -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>
) )
} }

View File

@@ -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>
) )
}} }}

View File

@@ -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)

View File

@@ -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()}

View File

@@ -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>
) )

View 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)
}

View File

@@ -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)