# Vicinae Extensions

> Build Vicinae launcher extensions with React/TypeScript, @vicinae/api, commands, and native UI components.

- **Type:** Skill
- **Install:** `agentstack add skill-brpaz-agent-skills-vicinae-extensions`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [brpaz](https://agentstack.voostack.com/s/brpaz)
- **Installs:** 0
- **Category:** [Agent Skills](https://agentstack.voostack.com/c/agent-skills)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [brpaz](https://github.com/brpaz)
- **Source:** https://github.com/brpaz/agent-skills/tree/main/skills/vicinae-extensions

## Install

```sh
agentstack add skill-brpaz-agent-skills-vicinae-extensions
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# Vicinae Extensions - React/TypeScript Extension Development

Use this skill when building extensions for Vicinae launcher, a high-performance native Linux launcher with Raycast compatibility. This covers the complete extension development workflow using the `@vicinae/api` TypeScript SDK.

## When to Use

- Creating a new Vicinae extension with React/TypeScript
- Implementing List, Grid, Form, or Detail view commands
- Integrating with Vicinae APIs (clipboard, notifications, preferences, storage)
- Porting an existing Raycast extension to Vicinae

## What is Vicinae?

Vicinae (pronounced "vee-CHEE-nay") is a keyboard-driven command launcher for Linux that integrates with:
- Native OS features (window management, clipboard)
- React/TypeScript extension system
- Raycast extension ecosystem (mostly compatible)
- Global extension store

**Architecture:** Extensions run in Node.js and render through a custom React reconciler to native Qt Widgets (no browser, no Electron, no HTML/CSS).

## Extension File Structure

```
my-extension/
├── package.json           # Extension metadata & dependencies
├── tsconfig.json          # TypeScript configuration
├── src/
│   ├── index.tsx         # Main command (default export)
│   ├── other-command.tsx # Additional commands
│   └── lib/
│       └── utils.ts      # Shared utilities
├── assets/               # Icons, images (optional)
│   └── icon.png
└── README.md            # Extension documentation
```

## package.json Structure

```json
{
  "name": "my-extension",
  "version": "1.0.0",
  "title": "My Extension",
  "description": "Brief description of what it does",
  "author": "Your Name",
  "license": "MIT",
  
  "main": "src/index.tsx",
  
  "scripts": {
    "dev": "vicinae dev",
    "build": "vicinae build"
  },
  
  "dependencies": {
    "@vicinae/api": "^0.19.0",
    "react": "^18.2.0"
  },
  
  "devDependencies": {
    "@types/node": "^20.0.0",
    "@types/react": "^18.2.0",
    "typescript": "^5.0.0"
  },
  
  "commands": [
    {
      "name": "search",
      "title": "Search Items",
      "description": "Search and browse items",
      "mode": "view",
      "keywords": ["find", "lookup"]
    },
    {
      "name": "create",
      "title": "Create Item",
      "description": "Create a new item",
      "mode": "view",
      "keywords": ["new", "add"]
    }
  ],
  
  "preferences": [
    {
      "name": "apiKey",
      "type": "password",
      "required": true,
      "title": "API Key",
      "description": "Your service API key"
    },
    {
      "name": "theme",
      "type": "dropdown",
      "required": false,
      "title": "Theme",
      "description": "UI theme preference",
      "default": "auto",
      "data": [
        { "title": "Auto", "value": "auto" },
        { "title": "Light", "value": "light" },
        { "title": "Dark", "value": "dark" }
      ]
    },
    {
      "name": "maxResults",
      "type": "textfield",
      "required": false,
      "title": "Max Results",
      "description": "Maximum number of results to show",
      "default": "10"
    }
  ]
}
```

### Commands Configuration

Each command must specify:
- `name` - File name in src/ (without .tsx)
- `title` - Display name in launcher
- `description` - What the command does
- `mode` - Always `"view"` for React components
- `keywords` - Optional search aliases

### Preferences Types

| Type | Usage | Value Type |
|------|-------|-----------|
| `textfield` | Single-line text input | string |
| `password` | Masked text input | string |
| `dropdown` | Select from options | string |
| `checkbox` | Boolean toggle | boolean (string "true"/"false") |

## Core UI Components

### List - Search & Browse Interface

The most common UI component for searchable item lists:

```typescript
import { ActionPanel, Action, List, Icon } from '@vicinae/api';
import { useState, useEffect } from 'react';

export default function SearchCommand() {
  const [items, setItems] = useState([]);
  const [isLoading, setIsLoading] = useState(true);
  const [searchText, setSearchText] = useState('');

  useEffect(() => {
    async function fetchItems() {
      setIsLoading(true);
      const results = await searchAPI(searchText);
      setItems(results);
      setIsLoading(false);
    }
    fetchItems();
  }, [searchText]);

  return (
    
      {items.map((item) => (
        
              
              
            
          }
        />
      ))}
    
  );
}
```

#### List Props

```typescript
interface ListProps {
  children: React.ReactNode;
  isLoading?: boolean;                    // Show loading indicator
  onSearchTextChange?: (text: string) => void;  // Search handler
  throttle?: boolean;                     // Debounce search (default: false)
  isShowingDetail?: boolean;              // Show detail pane
  searchBarAccessory?: React.ReactElement; // Dropdown filter
}
```

#### List.Item Props

```typescript
interface ListItemProps {
  id?: string;
  title: string;
  subtitle?: string;
  accessories?: Accessory[];              // Right-side metadata
  icon?: Icon | string;                   // Left icon
  actions?: React.ReactElement;
  detail?: React.ReactElement;
  keywords?: string[];                    // Search keywords
}

type Accessory = 
  | { text: string; icon?: Icon }
  | { icon: Icon; tooltip?: string }
  | { date: Date }
  | { tag: { value: string; color?: Color } };
```

#### List.Item.Detail - Detailed View

```typescript

  
            
            
            
            
              
            
          
        }
      />
    }
  />

