Claude Code Plugins

Community-maintained marketplace

Feedback

tool-animation

@swarm-agent/swarm
0
0

Add custom animations to tools in swarm-cli TUI. Use when creating spinner animations for tools, adding visual feedback during tool execution, or customizing how tools appear in the terminal interface.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name tool-animation
description Add custom animations to tools in swarm-cli TUI. Use when creating spinner animations for tools, adding visual feedback during tool execution, or customizing how tools appear in the terminal interface.
allowed-tools Read, Edit, Glob, Grep

Tool Animation Guide

Create custom animations for tools in the swarm-cli terminal UI. This skill covers the complete pattern for adding animated visual feedback during tool execution.

When to Use This Skill

  • Adding a new tool animation to an existing or new tool
  • Customizing how a tool displays during pending/running/completed states
  • Creating breathing, morphing, or wave animations for tools
  • Understanding the tool animation architecture

Architecture Overview

Tool animations in swarm-cli consist of three parts:

  1. Animation Components (tool-animations.tsx) - SolidJS components with animated spinners, icons, and state machines
  2. Tool Registration (session/index.tsx) - Maps tools to their animation components via ToolRegistry
  3. Spinner Definitions (tool-spinner-map.ts) - Optional: defines spinner presets for tools

File Locations

File Purpose
packages/swarm/src/cli/cmd/tui/ui/tool-animations.tsx Animation component definitions
packages/swarm/src/cli/cmd/tui/routes/session/index.tsx Tool registration and rendering
packages/swarm/src/cli/cmd/tui/ui/tool-spinner-map.ts Tool-to-spinner mapping
packages/swarm/src/cli/cmd/tui/ui/spinner-definitions.ts Reusable spinner frame definitions

Step 1: Create the Animation Component

Add your animation to tool-animations.tsx. Follow this pattern:

Animation State Machine Pattern

Each tool animation typically has four states matching tool execution:

// ============================================================================
// MY-TOOL ANIMATION - Description of the visual theme
// Pattern: brief description of the animation
// ============================================================================

// Color palette - use Catppuccin colors for consistency
const MYTOOL_COLOR = RGBA.fromHex("#cba6f7")  // Pick from Catppuccin palette

const MYTOOL_COLORS = {
  primary: MYTOOL_COLOR,
  success: RGBA.fromHex("#a6e3a1"),  // Green for success
  error: RGBA.fromHex("#f38ba8"),    // Pink for error
  dim: RGBA.fromHex("#6c7086"),      // Dimmed for resolved state
}

// Icon sequence for morphing (use Nerd Font icons)
const MYTOOL_ICONS = ["󰈙", "󰈞", "󰈝"]  // Example file icons

Morphing Icon Component

/**
 * MyToolMorph - Single icon morphing through states with breathing opacity
 */
export function MyToolMorph(props: { color?: RGBA }) {
  const baseColor = props.color ?? MYTOOL_COLOR
  const [frame, setFrame] = createSignal(0)
  const [opacity, setOpacity] = createSignal(0.7)

  // Morph through icons - 400ms per icon for smooth transitions
  const morphInterval = setInterval(() => {
    setFrame((f) => (f + 1) % MYTOOL_ICONS.length)
  }, 400)

  // Breathing opacity - 2 second cycle for zen feel
  const breathInterval = setInterval(() => {
    const now = Date.now()
    const phase = ((now / 2000) * Math.PI * 2) % (Math.PI * 2)
    setOpacity(0.5 + (Math.sin(phase) + 1) * 0.25)
  }, 50)

  onCleanup(() => {
    clearInterval(morphInterval)
    clearInterval(breathInterval)
  })

  const color = () => RGBA.fromInts(
    baseColor.r * 255,
    baseColor.g * 255,
    baseColor.b * 255,
    opacity() * 255
  )

  return <span style={{ fg: color() }}>{MYTOOL_ICONS[frame()]}</span>
}

Breathing Text Component

/**
 * MyToolText - Breathing "MyTool" text synchronized with icon
 */
export function MyToolText(props: { color?: RGBA }) {
  const baseColor = props.color ?? MYTOOL_COLOR
  const [opacity, setOpacity] = createSignal(0.7)

  const interval = setInterval(() => {
    const now = Date.now()
    const phase = ((now / 2000) * Math.PI * 2) % (Math.PI * 2)
    setOpacity(0.5 + (Math.sin(phase) + 1) * 0.25)
  }, 50)

  onCleanup(() => clearInterval(interval))

  const color = () => RGBA.fromInts(
    baseColor.r * 255,
    baseColor.g * 255,
    baseColor.b * 255,
    opacity() * 255
  )

  return <span style={{ fg: color() }}>mytool</span>
}

State Components

/**
 * MyToolPending - Initial state, just spinner + breathing text
 */
export function MyToolPending() {
  return (
    <box gap={0}>
      <text>
        <MyToolMorph color={MYTOOL_COLOR} /> <MyToolText color={MYTOOL_COLOR} />
      </text>
    </box>
  )
}

