feat: enhance message handling to sanitize stored user prompts and improve context display

This commit is contained in:
Gerard-Devlin 2026-06-11 01:10:04 +08:00
parent 140ec1c5bb
commit e652c694f8
3 changed files with 117 additions and 8 deletions

View file

@ -321,4 +321,48 @@ describe("useClaudeChatStore.resumeSession", () => {
expect(state.totalInputTokens).toBe(24);
expect(state.totalOutputTokens).toBe(12);
});
it("hides internal file and pasted-image context when restoring history", async () => {
const tempImagePath = [
"C:\\Users\\Devlin\\AppData\\Local\\Temp",
"ClaudePrism",
"chat-pastes",
"1781110224092-1-paste-1781110223586-1.png",
].join("\\");
const restoredPrompt = [
"[Currently open file: main.tex]",
"[Selection: Pasted image]",
"[Selected text:",
`[Temporary pasted image: ${tempImagePath}]`,
"Use this image file as visual context for the user's message.",
"]",
"",
"Please inspect this image",
].join("\n");
vi.mocked(invoke).mockResolvedValueOnce([
{
type: "user",
message: {
content: restoredPrompt,
},
},
{
type: "assistant",
message: {
content: [{ type: "text", text: "OK" }],
},
},
]);
await useClaudeChatStore.getState().resumeSession("session-with-image");
const state = useClaudeChatStore.getState();
const userContent = state.messages[0].message?.content as any;
expect(userContent).toBe("Pasted image\nPlease inspect this image");
expect(userContent).not.toContain("[Currently open file:");
expect(userContent).not.toContain("[Temporary pasted image:");
expect(state.title).toBe("Please inspect this image");
});
});

View file

@ -215,10 +215,15 @@ const UserMessage: FC<{ message: ClaudeStreamMessage }> = ({ message }) => {
if (!textContent) return null;
// Parse leading @file:line:col or ~@file:line context reference
const contextMatch = textContent.match(/^(~?@[^\n]+)\n([\s\S]*)$/);
const contextLabel = contextMatch?.[1] ?? null;
const bodyText = contextMatch ? contextMatch[2] : textContent;
const firstLineMatch = textContent.match(/^([^\n]+)\n([\s\S]*)$/);
const firstLine = firstLineMatch?.[1]?.trim() ?? "";
const hasContextLabel =
firstLine.startsWith("@") ||
firstLine.startsWith("~@") ||
/^Pasted image(?: \d+)?(?:, Pasted image(?: \d+)?)*$/.test(firstLine);
const contextLabel = hasContextLabel ? firstLine : null;
const bodyText =
hasContextLabel && firstLineMatch ? firstLineMatch[2] : textContent;
// Parse error block patterns for styled rendering:
// Lint single: "[Lint error in FILE:LINE]\n[Error: MSG]\n\nPrompt"

View file

@ -240,7 +240,10 @@ function stringifyBlockContent(value: unknown): string {
}
function messageContentText(message: ClaudeStreamMessage): string {
const blocks = message.message?.content ?? [];
const rawContent = (message.message as any)?.content;
if (typeof rawContent === "string") return rawContent.trim();
const blocks = rawContent ?? [];
const parts: string[] = [];
for (const block of blocks) {
if (block.type === "text" && block.text?.trim()) {
@ -258,6 +261,62 @@ function messageContentText(message: ClaudeStreamMessage): string {
return parts.join("\n").trim();
}
function displayTextForStoredUserPrompt(text: string): string {
const normalized = text.replace(/\r\n/g, "\n");
if (!/^\[(?:Currently open file|File): [^\]\n]*\]/.test(normalized)) {
return text;
}
const contextEnd = normalized.lastIndexOf("]\n\n");
if (contextEnd < 0) return text;
const contextText = normalized.slice(0, contextEnd + 1);
const body = normalized.slice(contextEnd + 3);
const selectionMatch = contextText.match(
/(?:^|\n)\[Selection: ([^\]\n]+)\]/,
);
const contextLabel = selectionMatch?.[1]?.trim();
if (!contextLabel) return body;
return body.trim() ? `${contextLabel}\n${body}` : contextLabel;
}
function sanitizeStoredUserMessageForDisplay(
message: ClaudeStreamMessage,
): ClaudeStreamMessage {
if (message.type !== "user") return message;
const rawContent = (message.message as any)?.content;
if (typeof rawContent === "string") {
const displayText = displayTextForStoredUserPrompt(rawContent);
return displayText === rawContent
? message
: {
...message,
message: { ...message.message, content: displayText as any },
};
}
if (!Array.isArray(rawContent)) return message;
let changed = false;
const content = rawContent.map((block) => {
if (block.type !== "text" || typeof block.text !== "string") {
return block;
}
const displayText = displayTextForStoredUserPrompt(block.text);
if (displayText === block.text) return block;
changed = true;
return { ...block, text: displayText };
});
return changed
? { ...message, message: { ...message.message, content } }
: message;
}
function buildProviderSwitchContext(
messages: ClaudeStreamMessage[],
maxChars = 18000,
@ -1150,14 +1209,15 @@ export const useClaudeChatStore = create<ClaudeChatState>()((set, get) => ({
});
// Filter to displayable message types and map to ClaudeStreamMessage
const messages: ClaudeStreamMessage[] = [];
const rawMessages: ClaudeStreamMessage[] = [];
for (const entry of history) {
const type = entry.type;
if (type === "user" || type === "assistant" || type === "result") {
messages.push(entry as ClaudeStreamMessage);
rawMessages.push(entry as ClaudeStreamMessage);
}
}
const messages = rawMessages.map(sanitizeStoredUserMessageForDisplay);
const totals = usageTotalsForMessages(messages);
const providerKey = inferProviderKeyFromHistory(history);
const selectedProviderCredentialId =
@ -1169,7 +1229,7 @@ export const useClaudeChatStore = create<ClaudeChatState>()((set, get) => ({
...applyTabUpdate(s, activeTabId, {
messages,
providerKey,
title: sessionTitle ?? titleForMessages(messages) ?? "New Chat",
title: sessionTitle ?? titleForMessages(rawMessages) ?? "New Chat",
totalInputTokens: totals.inputTokens,
totalOutputTokens: totals.outputTokens,
}),