Claude
Skills
Sign in
Back

raycast-extension

Included with Lifetime
$97 forever

Build Raycast extensions with React and TypeScript. Use when the user asks to create a Raycast extension, command, or tool.

Web Dev

What this skill does


# Raycast Extension Development

## Quick Start

1. Create project structure
2. Write package.json with extension config
3. Implement command in src/
4. Run `npm install && npm run dev`

## Project Structure

```
my-extension/
├── package.json          # Extension manifest + dependencies
├── tsconfig.json         # TypeScript config
├── .eslintrc.json        # ESLint config
├── raycast-env.d.ts      # Type definitions (auto-generated)
├── assets/
│   └── extension-icon.png  # 512x512 PNG icon
└── src/
    └── command-name.tsx    # Command implementation
```

## package.json Template

```json
{
  "name": "extension-name",
  "title": "Extension Title",
  "description": "What this extension does",
  "icon": "extension-icon.png",
  "author": "author-name",
  "categories": ["Productivity", "Developer Tools"],
  "license": "MIT",
  "commands": [
    {
      "name": "command-name",
      "title": "Command Title",
      "description": "What this command does",
      "mode": "view",
      "keywords": ["keyword1", "keyword2"]
    }
  ],
  "dependencies": {
    "@raycast/api": "^1.83.1",
    "@raycast/utils": "^1.17.0"
  },
  "devDependencies": {
    "@raycast/eslint-config": "^1.0.11",
    "@types/node": "22.5.4",
    "@types/react": "18.3.3",
    "eslint": "^8.57.0",
    "prettier": "^3.3.3",
    "typescript": "^5.5.4"
  },
  "scripts": {
    "build": "ray build --skip-types -e dist -o dist",
    "dev": "ray develop",
    "fix-lint": "ray lint --fix",
    "lint": "ray lint"
  }
}
```

## Command Modes

| Mode | Use Case |
|------|----------|
| `view` | Show UI with Detail, List, Form, Grid |
| `no-view` | Background task, clipboard, notifications only |
| `menu-bar` | Menu bar icon with dropdown |

## Hotkey Configuration

Add to command in package.json:
```json
"hotkey": {
  "modifiers": ["opt"],
  "key": "m"
}
```

Modifiers: `cmd`, `opt`, `ctrl`, `shift`

**Note**: Hotkeys in package.json are suggestions. Users set them in Raycast Preferences → Extensions.

## tsconfig.json

```json
{
  "$schema": "https://json.schemastore.org/tsconfig",
  "compilerOptions": {
    "allowJs": true,
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "isolatedModules": true,
    "jsx": "react-jsx",
    "lib": ["ES2022"],
    "module": "ES2022",
    "moduleResolution": "bundler",
    "noEmit": true,
    "resolveJsonModule": true,
    "skipLibCheck": true,
    "strict": true,
    "target": "ES2022"
  },
  "include": ["src/**/*", "raycast-env.d.ts"]
}
```

## .eslintrc.json

```json
{
  "root": true,
  "extends": ["@raycast"]
}
```

## Command Patterns

### No-View Command (Background Task)

```tsx
import { showHUD, Clipboard, showToast, Toast } from "@raycast/api";

export default async function Command() {
  const toast = await showToast({
    style: Toast.Style.Animated,
    title: "Working...",
  });

  try {
    // Do work
    const result = await doSomething();

    await Clipboard.copy(result);
    await showHUD("✅ Done!");
  } catch (error) {
    toast.style = Toast.Style.Failure;
    toast.title = "Failed";
    toast.message = error instanceof Error ? error.message : "Unknown error";
  }
}
```

### View Command (List)

```tsx
import { List, ActionPanel, Action } from "@raycast/api";

export default function Command() {
  return (
    <List>
      <List.Item
        title="Item"
        actions={
          <ActionPanel>
            <Action.CopyToClipboard content="text" />
          </ActionPanel>
        }
      />
    </List>
  );
}
```

### View Command (Detail)

```tsx
import { Detail } from "@raycast/api";

export default function Command() {
  const markdown = `# Hello World`;
  return <Detail markdown={markdown} />;
}
```

## Performance & Caching

### Instant Load Pattern (No Empty Flash)

Use synchronous cache read + async refresh for instant perceived load:

```tsx
import { List, Cache } from "@raycast/api";
import { useCachedPromise, withCache } from "@raycast/utils";

