mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 21:07:01 -06:00
fix(ui): steer restored queued messages
This commit is contained in:
@@ -1976,6 +1976,58 @@ describeControlUiE2e("Control UI mocked Gateway E2E", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("steers a restored queued message when only the session row reports the active run", async () => {
|
||||
const context = await newBrowserContext({
|
||||
locale: "en-US",
|
||||
serviceWorkers: "block",
|
||||
viewport: { height: 900, width: 1280 },
|
||||
});
|
||||
const page = await context.newPage();
|
||||
const gateway = await installMockGateway(page);
|
||||
|
||||
try {
|
||||
await page.goto(`${server.baseUrl}chat`);
|
||||
|
||||
await page.locator(".agent-chat__composer-combobox textarea").fill("keep this run active");
|
||||
await page.getByRole("button", { name: "Send message" }).click();
|
||||
await gateway.waitForRequest("chat.send");
|
||||
await page.getByRole("button", { name: "Stop generating" }).waitFor({ timeout: 10_000 });
|
||||
|
||||
const queuedPrompt = "steer this after restoring the queue";
|
||||
await page.locator(".agent-chat__composer-combobox textarea").fill(queuedPrompt);
|
||||
await page.getByRole("button", { name: "Queue message" }).click();
|
||||
await page.locator(".chat-queue").getByText(queuedPrompt).waitFor({ timeout: 10_000 });
|
||||
|
||||
await gateway.setMethodResponse(
|
||||
"sessions.list",
|
||||
chatSessionListResponse([
|
||||
{
|
||||
hasActiveRun: true,
|
||||
key: "main",
|
||||
kind: "direct",
|
||||
label: "Main",
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
]),
|
||||
);
|
||||
await page.reload();
|
||||
|
||||
const queue = page.locator(".chat-queue");
|
||||
await queue.getByText(queuedPrompt).waitFor({ timeout: 10_000 });
|
||||
await queue.getByRole("button", { name: "Steer" }).click();
|
||||
|
||||
const steerRequest = await gateway.waitForRequest("chat.send");
|
||||
expect(requireRecord(steerRequest.params)).toMatchObject({
|
||||
deliver: false,
|
||||
message: queuedPrompt,
|
||||
sessionKey: "main",
|
||||
});
|
||||
await queue.getByText(queuedPrompt).waitFor({ state: "detached", timeout: 10_000 });
|
||||
} finally {
|
||||
await closeBrowserContext(context);
|
||||
}
|
||||
});
|
||||
|
||||
it("scrolls a delayed pending send into view before the ACK resolves", async () => {
|
||||
const context = await newBrowserContext({
|
||||
locale: "en-US",
|
||||
|
||||
@@ -6686,6 +6686,39 @@ describe("handleSendChat", () => {
|
||||
expect(host.chatQueue[0]?.pendingRunId).toBe("run-1");
|
||||
});
|
||||
|
||||
it("steers a queued message when only the session row reports an active run", async () => {
|
||||
const request = vi.fn(async (method: string) => {
|
||||
if (method === "chat.send") {
|
||||
return { status: "started", runId: "steer-run" };
|
||||
}
|
||||
throw new Error(`Unexpected request: ${method}`);
|
||||
});
|
||||
const original = { id: "queued-1", text: "tighten the plan", createdAt: 1 };
|
||||
const host = makeHost({
|
||||
client: { request } as unknown as ChatHost["client"],
|
||||
chatQueue: [original],
|
||||
sessionKey: "agent:main:main",
|
||||
sessionsResult: createSessionsResult([
|
||||
row("agent:main:main", { hasActiveRun: true, status: "running" }),
|
||||
]),
|
||||
});
|
||||
expect(admitQueuedMessageForSession(host, host.sessionKey, original)).toBe(true);
|
||||
|
||||
await steerQueuedChatMessage(host, original.id);
|
||||
|
||||
expect(request).toHaveBeenCalledWith(
|
||||
"chat.send",
|
||||
expect.objectContaining({
|
||||
sessionKey: "agent:main:main",
|
||||
message: "tighten the plan",
|
||||
deliver: false,
|
||||
}),
|
||||
);
|
||||
expect(host.chatRunId).toBeNull();
|
||||
expect(host.chatQueue).toEqual([]);
|
||||
expect(listStoredChatOutboxes(host)).toEqual([]);
|
||||
});
|
||||
|
||||
it("does not steer a queued message without a durable claim", async () => {
|
||||
const request = vi.fn();
|
||||
const original = { id: "memory-only-steer", text: "do not lose this", createdAt: 1 };
|
||||
|
||||
@@ -1294,7 +1294,7 @@ async function sendDetachedCommandMessage(
|
||||
}
|
||||
|
||||
export async function steerQueuedChatMessage(host: ChatHost, id: string) {
|
||||
if (!host.connected || !host.chatRunId) {
|
||||
if (!host.connected || !hasAbortableSessionRun(host)) {
|
||||
return;
|
||||
}
|
||||
const activeRunId = host.chatRunId;
|
||||
@@ -1333,7 +1333,7 @@ export async function steerQueuedChatMessage(host: ChatHost, id: string) {
|
||||
createdAt: item.createdAt,
|
||||
kind: "steered",
|
||||
...(item.attachments?.length ? { attachments: item.attachments } : {}),
|
||||
pendingRunId: activeRunId,
|
||||
...(activeRunId ? { pendingRunId: activeRunId } : { sendState: "steering" as const }),
|
||||
};
|
||||
const hasTransientProjection = setTransientQueuedMessageProjection(
|
||||
host,
|
||||
@@ -1361,17 +1361,19 @@ export async function steerQueuedChatMessage(host: ChatHost, id: string) {
|
||||
hasAttachments ? attachments : undefined,
|
||||
() => visibleSessionMatches(host, itemSessionKey, item.agentId),
|
||||
);
|
||||
const pendingStillVisible = host.chatQueue.some(
|
||||
(entry) => entry.id === id && entry.pendingRunId === activeRunId,
|
||||
);
|
||||
replacePendingQueuedMessageProjection(
|
||||
host,
|
||||
itemSessionKey,
|
||||
id,
|
||||
activeRunId,
|
||||
claimed,
|
||||
item.agentId,
|
||||
);
|
||||
const pendingStillVisible = activeRunId
|
||||
? host.chatQueue.some((entry) => entry.id === id && entry.pendingRunId === activeRunId)
|
||||
: false;
|
||||
if (activeRunId) {
|
||||
replacePendingQueuedMessageProjection(
|
||||
host,
|
||||
itemSessionKey,
|
||||
id,
|
||||
activeRunId,
|
||||
claimed,
|
||||
item.agentId,
|
||||
);
|
||||
}
|
||||
clearTransientQueuedMessageProjection(host, itemSessionKey, id, item.agentId);
|
||||
const itemStillVisible = visibleSessionMatches(host, itemSessionKey, item.agentId);
|
||||
if (!ack) {
|
||||
|
||||
Reference in New Issue
Block a user