-
+
{props.hintElement}
{props.tokenCount !== undefined && (
diff --git a/src/browser/components/WorkspaceStatusIndicator.tsx b/src/browser/components/WorkspaceStatusIndicator.tsx
index 4837b664c7..27e4d1f8ed 100644
--- a/src/browser/components/WorkspaceStatusIndicator.tsx
+++ b/src/browser/components/WorkspaceStatusIndicator.tsx
@@ -1,11 +1,16 @@
import { useWorkspaceSidebarState } from "@/browser/stores/WorkspaceStore";
import { ModelDisplay } from "@/browser/components/Messages/ModelDisplay";
+import { Shimmer } from "@/browser/components/ai-elements/shimmer";
import { EmojiIcon } from "@/browser/components/icons/EmojiIcon";
+import { StreamingActivityIcon } from "@/browser/components/icons/StreamingActivityIcon";
import { CircleHelp, ExternalLinkIcon, Loader2 } from "lucide-react";
import { memo } from "react";
import { Tooltip, TooltipTrigger, TooltipContent } from "./ui/tooltip";
import { Button } from "./ui/button";
+const STREAMING_STATUS_SHIMMER_DURATION_SECONDS = 2;
+const STREAMING_STATUS_SHIMMER_COLOR = "var(--color-muted)";
+
export const WorkspaceStatusIndicator = memo<{
workspaceId: string;
fallbackModel: string;
@@ -63,23 +68,47 @@ export const WorkspaceStatusIndicator = memo<{
const modelToShow = canInterrupt ? (currentModel ?? fallbackModel) : fallbackModel;
const suffix = phase === "starting" ? "- starting..." : "- streaming...";
+ const isStreamingPhase = phase === "streaming";
return (
{phase === "starting" && (
)}
+ {isStreamingPhase && (
+
+ )}
{modelToShow ? (
<>
- {suffix}
+ {isStreamingPhase ? (
+
+ {suffix}
+
+ ) : (
+ {suffix}
+ )}
>
+ ) : isStreamingPhase ? (
+
+ Assistant - streaming...
+
) : (
-
- {phase === "starting" ? "Assistant - starting..." : "Assistant - streaming..."}
-
+ Assistant - starting...
)}
);
diff --git a/src/browser/components/icons/StreamingActivityIcon.tsx b/src/browser/components/icons/StreamingActivityIcon.tsx
new file mode 100644
index 0000000000..3339b2dcca
--- /dev/null
+++ b/src/browser/components/icons/StreamingActivityIcon.tsx
@@ -0,0 +1,43 @@
+import thinkingDotsSvg from "@/browser/assets/animations/thinking-dots.svg?raw";
+import { cn } from "@/common/lib/utils";
+import { useId } from "react";
+
+interface StreamingActivityIconProps {
+ className?: string;
+ shimmerColor: string;
+ shimmerDurationSeconds: number;
+}
+
+type StreamingActivityIconStyle = React.CSSProperties &
+ Record<"--shimmer-duration" | "--shimmer-color", string>;
+
+function scopeStreamingSvgIds(svgMarkup: string, idPrefix: string): string {
+ // The uploaded SVG contains many links. Multiple instances of
+ // identical IDs collide in the DOM, which can make later icons appear static.
+ return svgMarkup.replaceAll('"_R_G', `"${idPrefix}_R_G`).replaceAll("#_R_G", `#${idPrefix}_R_G`);
+}
+
+/**
+ * Animated streaming indicator used beside "...streaming" labels.
+ * The shimmer overlay shares the same duration and color as the adjacent text.
+ */
+export function StreamingActivityIcon(props: StreamingActivityIconProps) {
+ const style: StreamingActivityIconStyle = {
+ "--shimmer-duration": `${props.shimmerDurationSeconds}s`,
+ "--shimmer-color": props.shimmerColor,
+ };
+ const iconId = useId().replaceAll(":", "");
+ const scopedSvg = scopeStreamingSvgIds(thinkingDotsSvg, `mux_streaming_${iconId}`);
+
+ return (
+
+ );
+}
diff --git a/src/browser/styles/globals.css b/src/browser/styles/globals.css
index 62a7c135be..6210832648 100644
--- a/src/browser/styles/globals.css
+++ b/src/browser/styles/globals.css
@@ -1945,6 +1945,39 @@ pre code {
-webkit-box-decoration-break: clone;
}
+/* Shared shimmer overlay for non-text surfaces (e.g., streaming status icons). */
+.shimmer-surface {
+ position: relative;
+ display: inline-flex;
+ overflow: hidden;
+ isolation: isolate;
+}
+
+.streaming-activity-icon > svg {
+ width: 100%;
+ height: 100%;
+ display: block;
+}
+
+.shimmer-surface::after {
+ --shimmer-overlay: color-mix(in srgb, var(--shimmer-color, var(--color-muted-foreground)) 35%, black);
+ content: "";
+ position: absolute;
+ inset: 0;
+ pointer-events: none;
+ background: linear-gradient(
+ 90deg,
+ transparent 0%,
+ transparent 35%,
+ var(--shimmer-overlay) 50%,
+ transparent 65%,
+ transparent 100%
+ );
+ background-size: 300% 100%;
+ animation: shimmer-text-sweep var(--shimmer-duration, 1.4s) steps(42, end) infinite;
+ mix-blend-mode: multiply;
+}
+
@keyframes shimmer-text-sweep {
from {
background-position: 100% 0;