From f32acba4706a1039bc4ddab20b7b1e773652d8de Mon Sep 17 00:00:00 2001 From: Ryan Vogel <89211796+R44VC0RP@users.noreply.github.com> Date: Sun, 12 Jul 2026 17:16:46 +0000 Subject: [PATCH] feat(tui): undo recent prompt with double escape Co-authored-by: opencode-agent[bot] --- packages/tui/src/component/prompt/index.tsx | 23 ++++++++++++++++- .../src/component/prompt/quick-undo.test.ts | 21 ++++++++++++++++ .../tui/src/component/prompt/quick-undo.ts | 25 +++++++++++++++++++ 3 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 packages/tui/src/component/prompt/quick-undo.test.ts create mode 100644 packages/tui/src/component/prompt/quick-undo.ts diff --git a/packages/tui/src/component/prompt/index.tsx b/packages/tui/src/component/prompt/index.tsx index 00efcbed2887..eb97e4292086 100644 --- a/packages/tui/src/component/prompt/index.tsx +++ b/packages/tui/src/component/prompt/index.tsx @@ -57,6 +57,8 @@ import { usePromptWorkspace } from "./workspace" import { usePromptMove } from "./move" import { readLocalAttachment } from "./local-attachment" import { useLocation } from "../../context/location" +import { Identifier } from "@opencode-ai/core/id/id" +import { createQuickUndo } from "./quick-undo" registerOpencodeSpinner() @@ -296,6 +298,7 @@ export function Prompt(props: PromptProps) { extmarkToPartIndex: new Map(), interrupt: 0, }) + const quickUndo = createQuickUndo() createEffect( on( @@ -394,7 +397,7 @@ export function Prompt(props: PromptProps) { category: "Session", hidden: true, enabled: status().type !== "idle", - run: () => { + run: async () => { if (auto()?.visible) return if (!input.focused) return // TODO: this should be its own command @@ -404,6 +407,18 @@ export function Prompt(props: PromptProps) { } if (!props.sessionID) return + const recent = quickUndo.escape() + if (recent) { + await sdk.client.session.abort({ sessionID: props.sessionID }).catch(() => {}) + await sdk.client.session.revert({ sessionID: props.sessionID, messageID: recent.messageID }) + input.setText(recent.value.input) + setStore("prompt", recent.value) + restoreExtmarksFromParts(recent.value.parts) + input.gotoBufferEnd() + input.focus() + return + } + setStore("interrupt", store.interrupt + 1) setTimeout(() => { @@ -1089,11 +1104,17 @@ export function Prompt(props: PromptProps) { parts: nonTextParts.filter((x) => x.type === "file"), }) } else { + const messageID = Identifier.ascending("message") + quickUndo.submitted(messageID, { + input: store.prompt.input, + parts: [...unwrap(store.prompt.parts)], + }) move.startSubmit() sdk.client.session .prompt( { sessionID, + messageID, ...selectedModel, agent: agent.name, model: selectedModel, diff --git a/packages/tui/src/component/prompt/quick-undo.test.ts b/packages/tui/src/component/prompt/quick-undo.test.ts new file mode 100644 index 000000000000..fcd766eaaa1e --- /dev/null +++ b/packages/tui/src/component/prompt/quick-undo.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, test } from "bun:test" +import { createQuickUndo } from "./quick-undo" + +describe("quick undo", () => { + test("returns the submitted message on a second escape within two seconds", () => { + const undo = createQuickUndo() + undo.submitted("msg_1", "hello", 1_000) + + expect(undo.escape(1_500)).toBeUndefined() + expect(undo.escape(1_750)).toEqual({ messageID: "msg_1", value: "hello" }) + expect(undo.escape(1_800)).toBeUndefined() + }) + + test("expires two seconds after submission", () => { + const undo = createQuickUndo() + undo.submitted("msg_1", "hello", 1_000) + + expect(undo.escape(2_500)).toBeUndefined() + expect(undo.escape(3_001)).toBeUndefined() + }) +}) diff --git a/packages/tui/src/component/prompt/quick-undo.ts b/packages/tui/src/component/prompt/quick-undo.ts new file mode 100644 index 000000000000..5be481b40a4c --- /dev/null +++ b/packages/tui/src/component/prompt/quick-undo.ts @@ -0,0 +1,25 @@ +const WINDOW_MS = 2_000 + +export function createQuickUndo() { + let submitted: { messageID: string; value: T; time: number } | undefined + let escapedAt: number | undefined + + return { + submitted(messageID: string, value: T, time = Date.now()) { + submitted = { messageID, value, time } + escapedAt = undefined + }, + escape(time = Date.now()) { + if (!submitted || time - submitted.time > WINDOW_MS) return + if (escapedAt === undefined || time - escapedAt > WINDOW_MS) { + escapedAt = time + return + } + + const result = { messageID: submitted.messageID, value: submitted.value } + submitted = undefined + escapedAt = undefined + return result + }, + } +}