fix(webui): hide actions until turn end
This commit is contained in:
@@ -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");
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user