Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 4 additions & 23 deletions packages/app/src/components/dialog-fork.tsx
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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 (
Expand Down
30 changes: 30 additions & 0 deletions packages/app/src/hooks/use-fork-session.ts
Original file line number Diff line number Diff line change
@@ -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)
})
})
}
}
3 changes: 3 additions & 0 deletions packages/app/src/pages/session.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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()))
Expand Down Expand Up @@ -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"
/>
Expand Down
52 changes: 52 additions & 0 deletions packages/ui/src/components/message-nav.css
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
}

[data-slot="message-nav-item"] {
position: relative;
display: flex;
align-items: center;
align-self: stretch;
Expand Down Expand Up @@ -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;
}
Expand Down
34 changes: 25 additions & 9 deletions packages/ui/src/components/message-nav.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
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"> & {
messages: UserMessage[]
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 = () => (
<ul role="list" data-component="message-nav" data-size={local.size} {...others}>
Expand Down Expand Up @@ -39,6 +42,19 @@ export function MessageNav(
</Show>
</div>
</button>
<Show when={local.onFork}>
<Tooltip value="Fork from here" placement="top">
<button
data-slot="message-nav-fork-button"
onClick={(e) => {
e.stopPropagation()
local.onFork?.(message)
}}
>
<Icon name="branch" size="small" />
</button>
</Tooltip>
</Show>
</Match>
</Switch>
</li>
Expand All @@ -51,16 +67,16 @@ export function MessageNav(
return (
<Switch>
<Match when={local.size === "compact"}>
<Tooltip openDelay={0} closeDelay={300} placement="right-start" gutter={-40} shift={-10} overlap>
<Tooltip.Trigger as="div">{content()}</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content data-slot="message-nav-tooltip">
<KobalteTooltip openDelay={0} closeDelay={300} placement="right-start" gutter={-40} shift={-10} overlap>
<KobalteTooltip.Trigger as="div">{content()}</KobalteTooltip.Trigger>
<KobalteTooltip.Portal>
<KobalteTooltip.Content data-slot="message-nav-tooltip">
<div data-slot="message-nav-tooltip-content">
<MessageNav {...props} size="normal" class="" />
</div>
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip>
</KobalteTooltip.Content>
</KobalteTooltip.Portal>
</KobalteTooltip>
</Match>
<Match when={local.size === "normal"}>{content()}</Match>
</Switch>
Expand Down
13 changes: 12 additions & 1 deletion packages/ui/src/components/session-message-rail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,19 @@ export interface SessionMessageRailProps extends ComponentProps<"div"> {
current?: UserMessage
wide?: boolean
onMessageSelect: (message: UserMessage) => void
onFork?: (message: UserMessage) => void
}

export function SessionMessageRail(props: SessionMessageRailProps) {
const [local, others] = splitProps(props, ["messages", "current", "wide", "onMessageSelect", "class", "classList"])
const [local, others] = splitProps(props, [
"messages",
"current",
"wide",
"onMessageSelect",
"onFork",
"class",
"classList",
])

return (
<Show when={(local.messages?.length ?? 0) > 1}>
Expand All @@ -29,6 +38,7 @@ export function SessionMessageRail(props: SessionMessageRailProps) {
messages={local.messages}
current={local.current}
onMessageSelect={local.onMessageSelect}
onFork={local.onFork}
size="compact"
/>
</div>
Expand All @@ -37,6 +47,7 @@ export function SessionMessageRail(props: SessionMessageRailProps) {
messages={local.messages}
current={local.current}
onMessageSelect={local.onMessageSelect}
onFork={local.onFork}
size={local.wide ? "normal" : "compact"}
/>
</div>
Expand Down