const cache = new Cache();
const CACHE_KEY = "myData";

// Read cache synchronously at module load (before React renders)
function getInitialData(): MyData[] {
  const cached = cache.get(CACHE_KEY);
  if (cached) {
    try {
      return JSON.parse(cached);
    } catch {
      return [];
    }
  }
  return [];
}

// Expensive async operation wrapped with withCache (5 min TTL)
const fetchExpensiveData = withCache(
  async () => {
    // Your expensive operation here
    return await someSlowOperation();
  },
  { maxAge: 5 * 60 * 1000 }
);

async function fetchAllData(): Promise<MyData[]> {
  const data = await fetchExpensiveData();
  // Update cache for next launch
  cache.set(CACHE_KEY, JSON.stringify(data));
  return data;
}

export default function Command() {
  const { data, isLoading } = useCachedPromise(fetchAllData, [], {
    initialData: getInitialData(), // Sync read - instant render!
    keepPreviousData: true,
  });

  return (
    <List isLoading={isLoading && !data?.length}>
      {data?.map(item => <List.Item key={item.id} title={item.name} />)}
    </List>
  );
}
```

### Key Caching Utilities

| Utility | Purpose |
|---------|---------|
| `Cache` | Persistent disk cache, sync read/write |
| `withCache(fn, {maxAge})` | Wrap async functions with TTL cache |
| `useCachedPromise` | Stale-while-revalidate pattern |
| `LocalStorage` | Async key-value storage |

### Avoiding CLS (Content Layout Shift)

Load all data in ONE async function:

```tsx
// BAD - causes layout shift
const [customData, setCustomData] = useState([]);
useEffect(() => {
  loadCustomData().then(setCustomData); // Second render!
}, []);

// GOOD - single fetch, no shift
async function fetchAllData() {
  const [dataA, dataB] = await Promise.all([
    fetchDataA(),
    fetchDataB(),
  ]);
  return combineData(dataA, dataB);
}
```

### Non-Blocking Operations (Prevent UI Freeze)

**Root cause of "tiny delay"**: Sync operations (`execSync`, `statSync`, `readdirSync`) block the event loop during revalidation, freezing the UI even with cached data displayed.

```tsx
// BAD - blocks event loop, UI freezes during revalidation
import { execSync } from "child_process";
import { statSync, readdirSync, copyFileSync } from "fs";

function fetchData() {
  copyFileSync(src, dest);                    // Blocks!
  const result = execSync("sqlite3 query");   // Blocks!
  const entries = readdirSync(dir);           // Blocks!
  for (const entry of entries) {
    statSync(join(dir, entry));               // Blocks N times!
  }
}

// GOOD - fully async, UI renders cached data while refreshing
import { exec } from "child_process";
import { promisify } from "util";
import { stat, readdir, copyFile, access } from "fs/promises";

const execAsync = promisify(exec);

async function fetchData() {
  await copyFile(src, dest);                         // Non-blocking
  const { stdout } = await execAsync("sqlite3...");  // Non-blocking

  // Use withFileTypes to avoid extra stat calls
  const entries = await readdir(dir, { withFileTypes: true });
  const results = entries
    .filter(e => e.isDirectory())  // No stat needed!
    .map(e => ({ path: join(dir, e.name), name: e.name }));
}
```

**Key optimizations:**
1. Replace `execSync` with `promisify(exec)` for shell commands
2. Replace `existsSync` with `access()` from `fs/promises`
3. Replace `readdirSync` + `statSync` loop with `readdir(dir, { withFileTypes: true })`
4. Run all path validations in parallel with `Promise.all`
5. Use SQLite URI mode for direct read-only access (no file copy needed)

### SQLite Direct Access (Skip File Copy)

When reading SQLite databases from other apps (like Zed, VS Code, etc.), avoid copying the database file. Use URI mode for direct read-only access:

```tsx
// BAD - copies entire database file (slow, blocks)
import { copyFileSync, unlinkSync } from "fs";

const tempDb = `/tmp/copy-${Date.now()}.sqlite`;
copyFileSync(originalDb, tempDb);           // Expensive!
execSync(`sqlite3 "${tempDb}"
Files: 1
Size: 14.8 KB
Complexity: 18/100
Category: Web Dev

Related in Web Dev