Skip to content

tui: reverting during generation restores the prompt but does not revert #57383

Description

@yassine-safraoui

Description

While a turn is streaming, Message Actions → Revert puts the user message back into the prompt, but nothing is reverted and the turn keeps running. session.revert.stage returns SessionBusyError while execution is active; the TUI shows it only as a toast, after the prompt has already been filled. Pressing Enter then sends the restored text as a new message.

This is the same symptom as #53159, which is the likely duplicate. #53159 was closed without a fix shipping in v2: its fix PR #53161 ("fix(tui): abort running turn before reverting a message") was closed unmerged, and the v2.1.0 code still fills the prompt before attempting the revert.

Expected: either leave the prompt untouched when the revert fails, or ask for confirmation that reverting will stop the running turn, then interrupt, wait, and stage (the same order /undo uses).

Plugins

none

OpenCode version

2.1.0

Steps to reproduce

  1. Start a prompt that streams for a while.
  2. While it runs, open a user message's actions and choose Revert.
  3. The prompt is filled, the turn keeps running, and no revert happens.

Screenshot

No response

Operating System

Windows 11 (10.0.26100)

Terminal

Windows Terminal

Activity

  1. opencode-agent commented on Oct 20, 2026

    @opencode-agent
    Contributor

    Thanks for the clear report. I could reproduce this on 2.1.0, and it still happens on the latest dev (d814879).

    What I did, using a simulated model that streams a long reply:

    1. Sent a prompt and, while the reply was still streaming, opened the user message's Message Actions (through /timeline).
    2. Chose Revert.

    The prompt was filled with the old message immediately, and a Session is busy: ses_… toast appeared. The turn was not interrupted and streamed to the end. The original message and the reply stayed in the transcript, so nothing was reverted, and the old text was left in the prompt, ready to be sent again.

    The Revert action fills the prompt before it attempts the revert and never interrupts the running turn, unlike /undo, which interrupts, waits, stages and only then fills the prompt. The recording and screenshots below show it, and the script below reproduces it.

    Related: #53159 (same symptom on v1; its fix PR #53161 was not merged), #14014 and #43670 (the same problem in the Web UI).

    recording.mp4

    Screenshot

    Screenshot

    Reproduction script (drive.ts)
    // Repro for anomalyco/opencode#57383: Message Actions -> Revert while a turn is
    // streaming fills the prompt, but the revert fails (session busy) and the turn keeps running.
    import { Effect } from "effect"
    import { Llm, OpenCodeDriver } from "opencode-drive"
    
    const long = Array.from({ length: 40 }, (_, i) => `Chunk${i + 1}`).join("\n\n")
    
    export default OpenCodeDriver.use(
      {
        ...(process.env.OC_DEV ? { opencode: { dev: process.env.OC_DEV } } : {}),
        project: { git: true, files: { "src/value.ts": "export const value = 1\n" } },
        tui: { recording: true, keypressOverlay: true },
      },
      ({ ui, llm }) =>
        Effect.gen(function* () {
          // A slow streaming response so the turn is still running when we revert.
          yield* llm.queue(Llm.text(long, { delay: 300, chunkSize: 5 }))
          yield* ui.submit("Write a long answer about value.ts")
          yield* Effect.sleep(5000)
          yield* ui.screenshot("01-streaming")
    
          // Open the timeline and choose the user message -> Message Actions
          yield* ui.type("/timeline")
          yield* Effect.sleep(500)
          yield* ui.enter()
          yield* Effect.sleep(1000)
          yield* Effect.sleep(500)
          yield* ui.screenshot("02-timeline")
          yield* ui.enter()
          yield* ui.waitFor("Message Actions", { timeout: 15000 })
          yield* ui.screenshot("03-message-actions")
    
          // Pick "Revert"
          yield* ui.type("Revert")
          yield* Effect.sleep(300)
          yield* ui.enter()
          yield* Effect.sleep(1500)
          yield* ui.screenshot("04-after-revert")
    
          // The turn keeps streaming and the old message is still there.
          yield* Effect.sleep(4000)
          yield* ui.screenshot("05-still-running")
          // Let the turn finish: it was never interrupted, and nothing was reverted.
          yield* Effect.sleep(15000)
          yield* Effect.sleep(1500)
          yield* ui.screenshot("06-turn-finished-not-reverted")
        }),
    )

    Run it with opencode-drive: opencode-drive run ./drive.ts. It drives opencode from your PATH; set OC_DEV to the path of an OpenCode checkout to run that from source instead.

  2. yassine-safraoui commented on Oct 20, 2026

    @yassine-safraoui
    Author

    Omg you guys really cooked with the opencode-agent 😵

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions