Install
$ agentstack add skill-brpaz-agent-skills-vicinae-extensions ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →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
{
"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 launcherdescription- What the command doesmode- Always"view"for React componentskeywords- 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:
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
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
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
}
/>
}
/>
List.Section - Grouped Items
List.Dropdown - Filter Dropdown
}
>
{/* List items */}
Grid - Visual Grid Interface
For visual content (images, icons, cards):
import { ActionPanel, Action, Grid } from '@vicinae/api';
export default function GalleryCommand() {
const [items, setItems] = useState([]);
return (
{items.map((item) => (
}
/>
))}
);
}
Grid Props
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:
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
// 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:
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:
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
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:
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
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:
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:
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:
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
enum Toast.Style {
Success = "success", // Green checkmark
Failure = "failure", // Red X
Animated = "animated" // Loading spinner
}
Icons
Built-in icon system:
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:
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:
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:
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:
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:
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.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.