/**
 * MyToolRunning - Active state with input display + elapsed time
 */
export function MyToolRunning(props: { someInput?: string; startTime?: number }) {
  const { theme } = useTheme()

  return (
    <box gap={0}>
      <text>
        <MyToolMorph color={MYTOOL_COLOR} /> <MyToolText color={MYTOOL_COLOR} />
        <Show when={props.someInput}>
          {" "}<span style={{ fg: theme.text }}>{props.someInput}</span>
        </Show>
        <Show when={props.startTime}>
          {" "}<ElapsedTime startTime={props.startTime!} color={theme.textMuted} />
        </Show>
      </text>
    </box>
  )
}

/**
 * MyToolResolved - Completed state with success/error + metadata
 */
export function MyToolResolved(props: {
  success: boolean
  someInput?: string
  executionTime?: number
  resultCount?: number
}) {
  const { theme } = useTheme()
  const icon = props.success ? "✓" : "✗"
  const iconColor = props.success ? MYTOOL_COLORS.success : MYTOOL_COLORS.error
  const dimmedColor = RGBA.fromInts(
    MYTOOL_COLOR.r * 255,
    MYTOOL_COLOR.g * 255,
    MYTOOL_COLOR.b * 255,
    153  // ~60% opacity
  )

  const formatTime = (ms: number) => {
    if (ms < 1000) return `${ms}ms`
    if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`
    return `${Math.floor(ms / 60000)}m ${Math.floor((ms % 60000) / 1000)}s`
  }

  return (
    <box gap={0}>
      <text>
        <span style={{ fg: iconColor, bold: true }}>{icon}</span>
        {" "}<span style={{ fg: dimmedColor }}>mytool</span>
        <Show when={props.someInput}>
          {" "}<span style={{ fg: dimmedColor }}>{props.someInput}</span>
        </Show>
        <Show when={props.resultCount !== undefined}>
          {" "}<span style={{ fg: theme.textMuted }}>•</span>
          {" "}<span style={{ fg: theme.textMuted }}>{props.resultCount} items</span>
        </Show>
        <Show when={props.executionTime !== undefined}>
          {" "}<span style={{ fg: theme.textMuted }}>{formatTime(props.executionTime!)}</span>
        </Show>
      </text>
    </box>
  )
}

Complete Tool Animation (State Machine)

/**
 * MyToolToolAnimation - Complete state machine for mytool
 */
export function MyToolToolAnimation(props: {
  status: "pending" | "running" | "completed" | "error"
  someInput?: string
  startTime?: number
  executionTime?: number
  resultCount?: number
}) {
  return (
    <Switch>
      <Match when={props.status === "pending"}>
        <MyToolPending />
      </Match>
      <Match when={props.status === "running"}>
        <MyToolRunning someInput={props.someInput} startTime={props.startTime} />
      </Match>
      <Match when={props.status === "completed"}>
        <MyToolResolved
          success={true}
          someInput={props.someInput}
          executionTime={props.executionTime}
          resultCount={props.resultCount}
        />
      </Match>
      <Match when={props.status === "error"}>
        <MyToolResolved
          success={false}
          someInput={props.someInput}
          executionTime={props.executionTime}
        />
      </Match>
    </Switch>
  )
}

Step 2: Export the Animation

Add your animation to the exports in tool-animations.tsx:

export {
  // ... existing exports
  MyToolToolAnimation,
}

Step 3: Register the Tool Animation

In session/index.tsx, register your tool with ToolRegistry:

Import the Animation

At the top of the file, add the import:

import {
  // ... existing imports
  MyToolToolAnimation,
} from "@tui/ui/tool-animations"

Register the Tool

Add the registration after other tool registrations (around line 2900+):

ToolRegistry.register({
  name: "mytool",  // Must match the tool name exactly
  container: "inline",  // "inline" for single-line, "block" for multi-line
  render(props) {
    const executionTime = createMemo(() => {
      if (props.state.status === "completed" && props.state.time) {
        return props.state.time.end - props.state.time.start
      }
      return undefined
    })

    const startTime = createMemo(() => {
      if (props.state.status !== "pending" && props.state.time?.start) {
        return props.state.time.start
      }
      return undefined
    })

    return (
      <ToolCard status={props.state.status} inline={true}>
        <MyToolToolAnimation
          status={props.state.status}
          someInput={props.input.someField as string | undefined}
          startTime={startTime()}
          executionTime={executionTime()}
          resultCount={props.metadata.count as number | undefined}
        />
      </ToolCard>
    )
  },
})

Step 4: (Optional) Add Spinner Mapping

If you want a simple spinner without full custom animation, add to tool-spinner-map.ts:

export const TOOL_SPINNER_MAP: Record<string, ToolSpinnerConfig> = {
  // ... existing entries
  mytool: {
    spinner: "braille_fade",  // Use existing spinner name
    fallback: "snake_orbit",
  },
}

Examples from Existing Tools

Inline Tool (Single Line) - Memory Tool

// Registration
ToolRegistry.register<typeof MemoryTool>({
  name: "memory",
  container: "inline",  // Single-line display
  render(props) {
    const executionTime = createMemo(() => {
      if (props.state.status === "completed" && props.state.time) {
        return props.state.time.end - props.state.time.start
      }
      return undefined
    })

    return (
      <ToolCard status={props.state.status} inline={true}>
        <MemoryToolAnimation
          status={props.state.status}
          section={props.input.section}
          content={props.input.content}
          startTime={props.state.status !== "pending" && props.state.time?.start 
            ? props.state.time.start : undefined}
          executionTime={executionTime()}
        />
      </ToolCard>
    )
  },
})

Block Tool (Multi-Line) - TodoWrite Tool

// Registration
ToolRegistry.register<typeof TodoWriteTool>({
  name: "todowrite",
  container: "block",  // Multi-line display with content
  render(props) {
    const { theme } = useTheme()
    const isRunning = createMemo(() => props.state.status === "running")
    const isCompleted = createMemo(() => props.state.status === "completed")

    return (
      <ToolCard status={props.state.status} inline={false}>
        <box gap={1}>
          <ToolTitle
            icon="✓"
            fallback="Updating todos..."
            when={props.input.todos?.length}
            status={isRunning() ? "running" : undefined}
            color={TOOL_COLORS.todo}
            spinner="todowrite_spinner"
          >
            <span style={{ fg: theme.accent, bold: true }}>Todo List</span>
            <Show when={isRunning()}>{" "}<StreamingDots /></Show>
            <Show when={isCompleted()}>{" "}<SuccessCheckmark /></Show>
          </ToolTitle>
          {/* Additional content for block display */}
          <Show when={props.input.todos?.length}>
            <box>
              <For each={props.input.todos ?? []}>
                {(todo) => (
                  <text style={{ fg: theme.textMuted }}>
                    [{todo.status === "completed" ? "✓" : " "}] {todo.content}
                  </text>
                )}
              </For>
            </box>
          </Show>
        </box>
      </ToolCard>
    )
  },
})

Animation Patterns Reference

Breathing Animation

Smooth opacity pulsing (0.5 -> 1.0 -> 0.5):

const breathInterval = setInterval(() => {
  const now = Date.now()
  const phase = ((now / 2000) * Math.PI * 2) % (Math.PI * 2)
  setOpacity(0.5 + (Math.sin(phase) + 1) * 0.25)  // Range: 0.5 - 1.0
}, 50)

Wave Animation (Multiple Icons)

Staggered breathing for 3 icons:

const interval = setInterval(() => {
  const now = Date.now()
  const newOpacities = [0, 1, 2].map((i) => {
    const phase = ((now / 1800) * Math.PI * 2 + i * 0.7) % (Math.PI * 2)
    return 0.4 + (1 - 0.4) * ((Math.sin(phase) + 1) / 2)
  })
  setOpacities(newOpacities)
}, 50)

Icon Morphing

Cycle through icon sequence:

const morphInterval = setInterval(() => {
  setFrame((f) => (f + 1) % icons.length)
}, 400)  // 400ms per icon = 2.4s full cycle for 6 icons

Catppuccin Color Palette

Use these colors for consistency:

Color Hex Use Case
Rosewater #f5e0dc Subtle highlights
Flamingo #f2cdcd Soft accents
Pink #f5c2e7 Feminine tools
Mauve #cba6f7 Purple/search tools
Red #f38ba8 Errors, grep
Maroon #eba0ac Warnings
Peach #fab387 Write operations
Yellow #f9e2af Warnings, todos
Green #a6e3a1 Success, write
Teal #94e2d5 Read operations
Sky #89dceb Batch operations
Sapphire #74c7ec Web fetch
Blue #89b4fa Info, navigation
Lavender #b4befe Ask/question

Common Nerd Font Icons

Icon Code Use Case
󰈙 \u{f0219} File document
󰈞 \u{f021e} Search/find
󰍉 \u{f0349} Magnify glass
󰋗 \u{f02d7} Help circle
󰍡 \u{f0361} Message
󱜺 \u{f173a} Message question
\u{ea68} Git/source control
󰄳 \u{f0133} Checkbox checked
󰄰 \u{f0130} Checkbox empty
\u{2713} Checkmark
\u{2717} X mark

Checklist

When adding a new tool animation:

  • Create animation component in tool-animations.tsx
  • Export the main animation component
  • Import animation in session/index.tsx
  • Register tool with ToolRegistry.register()
  • Test all four states: pending, running, completed, error
  • Verify colors match Catppuccin palette
  • Check timing feels smooth (not too fast, not too slow)
  • Ensure metadata (time, counts) displays correctly

Tips

  • Use createMemo() for computed values that depend on props
  • Always call onCleanup() to clear intervals
  • Match animation timing to existing tools (2s breathing cycle, 400ms morph)
  • Use <Show when={...}> for conditional rendering
  • Prefer inline container for simple tools, block for tools with content