You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Bug: UI stuck in loading state after undoing and resending message #26654
After undoing (rolling back) a sent message and then resending, the UI fails to properly display the agent's running state. Instead of showing the normal AI execution flow (thinking steps, tool calls, etc.), the page only shows a loading spinner in the bottom-right corner indefinitely.
After restarting the app, the agent works normally and displays the full execution process. This confirms the issue is a UI state management bug related to the undo/rollback operation — not an agent processing failure.
Steps to reproduce:
Send a message to the agent and wait for the response
Undo/rollback the sent message (message shows as "已回滚消息")
Resend a message
Observe: UI only shows a spinning loading indicator, agent's running state (thinking, tool calls) is not displayed
Restart the app → agent works normally with full UI display
Expected behavior:
After resending a rolled-back message, the UI should correctly transition to show the agent's execution flow (thinking steps, tool usage, progress), just like a normal new message.
Actual behavior:
The UI remains stuck on a loading spinner and does not update to reflect the agent's actual running state.
We encountered a related but more severe variant of this bug. In our case, after undoing and quickly pressing Enter twice (the first triggers /undo, the second submits the restored text as a new prompt), the conversation and thinking output completely disappear — and the issue persists after restarting opencode.
Root cause analysis
The session.undo palette command (routes/session/index.tsx:598) has a fire-and-forget pattern that creates a race:
First Enter → autocomplete selects /undo → runs session.undo → starts revert() as fire-and-forget → immediately restores the input text via prompt?.set()
Second Enter (fast enough) → submitInner() reads the restored (non-empty) input → sends session.prompt() as a new message
Server-side race: revert() (slower, involves git snapshot) and prompt() (faster, just creates a message) run concurrently. If prompt() completes first:
New messages are created in the session (no revert state exists yet)
Then revert() completes and sets revert.messageID on the session
The session now has both revert state AND new messages beyond the revert point — persisted to SQLite
UI filter kicks in: The <Match when={revert()?.messageID && message.id >= revert()!.messageID}> at routes/session/index.tsx:1194 hides all messages with id >= revert.messageID, including the new prompt + AI thinking + AI response
Survives restart: Because the corrupt state (revert + new messages) is persisted in SQLite, restarting opencode reloads the same inconsistent session — the filter still hides everything
With a TPS plugin we could confirm the LLM actually processes the request (thinking, tool calls all happen server-side), but the TUI simply doesn't render anything due to the client-side revert filter.
Suggested fix direction
The race can be eliminated by moving prompt?.set() into the revert completion callback instead of running it immediately:
prompt?.set({input: "",parts: []})// clear input immediately to prevent resubmissionvoidsdk.client.session.revert({...}).then(()=>{prompt?.set(restoredText)// restore only after revert completestoBottom()})
This clears the input visually (preventing the second Enter from submitting stale text) and only restores the original message after the revert is fully committed — eliminating the race window entirely.
Further investigation confirms this is more severe than a UI-only bug — messages are physically deleted from the database, not just hidden.
The key is revert.cleanup() (called inside SessionPrompt.prompt()) which actually removes messages >= revert.messageID from the session data:
session/prompt.ts (approximate):
prompt() → revert.cleanup(session) → DELETE messages >= revert.messageID
→ clear revert state
→ create new user message
→ run AI loop
When the race occurs (revert() and prompt() run concurrently on the same session):
Scenario A — revert() completes first, then prompt() runs:
revert() sets revert.messageID = B.id (B is the user message being undone)
prompt() (from the second Enter) reads session → sees revert state
cleanup() deletes messages >= B.id — B and its AI response are gone
Creates new message D (containing A's text, restored by undo)
AI loop builds context from remaining messages: only A, A's response, and D
B and its AI response are permanently lost — the AI never sees them
Scenario B — prompt() completes first, then revert() runs:
prompt() reads session → no revert state → cleanup is no-op
Creates D, AI processes with full context (including B)
revert() completes → sets revert.messageID = B.id in the DB
Session now has: revert state + new messages beyond revert point
After restart, user sends C: "你刚才说了什么吗?"
prompt() loads session → sees revert state → cleanup() deletes messages >= B.id
B, B's AI response, D, and D's AI response are all deleted
AI loops with only [A, AI_response_to_A, C] — knows nothing about B
This matches the user's test exactly: after restart, the AI only remembers A and has no knowledge of B. The messages are being removed from the SQLite database, not just hidden in the TUI. This is a data-loss bug, not a display bug.
The root cause remains the same: prompt?.set() restores the input text before revert() completes, allowing the second Enter to submit a new message into the revert window. The fix should ensure the input is not restored until the revert is fully committed.
Activity
Small-tailqwq commentedon May 17, 2026
We encountered a related but more severe variant of this bug. In our case, after undoing and quickly pressing Enter twice (the first triggers
/undo, the second submits the restored text as a new prompt), the conversation and thinking output completely disappear — and the issue persists after restarting opencode.Root cause analysis
The
session.undopalette command (routes/session/index.tsx:598) has a fire-and-forget pattern that creates a race:The race:
/undo→ runssession.undo→ startsrevert()as fire-and-forget → immediately restores the input text viaprompt?.set()submitInner()reads the restored (non-empty) input → sendssession.prompt()as a new messagerevert()(slower, involves git snapshot) andprompt()(faster, just creates a message) run concurrently. Ifprompt()completes first:revert()completes and setsrevert.messageIDon the session<Match when={revert()?.messageID && message.id >= revert()!.messageID}>atroutes/session/index.tsx:1194hides all messages withid >= revert.messageID, including the new prompt + AI thinking + AI responserevert+ new messages) is persisted in SQLite, restarting opencode reloads the same inconsistent session — the filter still hides everythingWith a TPS plugin we could confirm the LLM actually processes the request (thinking, tool calls all happen server-side), but the TUI simply doesn't render anything due to the client-side revert filter.
Suggested fix direction
The race can be eliminated by moving
prompt?.set()into the revert completion callback instead of running it immediately:This clears the input visually (preventing the second Enter from submitting stale text) and only restores the original message after the revert is fully committed — eliminating the race window entirely.
Small-tailqwq commentedon May 17, 2026
Further investigation confirms this is more severe than a UI-only bug — messages are physically deleted from the database, not just hidden.
The key is
revert.cleanup()(called insideSessionPrompt.prompt()) which actually removes messages>= revert.messageIDfrom the session data:When the race occurs (
revert()andprompt()run concurrently on the same session):Scenario A —
revert()completes first, thenprompt()runs:revert()setsrevert.messageID = B.id(B is the user message being undone)prompt()(from the second Enter) reads session → sees revert statecleanup()deletes messages >= B.id — B and its AI response are goneScenario B —
prompt()completes first, thenrevert()runs:prompt()reads session → no revert state → cleanup is no-oprevert()completes → setsrevert.messageID = B.idin the DBprompt()loads session → sees revert state →cleanup()deletes messages >= B.idThis matches the user's test exactly: after restart, the AI only remembers A and has no knowledge of B. The messages are being removed from the SQLite database, not just hidden in the TUI. This is a data-loss bug, not a display bug.
The root cause remains the same:
prompt?.set()restores the input text beforerevert()completes, allowing the second Enter to submit a new message into the revert window. The fix should ensure the input is not restored until the revert is fully committed.github-actions commentedon Jul 17, 2026
To stay organized issues are automatically closed after 60 days of no activity. If the issue is still relevant please open a new one.