Show user message immediately when sending to a task opened from history (#12753)

* Show user message immediately when sending to a history-resumed task

Sending a message to a task opened from history routed through the
resume_task/resume_completed_task askResponse branch, which forced the
Thinking loader but never set the optimistic user_feedback bubble. The
extension only echoes the user's message after the full SDK session
resume completes, so the chat showed a Thinking indicator with no user
message until the (slow) resume finished.

Pass showPendingMessage on the resume branch like the other
non-streaming follow-up paths, so the user's message appears in the
chat immediately. The optimistic bubble reconciles with the extension's
say:user_feedback echo once the resume completes (identical raw text).

* Add changeset
This commit is contained in:
Saoud Rizwan
2026-07-30 14:28:18 -07:00
committed by GitHub
parent 3590b425eb
commit 16d0d04573
3 changed files with 87 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Show the user's message in chat immediately when sending to a task opened from history, instead of only a thinking indicator until the session resume finishes
@@ -349,6 +349,83 @@ describe("useMessageHandlers — send routing", () => {
expect(rollbackResponse(newerResponse)).toBe(newerResponse)
})
it("shows a pending chat bubble immediately when sending a message to a task resumed from history", async () => {
mockTurnState = { phase: "resumable", seq: 5 }
const historyConversation: ClineMessage[] = [
{ ts: 1, type: "say", say: "task", text: "task" },
{ ts: 2, type: "say", say: "text", text: "partial work" },
{ ts: 3, type: "ask", ask: "resume_task" },
]
const setPendingUserMessage = vi.fn()
const setPendingResponse = vi.fn()
const { result } = renderHook(() =>
useMessageHandlers(
historyConversation,
makeChatState(historyConversation, { setPendingUserMessage, setPendingResponse }),
),
)
await act(async () => {
await result.current.handleSendMessage("keep going", ["image.png"], ["a.ts"])
})
expect(newTask).not.toHaveBeenCalled()
expect(askResponse).toHaveBeenCalledTimes(1)
expect(askResponse).toHaveBeenCalledWith(
expect.objectContaining({
responseType: "yesButtonClicked",
text: "keep going",
images: ["image.png"],
files: ["a.ts"],
}),
)
expect(setPendingUserMessage).toHaveBeenCalledWith(
expect.objectContaining({
afterTs: 3,
message: expect.objectContaining({
type: "say",
say: "user_feedback",
text: "keep going",
images: ["image.png"],
files: ["a.ts"],
partial: false,
}),
}),
)
expect(setPendingResponse).toHaveBeenCalledWith({
id: 1,
turnStateSeq: 5,
messageCount: historyConversation.length,
})
})
it("shows a pending chat bubble when resuming a completed task from history", async () => {
mockTurnState = { phase: "completed", seq: 4 }
const historyConversation: ClineMessage[] = [
{ ts: 1, type: "say", say: "task", text: "task" },
{ ts: 2, type: "say", say: "completion_result", text: "all done" },
{ ts: 3, type: "ask", ask: "resume_completed_task" },
]
const setPendingUserMessage = vi.fn()
const { result } = renderHook(() =>
useMessageHandlers(historyConversation, makeChatState(historyConversation, { setPendingUserMessage })),
)
await act(async () => {
await result.current.handleSendMessage("one more thing", [], [])
})
expect(askResponse).toHaveBeenCalledWith(
expect.objectContaining({ responseType: "yesButtonClicked", text: "one more thing" }),
)
expect(setPendingUserMessage).toHaveBeenCalledWith(
expect.objectContaining({
afterTs: 3,
message: expect.objectContaining({ say: "user_feedback", text: "one more thing" }),
}),
)
})
it("does not show a pending chat bubble for a streaming follow-up that will be queued", async () => {
mockTurnState = { phase: "streaming", seq: 9 }
const streamingConversation: ClineMessage[] = [
@@ -204,6 +204,10 @@ export function useMessageHandlers(messages: ClineMessage[], chatState: ChatStat
// For resume_task and resume_completed_task, use yesButtonClicked to match Resume button behavior
// This ensures Enter key and Resume button work identically
if (clineAsk === "resume_task" || clineAsk === "resume_completed_task") {
// Resuming a task opened from history rebuilds the SDK session before the
// extension echoes say:user_feedback, so without an optimistic bubble the
// user's message would not appear until the (slow) resume finishes — the
// chat would show only the Thinking loader in the meantime.
await sendAskResponseWithPendingState(
AskResponseRequest.create({
responseType: "yesButtonClicked",
@@ -211,6 +215,7 @@ export function useMessageHandlers(messages: ClineMessage[], chatState: ChatStat
images,
files,
}),
{ showPendingMessage: turnState?.phase !== "streaming" },
)
messageSent = true
} else {