mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
fix(ui): reconcile completed chat sends
Fixes #87699.\n\nRoutes ACK-completed Control UI chat sends through the existing run lifecycle reconciliation path so stale selected-session rows cannot re-enable the composer/Stop state after the conversation has already completed.\n\nVerification: focused UI/unit tests, Control UI E2E chat-flow test, autoreview clean, Testbox changed gate tbx_01kt68xvz17fcnmd3wj6f7pk6f, and PR CI run 26872484363 green after failed-job rerun for transient runner setup failures.
This commit is contained in:
@@ -10,6 +10,7 @@ import {
|
||||
resetChatAttachmentPayloadStoreForTest,
|
||||
} from "./chat/attachment-payload-store.ts";
|
||||
import type { executeSlashCommand } from "./chat/slash-command-executor.ts";
|
||||
import { loadSessions } from "./controllers/sessions.ts";
|
||||
import type { GatewaySessionRow, SessionsListResult } from "./types.ts";
|
||||
|
||||
type ExecuteSlashCommand = typeof executeSlashCommand;
|
||||
@@ -2116,6 +2117,43 @@ describe("handleSendChat", () => {
|
||||
expect(userMessage.role).toBe("user");
|
||||
});
|
||||
|
||||
it("keeps ACK-completed sends idle when sessions.list returns a stale active row", async () => {
|
||||
const request = vi.fn(async (method: string, params?: unknown) => {
|
||||
if (method === "chat.send") {
|
||||
const payload = requireRecord(params, "chat send payload");
|
||||
return { runId: payload.idempotencyKey, status: "ok" };
|
||||
}
|
||||
if (method === "chat.history") {
|
||||
return { messages: [] };
|
||||
}
|
||||
if (method === "sessions.list") {
|
||||
return createSessionsResult([
|
||||
row("agent:main", { hasActiveRun: true, status: "running", startedAt: 1 }),
|
||||
]);
|
||||
}
|
||||
throw new Error(`Unexpected request: ${method}`);
|
||||
});
|
||||
const host = makeHost({
|
||||
client: { request } as unknown as ChatHost["client"],
|
||||
chatMessage: "already done",
|
||||
sessionsResult: createSessionsResult([
|
||||
row("agent:main", { hasActiveRun: true, status: "running", startedAt: 1 }),
|
||||
]),
|
||||
});
|
||||
|
||||
await handleSendChat(host);
|
||||
await Promise.resolve();
|
||||
await loadSessions(host as unknown as Parameters<typeof loadSessions>[0]);
|
||||
|
||||
expect(host.chatRunId).toBeNull();
|
||||
expect(host.chatStream).toBeNull();
|
||||
expect(hasAbortableSessionRun(host)).toBe(false);
|
||||
expect(host.sessionsResult?.sessions[0]).toMatchObject({
|
||||
hasActiveRun: false,
|
||||
status: "done",
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps delayed chat.send ACK effects scoped to the submitted session", async () => {
|
||||
const sent = createDeferred<unknown>();
|
||||
const request = vi.fn((method: string) => {
|
||||
|
||||
@@ -928,6 +928,7 @@ async function sendQueuedChatMessage(
|
||||
reconcileChatRunLifecycle(
|
||||
host as unknown as Parameters<typeof reconcileChatRunLifecycle>[0],
|
||||
{
|
||||
outcome: "done",
|
||||
sessionStatus: "done",
|
||||
runId: ack.runId,
|
||||
sessionKey,
|
||||
@@ -935,7 +936,8 @@ async function sendQueuedChatMessage(
|
||||
clearChatStream: true,
|
||||
clearToolStream: true,
|
||||
clearSideResultTerminalRuns: true,
|
||||
clearRunStatus: true,
|
||||
publishRunStatus: false,
|
||||
armLocalTerminalReconcile: true,
|
||||
},
|
||||
);
|
||||
void loadChatHistory(host as unknown as ChatState);
|
||||
|
||||
@@ -1538,6 +1538,20 @@ describe("sendChatMessage", () => {
|
||||
expect(state.chatRunId).toBeNull();
|
||||
expect(state.chatStream).toBeNull();
|
||||
expect(state.chatStreamStartedAt).toBeNull();
|
||||
const runState = state as ChatState & {
|
||||
chatRunStatus?: unknown;
|
||||
lastLocalTerminalReconcile?: unknown;
|
||||
};
|
||||
expect(runState.chatRunStatus).toMatchObject({
|
||||
phase: "done",
|
||||
runId: "gateway-complete-run",
|
||||
sessionKey: "main",
|
||||
});
|
||||
expect(runState.lastLocalTerminalReconcile).toMatchObject({
|
||||
phase: "done",
|
||||
runId: "gateway-complete-run",
|
||||
sessionKey: "main",
|
||||
});
|
||||
});
|
||||
|
||||
it("serializes non-image chat attachments as files", async () => {
|
||||
|
||||
@@ -949,9 +949,18 @@ export async function sendChatMessage(
|
||||
try {
|
||||
const ack = await requestChatSend(state, { message: msg, attachments, runId });
|
||||
if (ack.status === "ok") {
|
||||
state.chatRunId = null;
|
||||
state.chatStream = null;
|
||||
state.chatStreamStartedAt = null;
|
||||
reconcileChatRunLifecycle(
|
||||
state as unknown as Parameters<typeof reconcileChatRunLifecycle>[0],
|
||||
{
|
||||
outcome: "done",
|
||||
sessionStatus: "done",
|
||||
runId: ack.runId,
|
||||
sessionKey: state.sessionKey,
|
||||
clearLocalRun: true,
|
||||
clearChatStream: true,
|
||||
armLocalTerminalReconcile: true,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
state.chatRunId = ack.runId;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user