Web UI: keep sub-agent announce replies visible (#1977)
This commit is contained in:
@@ -19,6 +19,7 @@ Status: unreleased.
|
|||||||
- Onboarding: add Venice API key to non-interactive flow. (#1893) Thanks @jonisjongithub.
|
- Onboarding: add Venice API key to non-interactive flow. (#1893) Thanks @jonisjongithub.
|
||||||
- Tlon: format thread reply IDs as @ud. (#1837) Thanks @wca4a.
|
- Tlon: format thread reply IDs as @ud. (#1837) Thanks @wca4a.
|
||||||
- Gateway: prefer newest session metadata when combining stores. (#1823) Thanks @emanuelst.
|
- Gateway: prefer newest session metadata when combining stores. (#1823) Thanks @emanuelst.
|
||||||
|
- Web UI: keep sub-agent announce replies visible in WebChat. (#1977) Thanks @andrescardonas7.
|
||||||
- CI: increase Node heap size for macOS checks. (#1890) Thanks @realZachi.
|
- CI: increase Node heap size for macOS checks. (#1890) Thanks @realZachi.
|
||||||
- macOS: avoid crash when rendering code blocks by bumping Textual to 0.3.1. (#2033) Thanks @garricn.
|
- macOS: avoid crash when rendering code blocks by bumping Textual to 0.3.1. (#2033) Thanks @garricn.
|
||||||
- Browser: fall back to URL matching for extension relay target resolution. (#1999) Thanks @jonit-dev.
|
- Browser: fall back to URL matching for extension relay target resolution. (#1999) Thanks @jonit-dev.
|
||||||
|
|||||||
99
ui/src/ui/controllers/chat.test.ts
Normal file
99
ui/src/ui/controllers/chat.test.ts
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import {
|
||||||
|
handleChatEvent,
|
||||||
|
type ChatEventPayload,
|
||||||
|
type ChatState,
|
||||||
|
} from "./chat";
|
||||||
|
|
||||||
|
function createState(overrides: Partial<ChatState> = {}): ChatState {
|
||||||
|
return {
|
||||||
|
client: null,
|
||||||
|
connected: true,
|
||||||
|
sessionKey: "main",
|
||||||
|
chatLoading: false,
|
||||||
|
chatMessages: [],
|
||||||
|
chatThinkingLevel: null,
|
||||||
|
chatSending: false,
|
||||||
|
chatMessage: "",
|
||||||
|
chatRunId: null,
|
||||||
|
chatStream: null,
|
||||||
|
chatStreamStartedAt: null,
|
||||||
|
lastError: null,
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("handleChatEvent", () => {
|
||||||
|
it("returns null when payload is missing", () => {
|
||||||
|
const state = createState();
|
||||||
|
expect(handleChatEvent(state, undefined)).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns null when sessionKey does not match", () => {
|
||||||
|
const state = createState({ sessionKey: "main" });
|
||||||
|
const payload: ChatEventPayload = {
|
||||||
|
runId: "run-1",
|
||||||
|
sessionKey: "other",
|
||||||
|
state: "final",
|
||||||
|
};
|
||||||
|
expect(handleChatEvent(state, payload)).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns null for delta from another run", () => {
|
||||||
|
const state = createState({
|
||||||
|
sessionKey: "main",
|
||||||
|
chatRunId: "run-user",
|
||||||
|
chatStream: "Hello",
|
||||||
|
});
|
||||||
|
const payload: ChatEventPayload = {
|
||||||
|
runId: "run-announce",
|
||||||
|
sessionKey: "main",
|
||||||
|
state: "delta",
|
||||||
|
message: { role: "assistant", content: [{ type: "text", text: "Done" }] },
|
||||||
|
};
|
||||||
|
expect(handleChatEvent(state, payload)).toBe(null);
|
||||||
|
expect(state.chatRunId).toBe("run-user");
|
||||||
|
expect(state.chatStream).toBe("Hello");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns 'final' for final from another run (e.g. sub-agent announce) without clearing state", () => {
|
||||||
|
const state = createState({
|
||||||
|
sessionKey: "main",
|
||||||
|
chatRunId: "run-user",
|
||||||
|
chatStream: "Working...",
|
||||||
|
chatStreamStartedAt: 123,
|
||||||
|
});
|
||||||
|
const payload: ChatEventPayload = {
|
||||||
|
runId: "run-announce",
|
||||||
|
sessionKey: "main",
|
||||||
|
state: "final",
|
||||||
|
message: {
|
||||||
|
role: "assistant",
|
||||||
|
content: [{ type: "text", text: "Sub-agent findings" }],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
expect(handleChatEvent(state, payload)).toBe("final");
|
||||||
|
expect(state.chatRunId).toBe("run-user");
|
||||||
|
expect(state.chatStream).toBe("Working...");
|
||||||
|
expect(state.chatStreamStartedAt).toBe(123);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("processes final from own run and clears state", () => {
|
||||||
|
const state = createState({
|
||||||
|
sessionKey: "main",
|
||||||
|
chatRunId: "run-1",
|
||||||
|
chatStream: "Reply",
|
||||||
|
chatStreamStartedAt: 100,
|
||||||
|
});
|
||||||
|
const payload: ChatEventPayload = {
|
||||||
|
runId: "run-1",
|
||||||
|
sessionKey: "main",
|
||||||
|
state: "final",
|
||||||
|
};
|
||||||
|
expect(handleChatEvent(state, payload)).toBe("final");
|
||||||
|
expect(state.chatRunId).toBe(null);
|
||||||
|
expect(state.chatStream).toBe(null);
|
||||||
|
expect(state.chatStreamStartedAt).toBe(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { GatewayBrowserClient } from "../gateway";
|
|
||||||
import { extractText } from "../chat/message-extract";
|
import { extractText } from "../chat/message-extract";
|
||||||
|
import type { GatewayBrowserClient } from "../gateway";
|
||||||
import { generateUUID } from "../uuid";
|
import { generateUUID } from "../uuid";
|
||||||
|
|
||||||
export type ChatState = {
|
export type ChatState = {
|
||||||
@@ -115,8 +115,17 @@ export function handleChatEvent(
|
|||||||
) {
|
) {
|
||||||
if (!payload) return null;
|
if (!payload) return null;
|
||||||
if (payload.sessionKey !== state.sessionKey) return null;
|
if (payload.sessionKey !== state.sessionKey) return null;
|
||||||
if (payload.runId && state.chatRunId && payload.runId !== state.chatRunId)
|
|
||||||
|
// Final from another run (e.g. sub-agent announce): refresh history to show new message.
|
||||||
|
// See https://github.com/clawdbot/clawdbot/issues/1909
|
||||||
|
if (
|
||||||
|
payload.runId &&
|
||||||
|
state.chatRunId &&
|
||||||
|
payload.runId !== state.chatRunId
|
||||||
|
) {
|
||||||
|
if (payload.state === "final") return "final";
|
||||||
return null;
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (payload.state === "delta") {
|
if (payload.state === "delta") {
|
||||||
const next = extractText(payload.message);
|
const next = extractText(payload.message);
|
||||||
|
|||||||
Reference in New Issue
Block a user