Merge de780ee6d8 into 4583f88626
This commit is contained in:
commit
e62e3f20e5
@ -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);
|
||||||
|
|||||||
@ -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";
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user