```

#### List.Section - Grouped Items

```typescript

  
    
    
  
  
  
    
  

```

#### List.Dropdown - Filter Dropdown

```typescript

      
      
      
    
  }
>
  {/* List items */}

```

### Grid - Visual Grid Interface

For visual content (images, icons, cards):

```typescript
import { ActionPanel, Action, Grid } from '@vicinae/api';

export default function GalleryCommand() {
  const [items, setItems] = useState([]);

  return (
    
      
        {items.map((item) => (
          
                
              
            }
          />
        ))}
      
    
  );
}
```

#### Grid Props

```typescript
interface GridProps {
  children: React.ReactNode;
  columns?: number;                       // Number of columns (default: 5)
  aspectRatio?: "1" | "3/2" | "2/3" | "4/3" | "16/9";
  fit?: Grid.Fit;                         // How content fits
  inset?: Grid.Inset;                     // Padding
  isLoading?: boolean;
  searchBarPlaceholder?: string;
  onSearchTextChange?: (text: string) => void;
  throttle?: boolean;
}

enum Grid.Fit {
  Contain = "contain",  // Fit within bounds
  Fill = "fill"         // Fill completely
}

enum Grid.Inset {
  Small = "small",
  Medium = "medium",
  Large = "large"
}
```

### Form - Input Forms

For data collection and settings:

```typescript
import { ActionPanel, Action, Form, showToast, Toast } from '@vicinae/api';
import { useState } from 'react';

interface FormValues {
  name: string;
  email: string;
  category: string;
  priority: string[];
  notes: string;
  enabled: boolean;
  date: Date;
}

export default function CreateCommand() {
  const [nameError, setNameError] = useState();

  async function handleSubmit(values: FormValues) {
    if (!values.name) {
      setNameError("Name is required");
      return;
    }

    try {
      await createItem(values);
      await showToast({
        style: Toast.Style.Success,
        title: "Item created",
        message: values.name
      });
      // Could use popToRoot() or push() here
    } catch (error) {
      await showToast({
        style: Toast.Style.Failure,
        title: "Failed to create item",
        message: String(error)
      });
    }
  }

  return (
    
          
        
      }
    >
       setNameError(undefined)}
      />
      
      
      
      
        
        
      
      
      
        
        
        
      
      
      
      
      
      
      
      
      
      
      
    
  );
}
```

#### Form Field Types

| Component | Purpose | Value Type |
|-----------|---------|-----------|
| `Form.TextField` | Single-line text | string |
| `Form.TextArea` | Multi-line text | string |
| `Form.PasswordField` | Masked input | string |
| `Form.Dropdown` | Single selection | string |
| `Form.TagPicker` | Multiple selection | string[] |
| `Form.Checkbox` | Boolean toggle | boolean |
| `Form.DatePicker` | Date selection | Date |
| `Form.FilePicker` | File selection | string[] (paths) |
| `Form.Separator` | Visual divider | - |
| `Form.Description` | Help text | - |

#### Form Validation

```typescript
// Real-time validation
 {
    if (!value.includes('@')) {
      setEmailError("Invalid email");
    } else {
      setEmailError(undefined);
    }
  }}
