feat(memory-v2): close Phase A with auto-review, product events, and H5 recall UI.

Add candidate auto-review pipeline, shadow audit tooling, admin metrics page,
and user-visible memory recall hints in chat with phase-a readiness checks.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-08-01 17:14:06 +08:00
parent 666db0b939
commit 6f3e53a56a
50 changed files with 3730 additions and 38 deletions
+2
View File
@@ -187,6 +187,7 @@ export function ChatView({
sessionsHasMore,
sessionSearchQuery,
messages,
memoryRecallByMessageId,
messageHistoryLoadingMore,
messageHistoryHasMore,
messageHistoryTotal,
@@ -559,6 +560,7 @@ export function ChatView({
variant="full"
user={user}
messages={messages}
memoryRecallByMessageId={memoryRecallByMessageId}
historyLoadingMore={messageHistoryLoadingMore}
historyHasMore={messageHistoryHasMore}
historyTotal={messageHistoryTotal}
+47 -1
View File
@@ -2,6 +2,7 @@ import { useRef, useState } from 'react';
import { useUserAvatar } from '../hooks/useUserAvatar';
import type { Message } from '../types';
import { getDisplayText, getFileAttachments, getImageUrls, getRenderableImageUrls, getThinking, shouldShowChatMessage } from '../utils/message';
import { resolveMessageRecallKey } from '../utils/memoryFeedback';
import { getMessageSaveActions } from '../utils/messageSave';
import { renderMarkdown } from '../utils/markdown';
import { filterText } from '../utils/wordFilter';
@@ -169,6 +170,38 @@ function shouldShowEndTypingIndicator(messages: Message[], streaming: boolean) {
return true;
}
function MemoryRecallBadgeItem({
recall,
}: {
recall: { count: number; previews: string[] };
}) {
const [open, setOpen] = useState(false);
const hasPreviews = recall.previews.length > 0;
return (
<div className="memory-recall-badge-wrap">
<button
type="button"
className={`memory-recall-badge${open ? ' is-open' : ''}`}
aria-expanded={hasPreviews ? open : undefined}
onClick={() => {
if (hasPreviews) setOpen((current) => !current);
}}
>
<span> {recall.count} </span>
{hasPreviews && <ChevronIcon open={open} />}
</button>
{open && hasPreviews && (
<ul className="memory-recall-previews">
{recall.previews.map((preview, index) => (
<li key={`${index}-${preview.slice(0, 12)}`}>{preview}</li>
))}
</ul>
)}
</div>
);
}
function ToolBadge({ message, active }: { message: Message; active: boolean }) {
if (message.role !== 'assistant') return null;
const tools = message.content.filter((c) => c.type === 'toolRequest' || c.type === 'toolResponse');
@@ -291,6 +324,8 @@ function MessageRow({
compact = false,
activeToolMessage = false,
showInlineTyping = false,
memoryRecallByMessageId = {},
messageIndex = 0,
}: {
message: Message;
avatarUrl: string | null;
@@ -306,10 +341,13 @@ function MessageRow({
compact?: boolean;
activeToolMessage?: boolean;
showInlineTyping?: boolean;
memoryRecallByMessageId?: Record<string, { count: number; previews: string[] }>;
messageIndex?: number;
}) {
const [actionsOpen, setActionsOpen] = useState(false);
const rawText = getDisplayText(message);
const text = filterText(rawText);
const memoryRecall = memoryRecallByMessageId[resolveMessageRecallKey(message, messageIndex)];
const saveActions = getMessageSaveActions(text, { userId: publishUserId, username: publishUsername });
const thinking = getThinking(message);
const isUser = message.role === 'user';
@@ -326,7 +364,10 @@ function MessageRow({
{showInlineTyping ? (
<TypingBubble />
) : (
<ToolBadge message={message} active={activeToolMessage} />
<>
<ToolBadge message={message} active={activeToolMessage} />
{memoryRecall && <MemoryRecallBadgeItem recall={memoryRecall} />}
</>
)}
</div>
</div>
@@ -444,6 +485,7 @@ function MessageRow({
/>
)}
<ToolBadge message={message} active={activeToolMessage} />
{memoryRecall && <MemoryRecallBadgeItem recall={memoryRecall} />}
</div>
{showActionsToggle && (
<button
@@ -494,6 +536,7 @@ export function MessageList({
publishUserId,
publishUsername,
compact = false,
memoryRecallByMessageId = {},
}: {
messages: Message[];
streaming: boolean;
@@ -505,6 +548,7 @@ export function MessageList({
publishUserId?: string;
publishUsername?: string;
compact?: boolean;
memoryRecallByMessageId?: Record<string, { count: number; previews: string[] }>;
}) {
const { avatarUrl } = useUserAvatar();
const renderableMessages = messages.filter(shouldShowChatMessage);
@@ -561,6 +605,8 @@ export function MessageList({
compact={compact}
activeToolMessage={message === activeToolMessage}
showInlineTyping={showEndTyping && message === inlineTypingMessage}
memoryRecallByMessageId={memoryRecallByMessageId}
messageIndex={index}
/>
</div>
);
+21 -1
View File
@@ -1,12 +1,14 @@
import { useEffect, useMemo } from 'react';
import { useChat } from '../context/ChatProvider';
import { INSUFFICIENT_BALANCE_NOTICE } from '../hooks/useTKMindChat';
import { useGoalRunBanner } from '../hooks/useGoalRunBanner';
import type { PageEditSubChatBridge } from '../hooks/usePageEditSubChat';
import type { MindSpaceChatContext, MindSpaceSaveCategory, PortalUser, Message } from '../types';
import { formatContextChip } from '../utils/mindspaceChatContext';
import { shouldShowChatMessage } from '../utils/message';
import { ChatPanel } from './ChatPanel';
import { TKMindAvatar } from './TKMindAvatar';
import { GoalRunAwaitingBanner } from './GoalRunAwaitingBanner';
export function SpaceChatPanel({
open,
@@ -53,7 +55,15 @@ export function SpaceChatPanel({
uploadChatAttachment,
retryConnect,
} = chat;
const { capabilities, grantedSkills } = mainChat;
const { capabilities, grantedSkills, followAgentRun, goalRunEnabled } = mainChat;
const goalRunBanner = useGoalRunBanner({
userId: user.id,
sessionId: session?.id,
chatState,
submit: (text, options) => void submit(text, options),
followAgentRun,
featureEnabled: goalRunEnabled,
});
useEffect(() => {
if (!open) return;
@@ -130,6 +140,16 @@ export function SpaceChatPanel({
</div>
)}
{goalRunBanner.enabled && (
<GoalRunAwaitingBanner
items={goalRunBanner.items}
busy={goalRunBanner.bannerBusy}
onContinue={goalRunBanner.handleContinue}
onDismiss={goalRunBanner.dismissItem}
onRefresh={goalRunBanner.refresh}
/>
)}
<ChatPanel
variant="compact"
user={user}