fix: resolve web-read-progress skeleton screen and search-result card disappearing
Trigger auto deployment for soc-langgraph / build-and-deploy (push) Failing after 28s
Deploy LangGraph UI to Azure Static Web Apps / build-and-deploy (push) Failing after 38s

- 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:
gongzhiyong
2026-04-13 00:24:50 +08:00
co-authored by Claude Sonnet 4.6
parent 9cf6293198
commit 87d1a5f937
3 changed files with 49 additions and 19 deletions
@@ -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,
+17 -2
View File
@@ -325,11 +325,26 @@ export default function ToolCallStatus({
let uiItem: UIMsgLocal | undefined;
if (tc.name && UI_NAME_MAP[tc.name]) {
const uiName = UI_NAME_MAP[tc.name];
// 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;
matchCounters[uiName] = matchIdx + 1;
const candidates = uiItems.filter((ui) => ui.name === uiName);
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
const logEntry = executionLog
+16 -5
View File
@@ -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;
});
}
@@ -720,6 +729,7 @@ function App() {
"ticket-summary": 400,
"ticket-detail": 450,
"search-result": 500,
"web-read-progress": 550,
"sandbox-result": 600,
"canvas-doc": 700,
"reply-draft": 750,
@@ -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];
}));