From b18f2e2afa86137b5f30991229907c92f950e19b Mon Sep 17 00:00:00 2001 From: Linus Schlumberger Date: Tue, 17 Mar 2026 16:58:06 +0100 Subject: [PATCH] feat(app): add fork and revert functionality Close #8885 --- packages/app/src/pages/session.tsx | 10 +++++ .../src/pages/session/message-timeline.tsx | 4 ++ packages/ui/src/components/session-turn.css | 45 +++++++++++++++++++ packages/ui/src/components/session-turn.tsx | 44 +++++++++++++++++- 4 files changed, 102 insertions(+), 1 deletion(-) diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 6d29170081ab..197c55ada703 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -1732,6 +1732,16 @@ export default function Page() { }} renderedUserMessages={historyWindow.renderedUserMessages()} anchor={anchor} + onRevert={(messageID) => { + const sessionID = params.id + if (!sessionID) return + void revert({ sessionID, messageID }) + }} + onFork={(messageID) => { + const sessionID = params.id + if (!sessionID) return + void fork({ sessionID, messageID }) + }} /> diff --git a/packages/app/src/pages/session/message-timeline.tsx b/packages/app/src/pages/session/message-timeline.tsx index 74f2e8c2c12d..a98712263b8c 100644 --- a/packages/app/src/pages/session/message-timeline.tsx +++ b/packages/app/src/pages/session/message-timeline.tsx @@ -216,6 +216,8 @@ export function MessageTimeline(props: { onLoadEarlier: () => void renderedUserMessages: UserMessage[] anchor: (id: string) => string + onRevert?: (messageID: string) => void + onFork?: (messageID: string) => void }) { let touchGesture: number | undefined @@ -1007,6 +1009,8 @@ export function MessageTimeline(props: { showReasoningSummaries={settings.general.showReasoningSummaries()} shellToolDefaultOpen={settings.general.shellToolPartsExpanded()} editToolDefaultOpen={settings.general.editToolPartsExpanded()} + onRevert={props.onRevert ? () => props.onRevert!(messageID) : undefined} + onFork={props.onFork ? () => props.onFork!(messageID) : undefined} classes={{ root: "min-w-0 w-full relative", content: "flex flex-col justify-between !overflow-visible", diff --git a/packages/ui/src/components/session-turn.css b/packages/ui/src/components/session-turn.css index 26d918050d7f..47f641b6400c 100644 --- a/packages/ui/src/components/session-turn.css +++ b/packages/ui/src/components/session-turn.css @@ -28,6 +28,51 @@ min-width: 0; gap: 18px; overflow-anchor: none; + position: relative; + + [data-slot="session-turn-header"] { + position: absolute; + right: 0; + display: flex; + gap: 8px; + align-items: center; + padding-left: 16px; + background: linear-gradient(to right, transparent, var(--background-stronger) 12px); + opacity: 0; + transition: opacity 0.15s ease; + pointer-events: none; + } + + &:hover [data-slot="session-turn-header"] { + opacity: 1; + pointer-events: auto; + } + + [data-slot="session-turn-badge"] { + display: inline-flex; + align-items: center; + padding: 2px 6px; + border-radius: 4px; + font-family: var(--font-family-mono); + font-size: var(--font-size-x-small); + font-weight: var(--font-weight-medium); + line-height: var(--line-height-normal); + white-space: nowrap; + color: var(--text-base); + background: var(--surface-raised-base); + } + } + + [data-slot="session-turn-user-badges"] { + display: flex; + align-items: center; + gap: 6px; + padding-left: 16px; + } + + [data-slot="session-turn-message-actions"] { + display: flex; + gap: 4px; } [data-slot="session-turn-message-content"] { diff --git a/packages/ui/src/components/session-turn.tsx b/packages/ui/src/components/session-turn.tsx index 8c9c1ffe4030..e4afaea42bce 100644 --- a/packages/ui/src/components/session-turn.tsx +++ b/packages/ui/src/components/session-turn.tsx @@ -1,4 +1,10 @@ -import { AssistantMessage, type FileDiff, Message as MessageType, Part as PartType } from "@opencode-ai/sdk/v2/client" +import { + AssistantMessage, + type FileDiff, + Message as MessageType, + Part as PartType, + UserMessage, +} from "@opencode-ai/sdk/v2/client" import type { SessionStatus } from "@opencode-ai/sdk/v2" import { useData } from "../context" import { useFileComponent } from "../context/file" @@ -18,6 +24,10 @@ import { Icon } from "./icon" import { TextShimmer } from "./text-shimmer" import { SessionRetry } from "./session-retry" import { TextReveal } from "./text-reveal" +import { ProviderIcon } from "./provider-icon" +import type { IconName } from "./provider-icons/types" +import { Tooltip } from "./tooltip" +import { IconButton } from "./icon-button" import { createAutoScroll } from "../hooks" import { useI18n } from "../context/i18n" @@ -149,6 +159,8 @@ export function SessionTurn( active?: boolean status?: SessionStatus onUserInteracted?: () => void + onRevert?: () => void + onFork?: () => void classes?: { root?: string content?: string @@ -393,6 +405,36 @@ export function SessionTurn( data-slot="session-turn-message-container" class={props.classes?.container} > + +
+
+ + + + + + + + + + +
+
+ + {(message()! as UserMessage).agent} + + + + + {(message()! as UserMessage).model?.modelID} + + +
+
+