mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(ui): simplify grouped tool activity (#100318)
This commit is contained in:
committed by
GitHub
parent
c3bcbf59b1
commit
a5fdc07121
@@ -131,6 +131,7 @@ export type NormalizedMessage = {
|
||||
/** Tool card representation for inline tool call/result rendering */
|
||||
export type ToolCard = {
|
||||
id: string;
|
||||
callId?: string;
|
||||
name: string;
|
||||
args?: unknown;
|
||||
inputText?: string;
|
||||
|
||||
@@ -42,6 +42,20 @@ export function isToolResultMessage(message: unknown): boolean {
|
||||
return role === "toolresult" || role === "tool_result";
|
||||
}
|
||||
|
||||
export function isStandaloneToolMessageForDisplay(message: unknown): boolean {
|
||||
const m = message as Record<string, unknown>;
|
||||
const role = typeof m.role === "string" ? normalizeRoleForGrouping(m.role) : "unknown";
|
||||
return (
|
||||
role === "tool" ||
|
||||
typeof m.toolCallId === "string" ||
|
||||
typeof m.tool_call_id === "string" ||
|
||||
typeof m.toolUseId === "string" ||
|
||||
typeof m.tool_use_id === "string" ||
|
||||
typeof m.toolName === "string" ||
|
||||
typeof m.tool_name === "string"
|
||||
);
|
||||
}
|
||||
|
||||
function isTextContentBlock(
|
||||
item: Record<string, unknown>,
|
||||
role: string,
|
||||
@@ -349,7 +363,11 @@ export function normalizeMessage(message: unknown): NormalizedMessage {
|
||||
|
||||
// Detect tool messages by common gateway shapes.
|
||||
// Some tool events come through as assistant role with tool_* items in the content array.
|
||||
const hasToolId = typeof m.toolCallId === "string" || typeof m.tool_call_id === "string";
|
||||
const hasToolId =
|
||||
typeof m.toolCallId === "string" ||
|
||||
typeof m.tool_call_id === "string" ||
|
||||
typeof m.toolUseId === "string" ||
|
||||
typeof m.tool_use_id === "string";
|
||||
|
||||
const contentRaw = m.content;
|
||||
const contentItems = Array.isArray(contentRaw) ? contentRaw : null;
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
// Control UI chat domain owns pure tool-card extraction rules.
|
||||
import { extractCanvasFromText } from "../../../../src/chat/canvas-render.js";
|
||||
import {
|
||||
isToolCallContentType,
|
||||
isToolResultContentType,
|
||||
resolveToolUseId,
|
||||
} from "../../../../src/chat/tool-content.js";
|
||||
import type { ToolCard } from "./chat-types.ts";
|
||||
import { extractTextCached } from "./message-extract.ts";
|
||||
import { isToolResultMessage } from "./message-normalizer.ts";
|
||||
@@ -142,28 +147,41 @@ export function extractToolPreview(
|
||||
return extractCanvasFromText(outputText, toolName);
|
||||
}
|
||||
|
||||
function resolveToolCallId(
|
||||
item: Record<string, unknown>,
|
||||
message: Record<string, unknown>,
|
||||
): string | undefined {
|
||||
return (
|
||||
resolveToolUseId(item) ||
|
||||
(typeof item.callId === "string" && item.callId.trim()) ||
|
||||
(typeof message.toolCallId === "string" && message.toolCallId.trim()) ||
|
||||
(typeof message.tool_call_id === "string" && message.tool_call_id.trim()) ||
|
||||
(typeof message.toolUseId === "string" && message.toolUseId.trim()) ||
|
||||
(typeof message.tool_use_id === "string" && message.tool_use_id.trim()) ||
|
||||
undefined
|
||||
);
|
||||
}
|
||||
|
||||
function resolveToolName(item: Record<string, unknown>, message: Record<string, unknown>): string {
|
||||
return (
|
||||
(typeof item.name === "string" && item.name.trim()) ||
|
||||
(typeof message.toolName === "string" && message.toolName.trim()) ||
|
||||
(typeof message.tool_name === "string" && message.tool_name.trim()) ||
|
||||
"tool"
|
||||
);
|
||||
}
|
||||
|
||||
function resolveToolCardId(
|
||||
item: Record<string, unknown>,
|
||||
message: Record<string, unknown>,
|
||||
index: number,
|
||||
prefix = "tool",
|
||||
): string {
|
||||
const explicitId =
|
||||
(typeof item.id === "string" && item.id.trim()) ||
|
||||
(typeof item.toolCallId === "string" && item.toolCallId.trim()) ||
|
||||
(typeof item.tool_call_id === "string" && item.tool_call_id.trim()) ||
|
||||
(typeof item.callId === "string" && item.callId.trim()) ||
|
||||
(typeof message.toolCallId === "string" && message.toolCallId.trim()) ||
|
||||
(typeof message.tool_call_id === "string" && message.tool_call_id.trim()) ||
|
||||
"";
|
||||
const explicitId = resolveToolCallId(item, message);
|
||||
if (explicitId) {
|
||||
return `${prefix}:${explicitId}`;
|
||||
}
|
||||
const name =
|
||||
(typeof item.name === "string" && item.name.trim()) ||
|
||||
(typeof message.toolName === "string" && message.toolName.trim()) ||
|
||||
(typeof message.tool_name === "string" && message.tool_name.trim()) ||
|
||||
"tool";
|
||||
const name = resolveToolName(item, message);
|
||||
return `${prefix}:${name}:${index}`;
|
||||
}
|
||||
|
||||
@@ -196,6 +214,25 @@ export function formatCollapsedToolSummaryText(value: string | undefined): strin
|
||||
return withoutConnector || normalized;
|
||||
}
|
||||
|
||||
function collapsedToolTextKey(value: string | undefined): string | undefined {
|
||||
return formatCollapsedToolSummaryText(value)
|
||||
?.toLowerCase()
|
||||
.replace(/[\s._-]+/g, "");
|
||||
}
|
||||
|
||||
export function formatDistinctCollapsedToolSummaryText(
|
||||
value: string | undefined,
|
||||
label: string | undefined,
|
||||
): string | undefined {
|
||||
const displayValue = formatCollapsedToolSummaryText(value);
|
||||
if (!displayValue) {
|
||||
return undefined;
|
||||
}
|
||||
const valueKey = collapsedToolTextKey(displayValue);
|
||||
const labelKey = collapsedToolTextKey(label);
|
||||
return valueKey && labelKey && valueKey === labelKey ? undefined : displayValue;
|
||||
}
|
||||
|
||||
export function formatCollapsedToolPreviewText(value: string | undefined): string | undefined {
|
||||
const normalized = formatCollapsedToolSummaryText(value);
|
||||
if (!normalized) {
|
||||
@@ -237,16 +274,17 @@ export function extractToolCards(message: unknown, prefix = "tool"): ToolCard[]
|
||||
|
||||
for (let index = 0; index < content.length; index++) {
|
||||
const item = content[index] ?? {};
|
||||
const kind = (typeof item.type === "string" ? item.type : "").toLowerCase();
|
||||
const isToolCall =
|
||||
["toolcall", "tool_call", "tooluse", "tool_use"].includes(kind) ||
|
||||
isToolCallContentType(item.type) ||
|
||||
(typeof item.name === "string" &&
|
||||
(item.arguments != null || item.args != null || item.input != null));
|
||||
if (isToolCall) {
|
||||
const args = coerceArgs(item.arguments ?? item.args ?? item.input);
|
||||
const callId = resolveToolCallId(item, m);
|
||||
cards.push({
|
||||
id: resolveToolCardId(item, m, index, prefix),
|
||||
name: typeof item.name === "string" ? item.name : "tool",
|
||||
...(callId ? { callId } : {}),
|
||||
name: resolveToolName(item, m),
|
||||
args,
|
||||
inputText: serializeToolInput(args),
|
||||
messageId: transcriptMessageId,
|
||||
@@ -254,15 +292,17 @@ export function extractToolCards(message: unknown, prefix = "tool"): ToolCard[]
|
||||
continue;
|
||||
}
|
||||
|
||||
if (kind === "toolresult" || kind === "tool_result") {
|
||||
const name = typeof item.name === "string" ? item.name : "tool";
|
||||
if (isToolResultContentType(item.type)) {
|
||||
const name = resolveToolName(item, m);
|
||||
const cardId = resolveToolCardId(item, m, index, prefix);
|
||||
const callId = resolveToolCallId(item, m);
|
||||
const existing = findFirstUnmatchedCard(cards, cardId, name, fallbackMatchedCards);
|
||||
const text = extractToolText(item);
|
||||
const preview = extractToolPreview(text, name);
|
||||
const isError = readToolErrorFlag(item) ?? messageIsError;
|
||||
if (existing) {
|
||||
fallbackMatchedCards.add(existing);
|
||||
existing.callId ??= callId;
|
||||
existing.outputText = text;
|
||||
existing.preview = preview;
|
||||
if (isError !== undefined) {
|
||||
@@ -272,6 +312,7 @@ export function extractToolCards(message: unknown, prefix = "tool"): ToolCard[]
|
||||
}
|
||||
cards.push({
|
||||
id: cardId,
|
||||
...(callId ? { callId } : {}),
|
||||
name,
|
||||
outputText: text,
|
||||
messageId: transcriptMessageId,
|
||||
@@ -295,8 +336,10 @@ export function extractToolCards(message: unknown, prefix = "tool"): ToolCard[]
|
||||
(typeof m.tool_name === "string" && m.tool_name) ||
|
||||
"tool";
|
||||
const text = extractTextCached(message) ?? undefined;
|
||||
const callId = resolveToolCallId({}, m);
|
||||
cards.push({
|
||||
id: resolveToolCardId({}, m, 0, prefix),
|
||||
...(callId ? { callId } : {}),
|
||||
name,
|
||||
outputText: text,
|
||||
messageId: transcriptMessageId,
|
||||
|
||||
@@ -93,11 +93,18 @@ function activityAlignmentHtml() {
|
||||
<button class="chat-activity-group__summary chat-activity-group__summary--error" type="button">
|
||||
<span class="chat-activity-group__icon">${iconSvg()}</span>
|
||||
<span class="chat-activity-group__label">Activity: 2 tools</span>
|
||||
<span class="chat-activity-group__preview">Bash, Gateway</span>
|
||||
</button>
|
||||
<div class="chat-activity-group__body">
|
||||
<div class="chat-bubble" data-activity-call-bubble>
|
||||
<div class="chat-text">Bash searched a deliberately long workspace path with enough detail to occupy the activity row and expose mismatched width constraints.</div>
|
||||
<div class="chat-bubble chat-bubble--tool-shell" data-activity-call-row>
|
||||
<div class="chat-tools-inline">
|
||||
<div class="chat-tool-msg-collapse">
|
||||
<button class="chat-tool-msg-summary" type="button">
|
||||
<span class="chat-tool-msg-summary__icon">${iconSvg()}</span>
|
||||
<span class="chat-tool-msg-summary__label">Bash</span>
|
||||
<span class="chat-tool-msg-summary__names">search a deliberately long workspace path without extra card chrome</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-bubble chat-bubble--tool-shell">
|
||||
<div class="chat-tool-msg-collapse">
|
||||
@@ -503,17 +510,26 @@ describeBrowserLayout("chat responsive browser layout", () => {
|
||||
);
|
||||
|
||||
await expectNoHorizontalOverflow(page);
|
||||
const callBubble = await getRect(page, "[data-activity-call-bubble]");
|
||||
const callRow = await getRect(page, "[data-activity-call-row]");
|
||||
const errorSummary = await getRect(page, ".chat-tool-msg-summary--error");
|
||||
expect(Math.abs(callBubble.right - errorSummary.right)).toBeLessThanOrEqual(1);
|
||||
const selectionStyles = await page.evaluate(() => ({
|
||||
activity: getComputedStyle(
|
||||
document.querySelector<HTMLElement>(".chat-activity-group__summary")!,
|
||||
).userSelect,
|
||||
tool: getComputedStyle(document.querySelector<HTMLElement>(".chat-tool-msg-summary")!)
|
||||
.userSelect,
|
||||
}));
|
||||
expect(selectionStyles).toEqual({ activity: "text", tool: "text" });
|
||||
expect(Math.abs(callRow.right - errorSummary.right)).toBeLessThanOrEqual(1);
|
||||
expect(Math.abs(callRow.height - errorSummary.height)).toBeLessThanOrEqual(1);
|
||||
const styles = await page.evaluate(() => {
|
||||
const call = document.querySelector<HTMLElement>("[data-activity-call-row]")!;
|
||||
return {
|
||||
activity: getComputedStyle(
|
||||
document.querySelector<HTMLElement>(".chat-activity-group__summary")!,
|
||||
).userSelect,
|
||||
callBackground: getComputedStyle(call).backgroundColor,
|
||||
tool: getComputedStyle(document.querySelector<HTMLElement>(".chat-tool-msg-summary")!)
|
||||
.userSelect,
|
||||
};
|
||||
});
|
||||
expect(styles).toEqual({
|
||||
activity: "text",
|
||||
callBackground: "rgba(0, 0, 0, 0)",
|
||||
tool: "text",
|
||||
});
|
||||
} finally {
|
||||
await closeBrowserPage(page);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Control UI tests cover build chat items behavior.
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { MessageGroup } from "../../lib/chat/chat-types.ts";
|
||||
import { extractToolCards } from "../../lib/chat/tool-cards.ts";
|
||||
import {
|
||||
buildCachedChatItems,
|
||||
buildChatItems,
|
||||
@@ -96,6 +97,23 @@ describe("buildChatItems", () => {
|
||||
expect(groups[0].messages).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("groups and hides top-level tool-use id results consistently", () => {
|
||||
const message = {
|
||||
role: "assistant",
|
||||
toolUseId: "provider-result",
|
||||
toolName: "bash",
|
||||
content: "Provider output",
|
||||
timestamp: 1000,
|
||||
};
|
||||
|
||||
const visibleGroups = messageGroups({ messages: [message] });
|
||||
expect(visibleGroups).toHaveLength(1);
|
||||
expect(visibleGroups[0].role).toBe("tool");
|
||||
|
||||
const hiddenGroups = messageGroups({ messages: [message], showToolCalls: false });
|
||||
expect(hiddenGroups).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("keeps forwarded assistant display messages separate from local assistant replies", () => {
|
||||
const groups = messageGroups({
|
||||
messages: [
|
||||
@@ -146,6 +164,133 @@ describe("buildChatItems", () => {
|
||||
expect(toolGroups.map((group) => group.turnSucceeded)).toEqual([true, false]);
|
||||
});
|
||||
|
||||
it("coalesces adjacent tool calls and results into one activity item", () => {
|
||||
const groups = messageGroups({
|
||||
messages: [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "tool_use",
|
||||
id: "call-shell",
|
||||
name: "bash",
|
||||
input: { command: "run openclaw doctor" },
|
||||
},
|
||||
],
|
||||
timestamp: 1000,
|
||||
},
|
||||
{
|
||||
role: "toolResult",
|
||||
toolCallId: "call-shell",
|
||||
toolName: "bash",
|
||||
content: [
|
||||
{ type: "text", text: "Doctor complete" },
|
||||
{ type: "image", data: "fixture-image", mimeType: "image/png" },
|
||||
],
|
||||
isError: false,
|
||||
timestamp: 1001,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(groups).toHaveLength(1);
|
||||
expect(groups[0].role).toBe("tool");
|
||||
expect(groups[0].messages).toHaveLength(1);
|
||||
const cards = extractToolCards(groups[0].messages[0]?.message, "coalesced");
|
||||
expect(cards).toHaveLength(1);
|
||||
expect(cards[0]).toMatchObject({
|
||||
callId: "call-shell",
|
||||
name: "bash",
|
||||
outputText: "Doctor complete",
|
||||
});
|
||||
expect(firstMessageContent(groups[0])).toContainEqual({
|
||||
type: "image",
|
||||
data: "fixture-image",
|
||||
mimeType: "image/png",
|
||||
});
|
||||
});
|
||||
|
||||
it("coalesces provider-shaped result blocks by canonical tool-use id", () => {
|
||||
const groups = messageGroups({
|
||||
messages: [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "tool_use",
|
||||
toolUseId: "provider-call",
|
||||
name: "bash",
|
||||
input: { command: "provider command" },
|
||||
},
|
||||
],
|
||||
timestamp: 1000,
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "tool_result",
|
||||
tool_use_id: "provider-call",
|
||||
text: "Provider result",
|
||||
},
|
||||
],
|
||||
timestamp: 1001,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(groups).toHaveLength(1);
|
||||
expect(groups[0].messages).toHaveLength(1);
|
||||
const cards = extractToolCards(groups[0].messages[0]?.message, "provider-coalesced");
|
||||
expect(cards).toHaveLength(1);
|
||||
expect(cards[0]).toMatchObject({
|
||||
callId: "provider-call",
|
||||
name: "bash",
|
||||
outputText: "Provider result",
|
||||
});
|
||||
});
|
||||
|
||||
it("does not coalesce repeated call-only snapshots", () => {
|
||||
const callSnapshot = (timestamp: number) => ({
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "tool_use",
|
||||
id: "call-pending",
|
||||
name: "bash",
|
||||
input: { command: "still running" },
|
||||
},
|
||||
],
|
||||
timestamp,
|
||||
});
|
||||
const groups = messageGroups({ messages: [callSnapshot(1000), callSnapshot(1001)] });
|
||||
|
||||
expect(groups).toHaveLength(1);
|
||||
expect(groups[0].messages).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("keeps adjacent tool messages separate when their call ids differ", () => {
|
||||
const groups = messageGroups({
|
||||
messages: [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "tool_use", id: "call-a", name: "bash", input: { command: "one" } }],
|
||||
timestamp: 1000,
|
||||
},
|
||||
{
|
||||
role: "toolResult",
|
||||
toolCallId: "call-b",
|
||||
toolName: "bash",
|
||||
content: "Different call",
|
||||
timestamp: 1001,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(groups).toHaveLength(1);
|
||||
expect(groups[0].messages).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("keeps empty forwarded assistant display groups", () => {
|
||||
const groups = messageGroups({
|
||||
messages: [
|
||||
@@ -1238,6 +1383,31 @@ describe("tool expansion state", () => {
|
||||
syncToolCardExpansionState("main", [group], true);
|
||||
expect(getExpandedToolCards("main").get("assistant-1:toolcard:0")).toBe(true);
|
||||
});
|
||||
|
||||
it("auto-expands top-level tool-name result disclosures", () => {
|
||||
resetChatThreadState();
|
||||
const group: MessageGroup = {
|
||||
kind: "group",
|
||||
key: "tool-name-result",
|
||||
role: "tool",
|
||||
messages: [
|
||||
{
|
||||
key: "tool-name-result",
|
||||
message: {
|
||||
role: "assistant",
|
||||
toolName: "bash",
|
||||
content: "Tool output",
|
||||
},
|
||||
},
|
||||
],
|
||||
timestamp: 1,
|
||||
isStreaming: false,
|
||||
};
|
||||
|
||||
syncToolCardExpansionState("tool-name-session", [group], true);
|
||||
|
||||
expect(getExpandedToolCards("tool-name-session").get("toolmsg:tool-name-result")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("thread item cache", () => {
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
// Control UI chat module owns Chat thread item derivation and thread-local caches.
|
||||
import {
|
||||
isToolCallContentType,
|
||||
isToolResultContentType,
|
||||
} from "../../../../src/chat/tool-content.js";
|
||||
import type {
|
||||
ChatItem,
|
||||
MessageGroup,
|
||||
@@ -22,7 +26,7 @@ import {
|
||||
} from "../../lib/chat/heartbeat-display.ts";
|
||||
import { extractTextCached } from "../../lib/chat/message-extract.ts";
|
||||
import {
|
||||
isToolResultMessage,
|
||||
isStandaloneToolMessageForDisplay,
|
||||
normalizeMessage,
|
||||
stripMessageDisplayMetadataText,
|
||||
} from "../../lib/chat/message-normalizer.ts";
|
||||
@@ -283,6 +287,97 @@ function groupMessages(items: ChatItem[]): Array<ChatItem | MessageGroup> {
|
||||
return result;
|
||||
}
|
||||
|
||||
function mergeToolCallResultPair(callItem: ChatItem, resultItem: ChatItem): ChatItem | null {
|
||||
if (callItem.kind !== "message" || resultItem.kind !== "message") {
|
||||
return null;
|
||||
}
|
||||
const callMessage = asRecord(callItem.message);
|
||||
const resultMessage = asRecord(resultItem.message);
|
||||
if (!callMessage || !resultMessage) {
|
||||
return null;
|
||||
}
|
||||
const callRole = typeof callMessage.role === "string" ? callMessage.role.toLowerCase() : "";
|
||||
const normalizedResult = safeNormalizeMessage(resultItem.message);
|
||||
const resultRole = normalizedResult ? normalizeRoleForGrouping(normalizedResult.role) : "unknown";
|
||||
if (callRole !== "assistant" || resultRole !== "tool" || !Array.isArray(callMessage.content)) {
|
||||
return null;
|
||||
}
|
||||
const hasToolCallBlock = callMessage.content.some((block) =>
|
||||
isToolCallContentType(asRecord(block)?.type),
|
||||
);
|
||||
if (!hasToolCallBlock) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const callCards = extractToolCardsCached(callItem.message, `${callItem.key}:activity-call`);
|
||||
const resultCards = extractToolCardsCached(
|
||||
resultItem.message,
|
||||
`${resultItem.key}:activity-result`,
|
||||
);
|
||||
if (callCards.length !== 1 || resultCards.length !== 1) {
|
||||
return null;
|
||||
}
|
||||
const [callCard] = callCards;
|
||||
const [resultCard] = resultCards;
|
||||
const resultName = resultCard.name === "tool" ? callCard.name : resultCard.name;
|
||||
const rawResultContent = Array.isArray(resultMessage.content) ? resultMessage.content : [];
|
||||
const resultOnlyContent = rawResultContent.filter(
|
||||
(block) => !isToolCallContentType(asRecord(block)?.type),
|
||||
);
|
||||
const hasToolResultBlock = resultOnlyContent.some((block) =>
|
||||
isToolResultContentType(asRecord(block)?.type),
|
||||
);
|
||||
const hasToolResult =
|
||||
hasToolResultBlock || resultCard.outputText !== undefined || resultCard.isError !== undefined;
|
||||
if (
|
||||
!callCard.callId ||
|
||||
callCard.callId !== resultCard.callId ||
|
||||
!hasToolResult ||
|
||||
normalizeLowercaseStringOrEmpty(callCard.name) !== normalizeLowercaseStringOrEmpty(resultName)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const preservedResultContent = resultOnlyContent.filter(
|
||||
(block) => asRecord(block)?.type !== "text",
|
||||
);
|
||||
const resultContent = hasToolResultBlock
|
||||
? resultOnlyContent
|
||||
: [
|
||||
{
|
||||
type: "tool_result",
|
||||
id: resultCard.callId,
|
||||
name: resultName,
|
||||
text: resultCard.outputText ?? "",
|
||||
...(resultCard.isError !== undefined ? { isError: resultCard.isError } : {}),
|
||||
},
|
||||
...preservedResultContent,
|
||||
];
|
||||
const resultError = resultMessage.isError ?? resultMessage.is_error;
|
||||
return {
|
||||
...callItem,
|
||||
message: {
|
||||
...callMessage,
|
||||
content: [...callMessage.content, ...resultContent],
|
||||
...(typeof resultError === "boolean" ? { isError: resultError } : {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function coalesceToolActivityMessages(items: ChatItem[]): ChatItem[] {
|
||||
const coalesced: ChatItem[] = [];
|
||||
for (const item of items) {
|
||||
const previous = coalesced[coalesced.length - 1];
|
||||
const merged = previous ? mergeToolCallResultPair(previous, item) : null;
|
||||
if (merged) {
|
||||
coalesced[coalesced.length - 1] = merged;
|
||||
} else {
|
||||
coalesced.push(item);
|
||||
}
|
||||
}
|
||||
return coalesced;
|
||||
}
|
||||
|
||||
function assistantGroupHasReplyText(group: MessageGroup): boolean {
|
||||
return group.messages.some(({ message }) => Boolean(extractTextCached(message)?.trim()));
|
||||
}
|
||||
@@ -940,7 +1035,11 @@ export function buildChatItems(props: BuildChatItemsProps): Array<ChatItem | Mes
|
||||
}
|
||||
|
||||
return annotateToolTurnOutcome(
|
||||
groupMessages(collapseSequentialDuplicateMessages(sortChatItemsByVisibleTime(items))),
|
||||
groupMessages(
|
||||
collapseSequentialDuplicateMessages(
|
||||
coalesceToolActivityMessages(sortChatItemsByVisibleTime(items)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1055,17 +1154,7 @@ export function syncToolCardExpansionState(
|
||||
expanded.set(disclosureId, autoExpandToolCalls);
|
||||
initialized.add(disclosureId);
|
||||
}
|
||||
const messageRecord = entry.message as Record<string, unknown>;
|
||||
const role = typeof messageRecord.role === "string" ? messageRecord.role : "unknown";
|
||||
const normalizedRole = normalizeRoleForGrouping(role);
|
||||
const isToolMessage =
|
||||
isToolResultMessage(entry.message) ||
|
||||
normalizedRole === "tool" ||
|
||||
role.toLowerCase() === "toolresult" ||
|
||||
role.toLowerCase() === "tool_result" ||
|
||||
typeof messageRecord.toolCallId === "string" ||
|
||||
typeof messageRecord.tool_call_id === "string";
|
||||
if (!isToolMessage) {
|
||||
if (!isStandaloneToolMessageForDisplay(entry.message)) {
|
||||
continue;
|
||||
}
|
||||
const disclosureId = `toolmsg:${entry.key}`;
|
||||
|
||||
@@ -1104,8 +1104,9 @@ describe("grouped chat rendering", () => {
|
||||
|
||||
const activity = expectElement(container, ".chat-activity-group__summary", HTMLButtonElement);
|
||||
expect(activity.textContent).toContain("Activity: 2 tools");
|
||||
expect(activity.textContent).toContain("read_file");
|
||||
expect(activity.textContent).toContain("run_command");
|
||||
expect(activity.querySelector(".chat-activity-group__preview")).toBeNull();
|
||||
expect(activity.textContent).not.toContain("read_file");
|
||||
expect(activity.textContent).not.toContain("run_command");
|
||||
expect(container.querySelector(".chat-tool-msg-body")).toBeNull();
|
||||
});
|
||||
|
||||
@@ -1216,6 +1217,62 @@ describe("grouped chat rendering", () => {
|
||||
expect(container.querySelector(".chat-tool-msg-body")).toBeNull();
|
||||
});
|
||||
|
||||
it("keeps recovered coalesced tool failures neutral in the activity list", () => {
|
||||
const container = document.createElement("div");
|
||||
const group: MessageGroup = {
|
||||
kind: "group",
|
||||
key: "recovered-tool-group",
|
||||
role: "tool",
|
||||
turnSucceeded: true,
|
||||
messages: [
|
||||
{
|
||||
key: "recovered-tool-message",
|
||||
message: {
|
||||
role: "assistant",
|
||||
isError: true,
|
||||
content: [
|
||||
{
|
||||
type: "tool_use",
|
||||
id: "call-recovered",
|
||||
name: "bash",
|
||||
input: { command: "run fallback" },
|
||||
},
|
||||
{
|
||||
type: "tool_result",
|
||||
id: "call-recovered",
|
||||
name: "bash",
|
||||
text: "Primary path failed",
|
||||
isError: true,
|
||||
},
|
||||
],
|
||||
timestamp: 1000,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "recovered-followup",
|
||||
message: {
|
||||
role: "toolResult",
|
||||
toolCallId: "call-followup",
|
||||
toolName: "read_file",
|
||||
content: "Fallback context",
|
||||
timestamp: 1001,
|
||||
},
|
||||
},
|
||||
],
|
||||
timestamp: 1000,
|
||||
isStreaming: false,
|
||||
};
|
||||
|
||||
renderMessageGroups(container, [group], {
|
||||
isToolMessageExpanded: (id) => id === "activity:recovered-tool-group",
|
||||
});
|
||||
|
||||
const summaries = container.querySelectorAll(".chat-tool-msg-summary");
|
||||
expect(summaries).toHaveLength(2);
|
||||
expect(container.querySelector(".chat-tool-msg-summary--error")).toBeNull();
|
||||
expect(summaries[0]?.querySelector(".chat-tool-msg-summary__label")?.textContent).toBe("bash");
|
||||
});
|
||||
|
||||
it("hides grouped tool activity when tool calls are disabled", () => {
|
||||
const container = document.createElement("div");
|
||||
const group: MessageGroup = {
|
||||
@@ -1330,6 +1387,78 @@ describe("grouped chat rendering", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("renders assistant tool content as a flat concise tool row without a top-level call id", () => {
|
||||
const container = document.createElement("div");
|
||||
const message = {
|
||||
id: "assistant-tool-content",
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "tool_use",
|
||||
id: "call-content-only",
|
||||
name: "bash",
|
||||
input: { command: "bash" },
|
||||
},
|
||||
],
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
renderAssistantMessage(container, message, {
|
||||
isToolMessageExpanded: () => false,
|
||||
});
|
||||
|
||||
expectElement(container, ".chat-bubble--tool-shell", HTMLElement);
|
||||
const summary = expectElement(container, ".chat-tool-msg-summary", HTMLButtonElement);
|
||||
expect(summary.querySelector(".chat-tool-msg-summary__label")?.textContent).toBe("bash");
|
||||
expect(summary.querySelector(".chat-tool-msg-summary__names")).toBeNull();
|
||||
});
|
||||
|
||||
it("keeps top-level tool-name results collapsed", () => {
|
||||
const container = document.createElement("div");
|
||||
renderAssistantMessage(
|
||||
container,
|
||||
{
|
||||
role: "assistant",
|
||||
toolName: "bash",
|
||||
content: "A long tool result that should stay behind the disclosure.",
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
{ isToolMessageExpanded: () => false },
|
||||
);
|
||||
|
||||
expectElement(container, ".chat-bubble--tool-shell", HTMLElement);
|
||||
expectElement(container, ".chat-tool-msg-summary", HTMLButtonElement);
|
||||
expect(container.querySelector(".chat-tool-msg-body")).toBeNull();
|
||||
expect(container.querySelector(".chat-text")).toBeNull();
|
||||
});
|
||||
|
||||
it("omits normalized duplicate names from standalone tool results", () => {
|
||||
const container = document.createElement("div");
|
||||
const message = {
|
||||
role: "toolResult",
|
||||
toolCallId: "call-heartbeat",
|
||||
toolName: "heartbeat_respond",
|
||||
content: [
|
||||
{
|
||||
type: "tool_result",
|
||||
name: "heartbeat_respond",
|
||||
text: "Acknowledged",
|
||||
},
|
||||
],
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
renderAssistantMessage(container, message, {
|
||||
isToolMessageExpanded: () => false,
|
||||
});
|
||||
|
||||
const summary = expectElement(container, ".chat-tool-msg-summary", HTMLButtonElement);
|
||||
expect(summary.querySelector(".chat-tool-msg-summary__label")?.textContent).toBe(
|
||||
"heartbeat_respond",
|
||||
);
|
||||
expect(summary.querySelector(".chat-tool-msg-summary__names")).toBeNull();
|
||||
});
|
||||
|
||||
it("cleans collapsed tool connector copy while preserving expanded raw input", () => {
|
||||
const container = document.createElement("div");
|
||||
const message = {
|
||||
@@ -2531,6 +2660,36 @@ describe("grouped chat rendering", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps lifted assistant canvas previews beside flat tool rows", () => {
|
||||
const container = document.createElement("div");
|
||||
renderAssistantMessage(
|
||||
container,
|
||||
{
|
||||
id: "assistant-tool-canvas",
|
||||
role: "assistant",
|
||||
toolName: "bash",
|
||||
content: [
|
||||
{
|
||||
type: "tool_use",
|
||||
id: "call-tool-canvas",
|
||||
name: "bash",
|
||||
input: { command: "render preview" },
|
||||
},
|
||||
createAssistantCanvasBlock({ suffix: "tool_canvas" }),
|
||||
],
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
{ showToolCalls: true, isToolMessageExpanded: () => true },
|
||||
);
|
||||
|
||||
expectElement(container, ".chat-bubble--tool-shell", HTMLElement);
|
||||
const iframe = expectElement(container, ".chat-tool-card__preview-frame", HTMLIFrameElement);
|
||||
expect(iframe.getAttribute("src")).toBe(
|
||||
"/__openclaw__/canvas/documents/cv_inline_tool_canvas/index.html",
|
||||
);
|
||||
expect(container.querySelector(".chat-tool-msg-summary")).not.toBeNull();
|
||||
});
|
||||
|
||||
it("reserves layout space for assistant message actions", () => {
|
||||
const container = document.createElement("div");
|
||||
renderAssistantMessage(container, {
|
||||
|
||||
@@ -28,10 +28,14 @@ import {
|
||||
extractThinkingCached,
|
||||
formatReasoningMarkdown,
|
||||
} from "../../../lib/chat/message-extract.ts";
|
||||
import { isToolResultMessage, normalizeMessage } from "../../../lib/chat/message-normalizer.ts";
|
||||
import {
|
||||
isStandaloneToolMessageForDisplay,
|
||||
normalizeMessage,
|
||||
} from "../../../lib/chat/message-normalizer.ts";
|
||||
import { normalizeRoleForGrouping } from "../../../lib/chat/message-normalizer.ts";
|
||||
import {
|
||||
extractToolCardsCached,
|
||||
formatDistinctCollapsedToolSummaryText,
|
||||
formatCollapsedToolPreviewText,
|
||||
formatCollapsedToolSummaryText,
|
||||
isToolCardError,
|
||||
@@ -648,24 +652,6 @@ export function renderMessageGroup(group: MessageGroup, opts: RenderMessageGroup
|
||||
if (normalizedRole === "tool" && group.messages.length > 1) {
|
||||
const cards = group.messages.flatMap((item) => extractToolCardsCached(item.message, item.key));
|
||||
const toolCount = cards.length || group.messages.length;
|
||||
const toolLabels = [
|
||||
...new Set(
|
||||
cards.map(
|
||||
(card) =>
|
||||
resolveToolDisplay({
|
||||
name: card.name,
|
||||
args: card.args,
|
||||
detailMode: "explain",
|
||||
}).label,
|
||||
),
|
||||
),
|
||||
];
|
||||
const preview =
|
||||
toolLabels.length === 0
|
||||
? "Tool output"
|
||||
: toolLabels.length <= 3
|
||||
? toolLabels.join(", ")
|
||||
: `${toolLabels.slice(0, 2).join(", ")} +${toolLabels.length - 2} more`;
|
||||
const hasError = cards.some(isToolCardError) && group.turnSucceeded !== true;
|
||||
const activityDisclosureId = `activity:${group.key}`;
|
||||
const activityExpanded = opts.isToolMessageExpanded?.(activityDisclosureId) ?? hasError;
|
||||
@@ -694,7 +680,7 @@ export function renderMessageGroup(group: MessageGroup, opts: RenderMessageGroup
|
||||
type="button"
|
||||
aria-expanded=${String(activityExpanded)}
|
||||
aria-label=${hasError
|
||||
? `Activity: ${toolCount} tool${toolCount === 1 ? "" : "s"}, includes errors. ${preview}`
|
||||
? `Activity: ${toolCount} tool${toolCount === 1 ? "" : "s"}, includes errors.`
|
||||
: nothing}
|
||||
@click=${(event: MouseEvent) => {
|
||||
if (shouldToggleSelectableDisclosure(event)) {
|
||||
@@ -706,7 +692,6 @@ export function renderMessageGroup(group: MessageGroup, opts: RenderMessageGroup
|
||||
<span class="chat-activity-group__label"
|
||||
>Activity: ${toolCount} tool${toolCount === 1 ? "" : "s"}</span
|
||||
>
|
||||
<span class="chat-activity-group__preview">${preview}</span>
|
||||
<span
|
||||
class="collapse-chevron ${activityExpanded ? "" : "collapse-chevron--collapsed"}"
|
||||
aria-hidden="true"
|
||||
@@ -1678,6 +1663,7 @@ function renderInlineToolCards(
|
||||
onOpenSidebar?: (content: SidebarContent) => void;
|
||||
isToolExpanded?: (toolCardId: string) => boolean;
|
||||
onToggleToolExpanded?: (toolCardId: string) => void;
|
||||
turnSucceeded?: boolean;
|
||||
canvasPluginSurfaceUrl?: string | null;
|
||||
embedSandboxMode?: EmbedSandboxMode;
|
||||
allowExternalEmbedUrls?: boolean;
|
||||
@@ -1688,6 +1674,7 @@ function renderInlineToolCards(
|
||||
${toolCards.map((card, index) =>
|
||||
renderToolCard(card, {
|
||||
expanded: opts.isToolExpanded?.(`${opts.messageKey}:toolcard:${index}`) ?? false,
|
||||
turnSucceeded: opts.turnSucceeded,
|
||||
onToggleExpanded: opts.onToggleToolExpanded
|
||||
? () => opts.onToggleToolExpanded?.(`${opts.messageKey}:toolcard:${index}`)
|
||||
: () => undefined,
|
||||
@@ -1817,13 +1804,11 @@ function renderGroupedMessage(
|
||||
) {
|
||||
const m = message as Record<string, unknown>;
|
||||
const role = typeof m.role === "string" ? m.role : "unknown";
|
||||
const normalizedRole = normalizeRoleForGrouping(role);
|
||||
const isToolResult =
|
||||
isToolResultMessage(message) ||
|
||||
role.toLowerCase() === "toolresult" ||
|
||||
role.toLowerCase() === "tool_result" ||
|
||||
typeof m.toolCallId === "string" ||
|
||||
typeof m.tool_call_id === "string";
|
||||
const sourceRole = normalizeRoleForGrouping(role);
|
||||
const normalizedMessage = normalizeMessage(message);
|
||||
const normalizedRole = normalizeRoleForGrouping(normalizedMessage.role);
|
||||
const isToolShell = normalizedRole === "tool";
|
||||
const isStandaloneToolMessage = isStandaloneToolMessageForDisplay(message);
|
||||
|
||||
const toolCards = (opts.showToolCalls ?? true) ? extractToolCardsCached(message, messageKey) : [];
|
||||
const hasToolCards = toolCards.length > 0;
|
||||
@@ -1839,7 +1824,6 @@ function renderGroupedMessage(
|
||||
const pairingQrExpiryNotices = extractPairingQrExpiryNotices(message);
|
||||
const hasPairingQrExpiryNotices = pairingQrExpiryNotices.length > 0;
|
||||
|
||||
const normalizedMessage = normalizeMessage(message);
|
||||
const extractedText = normalizedMessage.content
|
||||
.reduce<string[]>((lines, item) => {
|
||||
if (item.type === "text" && typeof item.text === "string") {
|
||||
@@ -1884,11 +1868,10 @@ function renderGroupedMessage(
|
||||
// Detect pure-JSON messages and render as collapsible block
|
||||
const jsonResult = markdown && !opts.isStreaming ? detectJson(markdown) : null;
|
||||
|
||||
const isToolMessage = normalizedRole === "tool" || isToolResult;
|
||||
const reserveActionSpace = hasActions && !isToolMessage;
|
||||
const reserveActionSpace = hasActions && !isToolShell;
|
||||
const bubbleClasses = [
|
||||
"chat-bubble",
|
||||
isToolMessage ? "chat-bubble--tool-shell" : "",
|
||||
isToolShell ? "chat-bubble--tool-shell" : "",
|
||||
hasActions ? "has-copy" : "",
|
||||
reserveActionSpace ? "chat-bubble--has-actions" : "",
|
||||
opts.isStreaming ? "streaming" : "",
|
||||
@@ -1940,9 +1923,7 @@ function renderGroupedMessage(
|
||||
: toolNames.length <= 3
|
||||
? toolNames.join(", ")
|
||||
: `${toolNames.slice(0, 2).join(", ")} +${toolNames.length - 2} more`;
|
||||
const toolSummaryLabel = formatCollapsedToolSummaryText(toolSummaryLabelRaw);
|
||||
const toolPreview =
|
||||
markdown && !toolSummaryLabel ? (formatCollapsedToolPreviewText(markdown) ?? "") : "";
|
||||
const toolPreview = markdown ? (formatCollapsedToolPreviewText(markdown) ?? "") : "";
|
||||
const toolMessageLabelRaw = toolMessageHasError
|
||||
? "Tool error"
|
||||
: singleToolDisplayDetail && !markdown && !hasImages
|
||||
@@ -1952,7 +1933,23 @@ function renderGroupedMessage(
|
||||
: "Tool output";
|
||||
const toolMessageLabel =
|
||||
formatCollapsedToolSummaryText(toolMessageLabelRaw) ?? toolMessageLabelRaw;
|
||||
const toolSummaryLabel = formatDistinctCollapsedToolSummaryText(
|
||||
toolSummaryLabelRaw,
|
||||
toolMessageLabel,
|
||||
);
|
||||
const toolMessageIcon = singleToolDisplay ? renderChatIcon(singleToolDisplay.icon) : icons.zap;
|
||||
const assistantViewContent =
|
||||
sourceRole === "assistant" && assistantViewBlocks.length > 0
|
||||
? html`${assistantViewBlocks.map(
|
||||
(block) => html`${renderToolPreview(block.preview, "chat_message", {
|
||||
onOpenSidebar,
|
||||
rawText: block.rawText ?? null,
|
||||
canvasPluginSurfaceUrl: opts.canvasPluginSurfaceUrl,
|
||||
embedSandboxMode: opts.embedSandboxMode ?? "scripts",
|
||||
})}
|
||||
${block.rawText ? renderRawOutputToggle(block.rawText) : nothing}`,
|
||||
)}`
|
||||
: nothing;
|
||||
|
||||
const duplicateCount = Math.max(1, Math.floor(opts.duplicateCount ?? 1));
|
||||
|
||||
@@ -1975,7 +1972,7 @@ function renderGroupedMessage(
|
||||
${canCopyMarkdown ? renderCopyAsMarkdownButton(markdown!) : nothing}
|
||||
</div>`
|
||||
: nothing}
|
||||
${isToolMessage
|
||||
${isStandaloneToolMessage
|
||||
? html`
|
||||
<div
|
||||
class="chat-tool-msg-collapse chat-tool-msg-collapse--manual ${toolMessageExpanded
|
||||
@@ -2015,6 +2012,7 @@ function renderGroupedMessage(
|
||||
opts.onRequestUpdate,
|
||||
opts.onAssistantAttachmentLoaded,
|
||||
)}
|
||||
${assistantViewContent}
|
||||
${reasoningMarkdown
|
||||
? html`<div class="chat-thinking">
|
||||
${unsafeHTML(toSanitizedMarkdownHtml(reasoningMarkdown))}
|
||||
@@ -2053,6 +2051,7 @@ function renderGroupedMessage(
|
||||
onOpenSidebar,
|
||||
isToolExpanded: opts.isToolExpanded,
|
||||
onToggleToolExpanded: opts.onToggleToolExpanded,
|
||||
turnSucceeded: opts.turnSucceeded,
|
||||
canvasPluginSurfaceUrl: opts.canvasPluginSurfaceUrl,
|
||||
embedSandboxMode: opts.embedSandboxMode ?? "scripts",
|
||||
allowExternalEmbedUrls: opts.allowExternalEmbedUrls ?? false,
|
||||
@@ -2079,17 +2078,7 @@ function renderGroupedMessage(
|
||||
${unsafeHTML(toSanitizedMarkdownHtml(reasoningMarkdown))}
|
||||
</div>`
|
||||
: nothing}
|
||||
${normalizedRole === "assistant" && assistantViewBlocks.length > 0
|
||||
? html`${assistantViewBlocks.map(
|
||||
(block) => html`${renderToolPreview(block.preview, "chat_message", {
|
||||
onOpenSidebar,
|
||||
rawText: block.rawText ?? null,
|
||||
canvasPluginSurfaceUrl: opts.canvasPluginSurfaceUrl,
|
||||
embedSandboxMode: opts.embedSandboxMode ?? "scripts",
|
||||
})}
|
||||
${block.rawText ? renderRawOutputToggle(block.rawText) : nothing}`,
|
||||
)}`
|
||||
: nothing}
|
||||
${assistantViewContent}
|
||||
${jsonResult
|
||||
? html`<details class="chat-json-collapse">
|
||||
<summary class="chat-json-summary">
|
||||
@@ -2109,6 +2098,7 @@ function renderGroupedMessage(
|
||||
onOpenSidebar,
|
||||
isToolExpanded: opts.isToolExpanded,
|
||||
onToggleToolExpanded: opts.onToggleToolExpanded,
|
||||
turnSucceeded: opts.turnSucceeded,
|
||||
canvasPluginSurfaceUrl: opts.canvasPluginSurfaceUrl,
|
||||
embedSandboxMode: opts.embedSandboxMode ?? "scripts",
|
||||
allowExternalEmbedUrls: opts.allowExternalEmbedUrls ?? false,
|
||||
|
||||
@@ -104,6 +104,26 @@ describe("tool-card extraction", () => {
|
||||
}`);
|
||||
});
|
||||
|
||||
it("preserves legacy callId tool block identities", () => {
|
||||
const cards = extractToolCards(
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "tool_use",
|
||||
callId: "legacy-call-id",
|
||||
name: "bash",
|
||||
input: { command: "pwd" },
|
||||
},
|
||||
],
|
||||
},
|
||||
"legacy-call",
|
||||
);
|
||||
|
||||
expect(cards[0]?.callId).toBe("legacy-call-id");
|
||||
expect(cards[0]?.id).toBe("legacy-call:legacy-call-id");
|
||||
});
|
||||
|
||||
it("pairs interleaved nameless tool results in content order", () => {
|
||||
const cards = extractToolCards(
|
||||
{
|
||||
|
||||
@@ -29,6 +29,7 @@ vi.mock("../tool-display.ts", () => ({
|
||||
}));
|
||||
|
||||
import {
|
||||
formatDistinctCollapsedToolSummaryText,
|
||||
formatCollapsedToolPreviewText,
|
||||
formatCollapsedToolSummaryText,
|
||||
isToolErrorOutput,
|
||||
@@ -237,6 +238,16 @@ describe("tool-cards", () => {
|
||||
expect(formatCollapsedToolSummaryText(" ")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("omits normalized tool details that repeat the label", () => {
|
||||
expect(formatDistinctCollapsedToolSummaryText("bash", "Bash")).toBeUndefined();
|
||||
expect(
|
||||
formatDistinctCollapsedToolSummaryText("heartbeat_respond", "Heartbeat Respond"),
|
||||
).toBeUndefined();
|
||||
expect(formatDistinctCollapsedToolSummaryText("run openclaw doctor", "Bash")).toBe(
|
||||
"run openclaw doctor",
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps collapsed markdown previews bounded after display cleanup", () => {
|
||||
const preview = formatCollapsedToolPreviewText(`with ${"A".repeat(200)}`);
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import "../../../components/tooltip.ts";
|
||||
import { t } from "../../../i18n/index.ts";
|
||||
import type { ToolCard } from "../../../lib/chat/chat-types.ts";
|
||||
import {
|
||||
formatDistinctCollapsedToolSummaryText,
|
||||
formatCollapsedToolPreviewText,
|
||||
formatCollapsedToolSummaryText,
|
||||
isToolCardError,
|
||||
@@ -274,7 +275,7 @@ function renderCollapsedToolSummary(params: {
|
||||
}) {
|
||||
const { label, icon, name, expanded, isError, onToggleExpanded } = params;
|
||||
const displayLabel = formatCollapsedToolSummaryText(label) ?? label;
|
||||
const displayName = formatCollapsedToolSummaryText(name);
|
||||
const displayName = formatDistinctCollapsedToolSummaryText(name, displayLabel);
|
||||
return html`
|
||||
<button
|
||||
class="chat-tool-msg-summary ${isError ? "chat-tool-msg-summary--error" : ""}"
|
||||
@@ -335,6 +336,7 @@ export function renderToolCard(
|
||||
opts: {
|
||||
expanded: boolean;
|
||||
onToggleExpanded: (id: string) => void;
|
||||
turnSucceeded?: boolean;
|
||||
sessionKey?: string;
|
||||
agentId?: string;
|
||||
onOpenSidebar?: (content: SidebarContent) => void;
|
||||
@@ -344,7 +346,7 @@ export function renderToolCard(
|
||||
},
|
||||
) {
|
||||
const display = resolveToolDisplay({ name: card.name, args: card.args, detailMode: "explain" });
|
||||
const isError = isToolCardError(card);
|
||||
const isError = isToolCardError(card) && opts.turnSucceeded !== true;
|
||||
const summary = resolveCollapsedToolSummaryParts({
|
||||
card,
|
||||
displayLabel: display.label,
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.chat-bubble--tool-shell > .chat-tool-msg-collapse {
|
||||
.chat-bubble--tool-shell > .chat-tool-msg-collapse,
|
||||
.chat-bubble--tool-shell > .chat-tools-inline > .chat-tool-msg-collapse {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
@@ -572,17 +573,6 @@
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.chat-activity-group__preview {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
color: var(--muted);
|
||||
font-size: var(--control-ui-text-sm);
|
||||
line-height: 1.4;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Left rule groups the rows without wrapping them in another card. */
|
||||
.chat-activity-group__body {
|
||||
display: flex;
|
||||
|
||||
Reference in New Issue
Block a user