Skip to content

Commit a6f06dd

Browse files
committed
feat: add kangaroo animation option for progress indicators
Add optional jumping kangaroo animation as an alternative to the default spinner in progress indicators. Users can toggle this in Settings > UI. Implementation: - Created KangarooLoader component with inline SVG (0.7s bounce animation) - Updated ProgressIndicator to conditionally render based on user preference - Added "Use Jumping Kangaroo Animation" toggle in UI Settings - Implemented full state persistence across extension and webview Changes: - New component: webview-ui/src/components/chat/KangarooLoader.tsx - Updated: webview-ui/src/components/chat/ProgressIndicator.tsx - Updated: webview-ui/src/components/settings/UISettings.tsx - Updated: webview-ui/src/components/settings/SettingsView.tsx - Updated: webview-ui/src/context/ExtensionStateContext.tsx - Updated: src/shared/WebviewMessage.ts - Updated: src/shared/ExtensionMessage.ts - Updated: src/core/webview/webviewMessageHandler.ts - Updated: packages/types/src/global-settings.ts The kangaroo icon is an inline SVG (no external dependencies), ensuring fast rendering and perfect scaling. Default remains the standard spinner to maintain existing UX.
1 parent f3a505f commit a6f06dd

File tree

10 files changed

+155
-14
lines changed

10 files changed

+155
-14
lines changed

