mirror of
https://github.com/cline/cline.git
synced 2026-09-01 04:25:02 +08:00
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:
@@ -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
|
||||
+77
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user