/>

// Submit-time validation
function handleSubmit(values: FormValues) {
  const errors: Record = {};
  
  if (!values.name) errors.name = "Required";
  if (!values.email.includes('@')) errors.email = "Invalid email";
  
  if (Object.keys(errors).length > 0) {
    // Show errors (implementation depends on your approach)
    return;
  }
  
  // Proceed with submission
}
```

### Detail - Markdown Detail View

For displaying detailed information:

```typescript
import { ActionPanel, Action, Detail } from '@vicinae/api';

export default function ShowDetailCommand() {
  const markdown = `
# Main Title

## Section

Description with **bold** and *italic* text.

\`\`\`typescript
const code = "example";
\`\`\`

- List item 1
- List item 2

[Link](https://example.com)
  `;

  return (
    
          
          
          
          
            
            
          
        
      }
      actions={
        
          
          
        
      }
    />
  );
}
```

## Actions

Actions appear in the ActionPanel and execute commands:

```typescript
import { ActionPanel, Action, Icon } from '@vicinae/api';

  {/* Built-in Actions */}
  
  
  
  
  
  
  
  
  
  
  
  
  {/* Custom Actions */}
   {
      await performAction();
      await showToast({
        style: Toast.Style.Success,
        title: "Done"
      });
    }}
  />
  
  {/* Form Submit */}
   handleSubmit(values)}
  />
  
  {/* Sections */}
  
     {}} />
  
  
  
     {}} />
  

```

### Keyboard Shortcuts

```typescript
import { Keyboard } from '@vicinae/api';

 {}}
/>

// Available modifiers: "cmd", "ctrl", "opt", "shift"
// Keys: letters, numbers, "enter", "delete", "escape", "arrowUp", etc.
```

## Navigation

Push and pop between views:

```typescript
import { List, ActionPanel, Action, Detail, useNavigation } from '@vicinae/api';

function MainList() {
  const { push } = useNavigation();

  return (
    
      
             push()}
            />
          
        }
      />
    
  );
}

function DetailView({ itemId }: { itemId: string }) {
  const { pop } = useNavigation();

  return (
    
          
        
      }
    />
  );
}
```

### Navigation API

```typescript
const { push, pop } = useNavigation();

// Push new view
push();

// Pop current view
pop();

// Pop to root (close extension)
import { popToRoot } from '@vicinae/api';
await popToRoot();

// Close current window
import { closeMainWindow } from '@vicinae/api';
await closeMainWindow();
```

## Preferences

Access user preferences from package.json:

```typescript
import { getPreferenceValues } from '@vicinae/api';

interface Preferences {
  apiKey: string;
  theme: string;
  maxResults: string;
}

export default function Command() {
  const preferences = getPreferenceValues();
  
  const apiKey = preferences.apiKey;
  const maxResults = parseInt(preferences.maxResults) || 10;

  // Use preferences...
}
```

## Local Storage

Persist data between sessions:

```typescript
import { LocalStorage } from '@vicinae/api';

// Store data
await LocalStorage.setItem('key', 'value');
await LocalStorage.setItem('user', JSON.stringify({ id: 1, name: 'Alice' }));

// Retrieve data
const value = await LocalStorage.getItem('key');
const userJson = await LocalStorage.getItem('user');
const user = userJson ? JSON.parse(userJson) : null;

// Remove data
await LocalStorage.removeItem('key');

// Clear all
await LocalStorage.clear();

