- );
-});
+ },
+);
// Wrapper that runs the smooth-streaming jitter buffer on a single
// response block. Only used during live streaming — historical
@@ -167,6 +262,10 @@ export const BlockList: FC<{
hasUserResponseAfterAskQuestion = false,
urlTransform,
}) => {
+ const prefQuery = useQuery(preferenceSettings());
+ const thinkingDisplayMode: ThinkingDisplayMode =
+ prefQuery.data?.thinking_display_mode || "auto";
+
const toolByID = new Map(tools.map((tool) => [tool.id, tool]));
// Pre-compute which tool IDs have a corresponding block so
@@ -182,6 +281,12 @@ export const BlockList: FC<{
const remainingTools = tools.filter((tool) => !blockToolIDs.has(tool.id));
+ // A thinking block is actively streaming only when it is the
+ // very last block in the list. Once newer content arrives
+ // (response, tool call, etc.) the thinking phase is over.
+ const lastBlockIsThinking =
+ blocks.length > 0 && blocks[blocks.length - 1].type === "thinking";
+
return (
<>
{blocks.map((block, index) => {
@@ -214,8 +319,13 @@ export const BlockList: FC<{
key={`${keyPrefix}-thinking-${index}`}
id={`${keyPrefix}-thinking-${index}`}
text={block.text}
- isStreaming={isStreaming}
+ isStreaming={
+ isStreaming &&
+ lastBlockIsThinking &&
+ index === blocks.length - 1
+ }
urlTransform={urlTransform}
+ thinkingDisplayMode={thinkingDisplayMode}
/>
);
case "file-reference":
diff --git a/site/src/pages/AgentsPage/components/ChatConversation/StreamingOutput.stories.tsx b/site/src/pages/AgentsPage/components/ChatConversation/StreamingOutput.stories.tsx
index 60593bb61f..461c4c7803 100644
--- a/site/src/pages/AgentsPage/components/ChatConversation/StreamingOutput.stories.tsx
+++ b/site/src/pages/AgentsPage/components/ChatConversation/StreamingOutput.stories.tsx
@@ -73,7 +73,7 @@ export const ReconnectingAfterDisconnect: Story = {
expect(canvasElement.textContent).toMatch(/reconnecting in \d+s/i);
});
expect(canvas.queryByText("Unexpected error")).not.toBeInTheDocument();
- const thinkingMatches = canvas.getAllByText(/thinking\.\.\./i);
+ const thinkingMatches = canvas.getAllByText(/thinking\.\.\.$/i);
expect(thinkingMatches.length).toBeGreaterThanOrEqual(1);
},
};
@@ -256,7 +256,7 @@ export const RetryStartupTimeout: Story = {
/**
* During streaming, if only tool-call blocks have arrived (no text
- * or reasoning), the "Thinking..." indicator should still be visible
+ * or reasoning), the "Thinking" indicator should still be visible
* alongside the tool cards.
*/
export const ThinkingDuringStreamingWithToolCalls: Story = {
@@ -272,9 +272,9 @@ export const ThinkingDuringStreamingWithToolCalls: Story = {
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
- // "Thinking..." should still be visible during streaming
+ // "Thinking" should still be visible during streaming
// when only tool-call blocks have arrived.
- const matches = canvas.getAllByText("Thinking...");
+ const matches = canvas.getAllByText("Thinking");
expect(matches.length).toBeGreaterThanOrEqual(1);
},
};
diff --git a/site/src/pages/AgentsPage/components/ChatConversation/StreamingOutput.tsx b/site/src/pages/AgentsPage/components/ChatConversation/StreamingOutput.tsx
index fb846e9f50..ea634cf8c2 100644
--- a/site/src/pages/AgentsPage/components/ChatConversation/StreamingOutput.tsx
+++ b/site/src/pages/AgentsPage/components/ChatConversation/StreamingOutput.tsx
@@ -5,7 +5,6 @@ import {
ConversationItem,
Message,
MessageContent,
- Response,
Shimmer,
} from "../ChatElements";
import type { SubagentVariant } from "../ChatElements/tools/subagentDescriptor";
@@ -21,30 +20,22 @@ const hasTransientLiveStatus = (liveStatus: LiveStatusModel): boolean =>
/**
* True when the block list contains at least one text or reasoning
- * block. Tool-call and other non-text blocks don't count because
- * they don't replace the "Thinking..." placeholder visually.
+ * block. Tool-call blocks don't count; the placeholder should
+ * remain visible between tool calls so the user knows the model
+ * is still working.
*/
const hasTextOrReasoningBlock = (blocks: readonly RenderBlock[]): boolean =>
blocks.some((b) => b.type === "response" || b.type === "thinking");
/**
- * Stateless "Thinking..." shimmer used during the streaming phase
- * when no text or reasoning blocks have arrived yet. Unlike the
- * `StartingPlaceholder` in `ChatStatusCallout`, this has no
- * delayed-startup timer — the streaming phase is transient and
- * will be replaced as soon as real content arrives.
+ * Placeholder shown during streaming before text or reasoning
+ * blocks arrive. Uses the same shimmer animation as the
+ * collapsible thinking disclosure label.
*/
const StreamingThinkingPlaceholder: FC = () => (
-
+ How thinking blocks should be displayed by default. 'Auto' fully
+ expands during streaming, then auto-collapses when done. 'Preview'
+ auto-expands with a height constraint during streaming. 'Always
+ Expanded' shows full content. 'Always Collapsed' keeps them collapsed.
+
+
+
+ {mutation.isError && (
+
+ Failed to save your thinking display preference.
+