fix: preprompt would show after loading session (#8744)
Signed-off-by: Matt Toohey <contact@matttoohey.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -97,6 +97,14 @@ interface ContentSection {
|
||||
items: MessageContent[] | ToolChainItem[];
|
||||
}
|
||||
|
||||
/** Keep only content blocks whose audience includes "user" (or has no audience). */
|
||||
function filterUserVisibleContent(content: MessageContent[]): MessageContent[] {
|
||||
return content.filter((b) => {
|
||||
const aud = b.annotations?.audience;
|
||||
return !aud || aud.length === 0 || aud.includes("user");
|
||||
});
|
||||
}
|
||||
|
||||
function findMatchingToolChainIndex(
|
||||
items: ToolChainItem[],
|
||||
response: ToolResponseContent,
|
||||
@@ -224,29 +232,17 @@ function renderContentBlock(
|
||||
case "toolResponse":
|
||||
// Handled by groupContentSections toolChain rendering
|
||||
return null;
|
||||
case "thinking": {
|
||||
const th = content as ThinkingContent;
|
||||
return (
|
||||
<Reasoning
|
||||
key={`thinking-${index}`}
|
||||
isStreaming={isStreamingMsg}
|
||||
defaultOpen={false}
|
||||
>
|
||||
<ReasoningTrigger />
|
||||
<ReasoningContent>{th.text}</ReasoningContent>
|
||||
</Reasoning>
|
||||
);
|
||||
}
|
||||
case "thinking":
|
||||
case "reasoning": {
|
||||
const r = content as ReasoningContentType;
|
||||
const text = (content as ThinkingContent | ReasoningContentType).text;
|
||||
return (
|
||||
<Reasoning
|
||||
key={`reasoning-${index}`}
|
||||
key={`${content.type}-${index}`}
|
||||
isStreaming={isStreamingMsg}
|
||||
defaultOpen={false}
|
||||
>
|
||||
<ReasoningTrigger />
|
||||
<ReasoningContent>{r.text}</ReasoningContent>
|
||||
<ReasoningContent>{text}</ReasoningContent>
|
||||
</Reasoning>
|
||||
);
|
||||
}
|
||||
@@ -318,7 +314,10 @@ export const MessageBubble = memo(function MessageBubble({
|
||||
}: MessageBubbleProps) {
|
||||
const { t } = useTranslation(["chat", "common"]);
|
||||
const { formatDate } = useLocaleFormatting();
|
||||
const { role, content, created } = message;
|
||||
const { role, content: rawContent, created } = message;
|
||||
// Only user messages carry annotated blocks; skip the filter for others.
|
||||
const content =
|
||||
role === "user" ? filterUserVisibleContent(rawContent) : rawContent;
|
||||
const { handleContentClick, pathNotice } = useArtifactLinkHandler();
|
||||
const persona = useAgentStore((state) =>
|
||||
message.metadata?.personaId
|
||||
@@ -328,6 +327,9 @@ export const MessageBubble = memo(function MessageBubble({
|
||||
const { isCopied: isCopyConfirmed, copyToClipboard } = useCopyToClipboard();
|
||||
const personaAvatarUrl = useAvatarSrc(persona?.avatar);
|
||||
|
||||
// Skip empty user bubbles (all blocks filtered as assistant-only).
|
||||
if (role === "user" && content.length === 0) return null;
|
||||
|
||||
const textContent = content
|
||||
.filter((c): c is TextContent => c.type === "text")
|
||||
.map((c) => c.text)
|
||||
@@ -347,7 +349,6 @@ export const MessageBubble = memo(function MessageBubble({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const isUser = role === "user";
|
||||
const assistantProviderId = message.metadata?.providerId;
|
||||
const assistantProviderName = assistantProviderId
|
||||
|
||||
@@ -73,12 +73,15 @@ export async function acpSendMessage(
|
||||
throw new Error("Session not prepared. Call acpPrepareSession first.");
|
||||
}
|
||||
|
||||
const hasSystem = systemPrompt && systemPrompt.trim().length > 0;
|
||||
const effectivePrompt = hasSystem
|
||||
? `<persona-instructions>\n${systemPrompt}\n</persona-instructions>\n\n<user-message>\n${prompt}\n</user-message>`
|
||||
: prompt;
|
||||
|
||||
const content: ContentBlock[] = [{ type: "text", text: effectivePrompt }];
|
||||
const content: ContentBlock[] = [];
|
||||
if (systemPrompt?.trim()) {
|
||||
content.push({
|
||||
type: "text",
|
||||
text: systemPrompt,
|
||||
annotations: { audience: ["assistant"] },
|
||||
});
|
||||
}
|
||||
content.push({ type: "text", text: prompt });
|
||||
if (images) {
|
||||
for (const [data, mimeType] of images) {
|
||||
content.push({ type: "image", data, mimeType } as ContentBlock);
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
findLatestUnpairedToolRequest,
|
||||
} from "@/features/chat/hooks/replayBuffer";
|
||||
import type {
|
||||
TextContent,
|
||||
ToolRequestContent,
|
||||
ToolResponseContent,
|
||||
} from "@/shared/types/messages";
|
||||
@@ -196,32 +197,32 @@ function handleReplay(sessionId: string, update: SessionUpdate): void {
|
||||
}
|
||||
|
||||
case "user_message_chunk": {
|
||||
if (update.content.type !== "text" || !("text" in update.content)) break;
|
||||
const messageId = update.messageId ?? crypto.randomUUID();
|
||||
const buffer = ensureReplayBuffer(sessionId);
|
||||
const existing = getBufferedMessage(sessionId, messageId);
|
||||
// biome-ignore lint/suspicious/noExplicitAny: wire format has annotations but SDK types don't
|
||||
const rawAnn = (update.content as any).annotations;
|
||||
const ann: TextContent["annotations"] | undefined =
|
||||
typeof rawAnn === "object" && rawAnn !== null ? rawAnn : undefined;
|
||||
// Drop assistant-only blocks so they never enter chat state.
|
||||
if (
|
||||
!existing &&
|
||||
update.content.type === "text" &&
|
||||
"text" in update.content
|
||||
) {
|
||||
ann?.audience &&
|
||||
ann.audience.length > 0 &&
|
||||
!ann.audience.includes("user")
|
||||
)
|
||||
break;
|
||||
const textBlock = makeTextBlock(update.content.text, ann);
|
||||
if (!existing) {
|
||||
buffer.push({
|
||||
id: messageId,
|
||||
role: "user",
|
||||
created: Date.now(),
|
||||
content: [{ type: "text", text: update.content.text }],
|
||||
content: [textBlock],
|
||||
metadata: { userVisible: true, agentVisible: true },
|
||||
});
|
||||
} else if (
|
||||
existing &&
|
||||
update.content.type === "text" &&
|
||||
"text" in update.content
|
||||
) {
|
||||
const last = existing.content[existing.content.length - 1];
|
||||
if (last?.type === "text") {
|
||||
(last as { type: "text"; text: string }).text += update.content.text;
|
||||
} else {
|
||||
existing.content.push({ type: "text", text: update.content.text });
|
||||
}
|
||||
} else {
|
||||
existing.content.push(textBlock);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -486,6 +487,13 @@ function findStreamingMessageId(sessionId: string): string | null {
|
||||
.streamingMessageId;
|
||||
}
|
||||
|
||||
function makeTextBlock(
|
||||
text: string,
|
||||
ann?: TextContent["annotations"],
|
||||
): TextContent {
|
||||
return { type: "text", text, ...(ann ? { annotations: ann } : {}) };
|
||||
}
|
||||
|
||||
function findMessageInBuffer(
|
||||
sessionId: string,
|
||||
_toolCallId: string,
|
||||
|
||||
@@ -33,10 +33,19 @@ export type ChatAttachmentDraft =
|
||||
// Message roles
|
||||
export type MessageRole = "user" | "assistant" | "system";
|
||||
|
||||
/** ACP audience restriction — which roles may see a content block. */
|
||||
export type Audience = ("user" | "assistant")[];
|
||||
|
||||
/** ACP content-block annotations (mirrors the SDK's Annotations shape). */
|
||||
export interface ContentAnnotations {
|
||||
audience?: Audience;
|
||||
}
|
||||
|
||||
// Content block types
|
||||
export interface TextContent {
|
||||
type: "text";
|
||||
text: string;
|
||||
annotations?: ContentAnnotations;
|
||||
}
|
||||
|
||||
export interface ImageContent {
|
||||
@@ -44,6 +53,7 @@ export interface ImageContent {
|
||||
source:
|
||||
| { type: "base64"; mediaType: string; data: string }
|
||||
| { type: "url"; url: string };
|
||||
annotations?: ContentAnnotations;
|
||||
}
|
||||
|
||||
export type ToolCallStatus =
|
||||
@@ -67,6 +77,7 @@ export interface ToolRequestContent {
|
||||
status: ToolCallStatus;
|
||||
/** Epoch ms when the tool call started executing (set on event receipt). */
|
||||
startedAt?: number;
|
||||
annotations?: ContentAnnotations;
|
||||
}
|
||||
|
||||
export interface ToolResponseContent {
|
||||
@@ -75,20 +86,24 @@ export interface ToolResponseContent {
|
||||
name: string;
|
||||
result: string;
|
||||
isError: boolean;
|
||||
annotations?: ContentAnnotations;
|
||||
}
|
||||
|
||||
export interface ThinkingContent {
|
||||
type: "thinking";
|
||||
text: string;
|
||||
annotations?: ContentAnnotations;
|
||||
}
|
||||
|
||||
export interface RedactedThinkingContent {
|
||||
type: "redactedThinking";
|
||||
annotations?: ContentAnnotations;
|
||||
}
|
||||
|
||||
export interface ReasoningContent {
|
||||
type: "reasoning";
|
||||
text: string;
|
||||
annotations?: ContentAnnotations;
|
||||
}
|
||||
|
||||
export interface ActionRequiredContent {
|
||||
@@ -99,12 +114,14 @@ export interface ActionRequiredContent {
|
||||
toolName?: string;
|
||||
arguments?: Record<string, unknown>;
|
||||
schema?: Record<string, unknown>;
|
||||
annotations?: ContentAnnotations;
|
||||
}
|
||||
|
||||
export interface SystemNotificationContent {
|
||||
type: "systemNotification";
|
||||
notificationType: "compaction" | "info" | "warning" | "error";
|
||||
text: string;
|
||||
annotations?: ContentAnnotations;
|
||||
}
|
||||
|
||||
export type MessageContent =
|
||||
|
||||
Reference in New Issue
Block a user