packages/types/src/global-settings.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ export const globalSettingsSchema = z.object({
148148
includeTaskHistoryInEnhance: z.boolean().optional(),
149149
historyPreviewCollapsed: z.boolean().optional(),
150150
reasoningBlockCollapsed: z.boolean().optional(),
151+
useKangarooAnimation: z.boolean().optional(),
151152
profileThresholds: z.record(z.string(), z.number()).optional(),
152153
hasOpenedModeSelector: z.boolean().optional(),
153154
lastModeExportPath: z.string().optional(),

src/core/webview/ClineProvider.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1793,6 +1793,7 @@ export class ClineProvider
17931793
terminalCompressProgressBar,
17941794
historyPreviewCollapsed,
17951795
reasoningBlockCollapsed,
1796+
useKangarooAnimation,
17961797
cloudUserInfo,
17971798
cloudIsAuthenticated,
17981799
sharingEnabled,
@@ -1927,6 +1928,7 @@ export class ClineProvider
19271928
hasSystemPromptOverride,
19281929
historyPreviewCollapsed: historyPreviewCollapsed ?? false,
19291930
reasoningBlockCollapsed: reasoningBlockCollapsed ?? true,
1931+
useKangarooAnimation: useKangarooAnimation ?? false,
19301932
cloudUserInfo,
19311933
cloudIsAuthenticated: cloudIsAuthenticated ?? false,
19321934
cloudOrganizations,
@@ -2142,6 +2144,7 @@ export class ClineProvider
21422144
maxConcurrentFileReads: stateValues.maxConcurrentFileReads ?? 5,
21432145
historyPreviewCollapsed: stateValues.historyPreviewCollapsed ?? false,
21442146
reasoningBlockCollapsed: stateValues.reasoningBlockCollapsed ?? true,
2147+
useKangarooAnimation: stateValues.useKangarooAnimation ?? false,
21452148
cloudUserInfo,
21462149
cloudIsAuthenticated,
21472150
sharingEnabled,

src/core/webview/webviewMessageHandler.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1621,6 +1621,10 @@ export const webviewMessageHandler = async (
16211621
await updateGlobalState("reasoningBlockCollapsed", message.bool ?? true)
16221622
// No need to call postStateToWebview here as the UI already updated optimistically
16231623
break
1624+
case "useKangarooAnimation":
1625+
await updateGlobalState("useKangarooAnimation", message.bool ?? false)
1626+
// No need to call postStateToWebview here as the UI already updated optimistically
1627+
break
16241628
case "toggleApiConfigPin":
16251629
if (message.text) {
16261630
const currentPinned = getGlobalState("pinnedApiConfigs") ?? {}

src/shared/ExtensionMessage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,7 @@ export type ExtensionState = Pick<
288288
| "openRouterImageGenerationSelectedModel"
289289
| "includeTaskHistoryInEnhance"
290290
| "reasoningBlockCollapsed"
291+
| "useKangarooAnimation"
291292
> & {
292293
version: string
293294
clineMessages: ClineMessage[]

src/shared/WebviewMessage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ export interface WebviewMessage {
195195
| "profileThresholds"
196196
| "setHistoryPreviewCollapsed"
197197
| "setReasoningBlockCollapsed"
198+
| "useKangarooAnimation"
198199
| "openExternal"
199200
| "filterMarketplaceItems"
200201
| "marketplaceButtonClicked"
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import React from "react"
2+
3+
interface KangarooLoaderProps {
4+
className?: string
5+
style?: React.CSSProperties
6+
}
7+
8+
/**
9+
* KangarooLoader - Animated kangaroo icon for progress indication
10+
*
11+
* The kangaroo icon is an inline SVG (embedded directly in the component).
12+
* It uses a vector path definition to create the kangaroo silhouette.
13+
*
14+
* Size is fixed to match VSCodeProgressRing (16px) for consistent UI.
15+
* Animation duration is 0.7s for a smooth, quick bounce effect.
16+
*/
17+
export const KangarooLoader: React.FC<KangarooLoaderProps> = ({ className = "", style }) => {
18+
return (
19+
<div
20+
className={`inline-block ${className}`}
21+
style={{
22+
width: "16px",
23+
height: "16px",
24+
position: "relative",
25+
...style,
26+
}}>
27+
<div
28+
style={{
29+
width: "100%",
30+
height: "100%",
31+
position: "absolute",
32+
animation: "roo-jump 0.7s ease-in-out infinite",
33+
}}>
34+
<svg
35+
width="106"
36+
height="69"
37+
viewBox="0 0 106 69"
38+
fill="none"
39+
xmlns="http://www.w3.org/2000/svg"
40+
style={{
41+
width: "100%",
42+
height: "100%",
43+
color: "currentColor",
44+
filter: "drop-shadow(0 2px 4px rgba(0, 0, 0, 0.1))",
45+
}}>
46+
<path
47+
d="M88.4719 2.87946L86.5306 9.89906C86.4278 10.2707 86.0399 10.4854 85.6704 10.3752L53.106 0.659112C52.8894 0.594492 52.6549 0.640312 52.4786 0.781722L20.2279 26.6445C20.1338 26.72 20.0214 26.7694 19.9021 26.7878L0.713159 29.745C0.357849 29.7998 0.10453 30.1187 0.13164 30.4772L0.21502 31.5798C0.24165 31.932 0.53052 32.2069 0.88359 32.216L23.1724 32.7922L23.4266 32.7993L39.8958 24.0019C40.1264 23.8788 40.4068 23.8968 40.6197 24.0486L52.2875 32.3659C52.4705 32.4963 52.5783 32.7078 52.5762 32.9325L52.4778 43.9686C52.4765 44.1114 52.5197 44.2511 52.6014 44.3683L69.0144 67.9188C69.1431 68.1034 69.354 68.2135 69.5791 68.2135H74.7748C75.2932 68.2135 75.6255 67.6623 75.3836 67.2039L63.6853 45.0427C63.5709 44.8259 63.5804 44.5647 63.7101 44.3568L69.8094 34.5861C69.8758 34.4797 69.97 34.3935 70.0819 34.3367L91.8879 23.2712C92.1095 23.1588 92.3744 23.1745 92.5812 23.3123L98.8125 27.4657C98.9256 27.5411 99.0584 27.5813 99.1943 27.5813H104.856C105.404 27.5813 105.732 26.9716 105.43 26.514L89.7099 2.6839C89.3844 2.19053 88.6294 2.30979 88.4719 2.87946Z"
48+
fill="currentColor"
49+
/>
50+
</svg>
51+
</div>
52+
<style>{`
53+
@keyframes roo-jump {
54+
0%, 100% {
55+
transform: translateY(0) rotate(0deg);
56+
}
57+
25% {
58+
transform: translateY(-30%) rotate(-3deg);
59+
}
60+
50% {
61+
transform: translateY(-40%) rotate(0deg);
62+
}
63+
75% {
64+
transform: translateY(-30%) rotate(3deg);
65+
}
66+
}
67+
`}</style>
68+
</div>
69+
)
70+
}
Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,37 @@
11
import { VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react"
2+
import { useExtensionState } from "@src/context/ExtensionStateContext"
3+
import { KangarooLoader } from "./KangarooLoader"
24

3-
export const ProgressIndicator = () => (
4-
<div
5-
style={{
6-
width: "16px",
7-
height: "16px",
8-
display: "flex",
9-
alignItems: "center",
10-
justifyContent: "center",
11-
}}>
12-
<div style={{ transform: "scale(0.55)", transformOrigin: "center" }}>
13-
<VSCodeProgressRing />
5+
export const ProgressIndicator = () => {
6+
const { useKangarooAnimation } = useExtensionState()
7+
8+
if (useKangarooAnimation) {
9+
return (
10+
<div
11+
style={{
12+
width: "16px",
13+
height: "16px",
14+
display: "flex",
15+
alignItems: "center",
16+
justifyContent: "center",
17+
}}>
18+
<KangarooLoader />
19+
</div>
20+
)
21+
}
22+
23+
return (
24+
<div
25+
style={{
26+
width: "16px",
27+
height: "16px",
28+
display: "flex",
29+
alignItems: "center",
30+
justifyContent: "center",
31+
}}>
32+
<div style={{ transform: "scale(0.55)", transformOrigin: "center" }}>
33+
<VSCodeProgressRing />
34+
</div>
1435
</div>
15-
</div>
16-
)
36+
)
37+
}

webview-ui/src/components/settings/SettingsView.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
195195
openRouterImageApiKey,
196196
openRouterImageGenerationSelectedModel,
197197
reasoningBlockCollapsed,
198+
useKangarooAnimation,
198199
} = cachedState
199200

200201
const apiConfiguration = useMemo(() => cachedState.apiConfiguration ?? {}, [cachedState.apiConfiguration])
@@ -384,6 +385,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
384385
vscode.postMessage({ type: "updateSupportPrompt", values: customSupportPrompts || {} })
385386
vscode.postMessage({ type: "includeTaskHistoryInEnhance", bool: includeTaskHistoryInEnhance ?? true })
386387
vscode.postMessage({ type: "setReasoningBlockCollapsed", bool: reasoningBlockCollapsed ?? true })
388+
vscode.postMessage({ type: "useKangarooAnimation", bool: useKangarooAnimation ?? false })
387389
vscode.postMessage({ type: "upsertApiConfiguration", text: currentApiConfigName, apiConfiguration })
388390
vscode.postMessage({ type: "telemetrySetting", text: telemetrySetting })
389391
vscode.postMessage({ type: "profileThresholds", values: profileThresholds })
@@ -782,6 +784,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
782784
{activeTab === "ui" && (
783785
<UISettings
784786
reasoningBlockCollapsed={reasoningBlockCollapsed ?? true}
787+
useKangarooAnimation={useKangarooAnimation ?? false}
785788
setCachedStateField={setCachedStateField}
786789
/>
787790
)}

webview-ui/src/components/settings/UISettings.tsx

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,16 @@ import { ExtensionStateContextType } from "@/context/ExtensionStateContext"
1111

1212
interface UISettingsProps extends HTMLAttributes<HTMLDivElement> {
1313
reasoningBlockCollapsed: boolean
14+
useKangarooAnimation?: boolean
1415
setCachedStateField: SetCachedStateField<keyof ExtensionStateContextType>
1516
}
1617

17-
export const UISettings = ({ reasoningBlockCollapsed, setCachedStateField, ...props }: UISettingsProps) => {
18+
export const UISettings = ({
19+
reasoningBlockCollapsed,
20+
useKangarooAnimation = false,
21+
setCachedStateField,
22+
...props
23+
}: UISettingsProps) => {
1824
const { t } = useAppTranslation()
1925

2026
const handleReasoningBlockCollapsedChange = (value: boolean) => {
@@ -26,6 +32,15 @@ export const UISettings = ({ reasoningBlockCollapsed, setCachedStateField, ...pr
2632
})
2733
}
2834

35+
const handleKangarooAnimationChange = (value: boolean) => {
36+
setCachedStateField("useKangarooAnimation", value)
37+
38+
// Track telemetry event
39+
telemetryClient.capture("ui_settings_kangaroo_animation_changed", {
40+
enabled: value,
41+
})
42+
}
43+
2944
return (
3045
<div {...props}>
3146
<SectionHeader>
@@ -49,6 +64,19 @@ export const UISettings = ({ reasoningBlockCollapsed, setCachedStateField, ...pr
4964
{t("settings:ui.collapseThinking.description")}
5065
</div>
5166
</div>
67+
68+
{/* Kangaroo Animation Setting */}
69+
<div className="flex flex-col gap-1">
70+
<VSCodeCheckbox
71+
checked={useKangarooAnimation}
72+
onChange={(e: any) => handleKangarooAnimationChange(e.target.checked)}
73+
data-testid="kangaroo-animation-checkbox">
74+
<span className="font-medium">Use Jumping Kangaroo Animation</span>
75+
</VSCodeCheckbox>
76+
<div className="text-vscode-descriptionForeground text-sm ml-5 mt-1">
77+
Replace the default spinner with a jumping kangaroo animation for progress indicators
78+
</div>
79+
</div>
5280
</div>
5381
</Section>
5482
</div>

webview-ui/src/context/ExtensionStateContext.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,8 @@ export interface ExtensionStateContextType extends ExtensionState {
158158
setMaxDiagnosticMessages: (value: number) => void
159159
includeTaskHistoryInEnhance?: boolean
160160
setIncludeTaskHistoryInEnhance: (value: boolean) => void
161+
useKangarooAnimation: boolean
162+
setUseKangarooAnimation: (value: boolean) => void
161163
}
162164

163165
export const ExtensionStateContext = createContext<ExtensionStateContextType | undefined>(undefined)
@@ -285,6 +287,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
285287
global: {},
286288
})
287289
const [includeTaskHistoryInEnhance, setIncludeTaskHistoryInEnhance] = useState(true)
290+
const [useKangarooAnimation, setUseKangarooAnimation] = useState(false) // Default to false (use spinner)
288291

289292
const setListApiConfigMeta = useCallback(
290293
(value: ProviderSettingsEntry[]) => setState((prevState) => ({ ...prevState, listApiConfigMeta: value })),
@@ -322,6 +325,10 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
322325
if ((newState as any).includeTaskHistoryInEnhance !== undefined) {
323326
setIncludeTaskHistoryInEnhance((newState as any).includeTaskHistoryInEnhance)
324327
}
328+
// Update useKangarooAnimation if present in state message
329+
if ((newState as any).useKangarooAnimation !== undefined) {
330+
setUseKangarooAnimation((newState as any).useKangarooAnimation)
331+
}
325332
// Handle marketplace data if present in state message
326333
if (newState.marketplaceItems !== undefined) {
327334
setMarketplaceItems(newState.marketplaceItems)
@@ -559,6 +566,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
559566
},
560567
includeTaskHistoryInEnhance,
561568
setIncludeTaskHistoryInEnhance,
569+
useKangarooAnimation,
570+
setUseKangarooAnimation,
562571
}
563572

564573
return <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider>

0 commit comments

Comments
 (0)