feat: add optional jumping roo animation for progress indicators

- Created JumpingRoo component with CSS keyframes animation
- Added useJumpingRooAnimation setting to global settings
- Updated ProgressIndicator to conditionally render based on setting
- Added toggle in UI Settings panel
- Added translation labels for the new setting
- Persisted setting in backend state management

References #8747
This commit is contained in:
Roo Code 2025-10-21 12:32:12 +00:00
parent f3a505fd36
commit 88fc49d3f3
11 changed files with 177 additions and 14 deletions

View file

@ -148,6 +148,7 @@ export const globalSettingsSchema = z.object({
includeTaskHistoryInEnhance: z.boolean().optional(),
historyPreviewCollapsed: z.boolean().optional(),
reasoningBlockCollapsed: z.boolean().optional(),
useJumpingRooAnimation: z.boolean().optional(),
profileThresholds: z.record(z.string(), z.number()).optional(),
hasOpenedModeSelector: z.boolean().optional(),
lastModeExportPath: z.string().optional(),

View file

@ -1793,6 +1793,7 @@ export class ClineProvider
terminalCompressProgressBar,
historyPreviewCollapsed,
reasoningBlockCollapsed,
useJumpingRooAnimation,
cloudUserInfo,
cloudIsAuthenticated,
sharingEnabled,
@ -1927,6 +1928,7 @@ export class ClineProvider
hasSystemPromptOverride,
historyPreviewCollapsed: historyPreviewCollapsed ?? false,
reasoningBlockCollapsed: reasoningBlockCollapsed ?? true,
useJumpingRooAnimation: useJumpingRooAnimation ?? false,
cloudUserInfo,
cloudIsAuthenticated: cloudIsAuthenticated ?? false,
cloudOrganizations,
@ -2142,6 +2144,7 @@ export class ClineProvider
maxConcurrentFileReads: stateValues.maxConcurrentFileReads ?? 5,
historyPreviewCollapsed: stateValues.historyPreviewCollapsed ?? false,
reasoningBlockCollapsed: stateValues.reasoningBlockCollapsed ?? true,
useJumpingRooAnimation: stateValues.useJumpingRooAnimation ?? false,
cloudUserInfo,
cloudIsAuthenticated,
sharingEnabled,

View file

@ -1621,6 +1621,10 @@ export const webviewMessageHandler = async (
await updateGlobalState("reasoningBlockCollapsed", message.bool ?? true)
// No need to call postStateToWebview here as the UI already updated optimistically
break
case "setUseJumpingRooAnimation":
await updateGlobalState("useJumpingRooAnimation", message.bool ?? false)
// No need to call postStateToWebview here as the UI already updated optimistically
break
case "toggleApiConfigPin":
if (message.text) {
const currentPinned = getGlobalState("pinnedApiConfigs") ?? {}

View file

@ -288,6 +288,7 @@ export type ExtensionState = Pick<
| "openRouterImageGenerationSelectedModel"
| "includeTaskHistoryInEnhance"
| "reasoningBlockCollapsed"
| "useJumpingRooAnimation"
> & {
version: string
clineMessages: ClineMessage[]

View file

@ -195,6 +195,7 @@ export interface WebviewMessage {
| "profileThresholds"
| "setHistoryPreviewCollapsed"
| "setReasoningBlockCollapsed"
| "setUseJumpingRooAnimation"
| "openExternal"
| "filterMarketplaceItems"
| "marketplaceButtonClicked"

View file

@ -0,0 +1,101 @@
import React from "react"
export const JumpingRoo = () => (
<div
style={{
width: "16px",
height: "16px",
display: "flex",
alignItems: "center",
justifyContent: "center",
position: "relative",
}}>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
style={{
animation: "jumpingRoo 0.8s ease-in-out infinite",
}}>
{/* Kangaroo body */}
<ellipse cx="8" cy="10" rx="3.5" ry="4" fill="currentColor" opacity="0.9" />
{/* Kangaroo head */}
<circle cx="8" cy="5" r="2.5" fill="currentColor" opacity="0.9" />
{/* Kangaroo ears */}
<ellipse
cx="6.5"
cy="3.5"
rx="0.8"
ry="1.5"
fill="currentColor"
opacity="0.9"
transform="rotate(-15 6.5 3.5)"
/>
<ellipse
cx="9.5"
cy="3.5"
rx="0.8"
ry="1.5"
fill="currentColor"
opacity="0.9"
transform="rotate(15 9.5 3.5)"
/>
{/* Kangaroo tail */}
<path
d="M5 11 Q2 12 1 14"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
fill="none"
opacity="0.9"
/>
{/* Kangaroo legs */}
<rect x="6" y="12" width="1.5" height="3" rx="0.5" fill="currentColor" opacity="0.9" />
<rect x="8.5" y="12" width="1.5" height="3" rx="0.5" fill="currentColor" opacity="0.9" />
{/* Kangaroo arms */}
<rect
x="5"
y="8"
width="1"
height="2.5"
rx="0.5"
fill="currentColor"
opacity="0.9"
transform="rotate(-20 5.5 9.25)"
/>
<rect
x="10"
y="8"
width="1"
height="2.5"
rx="0.5"
fill="currentColor"
opacity="0.9"
transform="rotate(20 10.5 9.25)"
/>
{/* Kangaroo pouch */}
<path d="M6.5 11 Q8 12 9.5 11" stroke="currentColor" strokeWidth="0.5" fill="none" opacity="0.5" />
</svg>
<style>{`
@keyframes jumpingRoo {
0%, 100% {
transform: translateY(0) scaleY(1);
}
20% {
transform: translateY(0) scaleY(0.9);
}
40% {
transform: translateY(-4px) scaleY(1.1);
}
60% {
transform: translateY(-4px) scaleY(1.1);
}
80% {
transform: translateY(0) scaleY(0.95);
}
}
`}</style>
</div>
)

View file

@ -1,16 +1,26 @@
import { VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { JumpingRoo } from "./JumpingRoo"
export const ProgressIndicator = () => (
<div
style={{
width: "16px",
height: "16px",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}>
<div style={{ transform: "scale(0.55)", transformOrigin: "center" }}>
<VSCodeProgressRing />
export const ProgressIndicator = () => {
const { useJumpingRooAnimation } = useExtensionState()
if (useJumpingRooAnimation) {
return <JumpingRoo />
}
return (
<div
style={{
width: "16px",
height: "16px",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}>
<div style={{ transform: "scale(0.55)", transformOrigin: "center" }}>
<VSCodeProgressRing />
</div>
</div>
</div>
)
)
}

View file

@ -782,6 +782,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
{activeTab === "ui" && (
<UISettings
reasoningBlockCollapsed={reasoningBlockCollapsed ?? true}
useJumpingRooAnimation={extensionState.useJumpingRooAnimation}
setCachedStateField={setCachedStateField}
/>
)}

View file

@ -11,10 +11,16 @@ import { ExtensionStateContextType } from "@/context/ExtensionStateContext"
interface UISettingsProps extends HTMLAttributes<HTMLDivElement> {
reasoningBlockCollapsed: boolean
useJumpingRooAnimation?: boolean
setCachedStateField: SetCachedStateField<keyof ExtensionStateContextType>
}
export const UISettings = ({ reasoningBlockCollapsed, setCachedStateField, ...props }: UISettingsProps) => {
export const UISettings = ({
reasoningBlockCollapsed,
useJumpingRooAnimation,
setCachedStateField,
...props
}: UISettingsProps) => {
const { t } = useAppTranslation()
const handleReasoningBlockCollapsedChange = (value: boolean) => {
@ -26,6 +32,15 @@ export const UISettings = ({ reasoningBlockCollapsed, setCachedStateField, ...pr
})
}
const handleJumpingRooAnimationChange = (value: boolean) => {
setCachedStateField("useJumpingRooAnimation", value)
// Track telemetry event
telemetryClient.capture("ui_settings_jumping_roo_changed", {
enabled: value,
})
}
return (
<div {...props}>
<SectionHeader>
@ -49,6 +64,19 @@ export const UISettings = ({ reasoningBlockCollapsed, setCachedStateField, ...pr
{t("settings:ui.collapseThinking.description")}
</div>
</div>
{/* Jumping Roo Animation Setting */}
<div className="flex flex-col gap-1">
<VSCodeCheckbox
checked={useJumpingRooAnimation ?? false}
onChange={(e: any) => handleJumpingRooAnimationChange(e.target.checked)}
data-testid="jumping-roo-animation-checkbox">
<span className="font-medium">{t("settings:ui.jumpingRooAnimation.label")}</span>
</VSCodeCheckbox>
<div className="text-vscode-descriptionForeground text-sm ml-5 mt-1">
{t("settings:ui.jumpingRooAnimation.description")}
</div>
</div>
</div>
</Section>
</div>

View file

@ -145,6 +145,8 @@ export interface ExtensionStateContextType extends ExtensionState {
setTerminalCompressProgressBar: (value: boolean) => void
setHistoryPreviewCollapsed: (value: boolean) => void
setReasoningBlockCollapsed: (value: boolean) => void
useJumpingRooAnimation?: boolean
setUseJumpingRooAnimation: (value: boolean) => void
autoCondenseContext: boolean
setAutoCondenseContext: (value: boolean) => void
autoCondenseContextPercent: number
@ -285,6 +287,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
global: {},
})
const [includeTaskHistoryInEnhance, setIncludeTaskHistoryInEnhance] = useState(true)
const [useJumpingRooAnimation, setUseJumpingRooAnimation] = useState(false)
const setListApiConfigMeta = useCallback(
(value: ProviderSettingsEntry[]) => setState((prevState) => ({ ...prevState, listApiConfigMeta: value })),
@ -322,6 +325,10 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
if ((newState as any).includeTaskHistoryInEnhance !== undefined) {
setIncludeTaskHistoryInEnhance((newState as any).includeTaskHistoryInEnhance)
}
// Update useJumpingRooAnimation if present in state message
if ((newState as any).useJumpingRooAnimation !== undefined) {
setUseJumpingRooAnimation((newState as any).useJumpingRooAnimation)
}
// Handle marketplace data if present in state message
if (newState.marketplaceItems !== undefined) {
setMarketplaceItems(newState.marketplaceItems)
@ -537,6 +544,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
setState((prevState) => ({ ...prevState, historyPreviewCollapsed: value })),
setReasoningBlockCollapsed: (value) =>
setState((prevState) => ({ ...prevState, reasoningBlockCollapsed: value })),
useJumpingRooAnimation,
setUseJumpingRooAnimation,
setHasOpenedModeSelector: (value) => setState((prevState) => ({ ...prevState, hasOpenedModeSelector: value })),
setAutoCondenseContext: (value) => setState((prevState) => ({ ...prevState, autoCondenseContext: value })),
setAutoCondenseContextPercent: (value) =>

View file

@ -42,6 +42,10 @@
"collapseThinking": {
"label": "Collapse Thinking messages by default",
"description": "When enabled, thinking blocks will be collapsed by default until you interact with them"
},
"jumpingRooAnimation": {
"label": "Use Jumping Roo Animation",
"description": "Replace the standard spinner with a jumping kangaroo animation for progress indicators"
}
},
"prompts": {