mirror of
https://gitea.toothfairyai.com/ToothFairyAI/tf_code.git
synced 2026-04-22 16:44:36 +00:00
feat(app): restore to message and fork session (#17092)
This commit is contained in:
@@ -8,6 +8,7 @@ import { usePrompt } from "@/context/prompt"
|
||||
import { getSessionHandoff, setSessionHandoff } from "@/pages/session/handoff"
|
||||
import { SessionPermissionDock } from "@/pages/session/composer/session-permission-dock"
|
||||
import { SessionQuestionDock } from "@/pages/session/composer/session-question-dock"
|
||||
import { SessionRevertDock } from "@/pages/session/composer/session-revert-dock"
|
||||
import type { SessionComposerState } from "@/pages/session/composer/session-composer-state"
|
||||
import { SessionTodoDock } from "@/pages/session/composer/session-todo-dock"
|
||||
|
||||
@@ -20,6 +21,11 @@ export function SessionComposerRegion(props: {
|
||||
onNewSessionWorktreeReset: () => void
|
||||
onSubmit: () => void
|
||||
onResponseSubmit: () => void
|
||||
revert?: {
|
||||
items: { id: string; text: string }[]
|
||||
restoring?: string
|
||||
onRestore: (id: string) => void
|
||||
}
|
||||
setPromptDockRef: (el: HTMLDivElement) => void
|
||||
visualDuration?: number
|
||||
bounce?: number
|
||||
@@ -116,6 +122,8 @@ export function SessionComposerRegion(props: {
|
||||
const value = createMemo(() => Math.max(0, Math.min(1, progress())))
|
||||
const [height, setHeight] = createSignal(320)
|
||||
const dock = createMemo(() => (gate.ready && props.state.dock()) || value() > 0.001)
|
||||
const rolled = createMemo(() => (props.revert?.items.length ? props.revert : undefined))
|
||||
const lift = createMemo(() => (rolled() ? 18 : 36 * value()))
|
||||
const full = createMemo(() => Math.max(78, height()))
|
||||
const [contentRef, setContentRef] = createSignal<HTMLDivElement>()
|
||||
|
||||
@@ -170,9 +178,22 @@ export function SessionComposerRegion(props: {
|
||||
<Show
|
||||
when={prompt.ready()}
|
||||
fallback={
|
||||
<div class="w-full min-h-32 md:min-h-40 rounded-md border border-border-weak-base bg-background-base/50 px-4 py-3 text-text-weak whitespace-pre-wrap pointer-events-none">
|
||||
{handoffPrompt() || language.t("prompt.loading")}
|
||||
</div>
|
||||
<>
|
||||
<Show when={rolled()} keyed>
|
||||
{(revert) => (
|
||||
<div class="pb-2">
|
||||
<SessionRevertDock
|
||||
items={revert.items}
|
||||
restoring={revert.restoring}
|
||||
onRestore={revert.onRestore}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
<div class="w-full min-h-32 md:min-h-40 rounded-md border border-border-weak-base bg-background-base/50 px-4 py-3 text-text-weak whitespace-pre-wrap pointer-events-none">
|
||||
{handoffPrompt() || language.t("prompt.loading")}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Show when={dock()}>
|
||||
@@ -209,12 +230,23 @@ export function SessionComposerRegion(props: {
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={rolled()} keyed>
|
||||
{(revert) => (
|
||||
<div
|
||||
style={{
|
||||
"margin-top": `${-36 * value()}px`,
|
||||
}}
|
||||
>
|
||||
<SessionRevertDock items={revert.items} restoring={revert.restoring} onRestore={revert.onRestore} />
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
<div
|
||||
classList={{
|
||||
"relative z-10": true,
|
||||
}}
|
||||
style={{
|
||||
"margin-top": `${-36 * value()}px`,
|
||||
"margin-top": `${-lift()}px`,
|
||||
}}
|
||||
>
|
||||
<PromptInput
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import { For, Show, createMemo } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { DockTray } from "@opencode-ai/ui/dock-surface"
|
||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
import { useLanguage } from "@/context/language"
|
||||
|
||||
export function SessionRevertDock(props: {
|
||||
items: { id: string; text: string }[]
|
||||
restoring?: string
|
||||
onRestore: (id: string) => void
|
||||
}) {
|
||||
const language = useLanguage()
|
||||
const [store, setStore] = createStore({
|
||||
collapsed: false,
|
||||
})
|
||||
|
||||
const toggle = () => setStore("collapsed", (value) => !value)
|
||||
const total = createMemo(() => props.items.length)
|
||||
const label = createMemo(() =>
|
||||
language.t(total() === 1 ? "session.revertDock.summary.one" : "session.revertDock.summary.other", {
|
||||
count: total(),
|
||||
}),
|
||||
)
|
||||
const preview = createMemo(() => props.items[0]?.text ?? "")
|
||||
|
||||
return (
|
||||
<DockTray data-component="session-revert-dock">
|
||||
<div
|
||||
class="pl-3 pr-2 py-2 flex items-center gap-2"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={toggle}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key !== "Enter" && event.key !== " ") return
|
||||
event.preventDefault()
|
||||
toggle()
|
||||
}}
|
||||
>
|
||||
<span class="shrink-0 text-14-regular text-text-strong cursor-default">{label()}</span>
|
||||
<Show when={store.collapsed && preview()}>
|
||||
<span class="min-w-0 flex-1 truncate text-14-regular text-text-base cursor-default">{preview()}</span>
|
||||
</Show>
|
||||
<div class="ml-auto shrink-0">
|
||||
<IconButton
|
||||
data-collapsed={store.collapsed ? "true" : "false"}
|
||||
icon="chevron-down"
|
||||
size="normal"
|
||||
variant="ghost"
|
||||
style={{ transform: `rotate(${store.collapsed ? 180 : 0}deg)` }}
|
||||
onMouseDown={(event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
toggle()
|
||||
}}
|
||||
aria-label={
|
||||
store.collapsed ? language.t("session.revertDock.expand") : language.t("session.revertDock.collapse")
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Show when={store.collapsed}>
|
||||
<div class="h-5" aria-hidden="true" />
|
||||
</Show>
|
||||
|
||||
<Show when={!store.collapsed}>
|
||||
<div class="px-3 pb-11 flex flex-col gap-1.5 max-h-42 overflow-y-auto no-scrollbar">
|
||||
<For each={props.items}>
|
||||
{(item) => (
|
||||
<div class="flex items-center gap-2 min-w-0 rounded-[10px] border border-border-weak-base bg-background-stronger px-2.5 py-2">
|
||||
<span class="min-w-0 flex-1 truncate text-13-regular text-text-strong">{item.text}</span>
|
||||
<Button
|
||||
size="small"
|
||||
variant="secondary"
|
||||
class="shrink-0"
|
||||
disabled={!!props.restoring}
|
||||
onClick={() => props.onRestore(item.id)}
|
||||
>
|
||||
{language.t("session.revertDock.restore")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
</DockTray>
|
||||
)
|
||||
}
|
||||
@@ -36,6 +36,11 @@ type MessageComment = {
|
||||
const emptyMessages: MessageType[] = []
|
||||
const idle = { type: "idle" as const }
|
||||
|
||||
type UserActions = {
|
||||
fork?: (input: { sessionID: string; messageID: string }) => Promise<void> | void
|
||||
revert?: (input: { sessionID: string; messageID: string }) => Promise<void> | void
|
||||
}
|
||||
|
||||
const messageComments = (parts: Part[]): MessageComment[] =>
|
||||
parts.flatMap((part) => {
|
||||
if (part.type !== "text" || !(part as TextPart).synthetic) return []
|
||||
@@ -186,6 +191,7 @@ function createTimelineStaging(input: TimelineStageInput) {
|
||||
export function MessageTimeline(props: {
|
||||
mobileChanges: boolean
|
||||
mobileFallback: JSX.Element
|
||||
actions?: UserActions
|
||||
scroll: { overflow: boolean; bottom: boolean }
|
||||
onResumeScroll: () => void
|
||||
setScrollRef: (el: HTMLDivElement | undefined) => void
|
||||
@@ -805,6 +811,7 @@ export function MessageTimeline(props: {
|
||||
<SessionTurn
|
||||
sessionID={sessionID() ?? ""}
|
||||
messageID={messageID}
|
||||
actions={props.actions}
|
||||
active={active()}
|
||||
queued={queued()}
|
||||
status={active() ? sessionStatus() : undefined}
|
||||
|
||||
Reference in New Issue
Block a user