fix(web): label preamble proof blocks

Co-authored-by: Chisel <chisel@psiclawops.dev>
This commit is contained in:
ragesaq
2026-06-22 14:28:38 +00:00
committed by Ayaan Zaidi
parent 0aba518bbe
commit 8af9bf9264
7 changed files with 124 additions and 3 deletions
+23 -1
View File
@@ -246,6 +246,26 @@ img.chat-avatar {
padding-right: 70px;
}
.chat-message-source-label {
display: inline-flex;
align-items: center;
min-height: 18px;
margin: 0 0 6px;
padding: 2px 6px;
border: 1px solid color-mix(in srgb, var(--border) 78%, transparent);
border-radius: var(--radius-sm);
background: color-mix(in srgb, var(--secondary) 54%, transparent);
color: color-mix(in srgb, var(--text) 62%, var(--muted) 38%);
font-size: 11px;
font-weight: 650;
line-height: 1;
letter-spacing: 0;
}
.chat-bubble--tool-shell > .chat-message-source-label {
margin: 0 0 6px 1px;
}
.chat-duplicate-count {
display: inline-flex;
align-items: center;
@@ -283,7 +303,9 @@ img.chat-avatar {
pointer-events: auto;
}
@media (hover: none), (max-width: 768px), (max-width: 932px) and (max-height: 500px) and (orientation: landscape) {
@media (hover: none),
(max-width: 768px),
(max-width: 932px) and (max-height: 500px) and (orientation: landscape) {
.chat-bubble-actions {
opacity: 1;
pointer-events: auto;
+1
View File
@@ -524,6 +524,7 @@ describe("buildChatItems", () => {
text: "Visible reply",
startedAt: 1,
isStreaming: true,
source: "final",
},
]);
});
+3
View File
@@ -831,6 +831,7 @@ export function buildChatItems(props: BuildChatItemsProps): Array<ChatItem | Mes
text: visibleText,
startedAt: segment.ts,
isStreaming: false,
source: "commentary",
});
}
}
@@ -851,6 +852,7 @@ export function buildChatItems(props: BuildChatItemsProps): Array<ChatItem | Mes
text,
startedAt: segment.ts,
isStreaming: false,
source: "commentary",
});
}
}
@@ -868,6 +870,7 @@ export function buildChatItems(props: BuildChatItemsProps): Array<ChatItem | Mes
text: visibleText,
startedAt,
isStreaming: true,
source: "final",
});
}
} else if (props.stream.trim().length === 0) {
+60 -1
View File
@@ -891,7 +891,7 @@ describe("grouped chat rendering", () => {
it("omits streaming bubble class for completed stream segments", () => {
const container = document.createElement("div");
render(renderStreamingGroup("Completed segment", 1, false), container);
render(renderStreamingGroup("Completed segment", 1, false, "commentary"), container);
const bubble = container.querySelector(".chat-bubble");
expect(bubble?.classList.contains("streaming")).toBe(false);
@@ -912,6 +912,65 @@ describe("grouped chat rendering", () => {
expect(text?.textContent).toBe("**live**\nreply");
});
it("labels commentary, tool call, and final message blocks", () => {
const container = document.createElement("div");
render(
html`
${renderStreamingGroup("Checking workspace", 1, false, "commentary")}
${renderMessageGroup(
createMessageGroup(
{
role: "toolResult",
toolCallId: "call_1",
toolName: "shell",
content: "tool output",
timestamp: 2,
},
"tool",
),
{
showReasoning: true,
showToolCalls: true,
},
)}
${renderMessageGroup(
createMessageGroup(
{
role: "assistant",
content: [{ type: "text", text: "Done" }],
timestamp: 3,
},
"assistant",
),
{
showReasoning: true,
showToolCalls: true,
assistantName: "OpenClaw",
assistantAvatar: null,
},
)}
`,
container,
);
expect(
[...container.querySelectorAll(".chat-message-source-label")].map((node) =>
node.textContent?.trim(),
),
).toEqual(["Commentary", "Tool call", "Final message"]);
});
it("labels live final-answer streams as final messages", () => {
const container = document.createElement("div");
render(renderStreamingGroup("Streaming final answer", 1), container);
expect(container.querySelector(".chat-message-source-label")?.textContent?.trim()).toBe(
"Final message",
);
});
it("renders configured local user names", () => {
const renderUser = (opts: Partial<RenderMessageGroupOptions>) => {
const container = document.createElement("div");
+28
View File
@@ -387,6 +387,7 @@ export function renderStreamingGroup(
text: string,
startedAt: number,
isStreaming = true,
source: "commentary" | "final" = "final",
onOpenSidebar?: (content: SidebarContent) => void,
assistant?: AssistantIdentity,
basePath?: string,
@@ -403,6 +404,7 @@ export function renderStreamingGroup(
role: "assistant",
content: [{ type: "text", text }],
timestamp: startedAt,
openclawStreamFallback: { source: source === "commentary" ? "segment" : "current" },
},
`stream:${startedAt}`,
{ isStreaming, showReasoning: false },
@@ -446,6 +448,23 @@ type RenderMessageGroupOptions = {
type GroupedMessageRenderOptions = Parameters<typeof renderGroupedMessage>[2];
function streamFallbackSource(message: Record<string, unknown>): string | null {
const fallback = message.openclawStreamFallback;
if (!fallback || typeof fallback !== "object" || Array.isArray(fallback)) {
return null;
}
const source = (fallback as { source?: unknown }).source;
return typeof source === "string" ? source : null;
}
function renderMessageSourceLabel(label: string | null) {
return label
? html`<div class="chat-message-source-label" aria-label=${`${label} message block`}>
${label}
</div>`
: nothing;
}
function buildGroupedMessageRenderOptions(
group: MessageGroup,
item: MessageGroup["messages"][number],
@@ -1715,6 +1734,14 @@ function renderGroupedMessage(
const jsonResult = markdown && !opts.isStreaming ? detectJson(markdown) : null;
const isToolMessage = normalizedRole === "tool" || isToolResult;
const fallbackSource = streamFallbackSource(m);
const messageSourceLabel = isToolMessage
? "Tool call"
: normalizedRole === "assistant"
? fallbackSource === "segment"
? "Commentary"
: "Final message"
: null;
const reserveActionSpace = hasActions && !isToolMessage;
const bubbleClasses = [
"chat-bubble",
@@ -1791,6 +1818,7 @@ function renderGroupedMessage(
data-message-id=${messageKey}
data-message-text=${extractedText || nothing}
>
${renderMessageSourceLabel(messageSourceLabel)}
${renderReplyPill(normalizedMessage.replyTarget)}
${hasActions
? html`<div class="chat-bubble-actions">
+8 -1
View File
@@ -13,7 +13,14 @@ export type ChatItem =
action?: { kind: "session-checkpoints"; label: string };
timestamp: number;
}
| { kind: "stream"; key: string; text: string; startedAt: number; isStreaming: boolean }
| {
kind: "stream";
key: string;
text: string;
startedAt: number;
isStreaming: boolean;
source: "commentary" | "final";
}
| { kind: "reading-indicator"; key: string };
/** A group of consecutive messages from the same role (Slack-style layout) */
+1
View File
@@ -2388,6 +2388,7 @@ export function renderChat(props: ChatProps) {
item.text,
item.startedAt,
item.isStreaming,
item.source,
props.onOpenSidebar,
assistantIdentity,
props.basePath,