summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx5
-rw-r--r--packages/opencode/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx54
-rw-r--r--packages/opencode/src/cli/cmd/tui/routes/session/dialog-timeline.tsx2
-rw-r--r--packages/opencode/src/cli/cmd/tui/routes/session/index.tsx20
-rw-r--r--packages/opencode/src/config/config.ts2
5 files changed, 82 insertions, 1 deletions
diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx
index 627c3abab..bf656e890 100644
--- a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx
+++ b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx
@@ -271,6 +271,11 @@ export function Autocomplete(props: {
onSelect: () => command.trigger("session.timeline"),
},
{
+ display: "/fork",
+ description: "fork from message",
+ onSelect: () => command.trigger("session.fork"),
+ },
+ {
display: "/thinking",
description: "toggle thinking visibility",
onSelect: () => command.trigger("session.toggle.thinking"),
diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx
new file mode 100644
index 000000000..8b09e0786
--- /dev/null
+++ b/packages/opencode/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx
@@ -0,0 +1,54 @@
+import { createMemo, onMount } from "solid-js"
+import { useSync } from "@tui/context/sync"
+import { DialogSelect, type DialogSelectOption } from "@tui/ui/dialog-select"
+import type { TextPart } from "@opencode-ai/sdk/v2"
+import { Locale } from "@/util/locale"
+import { useSDK } from "@tui/context/sdk"
+import { useRoute } from "@tui/context/route"
+import { useDialog } from "../../ui/dialog"
+
+export function DialogForkFromTimeline(props: {
+ sessionID: string
+ onMove: (messageID: string) => void
+}) {
+ const sync = useSync()
+ const dialog = useDialog()
+ const sdk = useSDK()
+ const route = useRoute()
+
+ onMount(() => {
+ dialog.setSize("large")
+ })
+
+ const options = createMemo((): DialogSelectOption<string>[] => {
+ const messages = sync.data.message[props.sessionID] ?? []
+ const result = [] as DialogSelectOption<string>[]
+ for (const message of messages) {
+ if (message.role !== "user") continue
+ const part = (sync.data.part[message.id] ?? []).find(
+ (x) => x.type === "text" && !x.synthetic && !x.ignored,
+ ) as TextPart
+ if (!part) continue
+ result.push({
+ title: part.text.replace(/\n/g, " "),
+ value: message.id,
+ footer: Locale.time(message.time.created),
+ onSelect: async (dialog) => {
+ const forked = await sdk.client.session.fork({
+ sessionID: props.sessionID,
+ messageID: message.id,
+ })
+ route.navigate({
+ sessionID: forked.data!.id,
+ type: "session",
+ })
+ dialog.clear()
+ },
+ })
+ }
+ result.reverse()
+ return result
+ })
+
+ return <DialogSelect onMove={(option) => props.onMove(option.value)} title="Fork from message" options={options()} />
+}
diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/dialog-timeline.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/dialog-timeline.tsx
index da868221e..d5e8b36a3 100644
--- a/packages/opencode/src/cli/cmd/tui/routes/session/dialog-timeline.tsx
+++ b/packages/opencode/src/cli/cmd/tui/routes/session/dialog-timeline.tsx
@@ -24,7 +24,7 @@ export function DialogTimeline(props: {
const result = [] as DialogSelectOption<string>[]
for (const message of messages) {
if (message.role !== "user") continue
- const part = (sync.data.part[message.id] ?? []).find((x) => x.type === "text" && !x.synthetic) as TextPart
+ const part = (sync.data.part[message.id] ?? []).find((x) => x.type === "text" && !x.synthetic && !x.ignored,) as TextPart
if (!part) continue
result.push({
title: part.text.replace(/\n/g, " "),
diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
index 780809bd6..2d6f60cc0 100644
--- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
+++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
@@ -53,6 +53,7 @@ import { iife } from "@/util/iife"
import { DialogConfirm } from "@tui/ui/dialog-confirm"
import { DialogPrompt } from "@tui/ui/dialog-prompt"
import { DialogTimeline } from "./dialog-timeline"
+import { DialogForkFromTimeline } from "./dialog-fork-from-timeline"
import { DialogSessionRename } from "../../component/dialog-session-rename"
import { Sidebar } from "./sidebar"
import { LANGUAGE_EXTENSIONS } from "@/lsp/language"
@@ -296,6 +297,25 @@ export function Session() {
},
},
{
+ title: "Fork from message",
+ value: "session.fork",
+ keybind: "session_fork",
+ category: "Session",
+ onSelect: (dialog) => {
+ dialog.replace(() => (
+ <DialogForkFromTimeline
+ onMove={(messageID) => {
+ const child = scroll.getChildren().find((child) => {
+ return child.id === messageID
+ })
+ if (child) scroll.scrollBy(child.y - scroll.y - 1)
+ }}
+ sessionID={route.sessionID}
+ />
+ ))
+ },
+ },
+ {
title: "Compact session",
value: "session.compact",
keybind: "session_compact",
diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts
index 52fe478ee..a01cc832a 100644
--- a/packages/opencode/src/config/config.ts
+++ b/packages/opencode/src/config/config.ts
@@ -440,6 +440,8 @@ export namespace Config {
session_new: z.string().optional().default("<leader>n").describe("Create a new session"),
session_list: z.string().optional().default("<leader>l").describe("List all sessions"),
session_timeline: z.string().optional().default("<leader>g").describe("Show session timeline"),
+ session_fork: z.string().optional().default("none").describe("Fork session from message"),
+ session_rename: z.string().optional().default("none").describe("Rename session"),
session_share: z.string().optional().default("none").describe("Share current session"),
session_unshare: z.string().optional().default("none").describe("Unshare current session"),
session_interrupt: z.string().optional().default("escape").describe("Interrupt current session"),