// Get all items
const allItems: LocalStorage.Values = await LocalStorage.allItems();
// Returns: { key1: 'value1', key2: 'value2', ... }
```

## Toast Notifications

Show feedback to users:

```typescript
import { showToast, Toast } from '@vicinae/api';

// Success
await showToast({
  style: Toast.Style.Success,
  title: "Operation completed",
  message: "Details about what happened"
});

// Failure
await showToast({
  style: Toast.Style.Failure,
  title: "Operation failed",
  message: "Error details"
});

// Loading (with progress)
const toast = await showToast({
  style: Toast.Style.Animated,
  title: "Processing..."
});

// Update toast
toast.style = Toast.Style.Success;
toast.title = "Completed";
await toast.show();

// Hide toast
await toast.hide();
```

### Toast Styles

```typescript
enum Toast.Style {
  Success = "success",      // Green checkmark
  Failure = "failure",      // Red X
  Animated = "animated"     // Loading spinner
}
```

## Icons

Built-in icon system:

```typescript
import { Icon } from '@vicinae/api';

// Common icons:
Icon.Star
Icon.Heart
Icon.Bookmark
Icon.Tag
Icon.Folder
Icon.Document
Icon.Image
Icon.Video
Icon.Music
Icon.Code
Icon.Terminal
Icon.Globe
Icon.Link
Icon.Mail
Icon.Person
Icon.PersonCircle
Icon.Calendar
Icon.Clock
Icon.Bell
Icon.Checkmark
Icon.CheckCircle
Icon.XmarkCircle
Icon.Warning
Icon.Info
Icon.Trash
Icon.Pencil
Icon.Plus
Icon.Minus
Icon.Multiply
Icon.ArrowUp
Icon.ArrowDown
Icon.ArrowLeft
Icon.ArrowRight
Icon.ChevronUp
Icon.ChevronDown
Icon.ChevronLeft
Icon.ChevronRight
Icon.Download
Icon.Upload
Icon.Cloud
Icon.Eye
Icon.EyeSlash
Icon.Lock
Icon.LockUnlocked
Icon.Gear
Icon.Filter
Icon.MagnifyingGlass
Icon.List
Icon.Grid
Icon.Sidebar
Icon.Window
Icon.Play
Icon.Pause
Icon.Stop
Icon.Forward
Icon.Rewind
```

## Colors

Standard color system:

```typescript
import { Color } from '@vicinae/api';

// Available colors:
Color.Red
Color.Orange
Color.Yellow
Color.Green
Color.Blue
Color.Purple
Color.Magenta
Color.PrimaryText
Color.SecondaryText
```

## Clipboard

Interact with system clipboard:

```typescript
import { Clipboard } from '@vicinae/api';

// Copy text
await Clipboard.copy("text to copy");

// Copy with metadata (for paste handlers)
await Clipboard.copy("https://example.com", {
  transient: true  // Don't add to clipboard history
});

// Read clipboard
const text = await Clipboard.readText();

// Clear clipboard
await Clipboard.clear();
```

## File Search

Search files indexed by Vicinae:

```typescript
import { FileSearch } from '@vicinae/api';

const results = await FileSearch.search('document');

results.forEach(file => {
  console.log(file.path);     // Full path
  console.log(file.name);     // File name
});

// Use in List
export default function SearchFilesCommand() {
  const [results, setResults] = useState([]);

  return (
     {
        const files = await FileSearch.search(query);
        setResults(files);
      }}
    >
      {results.map((file) => (
        
              
              
            
          }
        />
      ))}
    
  );
}
```

## Environment

Access environment information:

```typescript
import { environment } from '@vicinae/api';

console.log(environment.commandName);      // Current command name
console.log(environment.extensionName);    // Extension identifier
console.log(environment.extensionPath);    // Extension directory path
console.log(environment.assetsPath);       // Assets directory path
console.log(environment.supportPath);      // Support files directory
```

## Error Handling

Proper error handling patterns:

```typescript
import { showToast, Toast } from '@vicinae/api';

export default function Command() {
  const [error, setError] = useState(null);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [brpaz](https://github.com/brpaz)
- **Source:** [brpaz/agent-skills](https://github.com/brpaz/agent-skills)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-brpaz-agent-skills-vicinae-extensions
- Seller: https://agentstack.voostack.com/s/brpaz
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
