diff --git a/app/page.tsx b/app/page.tsx
index 801488b..6a77642 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -100,7 +100,6 @@ export default forwardRef(function Home(_props, forwardedRef) {
const {
awaitingResponse,
setAwaitingResponse,
- thinkingStartTime,
setThinkingStartTime,
beginContentArrival,
resetThinkingState,
@@ -530,8 +529,6 @@ export default forwardRef(function Home(_props, forwardedRef) {
? { text: queuedMessage.text, attachments: queuedMessage.attachments as unknown[] | undefined }
: null;
- const thinkingLabel = isRunActive && lastCommand === "/compact" ? "Compacting" : undefined;
-
useEffect(() => {
const save = () => {
try { sessionStorage.setItem("mc-run-active", isRunActive ? "1" : "0"); } catch {}
@@ -793,9 +790,6 @@ export default forwardRef(function Home(_props, forwardedRef) {
onPin={handlePinSubagent}
onUnpin={handleUnpinSubagent}
zenMode={zenMode}
- isRunActive={isRunActive}
- thinkingStartTime={thinkingStartTime}
- thinkingLabel={thinkingLabel}
quotePopup={quotePopup}
quotePopupRef={quotePopupRef}
onAcceptQuote={handleAcceptQuote}
diff --git a/components/MessageRow.tsx b/components/MessageRow.tsx
index 78d0da9..b35ea01 100644
--- a/components/MessageRow.tsx
+++ b/components/MessageRow.tsx
@@ -5,6 +5,7 @@ import type { ContentPart, Message } from "@mc/types/chat";
import { getTextFromContent, getImages, getFiles } from "@mc/lib/messageUtils";
import { HEARTBEAT_MARKER, NO_REPLY_MARKER, SYSTEM_PREFIX, SYSTEM_MESSAGE_PREFIX, STOP_REASON_INJECTED, isToolCallPart, SPAWN_TOOL_NAME, hasUnquotedMarker, hasHeartbeatOnOwnLine, SQUIRCLE_RADIUS, MESSAGE_SEND_ANIMATION } from "@mc/lib/constants";
import { useExpandablePanel } from "@mc/hooks/useExpandablePanel";
+import { useElapsedSeconds } from "@mc/hooks/useElapsedSeconds";
import { SlideContent } from "@mc/components/SlideContent";
import { MarkdownContent } from "@mc/components/markdown/MarkdownContent";
import { StreamingText } from "@mc/components/StreamingText";
@@ -348,26 +349,28 @@ function ThinkingPill({ text, isStreaming }: { text: string; isStreaming?: boole
key={startIdx + i}
className="whitespace-pre-wrap break-words overflow-hidden animate-[thinkingSentence_0.5s_ease-out_both]"
>
- {sentence}
+ {sentence}
+ {i === visible.length - 1 && (
+
+ {isStreaming && (
+
+ .
+ .
+ .
+
+ )}
+
+
+ )}
))}
-
- {isStreaming && (
-
- .
- .
- .
-
- )}
-
-
{displayText}
@@ -474,6 +477,22 @@ function getAssistantDurationText(message: Message): string | null {
return null;
}
+function InlineThinkingIndicator({ startTime }: { startTime?: number }) {
+ const elapsed = useElapsedSeconds({ startTime });
+
+ return (
+
+ Thinking
+
+ .
+ .
+ .
+
+ {elapsed > 0 && {elapsed}s}
+
+ );
+}
+
function AssistantCopyButton({ text, durationText }: { text: string; durationText?: string | null }) {
const [copied, setCopied] = useState(false);
const [mounted, setMounted] = useState(false);
@@ -1027,6 +1046,9 @@ export function MessageRow({
: undefined}
>
{assistantBlocks.map(renderAssistantBlock)}
+ {isStreaming && message.role === "assistant" && (
+
+ )}
{showAssistantCopyButton ? : null}
diff --git a/components/ThinkingIndicator.tsx b/components/ThinkingIndicator.tsx
index 64fe7ad..a6f2a86 100644
--- a/components/ThinkingIndicator.tsx
+++ b/components/ThinkingIndicator.tsx
@@ -1,6 +1,6 @@
"use client";
-import { useEffect, useState, useRef } from "react";
+import { useElapsedSeconds } from "@mc/hooks/useElapsedSeconds";
interface ThinkingIndicatorProps {
visible: boolean;
@@ -9,37 +9,7 @@ interface ThinkingIndicatorProps {
}
export function ThinkingIndicator({ visible, startTime, label }: ThinkingIndicatorProps) {
- const [elapsedSeconds, setElapsedSeconds] = useState(0);
- const timerRef = useRef | null>(null);
-
- useEffect(() => {
- if (!visible) setElapsedSeconds(0);
- }, [visible]);
-
- // Update elapsed time every second
- useEffect(() => {
- if (!startTime || !visible) {
- if (timerRef.current) {
- clearInterval(timerRef.current);
- timerRef.current = null;
- }
- return;
- }
-
- const updateElapsed = () => {
- setElapsedSeconds(Math.floor((Date.now() - startTime) / 1000));
- };
- updateElapsed();
-
- timerRef.current = setInterval(updateElapsed, 1000);
-
- return () => {
- if (timerRef.current) {
- clearInterval(timerRef.current);
- timerRef.current = null;
- }
- };
- }, [startTime, visible]);
+ const elapsedSeconds = useElapsedSeconds({ startTime, active: visible });
const isCompacting = label === "Compacting";
const displayLabel = label || "Thinking";
diff --git a/components/chat/ChatViewport.tsx b/components/chat/ChatViewport.tsx
index bef7f74..4ca2cad 100644
--- a/components/chat/ChatViewport.tsx
+++ b/components/chat/ChatViewport.tsx
@@ -3,12 +3,10 @@
import React, { useMemo, useState, useEffect, useLayoutEffect, useCallback, useRef } from "react";
import { MessageRow } from "@mc/components/MessageRow";
-import { ThinkingIndicator } from "@mc/components/ThinkingIndicator";
import { ZenToggle } from "@mc/components/ZenToggle";
import { formatMessageTime, getMessageSide } from "@mc/lib/messageUtils";
import { STOP_REASON_INJECTED, isToolCallPart, MESSAGE_SEND_ANIMATION } from "@mc/lib/constants";
import { ZEN_SLIDE_MS, ZEN_FADE_MS, ZEN_TOGGLE_FRAME_MS } from "@mc/lib/chat/zenUi";
-import { getThinkingIndicatorBottom } from "@mc/lib/chat/layout";
import type { Message } from "@mc/types/chat";
import type { useSubagentStore } from "@mc/hooks/useSubagentStore";
import type { PluginActionHandler } from "@mc/lib/plugins/types";
@@ -47,9 +45,6 @@ interface ChatViewportProps {
}) => void;
onUnpin: () => void;
zenMode: boolean;
- isRunActive: boolean;
- thinkingStartTime: number | null;
- thinkingLabel?: string;
quotePopup: { x: number; y: number; text: string } | null;
quotePopupRef: React.RefObject;
onAcceptQuote: (text: string) => void;
@@ -81,9 +76,6 @@ export function ChatViewport({
onPin,
onUnpin,
zenMode,
- isRunActive,
- thinkingStartTime,
- thinkingLabel,
quotePopup,
quotePopupRef,
onAcceptQuote,
@@ -91,7 +83,6 @@ export function ChatViewport({
onAddInputAttachment,
}: ChatViewportProps) {
const detachedShell = isDetached && !detachedNoBorder;
- const thinkingIndicatorBottom = getThinkingIndicatorBottom({ isDetached, inputZoneHeight });
const [zenRenderMode, setZenRenderMode] = useState(zenMode);
const [expandedZenGroups, setExpandedZenGroups] = useState>({});
const [collapsingZenGroups, setCollapsingZenGroups] = useState>({});
@@ -850,11 +841,6 @@ export function ChatViewport({
-
{isDetached && !isNative && }
diff --git a/hooks/useElapsedSeconds.ts b/hooks/useElapsedSeconds.ts
new file mode 100644
index 0000000..15810c3
--- /dev/null
+++ b/hooks/useElapsedSeconds.ts
@@ -0,0 +1,36 @@
+"use client";
+
+import { useEffect, useState } from "react";
+
+interface UseElapsedSecondsOptions {
+ startTime?: number;
+ active?: boolean;
+}
+
+/**
+ * Tracks elapsed whole seconds from a start timestamp while active.
+ * Resets to 0 when inactive or no start time is provided.
+ */
+export function useElapsedSeconds({ startTime, active = true }: UseElapsedSecondsOptions): number {
+ const [elapsed, setElapsed] = useState(0);
+
+ useEffect(() => {
+ if (!startTime || !active) {
+ setElapsed(0);
+ return;
+ }
+
+ const updateElapsed = () => {
+ setElapsed(Math.floor((Date.now() - startTime) / 1000));
+ };
+
+ updateElapsed();
+ const timerId = window.setInterval(updateElapsed, 1000);
+
+ return () => {
+ window.clearInterval(timerId);
+ };
+ }, [active, startTime]);
+
+ return elapsed;
+}
diff --git a/tests/ChatViewport.zen.test.tsx b/tests/ChatViewport.zen.test.tsx
index 4fed6b0..ef5f0fa 100644
--- a/tests/ChatViewport.zen.test.tsx
+++ b/tests/ChatViewport.zen.test.tsx
@@ -38,8 +38,6 @@ function renderViewport(
onPin={() => {}}
onUnpin={() => {}}
zenMode={zenMode}
- isRunActive={false}
- thinkingStartTime={null}
quotePopup={null}
quotePopupRef={React.createRef()}
onAcceptQuote={() => {}}
@@ -178,8 +176,6 @@ describe("ChatViewport zen grouping", () => {
onPin={() => {}}
onUnpin={() => {}}
zenMode
- isRunActive={false}
- thinkingStartTime={null}
quotePopup={null}
quotePopupRef={React.createRef()}
onAcceptQuote={() => {}}
diff --git a/tests/MessageRow.test.tsx b/tests/MessageRow.test.tsx
index 1c64cb8..018d77b 100644
--- a/tests/MessageRow.test.tsx
+++ b/tests/MessageRow.test.tsx
@@ -308,6 +308,22 @@ describe("MessageRow", () => {
});
});
+ it("renders the collapsed thinking chevron inline with the last visible sentence", () => {
+ const message: Message = {
+ role: "assistant",
+ content: [],
+ reasoning: "One.\nTwo.\nThree.\nFour.\nFive.",
+ id: "test-thinking-inline-chevron",
+ };
+
+ render();
+
+ const lastVisibleSentence = screen.getByText("Five.");
+ const sentenceLine = lastVisibleSentence.closest("p");
+ expect(sentenceLine).not.toBeNull();
+ expect(sentenceLine?.querySelector("svg")).not.toBeNull();
+ });
+
it("unwraps markdown-style underscore emphasis in thinking blocks", () => {
const message: Message = {
role: "assistant",