feat(tui): show progress and confirmation when forking a session
Forking gave no feedback: the dialog stayed open during the (potentially slow) fork, then silently navigated to a near-identical copy. Now the dialog shows a "Forking session…" spinner while the fork runs, then lands on the new session with a "Forked session" toast (or an error toast on failure). Mount <Toast/> once in the app shell instead of per-route, so a toast fired around route.navigate survives the route remount.
This commit is contained in:
@@ -50,7 +50,7 @@ import { FrecencyProvider } from "./component/prompt/frecency"
|
||||
import { PromptStashProvider } from "./component/prompt/stash"
|
||||
import { DialogAlert } from "./ui/dialog-alert"
|
||||
import { DialogConfirm } from "./ui/dialog-confirm"
|
||||
import { ToastProvider, useToast } from "./ui/toast"
|
||||
import { Toast, ToastProvider, useToast } from "./ui/toast"
|
||||
import { createExit, ExitProvider, useExit, type Exit } from "./context/exit"
|
||||
import { Session as SessionApi } from "@/session/session"
|
||||
import { TuiEvent } from "./event"
|
||||
@@ -1072,6 +1072,8 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
|
||||
<TuiPluginRuntime.Slot name="app_bottom" />
|
||||
</box>
|
||||
<TuiPluginRuntime.Slot name="app" />
|
||||
{/* One Toast for the whole app, not per-route, so toasts survive route.navigate */}
|
||||
<Toast />
|
||||
</Show>
|
||||
<StartupLoading ready={ready} />
|
||||
</box>
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Prompt, type PromptRef } from "@tui/component/prompt"
|
||||
import { createEffect, createMemo, createSignal, onMount } from "solid-js"
|
||||
import { Logo } from "../component/logo"
|
||||
import { useSync } from "../context/sync"
|
||||
import { Toast } from "../ui/toast"
|
||||
import { useArgs } from "../context/args"
|
||||
import { useRouteData } from "@tui/context/route"
|
||||
import { usePromptRef } from "../context/prompt"
|
||||
@@ -83,7 +82,6 @@ export function Home() {
|
||||
</box>
|
||||
<TuiPluginRuntime.Slot name="home_bottom" />
|
||||
<box flexGrow={1} minHeight={0} />
|
||||
<Toast />
|
||||
</box>
|
||||
<box width="100%" flexShrink={0}>
|
||||
<TuiPluginRuntime.Slot name="home_footer" mode="single_winner" />
|
||||
|
||||
@@ -5,6 +5,8 @@ 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 { useToast } from "../../ui/toast"
|
||||
import { Spinner } from "../../component/spinner"
|
||||
import { useDialog, type DialogContext } from "../../ui/dialog"
|
||||
import type { PromptInfo } from "@tui/component/prompt/history"
|
||||
import { strip } from "@tui/component/prompt/part"
|
||||
@@ -14,24 +16,37 @@ export function DialogForkFromTimeline(props: { sessionID: string; onMove: (mess
|
||||
const dialog = useDialog()
|
||||
const sdk = useSDK()
|
||||
const route = useRoute()
|
||||
const toast = useToast()
|
||||
|
||||
onMount(() => {
|
||||
dialog.setSize("large")
|
||||
})
|
||||
|
||||
// Forking a large session can take a moment, so swap the dialog to a progress view instead of
|
||||
// leaving it open (which looks frozen), then navigate to the fork and confirm with a toast.
|
||||
const fork = async (dialog: DialogContext, messageID?: string, prompt?: PromptInfo) => {
|
||||
dialog.replace(() => (
|
||||
<box paddingLeft={2} paddingRight={2} paddingBottom={1}>
|
||||
<Spinner>Forking session…</Spinner>
|
||||
</box>
|
||||
))
|
||||
const forked = await sdk.client.session.fork({ sessionID: props.sessionID, messageID })
|
||||
if (!forked.data) {
|
||||
toast.show({ variant: "error", message: "Failed to fork session" })
|
||||
dialog.clear()
|
||||
return
|
||||
}
|
||||
route.navigate({ sessionID: forked.data.id, type: "session", prompt })
|
||||
dialog.clear()
|
||||
toast.show({ variant: "success", message: "Forked session", duration: 4000 })
|
||||
}
|
||||
|
||||
const options = createMemo((): DialogSelectOption<string | undefined>[] => {
|
||||
const messages = sync.data.message[props.sessionID] ?? []
|
||||
const fullSession = {
|
||||
title: "Full session",
|
||||
value: undefined,
|
||||
onSelect: async (dialog: DialogContext) => {
|
||||
const forked = await sdk.client.session.fork({ sessionID: props.sessionID })
|
||||
route.navigate({
|
||||
sessionID: forked.data!.id,
|
||||
type: "session",
|
||||
})
|
||||
dialog.clear()
|
||||
},
|
||||
onSelect: fork,
|
||||
} satisfies DialogSelectOption<string | undefined>
|
||||
const result = [] as DialogSelectOption<string | undefined>[]
|
||||
for (const message of messages) {
|
||||
@@ -44,13 +59,8 @@ export function DialogForkFromTimeline(props: { sessionID: string; onMove: (mess
|
||||
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,
|
||||
})
|
||||
const parts = sync.data.part[message.id] ?? []
|
||||
const prompt = parts.reduce(
|
||||
onSelect: (dialog) => {
|
||||
const prompt = (sync.data.part[message.id] ?? []).reduce(
|
||||
(agg, part) => {
|
||||
if (part.type === "text") {
|
||||
if (!part.synthetic) agg.input += part.text
|
||||
@@ -60,12 +70,7 @@ export function DialogForkFromTimeline(props: { sessionID: string; onMove: (mess
|
||||
},
|
||||
{ input: "", parts: [] as PromptInfo["parts"] },
|
||||
)
|
||||
route.navigate({
|
||||
sessionID: forked.data!.id,
|
||||
type: "session",
|
||||
prompt,
|
||||
})
|
||||
dialog.clear()
|
||||
return fork(dialog, message.id, prompt)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ import { LANGUAGE_EXTENSIONS } from "@/lsp/language"
|
||||
import parsers from "../../../../../../parsers-config.ts"
|
||||
import * as Clipboard from "../../util/clipboard"
|
||||
import { errorMessage } from "@/util/error"
|
||||
import { Toast, useToast } from "../../ui/toast"
|
||||
import { useToast } from "../../ui/toast"
|
||||
import { useKV } from "../../context/kv.tsx"
|
||||
import * as Editor from "../../util/editor"
|
||||
import stripAnsi from "strip-ansi"
|
||||
@@ -1263,7 +1263,6 @@ export function Session() {
|
||||
</Show>
|
||||
</box>
|
||||
</Show>
|
||||
<Toast />
|
||||
</box>
|
||||
<Show when={sidebarVisible()}>
|
||||
<Switch>
|
||||
|
||||
Reference in New Issue
Block a user