This commit is contained in:
Xinmin Zeng 2026-01-29 21:53:31 -05:00 committed by GitHub
commit e62e3f20e5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 168 additions and 1 deletions

View File

@ -18,6 +18,21 @@
justify-content: flex-start; justify-content: flex-start;
} }
.chat-group.system {
justify-content: center;
margin-left: 0;
margin-right: 0;
}
.chat-group.system .chat-group-messages {
align-items: center;
max-width: min(820px, 90%);
}
.chat-group.system .chat-group-footer {
justify-content: center;
}
.chat-group-messages { .chat-group-messages {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -89,6 +104,10 @@
color: var(--muted); color: var(--muted);
} }
.chat-group.system .chat-avatar {
display: none;
}
/* Image avatar support */ /* Image avatar support */
img.chat-avatar { img.chat-avatar {
display: block; display: block;
@ -228,6 +247,21 @@ img.chat-avatar {
border-color: transparent; border-color: transparent;
} }
.chat-group.system .chat-bubble {
background: var(--bg-muted);
border-color: var(--border);
color: var(--muted);
font-size: 12px;
line-height: 1.4;
padding: 8px 12px;
text-align: center;
}
:root[data-theme="light"] .chat-group.system .chat-bubble {
background: rgba(148, 163, 184, 0.12);
border-color: rgba(148, 163, 184, 0.35);
}
:root[data-theme="light"] .chat-group.user .chat-bubble { :root[data-theme="light"] .chat-group.user .chat-bubble {
border-color: rgba(234, 88, 12, 0.2); border-color: rgba(234, 88, 12, 0.2);
background: rgba(251, 146, 60, 0.12); background: rgba(251, 146, 60, 0.12);

View File

@ -120,12 +120,16 @@ export function renderMessageGroup(
? "You" ? "You"
: normalizedRole === "assistant" : normalizedRole === "assistant"
? assistantName ? assistantName
: normalizedRole === "system"
? "System"
: normalizedRole; : normalizedRole;
const roleClass = const roleClass =
normalizedRole === "user" normalizedRole === "user"
? "user" ? "user"
: normalizedRole === "assistant" : normalizedRole === "assistant"
? "assistant" ? "assistant"
: normalizedRole === "system"
? "system"
: "other"; : "other";
const timestamp = new Date(group.timestamp).toLocaleTimeString([], { const timestamp = new Date(group.timestamp).toLocaleTimeString([], {
hour: "numeric", hour: "numeric",
@ -171,6 +175,8 @@ function renderAvatar(
? "U" ? "U"
: normalized === "assistant" : normalized === "assistant"
? assistantName.charAt(0).toUpperCase() || "A" ? assistantName.charAt(0).toUpperCase() || "A"
: normalized === "system"
? "!"
: normalized === "tool" : normalized === "tool"
? "⚙" ? "⚙"
: "?"; : "?";
@ -179,6 +185,8 @@ function renderAvatar(
? "user" ? "user"
: normalized === "assistant" : normalized === "assistant"
? "assistant" ? "assistant"
: normalized === "system"
? "system"
: normalized === "tool" : normalized === "tool"
? "tool" ? "tool"
: "other"; : "other";

View File

@ -3,6 +3,7 @@ import {
normalizeMessage, normalizeMessage,
normalizeRoleForGrouping, normalizeRoleForGrouping,
isToolResultMessage, isToolResultMessage,
splitSystemPreface,
} from "./message-normalizer"; } from "./message-normalizer";
describe("message-normalizer", () => { describe("message-normalizer", () => {
@ -138,6 +139,48 @@ describe("message-normalizer", () => {
}); });
}); });
describe("splitSystemPreface", () => {
it("splits leading system block into system message", () => {
const normalized = normalizeMessage({
role: "user",
content: "System: [2026-01-26 10:00:00 UTC] Exec finished\n\nRun complete",
timestamp: 123,
});
const result = splitSystemPreface(normalized);
expect(result.systemMessage?.role).toBe("system");
expect(result.systemMessage?.content).toEqual([
{ type: "text", text: expect.stringContaining("Exec finished") },
]);
expect(result.message.content).toEqual([
{ type: "text", text: "Run complete" },
]);
});
it("keeps user message when no system prefix", () => {
const normalized = normalizeMessage({
role: "user",
content: "Hello",
timestamp: 123,
});
const result = splitSystemPreface(normalized);
expect(result.systemMessage).toBeNull();
expect(result.message.content).toEqual([{ type: "text", text: "Hello" }]);
});
it("creates system message even when remainder is empty", () => {
const normalized = normalizeMessage({
role: "user",
content: "System: [2026-01-26 10:00:00 UTC] Gateway restart",
timestamp: 123,
});
const result = splitSystemPreface(normalized);
expect(result.systemMessage?.content).toEqual([
{ type: "text", text: expect.stringContaining("Gateway restart") },
]);
expect(result.message.content).toEqual([]);
});
});
describe("isToolResultMessage", () => { describe("isToolResultMessage", () => {
it("returns true for toolresult role", () => { it("returns true for toolresult role", () => {
expect(isToolResultMessage({ role: "toolresult" })).toBe(true); expect(isToolResultMessage({ role: "toolresult" })).toBe(true);

View File

@ -59,6 +59,74 @@ export function normalizeMessage(message: unknown): NormalizedMessage {
return { role, content, timestamp, id }; return { role, content, timestamp, id };
} }
type SplitSystemMessageResult = {
systemMessage: NormalizedMessage | null;
message: NormalizedMessage;
};
const SYSTEM_LINE_PREFIX = "System: ";
function parseSystemLine(line: string): string | null {
const trimmed = line.trim();
if (!trimmed.startsWith(SYSTEM_LINE_PREFIX)) return null;
const payload = trimmed.slice(SYSTEM_LINE_PREFIX.length).trim();
return payload ? payload : null;
}
export function splitSystemPreface(
message: NormalizedMessage,
): SplitSystemMessageResult {
if (message.role.toLowerCase() !== "user") {
return { systemMessage: null, message };
}
const textParts = message.content
.filter((item) => item.type === "text" && typeof item.text === "string")
.map((item) => item.text ?? "")
.filter((text) => text.trim().length > 0);
if (textParts.length === 0) return { systemMessage: null, message };
const joined = textParts.join("\n").trim();
if (!joined.startsWith(SYSTEM_LINE_PREFIX)) {
return { systemMessage: null, message };
}
const lines = joined.split(/\r?\n/);
const systemLines: string[] = [];
let idx = 0;
while (idx < lines.length) {
const parsed = parseSystemLine(lines[idx] ?? "");
if (!parsed) break;
systemLines.push(parsed);
idx += 1;
}
if (systemLines.length === 0) return { systemMessage: null, message };
// Skip blank separator line after system block, if present.
while (idx < lines.length && !lines[idx]?.trim()) idx += 1;
const remainder = lines.slice(idx).join("\n").trim();
const systemMessage: NormalizedMessage = {
role: "system",
content: [{ type: "text", text: systemLines.join("\n") }],
timestamp: message.timestamp,
};
if (!remainder) {
return { systemMessage, message: { ...message, content: [] } };
}
return {
systemMessage,
message: {
...message,
content: [{ type: "text", text: remainder }],
},
};
}
/** /**
* Normalize role for grouping purposes. * Normalize role for grouping purposes.
*/ */

View File

@ -8,6 +8,7 @@ import { icons } from "../icons";
import { import {
normalizeMessage, normalizeMessage,
normalizeRoleForGrouping, normalizeRoleForGrouping,
splitSystemPreface,
} from "../chat/message-normalizer"; } from "../chat/message-normalizer";
import { import {
renderMessageGroup, renderMessageGroup,
@ -438,10 +439,23 @@ function buildChatItems(props: ChatProps): Array<ChatItem | MessageGroup> {
continue; continue;
} }
const split = splitSystemPreface(normalized);
if (split.systemMessage) {
items.push({
kind: "message",
key: `system:${messageKey(msg, i)}`,
message: split.systemMessage,
});
}
if (split.systemMessage && split.message.content.length === 0) {
continue;
}
items.push({ items.push({
kind: "message", kind: "message",
key: messageKey(msg, i), key: messageKey(msg, i),
message: msg, message: split.systemMessage ? split.message : msg,
}); });
} }
if (props.showThinking) { if (props.showThinking) {