diff --git a/packages/app/src/components/dialog-fork.tsx b/packages/app/src/components/dialog-fork.tsx index 472a1994f13f..da8561ae2f42 100644 --- a/packages/app/src/components/dialog-fork.tsx +++ b/packages/app/src/components/dialog-fork.tsx @@ -1,14 +1,11 @@ import { Component, createMemo } from "solid-js" -import { useNavigate, useParams } from "@solidjs/router" +import { useParams } from "@solidjs/router" import { useSync } from "@/context/sync" -import { useSDK } from "@/context/sdk" -import { usePrompt } from "@/context/prompt" import { useDialog } from "@opencode-ai/ui/context/dialog" import { Dialog } from "@opencode-ai/ui/dialog" import { List } from "@opencode-ai/ui/list" -import { extractPromptFromParts } from "@/utils/prompt" +import { useForkSession } from "@/hooks/use-fork-session" import type { TextPart as SDKTextPart } from "@opencode-ai/sdk/v2/client" -import { base64Encode } from "@opencode-ai/util/encode" interface ForkableMessage { id: string @@ -22,11 +19,9 @@ function formatTime(date: Date): string { export const DialogFork: Component = () => { const params = useParams() - const navigate = useNavigate() const sync = useSync() - const sdk = useSDK() - const prompt = usePrompt() const dialog = useDialog() + const forkSession = useForkSession() const messages = createMemo((): ForkableMessage[] => { const sessionID = params.id @@ -54,22 +49,8 @@ export const DialogFork: Component = () => { const handleSelect = (item: ForkableMessage | undefined) => { if (!item) return - - const sessionID = params.id - if (!sessionID) return - - const parts = sync.data.part[item.id] ?? [] - const restored = extractPromptFromParts(parts, { directory: sdk.directory }) - dialog.close() - - sdk.client.session.fork({ sessionID, messageID: item.id }).then((forked) => { - if (!forked.data) return - navigate(`/${base64Encode(sdk.directory)}/session/${forked.data.id}`) - requestAnimationFrame(() => { - prompt.set(restored) - }) - }) + forkSession(item.id) } return ( diff --git a/packages/app/src/hooks/use-fork-session.ts b/packages/app/src/hooks/use-fork-session.ts new file mode 100644 index 000000000000..0964994f6e3c --- /dev/null +++ b/packages/app/src/hooks/use-fork-session.ts @@ -0,0 +1,30 @@ +import { useNavigate, useParams } from "@solidjs/router" +import { useSync } from "@/context/sync" +import { useSDK } from "@/context/sdk" +import { usePrompt } from "@/context/prompt" +import { extractPromptFromParts } from "@/utils/prompt" +import { base64Encode } from "@opencode-ai/util/encode" + +export function useForkSession() { + const params = useParams() + const navigate = useNavigate() + const sync = useSync() + const sdk = useSDK() + const prompt = usePrompt() + + return (messageID: string) => { + const sessionID = params.id + if (!sessionID) return + + const parts = sync.data.part[messageID] ?? [] + const restored = extractPromptFromParts(parts, { directory: sdk.directory }) + + sdk.client.session.fork({ sessionID, messageID }).then((forked) => { + if (!forked.data) return + navigate(`/${base64Encode(sdk.directory)}/session/${forked.data.id}`) + requestAnimationFrame(() => { + prompt.set(restored) + }) + }) + } +} diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 69065a8fa7a0..b92edd97877f 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -53,6 +53,7 @@ import { import { usePlatform } from "@/context/platform" import { navMark, navParams } from "@/utils/perf" import { same } from "@/utils/same" +import { useForkSession } from "@/hooks/use-fork-session" type DiffStyle = "unified" | "split" @@ -167,6 +168,7 @@ export default function Page() { const sdk = useSDK() const prompt = usePrompt() const permission = usePermission() + const forkSession = useForkSession() const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`) const tabs = createMemo(() => layout.tabs(sessionKey())) const view = createMemo(() => layout.view(sessionKey())) @@ -1095,6 +1097,7 @@ export default function Page() { messages={visibleUserMessages()} current={activeMessage()} onMessageSelect={scrollToMessage} + onFork={(msg) => forkSession(msg.id)} 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 = () => (