feat(webui): track optimistic message delivery status (#5162)

This commit is contained in:
chengyongru
2026-07-29 23:15:45 +08:00
committed by GitHub
parent 5a28a6165c
commit 129b74b4cf
23 changed files with 431 additions and 21 deletions
@@ -11,9 +11,8 @@ interface StreamErrorNoticeProps {
}
/**
* Dismissible banner that surfaces transport-level faults the user needs to
* know about. Rendered above the composer so the message the fault referred
* to remains in view just above. ``role="alert"`` + ``aria-live="assertive"``
* Fallback banner for transport-level faults that cannot be attached to a
* visible failed message. ``role="alert"`` + ``aria-live="assertive"``
* ensures screen readers announce the failure.
*/
export function StreamErrorNotice({ error, onDismiss }: StreamErrorNoticeProps) {
@@ -107,7 +107,11 @@ export function ThreadMessages({
unit.type === "message" && unit.message.role === "assistant" && forkFlags[index]
? nextUserIndex
: undefined;
if (unit.type === "message" && unit.message.role === "user") nextUserIndex += 1;
if (
unit.type === "message"
&& unit.message.role === "user"
&& unit.message.deliveryStatus !== "failed"
) nextUserIndex += 1;
return (
<ThreadDisplayUnit
+20 -3
View File
@@ -31,7 +31,7 @@ import {
installedMcpPresetsFromPayload,
isMcpPresetsPayload,
} from "@/lib/mcp-preset-events";
import type { CanonicalRunSnapshot } from "@/lib/nanobot-client";
import type { CanonicalRunSnapshot, StreamError } from "@/lib/nanobot-client";
import { inferProviderFromModelName, providerDisplayLabel } from "@/lib/provider-brand";
import type {
ChatSummary,
@@ -222,11 +222,28 @@ function latestActiveTurnId(messages: UIMessage[]): string | null {
}
for (let index = messages.length - 1; index >= 0; index -= 1) {
const message = messages[index];
if (message.role === "user" && message.turnId) return message.turnId;
if (
message.role === "user"
&& message.deliveryStatus !== "failed"
&& message.turnId
) return message.turnId;
}
return null;
}
function hasInlineDeliveryError(
messages: UIMessage[],
error: StreamError | null,
): boolean {
if (!error?.turnId) return false;
return messages.some((message) => (
message.role === "user"
&& message.turnId === error.turnId
&& message.deliveryStatus === "failed"
&& message.deliveryErrorKind === error.kind
));
}
function completedAssistantTurnIds(messages: UIMessage[]): string[] {
return Array.from(new Set(
messages
@@ -1283,7 +1300,7 @@ export function ThreadShell({
const composer = (
<>
{streamError ? (
{streamError && !hasInlineDeliveryError(messages, streamError) ? (
<StreamErrorNotice
error={streamError}
onDismiss={dismissStreamError}
@@ -250,7 +250,9 @@ export const ThreadViewport = forwardRef<ThreadViewportHandle, ThreadViewportPro
const hiddenUserMessageCount =
userMessageOffset
+ (hiddenMessageCount > 0
? messages.slice(0, hiddenMessageCount).filter((message) => message.role === "user").length
? messages.slice(0, hiddenMessageCount).filter(
(message) => message.role === "user" && message.deliveryStatus !== "failed",
).length
: 0);
const visibleForkBoundaryMessageCount =
forkBoundaryMessageCount !== null && forkBoundaryMessageCount > hiddenMessageCount