feat(app): restore to message and fork session (#17092)

This commit is contained in:
Adam 2026-03-11 16:34:48 -05:00 committed by GitHub
parent 58f45ae22b
commit fbd9b7cf4f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 328 additions and 6 deletions

View File

@ -530,6 +530,11 @@ export const dict = {
"session.todo.title": "Todos",
"session.todo.collapse": "Collapse",
"session.todo.expand": "Expand",
"session.revertDock.summary.one": "{{count}} rolled back message",
"session.revertDock.summary.other": "{{count}} rolled back messages",
"session.revertDock.collapse": "Collapse rolled back messages",
"session.revertDock.expand": "Expand rolled back messages",
"session.revertDock.restore": "Restore message",
"session.new.title": "Build anything",
"session.new.worktree.main": "Main branch",

View File

@ -43,6 +43,7 @@ import { SessionSidePanel } from "@/pages/session/session-side-panel"
import { TerminalPanel } from "@/pages/session/terminal-panel"
import { useSessionCommands } from "@/pages/session/use-session-commands"
import { useSessionHashScroll } from "@/pages/session/use-session-hash-scroll"
import { extractPromptFromParts } from "@/utils/prompt"
import { same } from "@/utils/same"
import { formatServerError } from "@/utils/server-errors"
@ -286,6 +287,7 @@ export default function Page() {
const [ui, setUi] = createStore({
git: false,
pendingMessage: undefined as string | undefined,
restoring: undefined as string | undefined,
reviewSnap: false,
scrollGesture: 0,
scroll: {
@ -1179,6 +1181,110 @@ export default function Page() {
scroller: () => scroller,
})
const draft = (id: string) =>
extractPromptFromParts(sync.data.part[id] ?? [], {
directory: sdk.directory,
attachmentName: language.t("common.attachment"),
})
const line = (id: string) => {
const text = draft(id)
.map((part) => (part.type === "image" ? `[image:${part.filename}]` : part.content))
.join("")
.replace(/\s+/g, " ")
.trim()
if (text) return text
return `[${language.t("common.attachment")}]`
}
const fail = (err: unknown) => {
showToast({
variant: "error",
title: language.t("common.requestFailed"),
description: formatServerError(err, language.t),
})
}
const busy = (sessionID: string) => {
if (sync.data.session_status[sessionID]?.type !== "idle") return true
return (sync.data.message[sessionID] ?? []).some(
(item) => item.role === "assistant" && typeof item.time.completed !== "number",
)
}
const halt = (sessionID: string) =>
busy(sessionID) ? sdk.client.session.abort({ sessionID }).catch(() => {}) : Promise.resolve()
const fork = (input: { sessionID: string; messageID: string }) => {
const value = draft(input.messageID)
return sdk.client.session
.fork(input)
.then((result) => {
const next = result.data
if (!next) {
showToast({
variant: "error",
title: language.t("common.requestFailed"),
})
return
}
navigate(`/${base64Encode(sdk.directory)}/session/${next.id}`)
requestAnimationFrame(() => {
prompt.set(value)
})
})
.catch(fail)
}
const revert = (input: { sessionID: string; messageID: string }) => {
const value = draft(input.messageID)
return halt(input.sessionID)
.then(() => sdk.client.session.revert(input))
.then(() => {
prompt.set(value)
})
.catch(fail)
}
const restore = (id: string) => {
const sessionID = params.id
if (!sessionID || ui.restoring) return
const next = userMessages().find((item) => item.id > id)
setUi("restoring", id)
const task = !next
? halt(sessionID)
.then(() => sdk.client.session.unrevert({ sessionID }))
.then(() => {
prompt.reset()
})
: halt(sessionID)
.then(() =>
sdk.client.session.revert({
sessionID,
messageID: next.id,
}),
)
.then(() => {
prompt.set(draft(next.id))
})
return task.catch(fail).finally(() => {
setUi("restoring", (value) => (value === id ? undefined : value))
})
}
const rolled = createMemo(() => {
const id = revertMessageID()
if (!id) return []
return userMessages()
.filter((item) => item.id >= id)
.map((item) => ({ id: item.id, text: line(item.id) }))
})
const actions = { fork, revert }
createResizeObserver(
() => promptDock,
({ height }) => {
@ -1268,6 +1374,7 @@ export default function Page() {
loadingClass: "px-4 py-4 text-text-weak",
emptyClass: "h-full pb-64 -mt-4 flex flex-col items-center justify-center text-center gap-6",
})}
actions={actions}
scroll={ui.scroll}
onResumeScroll={resumeScroll}
setScrollRef={setScrollRef}
@ -1333,6 +1440,15 @@ export default function Page() {
resumeScroll()
}}
onResponseSubmit={resumeScroll}
revert={
rolled().length > 0
? {
items: rolled(),
restoring: ui.restoring,
onRestore: restore,
}
: undefined
}
setPromptDockRef={(el) => {
promptDock = el
}}

View File

@ -8,6 +8,7 @@ import { usePrompt } from "@/context/prompt"
import { getSessionHandoff, setSessionHandoff } from "@/pages/session/handoff"
import { SessionPermissionDock } from "@/pages/session/composer/session-permission-dock"
import { SessionQuestionDock } from "@/pages/session/composer/session-question-dock"
import { SessionRevertDock } from "@/pages/session/composer/session-revert-dock"
import type { SessionComposerState } from "@/pages/session/composer/session-composer-state"
import { SessionTodoDock } from "@/pages/session/composer/session-todo-dock"
@ -20,6 +21,11 @@ export function SessionComposerRegion(props: {
onNewSessionWorktreeReset: () => void
onSubmit: () => void
onResponseSubmit: () => void
revert?: {
items: { id: string; text: string }[]
restoring?: string
onRestore: (id: string) => void
}
setPromptDockRef: (el: HTMLDivElement) => void
visualDuration?: number
bounce?: number
@ -116,6 +122,8 @@ export function SessionComposerRegion(props: {
const value = createMemo(() => Math.max(0, Math.min(1, progress())))
const [height, setHeight] = createSignal(320)
const dock = createMemo(() => (gate.ready && props.state.dock()) || value() > 0.001)
const rolled = createMemo(() => (props.revert?.items.length ? props.revert : undefined))
const lift = createMemo(() => (rolled() ? 18 : 36 * value()))
const full = createMemo(() => Math.max(78, height()))
const [contentRef, setContentRef] = createSignal<HTMLDivElement>()
@ -170,9 +178,22 @@ export function SessionComposerRegion(props: {
<Show
when={prompt.ready()}
fallback={
<div class="w-full min-h-32 md:min-h-40 rounded-md border border-border-weak-base bg-background-base/50 px-4 py-3 text-text-weak whitespace-pre-wrap pointer-events-none">
{handoffPrompt() || language.t("prompt.loading")}
</div>
<>
<Show when={rolled()} keyed>
{(revert) => (
<div class="pb-2">
<SessionRevertDock
items={revert.items}
restoring={revert.restoring}
onRestore={revert.onRestore}
/>
</div>
)}
</Show>
<div class="w-full min-h-32 md:min-h-40 rounded-md border border-border-weak-base bg-background-base/50 px-4 py-3 text-text-weak whitespace-pre-wrap pointer-events-none">
{handoffPrompt() || language.t("prompt.loading")}
</div>
</>
}
>
<Show when={dock()}>
@ -209,12 +230,23 @@ export function SessionComposerRegion(props: {
</div>
</div>
</Show>
<Show when={rolled()} keyed>
{(revert) => (
<div
style={{
"margin-top": `${-36 * value()}px`,
}}
>
<SessionRevertDock items={revert.items} restoring={revert.restoring} onRestore={revert.onRestore} />
</div>
)}
</Show>
<div
classList={{
"relative z-10": true,
}}
style={{
"margin-top": `${-36 * value()}px`,
"margin-top": `${-lift()}px`,
}}
>
<PromptInput

View File

@ -0,0 +1,92 @@
import { For, Show, createMemo } from "solid-js"
import { createStore } from "solid-js/store"
import { Button } from "@opencode-ai/ui/button"
import { DockTray } from "@opencode-ai/ui/dock-surface"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { useLanguage } from "@/context/language"
export function SessionRevertDock(props: {
items: { id: string; text: string }[]
restoring?: string
onRestore: (id: string) => void
}) {
const language = useLanguage()
const [store, setStore] = createStore({
collapsed: false,
})
const toggle = () => setStore("collapsed", (value) => !value)
const total = createMemo(() => props.items.length)
const label = createMemo(() =>
language.t(total() === 1 ? "session.revertDock.summary.one" : "session.revertDock.summary.other", {
count: total(),
}),
)
const preview = createMemo(() => props.items[0]?.text ?? "")
return (
<DockTray data-component="session-revert-dock">
<div
class="pl-3 pr-2 py-2 flex items-center gap-2"
role="button"
tabIndex={0}
onClick={toggle}
onKeyDown={(event) => {
if (event.key !== "Enter" && event.key !== " ") return
event.preventDefault()
toggle()
}}
>
<span class="shrink-0 text-14-regular text-text-strong cursor-default">{label()}</span>
<Show when={store.collapsed && preview()}>
<span class="min-w-0 flex-1 truncate text-14-regular text-text-base cursor-default">{preview()}</span>
</Show>
<div class="ml-auto shrink-0">
<IconButton
data-collapsed={store.collapsed ? "true" : "false"}
icon="chevron-down"
size="normal"
variant="ghost"
style={{ transform: `rotate(${store.collapsed ? 180 : 0}deg)` }}
onMouseDown={(event) => {
event.preventDefault()
event.stopPropagation()
}}
onClick={(event) => {
event.stopPropagation()
toggle()
}}
aria-label={
store.collapsed ? language.t("session.revertDock.expand") : language.t("session.revertDock.collapse")
}
/>
</div>
</div>
<Show when={store.collapsed}>
<div class="h-5" aria-hidden="true" />
</Show>
<Show when={!store.collapsed}>
<div class="px-3 pb-11 flex flex-col gap-1.5 max-h-42 overflow-y-auto no-scrollbar">
<For each={props.items}>
{(item) => (
<div class="flex items-center gap-2 min-w-0 rounded-[10px] border border-border-weak-base bg-background-stronger px-2.5 py-2">
<span class="min-w-0 flex-1 truncate text-13-regular text-text-strong">{item.text}</span>
<Button
size="small"
variant="secondary"
class="shrink-0"
disabled={!!props.restoring}
onClick={() => props.onRestore(item.id)}
>
{language.t("session.revertDock.restore")}
</Button>
</div>
)}
</For>
</div>
</Show>
</DockTray>
)
}

View File

@ -36,6 +36,11 @@ type MessageComment = {
const emptyMessages: MessageType[] = []
const idle = { type: "idle" as const }
type UserActions = {
fork?: (input: { sessionID: string; messageID: string }) => Promise<void> | void
revert?: (input: { sessionID: string; messageID: string }) => Promise<void> | void
}
const messageComments = (parts: Part[]): MessageComment[] =>
parts.flatMap((part) => {
if (part.type !== "text" || !(part as TextPart).synthetic) return []
@ -186,6 +191,7 @@ function createTimelineStaging(input: TimelineStageInput) {
export function MessageTimeline(props: {
mobileChanges: boolean
mobileFallback: JSX.Element
actions?: UserActions
scroll: { overflow: boolean; bottom: boolean }
onResumeScroll: () => void
setScrollRef: (el: HTMLDivElement | undefined) => void
@ -805,6 +811,7 @@ export function MessageTimeline(props: {
<SessionTurn
sessionID={sessionID() ?? ""}
messageID={messageID}
actions={props.actions}
active={active()}
queued={queued()}
status={active() ? sessionStatus() : undefined}

View File

@ -9,6 +9,7 @@ const icons = {
"bubble-5": `<path d="M18.3327 9.99935C18.3327 5.57227 15.0919 2.91602 9.99935 2.91602C4.90676 2.91602 1.66602 5.57227 1.66602 9.99935C1.66602 11.1487 2.45505 13.1006 2.57637 13.3939C2.58707 13.4197 2.59766 13.4434 2.60729 13.4697C2.69121 13.6987 3.04209 14.9354 1.66602 16.7674C3.51787 17.6528 5.48453 16.1973 5.48453 16.1973C6.84518 16.9193 8.46417 17.0827 9.99935 17.0827C15.0919 17.0827 18.3327 14.4264 18.3327 9.99935Z" stroke="currentColor" stroke-linecap="square"/>`,
prompt: `<path d="M14.5841 12.0807H17.9193V2.91406H5.6276V6.2474M14.5859 6.2474H2.08594V15.4141H5.0026V17.4974L8.7526 15.4141H14.5859V6.2474Z" stroke="currentColor" stroke-linecap="square"/>`,
brain: `<path d="M13.332 8.7487C11.4911 8.7487 9.9987 7.25631 9.9987 5.41536M6.66536 11.2487C8.50631 11.2487 9.9987 12.7411 9.9987 14.582M9.9987 2.78209L9.9987 17.0658M16.004 15.0475C17.1255 14.5876 17.9154 13.4849 17.9154 12.1978C17.9154 11.3363 17.5615 10.5575 16.9913 9.9987C17.5615 9.43991 17.9154 8.66108 17.9154 7.79962C17.9154 6.21199 16.7136 4.90504 15.1702 4.73878C14.7858 3.21216 13.4039 2.08203 11.758 2.08203C11.1171 2.08203 10.5162 2.25337 9.9987 2.55275C9.48117 2.25337 8.88032 2.08203 8.23944 2.08203C6.59353 2.08203 5.21157 3.21216 4.82722 4.73878C3.28377 4.90504 2.08203 6.21199 2.08203 7.79962C2.08203 8.66108 2.43585 9.43991 3.00609 9.9987C2.43585 10.5575 2.08203 11.3363 2.08203 12.1978C2.08203 13.4849 2.87191 14.5876 3.99339 15.0475C4.46688 16.7033 5.9917 17.9154 7.79962 17.9154C8.61335 17.9154 9.36972 17.6698 9.9987 17.2488C10.6277 17.6698 11.384 17.9154 12.1978 17.9154C14.0057 17.9154 15.5305 16.7033 16.004 15.0475Z" stroke="currentColor"/>`,
fork: `<path d="M2.91602 7.91406L2.91602 2.91406H7.91602M12.0827 2.91406H17.0827L17.0827 7.91406M9.99935 9.9974L9.99935 17.0807M9.99935 9.9974L3.33268 3.33073M9.99935 9.9974L16.666 3.33073" stroke="currentColor" stroke-linecap="square"/>`,
"bullet-list": `<path d="M9.58329 13.7497H17.0833M9.58329 6.24967H17.0833M6.24996 6.24967C6.24996 7.17015 5.50377 7.91634 4.58329 7.91634C3.66282 7.91634 2.91663 7.17015 2.91663 6.24967C2.91663 5.3292 3.66282 4.58301 4.58329 4.58301C5.50377 4.58301 6.24996 5.3292 6.24996 6.24967ZM6.24996 13.7497C6.24996 14.6701 5.50377 15.4163 4.58329 15.4163C3.66282 15.4163 2.91663 14.6701 2.91663 13.7497C2.91663 12.8292 3.66282 12.083 4.58329 12.083C5.50377 12.083 6.24996 12.8292 6.24996 13.7497Z" stroke="currentColor" stroke-linecap="square"/>`,
"check-small": `<path d="M6.5 11.4412L8.97059 13.5L13.5 6.5" stroke="currentColor" stroke-linecap="square"/>`,
"chevron-down": `<path d="M6.6665 8.33325L9.99984 11.6666L13.3332 8.33325" stroke="currentColor" stroke-linecap="square"/>`,
@ -80,6 +81,7 @@ const icons = {
selector: `<path d="M6.66626 12.5033L9.99959 15.8366L13.3329 12.5033M6.66626 7.50326L9.99959 4.16992L13.3329 7.50326" stroke="currentColor" stroke-linecap="square"/>`,
"arrow-down-to-line": `<path d="M15.2083 11.6667L10 16.875L4.79167 11.6667M10 16.25V3.125" stroke="currentColor" stroke-width="1.25" stroke-linecap="square"/>`,
warning: `<path d="M10 7.91667V11.6667M10 13.7417V13.75M10 2.5L1.875 16.25H18.125L10 2.5Z" stroke="currentColor" stroke-linecap="square"/>`,
reset: `<path d="M5.83333 4.16406L2.5 7.4974L5.83333 10.8307M3.33333 7.4974H17.9167V15.4141H10" stroke="currentColor" stroke-linecap="square"/>`,
link: `<path d="M2.08334 12.0833L1.72979 11.7298L1.37624 12.0833L1.72979 12.4369L2.08334 12.0833ZM7.91668 17.9167L7.56312 18.2702L7.91668 18.6238L8.27023 18.2702L7.91668 17.9167ZM17.9167 7.91666L18.2702 8.27022L18.6238 7.91666L18.2702 7.56311L17.9167 7.91666ZM12.0833 2.08333L12.4369 1.72977L12.0833 1.37622L11.7298 1.72977L12.0833 2.08333ZM8.39646 5.06311L8.0429 5.41666L8.75001 6.12377L9.10356 5.77021L8.75001 5.41666L8.39646 5.06311ZM5.77023 9.10355L6.12378 8.74999L5.41668 8.04289L5.06312 8.39644L5.41668 8.74999L5.77023 9.10355ZM14.2298 10.8964L13.8762 11.25L14.5833 11.9571L14.9369 11.6035L14.5833 11.25L14.2298 10.8964ZM11.6036 14.9369L11.9571 14.5833L11.25 13.8762L10.8965 14.2298L11.25 14.5833L11.6036 14.9369ZM7.14646 12.1464L6.7929 12.5L7.50001 13.2071L7.85356 12.8535L7.50001 12.5L7.14646 12.1464ZM12.8536 7.85355L13.2071 7.49999L12.5 6.79289L12.1465 7.14644L12.5 7.49999L12.8536 7.85355ZM2.08334 12.0833L1.72979 12.4369L7.56312 18.2702L7.91668 17.9167L8.27023 17.5631L2.4369 11.7298L2.08334 12.0833ZM17.9167 7.91666L18.2702 7.56311L12.4369 1.72977L12.0833 2.08333L11.7298 2.43688L17.5631 8.27022L17.9167 7.91666ZM12.0833 2.08333L11.7298 1.72977L8.39646 5.06311L8.75001 5.41666L9.10356 5.77021L12.4369 2.43688L12.0833 2.08333ZM5.41668 8.74999L5.06312 8.39644L1.72979 11.7298L2.08334 12.0833L2.4369 12.4369L5.77023 9.10355L5.41668 8.74999ZM14.5833 11.25L14.9369 11.6035L18.2702 8.27022L17.9167 7.91666L17.5631 7.56311L14.2298 10.8964L14.5833 11.25ZM7.91668 17.9167L8.27023 18.2702L11.6036 14.9369L11.25 14.5833L10.8965 14.2298L7.56312 17.5631L7.91668 17.9167ZM7.50001 12.5L7.85356 12.8535L12.8536 7.85355L12.5 7.49999L12.1465 7.14644L7.14646 12.1464L7.50001 12.5Z" fill="currentColor"/>`,
providers: `<path d="M10.0001 4.37562V2.875M13 4.37793V2.87793M7.00014 4.37793V2.875M10 17.1279V15.6279M13 17.1279V15.6279M7 17.1279V15.6279M15.625 13.0029H17.125M15.625 7.00293H17.125M15.625 10.0029H17.125M2.875 10.0029H4.375M2.875 13.0029H4.375M2.875 7.00293H4.375M4.375 4.37793H15.625V15.6279H4.375V4.37793ZM12.6241 10.0022C12.6241 11.4519 11.4488 12.6272 9.99908 12.6272C8.54934 12.6272 7.37408 11.4519 7.37408 10.0022C7.37408 8.55245 8.54934 7.3772 9.99908 7.3772C11.4488 7.3772 12.6241 8.55245 12.6241 10.0022Z" stroke="currentColor" stroke-linecap="square"/>`,
models: `<path fill-rule="evenodd" clip-rule="evenodd" d="M17.5 10C12.2917 10 10 12.2917 10 17.5C10 12.2917 7.70833 10 2.5 10C7.70833 10 10 7.70833 10 2.5C10 7.70833 12.2917 10 17.5 10Z" stroke="currentColor"/>`,

View File

@ -129,12 +129,20 @@ function DiagnosticsDisplay(props: { diagnostics: Diagnostic[] }): JSX.Element {
export interface MessageProps {
message: MessageType
parts: PartType[]
actions?: UserActions
showAssistantCopyPartID?: string | null
interrupted?: boolean
queued?: boolean
showReasoningSummaries?: boolean
}
export type SessionAction = (input: { sessionID: string; messageID: string }) => Promise<void> | void
export type UserActions = {
fork?: SessionAction
revert?: SessionAction
}
export interface MessagePartProps {
part: PartType
message: MessageType
@ -676,6 +684,7 @@ export function Message(props: MessageProps) {
<UserMessageDisplay
message={userMessage() as UserMessage}
parts={props.parts}
actions={props.actions}
interrupted={props.interrupted}
queued={props.queued}
/>
@ -872,6 +881,7 @@ function ContextToolGroup(props: { parts: ToolPart[]; busy?: boolean }) {
export function UserMessageDisplay(props: {
message: UserMessage
parts: PartType[]
actions?: UserActions
interrupted?: boolean
queued?: boolean
}) {
@ -879,6 +889,7 @@ export function UserMessageDisplay(props: {
const dialog = useDialog()
const i18n = useI18n()
const [copied, setCopied] = createSignal(false)
const [busy, setBusy] = createSignal<"fork" | "revert" | undefined>()
const textPart = createMemo(
() => props.parts?.find((p) => p.type === "text" && !(p as TextPart).synthetic) as TextPart | undefined,
@ -945,6 +956,22 @@ export function UserMessageDisplay(props: {
setTimeout(() => setCopied(false), 2000)
}
const run = (kind: "fork" | "revert") => {
const act = kind === "fork" ? props.actions?.fork : props.actions?.revert
if (!act || busy()) return
setBusy(kind)
void Promise.resolve()
.then(() =>
act({
sessionID: props.message.sessionID,
messageID: props.message.id,
}),
)
.finally(() => {
if (busy() === kind) setBusy(undefined)
})
}
return (
<div data-component="user-message" data-interrupted={props.interrupted ? "" : undefined}>
<Show when={attachments().length > 0}>
@ -1012,6 +1039,38 @@ export function UserMessageDisplay(props: {
</Show>
</span>
</Show>
<Show when={props.actions?.fork}>
<Tooltip value={i18n.t("ui.message.forkMessage")} placement="top" gutter={4}>
<IconButton
icon="fork"
size="normal"
variant="ghost"
disabled={!!busy()}
onMouseDown={(e) => e.preventDefault()}
onClick={(event) => {
event.stopPropagation()
run("fork")
}}
aria-label={i18n.t("ui.message.forkMessage")}
/>
</Tooltip>
</Show>
<Show when={props.actions?.revert}>
<Tooltip value={i18n.t("ui.message.revertMessage")} placement="top" gutter={4}>
<IconButton
icon="reset"
size="normal"
variant="ghost"
disabled={!!busy()}
onMouseDown={(e) => e.preventDefault()}
onClick={(event) => {
event.stopPropagation()
run("revert")
}}
aria-label={i18n.t("ui.message.revertMessage")}
/>
</Tooltip>
</Show>
<Tooltip
value={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.message.copyMessage")}
placement="top"

View File

@ -7,7 +7,7 @@ import { Binary } from "@opencode-ai/util/binary"
import { getDirectory, getFilename } from "@opencode-ai/util/path"
import { createEffect, createMemo, createSignal, For, on, ParentProps, Show } from "solid-js"
import { Dynamic } from "solid-js/web"
import { AssistantParts, Message, Part, PART_MAPPING } from "./message-part"
import { AssistantParts, Message, Part, PART_MAPPING, type UserActions } from "./message-part"
import { Card } from "./card"
import { Accordion } from "./accordion"
import { StickyAccordionHeader } from "./sticky-accordion-header"
@ -141,6 +141,7 @@ export function SessionTurn(
props: ParentProps<{
sessionID: string
messageID: string
actions?: UserActions
showReasoningSummaries?: boolean
shellToolDefaultOpen?: boolean
editToolDefaultOpen?: boolean
@ -395,7 +396,13 @@ export function SessionTurn(
class={props.classes?.container}
>
<div data-slot="session-turn-message-content" aria-live="off">
<Message message={message()!} parts={parts()} interrupted={interrupted()} queued={queued()} />
<Message
message={message()!}
parts={parts()}
actions={props.actions}
interrupted={interrupted()}
queued={queued()}
/>
</div>
<Show when={compaction()}>
<div data-slot="session-turn-compaction">

View File

@ -127,6 +127,8 @@ export const dict: Record<string, string> = {
"ui.message.collapse": "Collapse message",
"ui.message.copy": "Copy",
"ui.message.copyMessage": "Copy message",
"ui.message.forkMessage": "Fork to new session",
"ui.message.revertMessage": "Reset to this point",
"ui.message.copyResponse": "Copy response",
"ui.message.copied": "Copied",
"ui.message.interrupted": "Interrupted",