mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
fix(site): fix action bar hidden after null-returning assistant messages (#24566)
closes CODAGT-121 When an invisible assistant message (e.g. a `provider_executed` tool-result with no text content) appeared between a visible assistant message and the next user message, the visible assistant's action bar was incorrectly hidden. The chain logic computed `isLastInChain` from the raw `parsedMessages` array, which includes entries that `ChatMessageItem` returns `null` for. Extract a shared `isTimelineMessageVisible` helper that encodes the three null-return conditions (`provider_executed` tool-result-only, all-provider-executed parts, metadata-only) in one place. Use it both to guard the early return in `ChatMessageItem` and to skip invisible entries when computing `isLastInChain`, so chain boundaries are based on the next *rendered* message.
This commit is contained in:
+62
@@ -1607,3 +1607,65 @@ export const NoRenderableContentFallbackSpacing: Story = {
|
||||
).toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Regression: action bar must appear on the last *visible* assistant
|
||||
* message even when invisible assistant messages (provider-executed
|
||||
* tool-result-only) follow it before the next user turn.
|
||||
*/
|
||||
export const AssistantActionBarAfterHiddenMessages: Story = {
|
||||
args: {
|
||||
...defaultArgs,
|
||||
parsedMessages: buildMessages([
|
||||
{
|
||||
...baseMessage,
|
||||
id: 1,
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "Help me refactor" }],
|
||||
},
|
||||
{
|
||||
...baseMessage,
|
||||
id: 2,
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "text", text: "Here is the **refactored** version." },
|
||||
],
|
||||
},
|
||||
{
|
||||
...baseMessage,
|
||||
id: 3,
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "tool-result",
|
||||
tool_call_id: "provider-tool-1",
|
||||
result: { output: "done" },
|
||||
provider_executed: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
...baseMessage,
|
||||
id: 4,
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "Thanks!" }],
|
||||
},
|
||||
]),
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
// Force the hover-reveal action bars visible using stable test IDs.
|
||||
for (const el of canvasElement.querySelectorAll(
|
||||
'[data-testid="message-actions"]',
|
||||
)) {
|
||||
if (el instanceof HTMLElement) {
|
||||
el.style.opacity = "1";
|
||||
}
|
||||
}
|
||||
// 2 user messages + 1 visible assistant = 3 action bars.
|
||||
// The invisible provider-executed tool-result message (id=3)
|
||||
// must not prevent the assistant (id=2) from showing its bar.
|
||||
const actions = canvas.getAllByTestId("message-actions");
|
||||
expect(actions).toHaveLength(3);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -821,6 +821,28 @@ const StickyUserMessage = memo<{
|
||||
},
|
||||
);
|
||||
|
||||
function computeLastInChainFlags(
|
||||
parsedMessages: readonly ParsedMessageEntry[],
|
||||
): boolean[] {
|
||||
const flags = new Array<boolean>(parsedMessages.length).fill(false);
|
||||
let nextVisibleIsUser = true; // no next visible => treat as chain end
|
||||
for (let i = parsedMessages.length - 1; i >= 0; i--) {
|
||||
const entry = parsedMessages[i];
|
||||
const { shouldHide } = deriveMessageDisplayState({
|
||||
message: entry.message,
|
||||
parsed: entry.parsed,
|
||||
hideActions: false,
|
||||
});
|
||||
if (entry.message.role !== "user") {
|
||||
flags[i] = nextVisibleIsUser;
|
||||
}
|
||||
if (!shouldHide) {
|
||||
nextVisibleIsUser = entry.message.role === "user";
|
||||
}
|
||||
}
|
||||
return flags;
|
||||
}
|
||||
|
||||
interface ConversationTimelineProps {
|
||||
parsedMessages: readonly ParsedMessageEntry[];
|
||||
subagentTitles: Map<string, string>;
|
||||
@@ -854,6 +876,8 @@ export const ConversationTimeline = memo<ConversationTimelineProps>(
|
||||
mcpServers,
|
||||
showDesktopPreviews,
|
||||
}) => {
|
||||
const lastInChainFlags = computeLastInChainFlags(parsedMessages);
|
||||
|
||||
if (parsedMessages.length === 0) {
|
||||
return null;
|
||||
}
|
||||
@@ -929,10 +953,10 @@ export const ConversationTimeline = memo<ConversationTimelineProps>(
|
||||
/>
|
||||
);
|
||||
}
|
||||
// Hide actions on assistant messages that are not
|
||||
// the last in a consecutive assistant chain.
|
||||
const next = parsedMessages[msgIdx + 1];
|
||||
const isLastInChain = !next || next.message.role === "user";
|
||||
// Hide actions on assistant messages that are not the
|
||||
// last in a consecutive assistant chain. Flags are
|
||||
// precomputed in a single reverse pass above.
|
||||
const isLastInChain = lastInChainFlags[msgIdx];
|
||||
return (
|
||||
<ChatMessageItem
|
||||
key={message.id}
|
||||
|
||||
Reference in New Issue
Block a user