From d7e1cde37f6b2af48fd6274d99f9085f32080b55 Mon Sep 17 00:00:00 2001 From: dbpolito Date: Sun, 11 Jan 2026 16:08:00 -0300 Subject: [PATCH 1/2] feat(desktop): Fork Message from Message Nav --- packages/app/src/pages/session.tsx | 17 ++++++ packages/ui/src/components/message-nav.css | 52 +++++++++++++++++++ packages/ui/src/components/message-nav.tsx | 34 ++++++++---- .../src/components/session-message-rail.tsx | 13 ++++- 4 files changed, 106 insertions(+), 10 deletions(-) diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 69065a8fa7a0..c5fde864b1d1 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -908,6 +908,22 @@ export default function Page() { updateHash(message.id) } + const forkFromMessage = (message: UserMessage) => { + const sessionID = params.id + if (!sessionID) return + + const parts = sync.data.part[message.id] ?? [] + const restored = extractPromptFromParts(parts, { directory: sdk.directory }) + + sdk.client.session.fork({ sessionID, messageID: message.id }).then((forked) => { + if (!forked.data) return + navigate(`/${base64Encode(sdk.directory)}/session/${forked.data.id}`) + requestAnimationFrame(() => { + prompt.set(restored) + }) + }) + } + const getActiveMessageId = (container: HTMLDivElement) => { const cutoff = container.scrollTop + 100 const nodes = container.querySelectorAll("[data-message-id]") @@ -1095,6 +1111,7 @@ export default function Page() { messages={visibleUserMessages()} current={activeMessage()} onMessageSelect={scrollToMessage} + onFork={forkFromMessage} wide={!showTabs()} class="pointer-events-auto" /> diff --git a/packages/ui/src/components/message-nav.css b/packages/ui/src/components/message-nav.css index 465bd66fe5b1..3ef07179bb0a 100644 --- a/packages/ui/src/components/message-nav.css +++ b/packages/ui/src/components/message-nav.css @@ -13,6 +13,7 @@ } [data-slot="message-nav-item"] { + position: relative; display: flex; align-items: center; align-self: stretch; @@ -91,6 +92,57 @@ color: var(--text-base); } +[data-slot="message-nav-item"] > [data-component="tooltip-trigger"] { + position: absolute; + top: 50%; + right: 4px; + transform: translateY(-50%); + opacity: 0; + transition: opacity 0.15s; +} + +[data-slot="message-nav-item"]:hover > [data-component="tooltip-trigger"] { + opacity: 1; +} + +[data-slot="message-nav-fork-button"] { + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border: none; + background-color: var(--smoke-dark-5); + padding: 0; + cursor: pointer; + color: var(--icon-base); + border-radius: var(--radius-sm); + transition: + background-color 0.15s, + color 0.15s; +} + +[data-slot="message-nav-fork-button"]:hover { + background-color: var(--smoke-dark-6); + color: var(--icon-strong-base); +} + +[data-slot="message-nav-fork-button"]:active { + background-color: var(--smoke-dark-7); +} + +[data-slot="message-nav-tooltip-content"] [data-slot="message-nav-fork-button"] { + background-color: var(--smoke-dark-6); +} + +[data-slot="message-nav-tooltip-content"] [data-slot="message-nav-fork-button"]:hover { + background-color: var(--smoke-dark-7); +} + +[data-slot="message-nav-tooltip-content"] [data-slot="message-nav-fork-button"]:active { + background-color: var(--smoke-dark-8); +} + [data-slot="message-nav-tooltip"] { z-index: 1000; } diff --git a/packages/ui/src/components/message-nav.tsx b/packages/ui/src/components/message-nav.tsx index 7416cfd9398a..463e1e58901f 100644 --- a/packages/ui/src/components/message-nav.tsx +++ b/packages/ui/src/components/message-nav.tsx @@ -1,7 +1,9 @@ import { UserMessage } from "@opencode-ai/sdk/v2" import { ComponentProps, For, Match, Show, splitProps, Switch } from "solid-js" import { DiffChanges } from "./diff-changes" -import { Tooltip } from "@kobalte/core/tooltip" +import { Tooltip as KobalteTooltip } from "@kobalte/core/tooltip" +import { Tooltip } from "./tooltip" +import { Icon } from "./icon" export function MessageNav( props: ComponentProps<"ul"> & { @@ -9,9 +11,10 @@ export function MessageNav( current?: UserMessage size: "normal" | "compact" onMessageSelect: (message: UserMessage) => void + onFork?: (message: UserMessage) => void }, ) { - const [local, others] = splitProps(props, ["messages", "current", "size", "onMessageSelect"]) + const [local, others] = splitProps(props, ["messages", "current", "size", "onMessageSelect", "onFork"]) const content = () => (