mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 20:35:39 -06:00
fix(web): label preamble proof blocks
Co-authored-by: Chisel <chisel@psiclawops.dev>
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -524,6 +524,7 @@ describe("buildChatItems", () => {
|
||||
text: "Visible reply",
|
||||
startedAt: 1,
|
||||
isStreaming: true,
|
||||
source: "final",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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) */
|
||||
|
||||
@@ -2388,6 +2388,7 @@ export function renderChat(props: ChatProps) {
|
||||
item.text,
|
||||
item.startedAt,
|
||||
item.isStreaming,
|
||||
item.source,
|
||||
props.onOpenSidebar,
|
||||
assistantIdentity,
|
||||
props.basePath,
|
||||
|
||||
Reference in New Issue
Block a user