mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-07 02:22:46 +00:00
fix(ui): cover assistant responses input blocks
This commit is contained in:
@@ -580,7 +580,7 @@ function extractAssistantTextForSilentCheck(message: unknown): string | undefine
|
||||
return undefined;
|
||||
}
|
||||
const typed = block as { type?: unknown; text?: unknown };
|
||||
if ((typed.type !== "text" && typed.type !== "output_text") || typeof typed.text !== "string") {
|
||||
if (!isAssistantTextContentType(typed.type) || typeof typed.text !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
texts.push(typed.text);
|
||||
@@ -588,6 +588,10 @@ function extractAssistantTextForSilentCheck(message: unknown): string | undefine
|
||||
return texts.length > 0 ? texts.join("\n") : undefined;
|
||||
}
|
||||
|
||||
function isAssistantTextContentType(type: unknown): boolean {
|
||||
return type === "text" || type === "input_text" || type === "output_text";
|
||||
}
|
||||
|
||||
function hasAssistantNonTextContent(message: unknown): boolean {
|
||||
if (!message || typeof message !== "object") {
|
||||
return false;
|
||||
@@ -600,8 +604,7 @@ function hasAssistantNonTextContent(message: unknown): boolean {
|
||||
(block) =>
|
||||
block &&
|
||||
typeof block === "object" &&
|
||||
(block as { type?: unknown }).type !== "text" &&
|
||||
(block as { type?: unknown }).type !== "output_text",
|
||||
!isAssistantTextContentType((block as { type?: unknown }).type),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -623,7 +626,11 @@ function hasAssistantMixedToolVisibleText(message: unknown): boolean {
|
||||
if (isToolHistoryBlockType(entry.type)) {
|
||||
hasToolHistoryBlock = true;
|
||||
}
|
||||
if (entry.type === "text" && typeof entry.text === "string" && entry.text.trim()) {
|
||||
if (
|
||||
isAssistantTextContentType(entry.type) &&
|
||||
typeof entry.text === "string" &&
|
||||
entry.text.trim()
|
||||
) {
|
||||
hasText = true;
|
||||
}
|
||||
}
|
||||
@@ -1648,7 +1655,7 @@ function projectEmptyAssistantErrorMessages(
|
||||
}
|
||||
const type = (block as { type?: unknown }).type;
|
||||
return (
|
||||
type !== "text" &&
|
||||
!isAssistantTextContentType(type) &&
|
||||
type !== "thinking" &&
|
||||
type !== "reasoning" &&
|
||||
type !== "redacted_thinking"
|
||||
@@ -1669,7 +1676,7 @@ function projectEmptyAssistantErrorMessages(
|
||||
continue;
|
||||
}
|
||||
const entry = block as { type?: unknown; text?: unknown };
|
||||
if (entry.type === "text" && typeof entry.text === "string") {
|
||||
if (isAssistantTextContentType(entry.type) && typeof entry.text === "string") {
|
||||
visibleTexts.push(entry.text);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,20 @@ describe("stripEnvelopeFromMessage", () => {
|
||||
expect(assistant.content?.[0]?.text).toBe("Assistant body");
|
||||
});
|
||||
|
||||
test("strips internal metadata from assistant input_text blocks", () => {
|
||||
const assistant = stripEnvelopeFromMessage({
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "input_text",
|
||||
text: 'Conversation info (untrusted metadata):\n```json\n{"message_id":"123"}\n```\n\nAssistant body',
|
||||
},
|
||||
],
|
||||
}) as { content?: Array<{ text?: string }> };
|
||||
|
||||
expect(assistant.content?.[0]?.text).toBe("Assistant body");
|
||||
});
|
||||
|
||||
test("does not strip inline message_id text that is part of a line", () => {
|
||||
const input = {
|
||||
role: "user",
|
||||
|
||||
@@ -59,7 +59,7 @@ function stripEnvelopeFromContentWithRole(
|
||||
const isRoleTextBlock =
|
||||
entry.type === "text" ||
|
||||
(role === "user" && entry.type === "input_text") ||
|
||||
(role === "assistant" && entry.type === "output_text");
|
||||
(role === "assistant" && (entry.type === "input_text" || entry.type === "output_text"));
|
||||
if (!isRoleTextBlock || typeof entry.text !== "string") {
|
||||
return item;
|
||||
}
|
||||
|
||||
@@ -941,6 +941,43 @@ describe("projectRecentChatDisplayMessages", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["output_text", ""],
|
||||
["output_text", "NO_REPLY"],
|
||||
["input_text", ""],
|
||||
["input_text", "NO_REPLY"],
|
||||
])("projects hidden %s assistant errors %j as a generic safe failure", (type, text) => {
|
||||
const result = projectRecentChatDisplayMessages([
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type, text }],
|
||||
stopReason: "error",
|
||||
errorMessage: "Connection error.",
|
||||
timestamp: 1,
|
||||
},
|
||||
]);
|
||||
|
||||
expect(result[0]?.content).toEqual([
|
||||
{ type: "text", text: "The agent run failed before producing a reply." },
|
||||
]);
|
||||
});
|
||||
|
||||
it("preserves visible output_text from a failed assistant turn", () => {
|
||||
const result = projectRecentChatDisplayMessages([
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "output_text", text: "A partial reply before the run failed." }],
|
||||
stopReason: "error",
|
||||
errorMessage: "Connection error.",
|
||||
timestamp: 1,
|
||||
},
|
||||
]);
|
||||
|
||||
expect(result[0]?.content).toEqual([
|
||||
{ type: "output_text", text: "A partial reply before the run failed." },
|
||||
]);
|
||||
});
|
||||
|
||||
it("projects thinking-only assistant errors as a generic safe failure", () => {
|
||||
const result = projectRecentChatDisplayMessages([
|
||||
{
|
||||
|
||||
@@ -714,9 +714,22 @@ describe("gateway server chat", () => {
|
||||
content: [{ type: "output_text", text: "visible response" }],
|
||||
timestamp: 2,
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "input_text", text: "NO_REPLY" }],
|
||||
timestamp: 3,
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "input_text", text: "visible assistant input" }],
|
||||
timestamp: 4,
|
||||
},
|
||||
]);
|
||||
|
||||
expect(collectHistoryTextValues(historyMessages)).toEqual(["visible response"]);
|
||||
expect(collectHistoryTextValues(historyMessages)).toEqual([
|
||||
"visible response",
|
||||
"visible assistant input",
|
||||
]);
|
||||
});
|
||||
|
||||
test("chat.history mirrors current-session message tool sends before NO_REPLY", async () => {
|
||||
|
||||
@@ -147,6 +147,15 @@ describe("extractAssistantVisibleText", () => {
|
||||
).toBe("Persisted assistant answer");
|
||||
});
|
||||
|
||||
it("extracts persisted Responses assistant input_text blocks", () => {
|
||||
expect(
|
||||
extractAssistantVisibleText({
|
||||
role: "assistant",
|
||||
content: [{ type: "input_text", text: "Persisted assistant input" }],
|
||||
}),
|
||||
).toBe("Persisted assistant input");
|
||||
});
|
||||
|
||||
it("does not mix unphased legacy text into final_answer output", () => {
|
||||
expect(
|
||||
extractAssistantVisibleText({
|
||||
|
||||
@@ -24,7 +24,7 @@ export function extractFirstTextBlock(message: unknown): string | undefined {
|
||||
export type AssistantPhase = "commentary" | "final_answer";
|
||||
|
||||
function isAssistantTextContentBlockType(value: unknown): boolean {
|
||||
return value === "text" || value === "output_text";
|
||||
return value === "text" || value === "input_text" || value === "output_text";
|
||||
}
|
||||
|
||||
/** Narrows unknown phase metadata to assistant text phases that affect visibility. */
|
||||
|
||||
@@ -59,7 +59,7 @@ describe("extractTextCached", () => {
|
||||
).toBe("Persisted assistant answer");
|
||||
});
|
||||
|
||||
it("ignores persisted Responses text blocks from the wrong role", () => {
|
||||
it("accepts assistant Responses input blocks but ignores user output blocks", () => {
|
||||
expect(
|
||||
extractText({
|
||||
role: "user",
|
||||
@@ -71,7 +71,7 @@ describe("extractTextCached", () => {
|
||||
role: "assistant",
|
||||
content: [{ type: "input_text", text: "User-only block" }],
|
||||
}),
|
||||
).toBeNull();
|
||||
).toBe("User-only block");
|
||||
});
|
||||
|
||||
it("prefers final_answer assistant text over commentary text", () => {
|
||||
|
||||
@@ -13,7 +13,7 @@ function isTextContentBlockType(value: unknown, role: string): boolean {
|
||||
return (
|
||||
value === "text" ||
|
||||
(role === "user" && value === "input_text") ||
|
||||
(role === "assistant" && value === "output_text")
|
||||
(role === "assistant" && (value === "input_text" || value === "output_text"))
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -111,7 +111,7 @@ describe("message-normalizer", () => {
|
||||
expect(assistant.content).toEqual([{ type: "text", text: "Persisted assistant answer" }]);
|
||||
});
|
||||
|
||||
it("does not reinterpret persisted Responses text blocks from the wrong role", () => {
|
||||
it("accepts assistant Responses input blocks but rejects user output blocks", () => {
|
||||
const user = normalizeMessage({
|
||||
role: "user",
|
||||
content: [{ type: "output_text", text: "Assistant-only block" }],
|
||||
@@ -122,7 +122,7 @@ describe("message-normalizer", () => {
|
||||
});
|
||||
|
||||
expect(user.content).not.toContainEqual({ type: "text", text: "Assistant-only block" });
|
||||
expect(assistant.content).not.toContainEqual({ type: "text", text: "User-only block" });
|
||||
expect(assistant.content).toContainEqual({ type: "text", text: "User-only block" });
|
||||
});
|
||||
|
||||
it("normalizes structured base64 audio content blocks as renderable attachments", () => {
|
||||
|
||||
@@ -23,7 +23,7 @@ function isTextContentBlock(
|
||||
typeof item.text === "string" &&
|
||||
(item.type === "text" ||
|
||||
(role === "user" && item.type === "input_text") ||
|
||||
(role === "assistant" && item.type === "output_text"))
|
||||
(role === "assistant" && (item.type === "input_text" || item.type === "output_text")))
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user