feat(tui): wire up undo/redo and revert for V2 sessions (#34263)
This commit is contained in:
parent
cd942d0669
commit
d1d7ebc2c6
20 changed files with 478 additions and 87 deletions
|
|
@ -13,6 +13,7 @@ import { useLocal } from "../context/local"
|
|||
import { createDebouncedSignal } from "../util/signal"
|
||||
import { useToast } from "../ui/toast"
|
||||
import { useCommandShortcut } from "../keymap"
|
||||
import { DialogSessionRename } from "./dialog-session-rename"
|
||||
import { Spinner } from "./spinner"
|
||||
|
||||
export function DialogSessionList() {
|
||||
|
|
@ -121,7 +122,8 @@ export function DialogSessionList() {
|
|||
{
|
||||
command: "session.rename",
|
||||
title: "rename",
|
||||
onTrigger: () => unavailable("Renaming"),
|
||||
onTrigger: (option: { value: string }) =>
|
||||
DialogSessionRename.show(dialog, option.value, data.session.get(option.value)?.title),
|
||||
},
|
||||
]}
|
||||
footerHints={quickSwitchFooterHints()}
|
||||
|
|
|
|||
|
|
@ -1,31 +1,33 @@
|
|||
import { DialogPrompt } from "../ui/dialog-prompt"
|
||||
import { useDialog } from "../ui/dialog"
|
||||
import { useSync } from "../context/sync"
|
||||
import { createMemo } from "solid-js"
|
||||
import { type DialogContext, useDialog } from "../ui/dialog"
|
||||
import { useSDK } from "../context/sdk"
|
||||
import { useToast } from "../ui/toast"
|
||||
import { errorMessage } from "../util/error"
|
||||
|
||||
interface DialogSessionRenameProps {
|
||||
session: string
|
||||
}
|
||||
|
||||
export function DialogSessionRename(props: DialogSessionRenameProps) {
|
||||
export function DialogSessionRename(props: { sessionID: string; currentTitle?: string }) {
|
||||
const dialog = useDialog()
|
||||
const sync = useSync()
|
||||
const sdk = useSDK()
|
||||
const session = createMemo(() => sync.session.get(props.session))
|
||||
const toast = useToast()
|
||||
|
||||
return (
|
||||
<DialogPrompt
|
||||
title="Rename Session"
|
||||
value={session()?.title}
|
||||
title="Rename session"
|
||||
placeholder="Session title"
|
||||
value={props.currentTitle}
|
||||
onConfirm={(value) => {
|
||||
void sdk.client.session.update({
|
||||
sessionID: props.session,
|
||||
title: value,
|
||||
})
|
||||
dialog.clear()
|
||||
const title = value.trim()
|
||||
if (!title) return
|
||||
void sdk.client.v2.session
|
||||
.rename({ sessionID: props.sessionID, title }, { throwOnError: true })
|
||||
.then(() => dialog.clear())
|
||||
.catch((error) =>
|
||||
toast.show({ message: `Failed to rename session: ${errorMessage(error)}`, variant: "error", duration: 5000 }),
|
||||
)
|
||||
}}
|
||||
onCancel={() => dialog.clear()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
DialogSessionRename.show = (dialog: DialogContext, sessionID: string, currentTitle?: string) =>
|
||||
dialog.replace(() => <DialogSessionRename sessionID={sessionID} currentTitle={currentTitle} />)
|
||||
|
|
|
|||
|
|
@ -1104,6 +1104,13 @@ export function Prompt(props: PromptProps) {
|
|||
{ throwOnError: true },
|
||||
)
|
||||
}
|
||||
if (session?.revert) {
|
||||
const revertResult = await sdk.client.v2.session.revert.commit({ sessionID })
|
||||
if (revertResult.error) {
|
||||
toast.show({ title: "Failed to commit revert", message: errorMessage(revertResult.error), variant: "error" })
|
||||
return false
|
||||
}
|
||||
}
|
||||
const result = await sdk.client.v2.session.prompt({
|
||||
sessionID,
|
||||
prompt: {
|
||||
|
|
|
|||
|
|
@ -169,6 +169,10 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
})
|
||||
})
|
||||
break
|
||||
case "session.next.renamed":
|
||||
if (store.session.info[event.data.sessionID])
|
||||
setStore("session", "info", event.data.sessionID, "title", event.data.title)
|
||||
break
|
||||
case "session.next.prompted": {
|
||||
setStore("session", "status", event.data.sessionID, "running")
|
||||
message.update(event.data.sessionID, (draft, index) => {
|
||||
|
|
@ -405,6 +409,15 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
case "session.next.compaction.started":
|
||||
setStore("session", "status", event.data.sessionID, "running")
|
||||
break
|
||||
case "session.next.revert.staged":
|
||||
if (store.session.info[event.data.sessionID])
|
||||
setStore("session", "info", event.data.sessionID, "revert", event.data.revert)
|
||||
break
|
||||
case "session.next.revert.cleared":
|
||||
case "session.next.revert.committed":
|
||||
if (store.session.info[event.data.sessionID])
|
||||
setStore("session", "info", event.data.sessionID, "revert", undefined)
|
||||
break
|
||||
case "session.next.compaction.delta":
|
||||
break
|
||||
case "session.next.compaction.ended":
|
||||
|
|
|
|||
|
|
@ -3,11 +3,14 @@ import { useData } from "../../context/data"
|
|||
import { DialogSelect } from "../../ui/dialog-select"
|
||||
import { useClipboard } from "../../context/clipboard"
|
||||
import { useToast } from "../../ui/toast"
|
||||
import { useSDK } from "../../context/sdk"
|
||||
import { errorMessage } from "../../util/error"
|
||||
|
||||
export function DialogMessage(props: { messageID: string; sessionID: string; setPrompt?: unknown }) {
|
||||
const data = useData()
|
||||
const clipboard = useClipboard()
|
||||
const toast = useToast()
|
||||
const sdk = useSDK()
|
||||
const message = createMemo(() =>
|
||||
data.session.message.get(props.sessionID, props.messageID),
|
||||
)
|
||||
|
|
@ -20,8 +23,12 @@ export function DialogMessage(props: { messageID: string; sessionID: string; set
|
|||
title: "Revert",
|
||||
value: "session.revert",
|
||||
description: "undo messages and file changes",
|
||||
onSelect: (dialog) => {
|
||||
toast.show({ message: "Reverting is not implemented for V2 sessions yet", variant: "error", duration: 5000 })
|
||||
onSelect: async (dialog) => {
|
||||
const result = await sdk.client.v2.session.revert.stage({
|
||||
sessionID: props.sessionID,
|
||||
messageID: props.messageID,
|
||||
})
|
||||
if (result.error) toast.show({ message: errorMessage(result.error), variant: "error", duration: 5000 })
|
||||
dialog.clear()
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ import { useSDK } from "../../context/sdk"
|
|||
import { useEditorContext } from "../../context/editor"
|
||||
import { openEditor } from "../../editor"
|
||||
import { useDialog } from "../../ui/dialog"
|
||||
import { DialogSessionRename } from "../../component/dialog-session-rename"
|
||||
import { TodoItem } from "../../component/todo-item"
|
||||
import { DialogMessage } from "./dialog-message"
|
||||
import { Sidebar } from "./sidebar"
|
||||
|
|
@ -335,7 +336,7 @@ export function Session() {
|
|||
value: "session.rename",
|
||||
category: "Session",
|
||||
slash: { name: "rename" },
|
||||
run: () => unavailable("Renaming"),
|
||||
run: () => DialogSessionRename.show(dialog, route.sessionID, session()?.title),
|
||||
},
|
||||
{
|
||||
title: "Jump to message",
|
||||
|
|
@ -377,7 +378,28 @@ export function Session() {
|
|||
value: "session.undo",
|
||||
category: "Session",
|
||||
slash: { name: "undo" },
|
||||
run: () => unavailable("Undo"),
|
||||
run: () => {
|
||||
void (async () => {
|
||||
const boundary = session()?.revert?.messageID
|
||||
const list = messages()
|
||||
let target: string | undefined
|
||||
for (let i = list.length - 1; i >= 0; i--) {
|
||||
const message = list[i]
|
||||
if (message.type !== "user" || !message.text.trim()) continue
|
||||
if (boundary && message.id >= boundary) continue
|
||||
target = message.id
|
||||
break
|
||||
}
|
||||
if (!target) {
|
||||
toast.show({ message: "Nothing to undo", variant: "error", duration: 3000 })
|
||||
dialog.clear()
|
||||
return
|
||||
}
|
||||
const result = await sdk.client.v2.session.revert.stage({ sessionID: route.sessionID, messageID: target })
|
||||
if (result.error) toast.show({ message: errorMessage(result.error), variant: "error", duration: 5000 })
|
||||
dialog.clear()
|
||||
})()
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Redo",
|
||||
|
|
@ -385,7 +407,13 @@ export function Session() {
|
|||
category: "Session",
|
||||
enabled: !!session()?.revert?.messageID,
|
||||
slash: { name: "redo" },
|
||||
run: () => unavailable("Redo"),
|
||||
run: () => {
|
||||
void (async () => {
|
||||
const result = await sdk.client.v2.session.revert.clear({ sessionID: route.sessionID })
|
||||
if (result.error) toast.show({ message: errorMessage(result.error), variant: "error", duration: 5000 })
|
||||
dialog.clear()
|
||||
})()
|
||||
},
|
||||
},
|
||||
{
|
||||
title: sidebarVisible() ? "Hide sidebar" : "Show sidebar",
|
||||
|
|
@ -848,6 +876,11 @@ export function Session() {
|
|||
/>
|
||||
)}
|
||||
</For>
|
||||
<Show when={session()?.revert?.messageID}>
|
||||
<RevertMessage
|
||||
count={messages().filter((message) => message.id > session()!.revert!.messageID).length}
|
||||
/>
|
||||
</Show>
|
||||
</scrollbox>
|
||||
<box flexShrink={0}>
|
||||
<Show when={permissions().length > 0}>
|
||||
|
|
@ -1136,9 +1169,10 @@ function CompactionMessage() {
|
|||
|
||||
function RevertMessage(props: { count: number }) {
|
||||
const { theme } = useTheme()
|
||||
const dialog = useDialog()
|
||||
const renderer = useRenderer()
|
||||
const route = useRouteData("session")
|
||||
const sdk = useSDK()
|
||||
const toast = useToast()
|
||||
const renderer = useRenderer()
|
||||
const [hover, setHover] = createSignal(false)
|
||||
return (
|
||||
<box
|
||||
|
|
@ -1146,8 +1180,10 @@ function RevertMessage(props: { count: number }) {
|
|||
onMouseOut={() => setHover(false)}
|
||||
onMouseUp={() => {
|
||||
if (renderer.getSelection()?.getSelectedText()) return
|
||||
toast.show({ message: "Redo is not implemented for V2 sessions yet", variant: "error", duration: 5000 })
|
||||
dialog.clear()
|
||||
void (async () => {
|
||||
const result = await sdk.client.v2.session.revert.clear({ sessionID: route.sessionID })
|
||||
if (result.error) toast.show({ message: errorMessage(result.error), variant: "error", duration: 5000 })
|
||||
})()
|
||||
}}
|
||||
flexShrink={0}
|
||||
border={["left"]}
|
||||
|
|
@ -1156,7 +1192,7 @@ function RevertMessage(props: { count: number }) {
|
|||
>
|
||||
<box paddingTop={1} paddingBottom={1} paddingLeft={2} backgroundColor={hover() ? theme.backgroundElement : theme.backgroundPanel}>
|
||||
<text fg={theme.textMuted}>{props.count} message{props.count === 1 ? "" : "s"} reverted</text>
|
||||
<text fg={theme.textMuted}>Redo is not implemented for V2 sessions yet</text>
|
||||
<text fg={theme.textMuted}>Click to redo</text>
|
||||
</box>
|
||||
</box>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -23,6 +23,13 @@ export type SessionRow =
|
|||
export function createSessionRows(sessionID: Accessor<string>) {
|
||||
const data = useData()
|
||||
const [rows, setRows] = createStore<SessionRow[]>([])
|
||||
const revertBoundary = () => data.session.get(sessionID())?.revert?.messageID
|
||||
|
||||
function reduce() {
|
||||
const messages = data.session.message.list(sessionID())
|
||||
const boundary = revertBoundary()
|
||||
return reduceSessionRows(boundary ? messages.filter((message) => message.id <= boundary) : messages)
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
const pending = new Set(
|
||||
|
|
@ -44,17 +51,24 @@ export function createSessionRows(sessionID: Accessor<string>) {
|
|||
|
||||
createEffect(
|
||||
on(sessionID, (id) => {
|
||||
setRows(reconcile(reduceSessionRows(data.session.message.list(id))))
|
||||
setRows(reconcile(reduce()))
|
||||
void data.session.message.refresh(id).then(
|
||||
() => {
|
||||
if (sessionID() !== id) return
|
||||
setRows(reconcile(reduceSessionRows(data.session.message.list(id))))
|
||||
setRows(reconcile(reduce()))
|
||||
},
|
||||
() => undefined,
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
// Re-reduce when the revert boundary changes (stage/clear/commit).
|
||||
createEffect(
|
||||
on(revertBoundary, () => {
|
||||
setRows(reconcile(reduce()))
|
||||
}),
|
||||
)
|
||||
|
||||
const appendMessage = (messageID: string) =>
|
||||
setRows(
|
||||
produce((draft) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue