diff --git a/docs/STYLEGUIDE.md b/docs/STYLEGUIDE.md
index e21aaa89228..794e5c4c640 100644
--- a/docs/STYLEGUIDE.md
+++ b/docs/STYLEGUIDE.md
@@ -264,7 +264,7 @@ Use this rubric when reviewing any Orca IDE screen, screenshot, or prototype. A
- **Layout density:** avoid jamming controls together. Preserve breathing room around the primary workflow, reduce competing buttons, and keep toolbar groups visually distinct. Dense screens are acceptable only when grouping, alignment, and hierarchy make scanning faster.
- **Cards and containers:** cards must be visually distinct from their parent surface through the existing `card`/`border` treatment. Avoid nesting cards inside cards. If a section is not a repeated item, modal, or framed tool, consider an unframed layout or full-width band instead.
- **Side-by-side layouts:** default to row-by-row layouts for complex workflows because they are easier to align and scan. Use side-by-side layouts only when space is constrained or comparison is the point, then polish column widths, baselines, and wrapping states carefully.
-- **Animation:** use subtle animation to soften expanding/collapsing content and prevent jumpy layout changes. Animation should clarify continuity, not decorate. Respect reduced-motion settings.
+- **Animation:** use subtle animation to soften expanding/collapsing content and prevent jumpy layout changes. Animation should clarify continuity, not decorate. Respect reduced motion element by element: turn movement off with `motion-reduce:` (slides, rotations, size changes); give spinners, pulses and progress indicators a still state that still says "working" (`LoadingSpinner` does this for spinners) instead of freezing them; short opacity fades can stay.
- **SSH and latency:** assume actions may run remotely. Disable submit controls immediately, delay visible loading feedback when appropriate, and keep focus stable while remote data arrives.
### 1. Match in-flight feedback to perceived duration
diff --git a/src/renderer/src/components/LoadingSpinner.tsx b/src/renderer/src/components/LoadingSpinner.tsx
new file mode 100644
index 00000000000..7fa9d17d553
--- /dev/null
+++ b/src/renderer/src/components/LoadingSpinner.tsx
@@ -0,0 +1,14 @@
+import React from 'react'
+import { Hourglass, Loader2 } from 'lucide-react'
+import { cn } from '@/lib/utils'
+
+// Why: a spinner frozen by reduced motion reads as stuck, so the still state is
+// a different icon that says "working" without moving.
+export function LoadingSpinner({ className }: { className?: string }): React.JSX.Element {
+ return (
+ <>
+
+
+ >
+ )
+}
diff --git a/src/renderer/src/components/native-chat/NativeChatAutocompleteMenus.tsx b/src/renderer/src/components/native-chat/NativeChatAutocompleteMenus.tsx
index 00825a7cf82..861c3737abd 100644
--- a/src/renderer/src/components/native-chat/NativeChatAutocompleteMenus.tsx
+++ b/src/renderer/src/components/native-chat/NativeChatAutocompleteMenus.tsx
@@ -1,5 +1,5 @@
import { memo, useEffect, useRef } from 'react'
-import { Loader2, Package, RotateCcw } from 'lucide-react'
+import { Package, RotateCcw } from 'lucide-react'
import { translate } from '@/i18n/i18n'
import { cn } from '@/lib/utils'
import type { SkillSourceKind } from '../../../../shared/skills'
@@ -7,6 +7,7 @@ import { FilenameFirstPath } from '@/components/file-path-cursor-tooltip'
import { getFileTypeIcon } from '@/lib/file-type-icons'
import type { ComposerAutocomplete, NativeChatPickerItem } from './native-chat-composer-state'
import type { NativeChatMentionFiles } from './use-native-chat-mention-files'
+import { LoadingSpinner } from '@/components/LoadingSpinner'
export const NativeChatPickerMenu = memo(function NativeChatPickerMenu({
autocomplete,
@@ -72,7 +73,7 @@ export const NativeChatPickerMenu = memo(function NativeChatPickerMenu({
{showSkillsHeading ? : null}
{autocomplete.skillStatus === 'loading' ? (
-
+
{translate('components.native-chat.composer.loadingSkills', 'Loading skills...')}
) : null}
@@ -353,7 +354,7 @@ export const NativeChatMentionMenu = memo(function NativeChatMentionMenu({
})}
{statusText ? (
- {mention.loading ? : null}
+ {mention.loading ? : null}
{statusText}
) : null}
diff --git a/src/renderer/src/components/native-chat/NativeChatAwaitingInputRow.tsx b/src/renderer/src/components/native-chat/NativeChatAwaitingInputRow.tsx
index ecf94cba8af..fff0718ee65 100644
--- a/src/renderer/src/components/native-chat/NativeChatAwaitingInputRow.tsx
+++ b/src/renderer/src/components/native-chat/NativeChatAwaitingInputRow.tsx
@@ -93,7 +93,7 @@ export function NativeChatAwaitingInputRow({
diff --git a/src/renderer/src/components/native-chat/NativeChatDiffCard.tsx b/src/renderer/src/components/native-chat/NativeChatDiffCard.tsx
index 6c7b16cbc67..238e0907c56 100644
--- a/src/renderer/src/components/native-chat/NativeChatDiffCard.tsx
+++ b/src/renderer/src/components/native-chat/NativeChatDiffCard.tsx
@@ -217,7 +217,7 @@ export function NativeChatDiffCard({
{hasBody ? (
diff --git a/src/renderer/src/components/native-chat/NativeChatImageAttachmentPreview.tsx b/src/renderer/src/components/native-chat/NativeChatImageAttachmentPreview.tsx
index a46a6c2eedd..80c4ea7a4df 100644
--- a/src/renderer/src/components/native-chat/NativeChatImageAttachmentPreview.tsx
+++ b/src/renderer/src/components/native-chat/NativeChatImageAttachmentPreview.tsx
@@ -1,5 +1,5 @@
import { useEffect, useMemo, useRef, useState } from 'react'
-import { FileText, Image as ImageIcon, ImageOff, Loader2, X } from 'lucide-react'
+import { FileText, Image as ImageIcon, ImageOff, X } from 'lucide-react'
import { Dialog, DialogContent, DialogDescription, DialogTitle } from '@/components/ui/dialog'
import { translate } from '@/i18n/i18n'
import { basename } from '@/lib/path'
@@ -12,6 +12,7 @@ import {
import type { NativeChatComposerImageAttachment } from './NativeChatComposerField'
import { isAgentSessionAttachmentStorePath } from '../../../../shared/agent-session-attachments'
import { chatImageAccess } from '@/lib/local-file-access'
+import { LoadingSpinner } from '@/components/LoadingSpinner'
type Props = {
attachment: NativeChatComposerImageAttachment
@@ -218,7 +219,7 @@ function NativeChatImageThumbnail({
{isPending ? (
-
+
) : null}
onRemove(attachment.id)} />
@@ -244,7 +245,7 @@ function NativeChatImageThumbnail({
{isPending ? (
<>
-
+
{pendingLabel}
>
) : (
diff --git a/src/renderer/src/components/native-chat/NativeChatLoadingCue.tsx b/src/renderer/src/components/native-chat/NativeChatLoadingCue.tsx
index fcefa2f4555..102cff72f74 100644
--- a/src/renderer/src/components/native-chat/NativeChatLoadingCue.tsx
+++ b/src/renderer/src/components/native-chat/NativeChatLoadingCue.tsx
@@ -1,5 +1,5 @@
-import { Loader2 } from 'lucide-react'
import { translate } from '@/i18n/i18n'
+import { LoadingSpinner } from '@/components/LoadingSpinner'
/** The structured chat's pane while its first read runs. No visible text, and a CSS animation
* delay keeps it invisible unless the read lasts, so a quick read paints nothing. */
@@ -11,7 +11,7 @@ export function NativeChatLoadingCue(): React.JSX.Element {
data-native-chat-loading-cue="true"
className="flex h-full w-full items-center justify-center animate-in fade-in delay-250 [--tw-animation-fill-mode:backwards]"
>
-
+