fix: resolve web-read-progress skeleton screen and search-result card disappearing
- Add web_read -> web-read-progress mapping to standaloneUiItems filter in main.tsx so web-read-progress cards are no longer mistakenly treated as standalone - Add web-read-progress to CARD_TYPE_PRIORITY (550) to ensure stable sort order and prevent search-result cards (500) from being reordered relative to web-read cards - Add unique card_id (containing tc.id) to google_search and web_search_deep ui.push calls so each search result card can be precisely identified - Improve uiItem matching in ToolCallStatus to first try exact match via card_id containing tc.id, falling back to index-based matching only when necessary; this prevents index offset bugs when multiple same-type tools run concurrently Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
9cf6293198
commit
87d1a5f937
@@ -59,6 +59,7 @@ export async function toolExecutorNode(
|
||||
switch (name) {
|
||||
case "google_search": {
|
||||
const parsed = googleSearchSchema.parse(args);
|
||||
const googleCardId = `google-search-${tc.id}`;
|
||||
let results: Array<{ title: string; url: string; snippet: string }>;
|
||||
let knowledgeGraph: { title: string; description: string } | undefined;
|
||||
let fallbackUsed = false;
|
||||
@@ -103,6 +104,7 @@ export async function toolExecutorNode(
|
||||
{
|
||||
name: "search-result",
|
||||
props: {
|
||||
card_id: googleCardId,
|
||||
query: parsed.query,
|
||||
total: results.length,
|
||||
results,
|
||||
@@ -139,6 +141,7 @@ export async function toolExecutorNode(
|
||||
|
||||
case "web_search_deep": {
|
||||
const parsed = webSearchDeepSchema.parse(args);
|
||||
const deepCardId = `web-search-deep-${tc.id}`;
|
||||
let enriched: Array<{ title: string; url: string; snippet: string }>;
|
||||
let fallbackUsed = false;
|
||||
let readerAllFailed = false;
|
||||
@@ -202,6 +205,7 @@ export async function toolExecutorNode(
|
||||
{
|
||||
name: "search-result",
|
||||
props: {
|
||||
card_id: deepCardId,
|
||||
query: parsed.query,
|
||||
total: enriched.length,
|
||||
results: enriched,
|
||||
|
||||
@@ -325,10 +325,25 @@ export default function ToolCallStatus({
|
||||
let uiItem: UIMsgLocal | undefined;
|
||||
if (tc.name && UI_NAME_MAP[tc.name]) {
|
||||
const uiName = UI_NAME_MAP[tc.name];
|
||||
const matchIdx = matchCounters[uiName] ?? 0;
|
||||
matchCounters[uiName] = matchIdx + 1;
|
||||
const candidates = uiItems.filter((ui) => ui.name === uiName);
|
||||
uiItem = candidates[matchIdx];
|
||||
// First try exact match: card_id contains the tool call id (e.g. "google-search-<tc.id>")
|
||||
if (tc.id) {
|
||||
uiItem = uiItems.find(
|
||||
(ui) => ui.name === uiName && typeof ui.props?.card_id === "string" && (ui.props.card_id as string).includes(tc.id!),
|
||||
);
|
||||
}
|
||||
// Fallback: match by index (for cards without card_id or where id is not embedded)
|
||||
if (!uiItem) {
|
||||
const matchIdx = matchCounters[uiName] ?? 0;
|
||||
const candidates = uiItems.filter(
|
||||
(ui) => ui.name === uiName && (
|
||||
!tc.id ||
|
||||
typeof ui.props?.card_id !== "string" ||
|
||||
!(ui.props.card_id as string).includes(tc.id)
|
||||
),
|
||||
);
|
||||
uiItem = candidates[matchIdx];
|
||||
}
|
||||
matchCounters[uiName] = (matchCounters[uiName] ?? 0) + 1;
|
||||
}
|
||||
|
||||
// 优先按 toolCallId 精确匹配,降级按 index
|
||||
|
||||
+26
-15
@@ -49,18 +49,27 @@ class CardErrorBoundary extends Component<
|
||||
|
||||
// ─── Card deduplication by card_id ──────────────────────────────────────────
|
||||
// When the backend pushes loading then complete cards with the same card_id,
|
||||
// keep only the last (most recent) card per card_id. Cards without card_id pass through.
|
||||
// keep only the terminal-state card per card_id. Cards without card_id pass through.
|
||||
// Priority: done/error > reading/any-other (prevents race where parallel tool
|
||||
// executions push reading and done out of order, retaining the skeleton instead
|
||||
// of the completed content).
|
||||
function deduplicateUiItems(items: UIMsgLocal[]): UIMsgLocal[] {
|
||||
const seen = new Map<string, number>();
|
||||
// Walk forward to find the last index for each card_id
|
||||
const seen = new Map<string, { idx: number; status: string }>();
|
||||
items.forEach((item, idx) => {
|
||||
const cardId = item.props?.card_id as string | undefined;
|
||||
if (cardId) seen.set(cardId, idx);
|
||||
if (!cardId) return;
|
||||
const status = item.props?.status as string | undefined;
|
||||
const prev = seen.get(cardId);
|
||||
// Upgrade to done/error unconditionally; for other statuses keep the last occurrence
|
||||
if (!prev || status === "done" || status === "error" ||
|
||||
(prev.status !== "done" && prev.status !== "error")) {
|
||||
seen.set(cardId, { idx, status: status ?? "" });
|
||||
}
|
||||
});
|
||||
return items.filter((item, idx) => {
|
||||
const cardId = item.props?.card_id as string | undefined;
|
||||
if (!cardId) return true;
|
||||
return seen.get(cardId) === idx;
|
||||
return seen.get(cardId)?.idx === idx;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -714,16 +723,17 @@ function App() {
|
||||
);
|
||||
// Sort by card type priority first, then by sort_key for deterministic order
|
||||
const CARD_TYPE_PRIORITY: Record<string, number> = {
|
||||
"error-result": 100,
|
||||
"chart-result": 200,
|
||||
"knowledge-result": 300,
|
||||
"ticket-summary": 400,
|
||||
"ticket-detail": 450,
|
||||
"search-result": 500,
|
||||
"sandbox-result": 600,
|
||||
"canvas-doc": 700,
|
||||
"reply-draft": 750,
|
||||
"next-actions": 900,
|
||||
"error-result": 100,
|
||||
"chart-result": 200,
|
||||
"knowledge-result": 300,
|
||||
"ticket-summary": 400,
|
||||
"ticket-detail": 450,
|
||||
"search-result": 500,
|
||||
"web-read-progress": 550,
|
||||
"sandbox-result": 600,
|
||||
"canvas-doc": 700,
|
||||
"reply-draft": 750,
|
||||
"next-actions": 900,
|
||||
};
|
||||
const uiItems = [...uiItemsRaw].sort((a, b) => {
|
||||
const priorityA = CARD_TYPE_PRIORITY[a.name] ?? 500;
|
||||
@@ -842,6 +852,7 @@ function App() {
|
||||
google_search: "search-result",
|
||||
web_search_deep: "search-result",
|
||||
sandbox_run: "sandbox-result",
|
||||
web_read: "web-read-progress",
|
||||
};
|
||||
return tc.name && ui.name === nameMap[tc.name];
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user