fix(webui): hide actions until turn end

This commit is contained in:
Zhou
2026-08-16 00:14:42 +08:00
committed by Xubin Ren
parent 5e84055dbb
commit 48126f049d
5 changed files with 213 additions and 8 deletions
+5 -1
View File
@@ -52,6 +52,8 @@ import type {
interface MessageBubbleProps {
message: UIMessage;
/** The containing agent turn has not received turn_end yet. */
isTurnStreaming?: boolean;
/** Give temporary-chat user turns the dashed private-mode treatment. */
temporary?: boolean;
/** When false, hide this message's copy button. Default true. */
@@ -260,6 +262,7 @@ function UserDeliveryStatus({
/** Render user turns as compact bubbles and assistant turns as document-like prose. */
export function MessageBubble({
message,
isTurnStreaming = false,
temporary = false,
showCopyAction = true,
cliApps = [],
@@ -381,7 +384,8 @@ export function MessageBubble({
: "";
const automationTriggeredLabel = t("message.automationTriggered");
const showAssistantActions = message.role === "assistant" && !message.isStreaming && !empty;
const showAssistantActions =
message.role === "assistant" && !message.isStreaming && !isTurnStreaming && !empty;
const showCopyButton = showCopyAction && showAssistantActions;
const showForkButton = showAssistantActions && !!onForkFromHere;
const forkLabel = t("message.forkFromHere");
+34 -1
View File
@@ -91,6 +91,9 @@ export function ThreadMessages({
&& pendingTurn !== null
&& !pendingTurn.hasVisibleOutput
) ? pendingTurn : null;
const currentTurnStartIndex = isStreaming
? activeTurnStartIndex(units, activeTurnId)
: units.length;
const unitKeys = useMemo(() => unitKeysForDisplay(units), [units]);
let nextUserIndex = hiddenUserMessageCount;
@@ -140,7 +143,15 @@ export function ThreadMessages({
userPromptId={userPromptId}
hasBodyBelow={hasBodyBelow}
deferOffscreenRender={deferOffscreenRender}
isTurnStreaming={liveActivityClusterIndices.has(index)}
isTurnStreaming={
unit.type === "activity"
? liveActivityClusterIndices.has(index)
: isStreaming && (
unit.message.turnId
? unit.message.turnId === activeTurnId
: index > currentTurnStartIndex
)
}
forkIndex={forkIndex}
showForkBoundary={index === forkBoundaryAfterUnitIndex}
forkBoundaryLabel={t("thread.forkedFromHistory")}
@@ -280,6 +291,7 @@ const ThreadDisplayUnit = memo(function ThreadDisplayUnit({
) : (
<MessageBubble
message={unit.message}
isTurnStreaming={isTurnStreaming}
temporary={temporary}
cliApps={cliApps}
mcpPresets={mcpPresets}
@@ -317,6 +329,27 @@ function threadDisplayUnitPropsEqual(
);
}
function activeTurnStartIndex(units: DisplayUnit[], activeTurnId: string | null): number {
if (activeTurnId) {
const index = units.findIndex((unit) => (
unit.type === "message"
&& unit.message.role === "user"
&& unit.message.deliveryStatus !== "failed"
&& unit.message.turnId === activeTurnId
));
if (index >= 0) return index;
}
for (let i = units.length - 1; i >= 0; i -= 1) {
const unit = units[i];
if (
unit.type === "message"
&& unit.message.role === "user"
&& unit.message.deliveryStatus !== "failed"
) return i;
}
return -1;
}
function displayUnitsEqual(previous: DisplayUnit, next: DisplayUnit): boolean {
if (previous.type !== next.type) return false;
if (previous.type === "message" && next.type === "message") {
+10 -6
View File
@@ -216,17 +216,18 @@ function isStaleThreadSnapshot(
return snapshot.every((message, index) => sameMessageShape(current[index], message));
}
function latestActiveTurnId(messages: UIMessage[]): string | null {
function latestActiveTurnId(messages: UIMessage[], runStartedAt: number | null): string | null {
for (let index = messages.length - 1; index >= 0; index -= 1) {
const message = messages[index];
if (message.isStreaming && message.turnId) return message.turnId;
}
if (runStartedAt === null) return null;
for (let index = messages.length - 1; index >= 0; index -= 1) {
const message = messages[index];
if (
message.role === "user"
&& message.deliveryStatus !== "failed"
message.role !== "user"
&& message.turnId
&& message.createdAt >= runStartedAt * 1000
) return message.turnId;
}
return null;
@@ -808,8 +809,8 @@ export function ThreadShell({
const currentGoalState = messagesReady ? goalState : undefined;
const turnActive = messagesReady && (isStreaming || currentRunStartedAt !== null);
const restoredViewportTurnId = useMemo(
() => turnActive ? latestActiveTurnId(displayMessages) : null,
[displayMessages, turnActive],
() => turnActive ? latestActiveTurnId(displayMessages, currentRunStartedAt) : null,
[currentRunStartedAt, displayMessages, turnActive],
);
const rememberedViewportTurnId = chatId
? activeViewportTurnByChatIdRef.current.get(chatId) ?? null
@@ -818,7 +819,10 @@ export function ThreadShell({
? client.getRunTurnId(chatId)
: null;
const viewportTurnId = messagesReady && turnActive
? canonicalRunTurnId ?? rememberedViewportTurnId ?? restoredViewportTurnId
? canonicalRunTurnId
?? rememberedViewportTurnId
?? historyActiveTurnId
?? restoredViewportTurnId
: null;
const activeTurnStartedHere =
viewportTurnId !== null && viewportTurnId === submittedViewportTurnId;