Replace running chat spinner with paperclip animation

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-06-16 17:55:42 +00:00
1 parent a8e4bfbdeb
commit f37500bd07
5 files changed
+97 -26

No files matched your search

@@ -0,0 +1,23 @@
import type { SVGProps } from "react";
import { cn } from "../lib/utils";
export function AnimatedPaperclipIcon({ className, ...props }: SVGProps<SVGSVGElement>) {
return (
<svg
viewBox="-1 -1 26 26"
className={cn("paperclip-thinking-icon", className)}
aria-hidden="true"
{...props}
>
<path
className="paperclip-thinking-icon-path"
d="M16 6 l-8.414 8.586 a2.000 2.000 0 0 0 2.828 2.828 l8.414 -8.586 a4.000 4.000 0 1 0 -5.657 -5.657 l-8.379 8.551 a6.000 6.000 0 1 0 8.485 8.485 l8.379 -8.551"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
+7 -10
View File
@@ -167,6 +167,7 @@ import { IssueBlockedNotice } from "./IssueBlockedNotice";
import { IssueAssignedBacklogNotice } from "./IssueAssignedBacklogNotice";
import { IssueRecoveryActionCard, type RecoveryResolveOutcome } from "./IssueRecoveryActionCard";
import { SourceTrustBadge } from "./SourceTrustBadge";
import { AnimatedPaperclipIcon } from "./AnimatedPaperclipIcon";
interface IssueChatMessageContext {
feedbackDataSharingPreference: FeedbackDataSharingPreference;
@@ -893,10 +894,10 @@ function IssueChatChainOfThought({
onClick={() => hasContent && setExpanded((v) => !v)}
>
<span className="inline-flex items-center gap-2 text-sm font-medium text-foreground/80">
{agentIcon ? (
{isActive ? (
<AnimatedPaperclipIcon className="h-4 w-4 shrink-0 text-muted-foreground" />
) : agentIcon ? (
<AgentIcon icon={agentIcon} className="h-4 w-4 shrink-0" />
) : isActive ? (
<Loader2 className="h-4 w-4 shrink-0 animate-spin text-muted-foreground" />
) : (
<span className="flex h-4 w-4 shrink-0 items-center justify-center">
<span className="h-1.5 w-1.5 rounded-full bg-emerald-500/70" />
@@ -1021,7 +1022,7 @@ function IssueChatRollingToolPart({ toolParts }: { toolParts: ToolCallMessagePar
<div className="flex gap-2 px-1">
<div className="flex flex-col items-center pt-0.5">
{isRunning ? (
<Loader2 className="h-3.5 w-3.5 shrink-0 animate-spin text-muted-foreground/50" />
<AnimatedPaperclipIcon className="h-3.5 w-3.5 shrink-0 text-muted-foreground/50" />
) : (
<ToolIcon className="h-3.5 w-3.5 shrink-0 text-muted-foreground/50" />
)}
@@ -1792,7 +1793,7 @@ function IssueChatAssistantMessage({
) : null}
{isRunning ? (
<span className="inline-flex items-center gap-1 rounded-full border border-cyan-400/40 bg-cyan-500/10 px-2 py-0.5 text-[10px] font-medium uppercase tracking-[0.14em] text-cyan-700 dark:text-cyan-200">
<Loader2 className="h-3 w-3 animate-spin" />
<AnimatedPaperclipIcon className="h-3 w-3" />
Running
</span>
) : null}
@@ -1810,11 +1811,7 @@ function IssueChatAssistantMessage({
{message.content.length === 0 && waitingText ? (
<div className="flex items-center gap-2.5 rounded-lg px-1 py-2">
<span className="inline-flex items-center gap-2 text-sm font-medium text-foreground/80">
{agentIcon ? (
<AgentIcon icon={agentIcon} className="h-4 w-4 shrink-0" />
) : (
<Loader2 className="h-4 w-4 shrink-0 animate-spin text-muted-foreground" />
)}
<AnimatedPaperclipIcon className="h-4 w-4 shrink-0 text-muted-foreground" />
<span className="shimmer-text">{waitingText}</span>
</span>
</div>
+7 -10
View File
@@ -154,6 +154,7 @@ import { IssueBlockedNotice } from "./IssueBlockedNotice";
import { IssueAssignedBacklogNotice } from "./IssueAssignedBacklogNotice";
import { IssueRecoveryActionCard, type RecoveryResolveOutcome } from "./IssueRecoveryActionCard";
import { SourceTrustBadge } from "./SourceTrustBadge";
import { AnimatedPaperclipIcon } from "./AnimatedPaperclipIcon";
interface IssueChatMessageContext {
feedbackDataSharingPreference: FeedbackDataSharingPreference;
@@ -918,10 +919,10 @@ function IssueChatChainOfThought({
onClick={() => hasContent && setExpanded((v) => !v)}
>
<span className="inline-flex items-center gap-2 text-sm font-medium text-foreground/80">
{agentIcon ? (
{isActive ? (
<AnimatedPaperclipIcon className="h-4 w-4 shrink-0 text-muted-foreground" />
) : agentIcon ? (
<AgentIcon icon={agentIcon} className="h-4 w-4 shrink-0" />
) : isActive ? (
<Loader2 className="h-4 w-4 shrink-0 animate-spin text-muted-foreground" />
) : (
<span className="flex h-4 w-4 shrink-0 items-center justify-center">
<span className="h-1.5 w-1.5 rounded-full bg-emerald-500/70" />
@@ -1046,7 +1047,7 @@ function IssueChatRollingToolPart({ toolParts }: { toolParts: ToolCallMessagePar
<div className="flex gap-2 px-1">
<div className="flex flex-col items-center pt-0.5">
{isRunning ? (
<Loader2 className="h-3.5 w-3.5 shrink-0 animate-spin text-muted-foreground/50" />
<AnimatedPaperclipIcon className="h-3.5 w-3.5 shrink-0 text-muted-foreground/50" />
) : (
<ToolIcon className="h-3.5 w-3.5 shrink-0 text-muted-foreground/50" />
)}
@@ -1642,7 +1643,7 @@ function IssueChatAssistantMessage({
) : null}
{isRunning ? (
<span className="inline-flex items-center gap-1 rounded-full border border-cyan-400/40 bg-cyan-500/10 px-2 py-0.5 text-[10px] font-medium uppercase tracking-[0.14em] text-cyan-700 dark:text-cyan-200">
<Loader2 className="h-3 w-3 animate-spin" />
<AnimatedPaperclipIcon className="h-3 w-3" />
Running
</span>
) : null}
@@ -1660,11 +1661,7 @@ function IssueChatAssistantMessage({
{message.content.length === 0 && waitingText ? (
<div className="flex items-center gap-2.5 rounded-lg px-1 py-2">
<span className="inline-flex items-center gap-2 text-sm font-medium text-foreground/80">
{agentIcon ? (
<AgentIcon icon={agentIcon} className="h-4 w-4 shrink-0" />
) : (
<Loader2 className="h-4 w-4 shrink-0 animate-spin text-muted-foreground" />
)}
<AnimatedPaperclipIcon className="h-4 w-4 shrink-0 text-muted-foreground" />
<span className="shimmer-text">{waitingText}</span>
</span>
</div>
@@ -12,6 +12,7 @@ import {
User,
Wrench,
} from "lucide-react";
import { AnimatedPaperclipIcon } from "../AnimatedPaperclipIcon";
export type TranscriptMode = "nice" | "raw";
export type TranscriptDensity = "comfortable" | "compact";
@@ -742,7 +743,7 @@ function TranscriptToolCard({
) : block.status === "completed" ? (
<Check className={iconClass} />
) : (
<Wrench className={iconClass} />
<AnimatedPaperclipIcon className={iconClass} />
)}
<div className="min-w-0 flex-1">
<div className="flex flex-wrap items-center gap-x-2 gap-y-1">
@@ -855,10 +856,13 @@ function TranscriptCommandGroup({
isRunning
? "border-cyan-500/25 bg-cyan-500/[0.08] text-cyan-600 dark:text-cyan-300"
: "border-border/70 bg-background text-foreground/55",
isRunning && "animate-pulse",
)}
>
<TerminalSquare className="h-3.5 w-3.5" />
{isRunning ? (
<AnimatedPaperclipIcon className="h-3.5 w-3.5" />
) : (
<TerminalSquare className="h-3.5 w-3.5" />
)}
</span>
))}
</div>
@@ -980,10 +984,13 @@ function TranscriptToolGroup({
: isItemError
? "border-red-500/25 bg-red-500/[0.08] text-red-600 dark:text-red-300"
: "border-border/70 bg-background text-foreground/55",
isItemRunning && "animate-pulse",
)}
>
<Wrench className="h-3.5 w-3.5" />
{isItemRunning ? (
<AnimatedPaperclipIcon className="h-3.5 w-3.5" />
) : (
<Wrench className="h-3.5 w-3.5" />
)}
</span>
);
})}
@@ -1020,7 +1027,11 @@ function TranscriptToolGroup({
? "border-cyan-500/25 bg-cyan-500/[0.08] text-cyan-600 dark:text-cyan-300"
: "border-border/70 bg-background text-foreground/55",
)}>
<Wrench className="h-3 w-3" />
{item.status === "running" ? (
<AnimatedPaperclipIcon className="h-3 w-3" />
) : (
<Wrench className="h-3 w-3" />
)}
</span>
<span className={cn("text-[10px] font-semibold uppercase tracking-[0.14em] text-muted-foreground")}>
{humanizeLabel(item.name)}
+43
View File
@@ -464,12 +464,55 @@
animation: shimmer-text-slide 2.5s linear infinite;
}
.paperclip-thinking-icon {
transform-origin: 50% 50%;
}
@keyframes paperclip-thinking-draw {
0% {
stroke-dasharray: 0 85.717;
stroke-dashoffset: -85.717;
opacity: 1;
animation-timing-function: cubic-bezier(0.455, 0.03, 0.515, 0.955);
}
39.0625% {
stroke-dasharray: 85.717 85.717;
stroke-dashoffset: 0;
opacity: 1;
animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);
}
78.125% {
stroke-dasharray: 0 85.717;
stroke-dashoffset: 0;
opacity: 1;
}
78.225% {
opacity: 0;
}
100% {
stroke-dasharray: 0 85.717;
stroke-dashoffset: 0;
opacity: 0;
}
}
.paperclip-thinking-icon-path {
animation: paperclip-thinking-draw 1s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
.shimmer-text {
animation: none;
-webkit-text-fill-color: unset;
background: none;
}
.paperclip-thinking-icon-path {
animation: none;
opacity: 1;
stroke-dasharray: 85.717 85.717;
stroke-dashoffset: 0;
}
}
/* Agent heartbeat capsule motion (PAP-75).