Creating Extensions
Creating Extensions
Xplorer supports eight extension categories. This guide shows you how to build each one using the high-level registration APIs.
Extension Categories
| Category | API | Purpose |
| ------------ | ------------------------ | --------------------------------------------- |
| theme | Theme.register() | Custom color schemes |
| panel | Sidebar.register() | Right sidebar panels with React UI |
| panel | SidebarTab.register() | Left sidebar tabs (alongside Explorer/Search) |
| preview | Preview.register() | Custom file preview renderers |
| command | Command.register() | Commands with keyboard shortcuts |
| action | ContextMenu.register() | Right-click context menu items |
| bottom-tab | BottomTab.register() | Bottom panel tabs (alongside Terminal) |
| editor | Editor.register() | Custom file editor handlers |
Theme Extension
The simplest extension type. Pass a colors object and the SDK generates all CSS variables automatically.
Example: Midnight Theme
import { Theme } from '@xplorer/extension-sdk';
Theme.register({
id: 'midnight',
name: 'Midnight',
colors: {
bg: '#0d1117',
surface: '#161b22',
surfaceLight: '#21262d',
border: '#30363d',
text: '#c9d1d9',
textMuted: '#8b949e',
blue: '#58a6ff',
green: '#3fb950',
red: '#f85149',
yellow: '#d29922',
orange: '#d29922',
pink: '#f778ba',
cyan: '#56d4dd',
purple: '#bc8cff',
},
});
That's it — the theme appears in Settings > Themes automatically.
With Custom Background + Extra CSS
Theme.register({
id: 'cyberpunk',
name: 'Cyberpunk',
background: 'linear-gradient(145deg, #0a0a0f, #130a18, #0a0a0f)',
colors: { bg: '#0a0a0f', surface: '#151520' /* ... */ },
css: `
.theme-cyberpunk ::selection { background-color: rgba(240, 225, 48, 0.3); }
.theme-cyberpunk ::-webkit-scrollbar-thumb { background: linear-gradient(#f0e130, #00fff0); }
`,
});
Key Points
- The
idbecomes the CSS class name:.theme-{id} - The
colorsobject auto-generates all--xp-*CSS variables - Optional
cssproperty for advanced customizations (scrollbar, selection, glow effects) - Optional
backgroundproperty for gradient HTML backgrounds
Panel Extension (Sidebar)
Panel extensions render React UI in the right sidebar.
Example: Bookmarks Panel
import { Sidebar, type XplorerAPI } from '@xplorer/extension-sdk';
declare const React: typeof import('react');
const { useState, useEffect } = React;
let api: XplorerAPI;
function BookmarksPanel() {
const [bookmarks, setBookmarks] = useState<string[]>([]);
useEffect(() => {
api.settings.get<string[]>('bookmarks', []).then((saved) => {
setBookmarks(saved ?? []);
});
}, []);
const addCurrent = async () => {
const path = api.navigation.getCurrentPath();
const updated = [...bookmarks, path];
setBookmarks(updated);
await api.settings.set('bookmarks', updated);
};
return React.createElement(
'div',
{ style: { padding: 12 } },
React.createElement('button', { onClick: addCurrent }, 'Bookmark Current'),
React.createElement(
'ul',
null,
...bookmarks.map((path) =>
React.createElement(
'li',
{
key: path,
onClick: () => api.navigation.navigateTo(path),
style: { cursor: 'pointer', fontSize: 12, padding: '4px 0' },
},
path,
),
),
),
);
}
Sidebar.register({
id: 'bookmarks',
title: 'Bookmarks',
icon: 'star',
onActivate: (injectedApi) => {
api = injectedApi;
},
render: () => React.createElement(BookmarksPanel),
});
Key Points
- Use
onActivateto capture theXplorerAPIreference - The
renderfunction receives{ currentPath, selectedFiles }as props - The panel icon appears in the vertical extension bar
Preview Extension
Preview extensions render custom file previews. When a user selects a file, Xplorer asks each preview extension if it can handle it.
Example: CSV Preview
import { Preview, type XplorerAPI } from '@xplorer/extension-sdk';
declare const React: typeof import('react');
const { useState, useEffect } = React;
let api: XplorerAPI;
function CsvViewer({ filePath }: { filePath: string }) {
const [rows, setRows] = useState<string[][]>([]);
useEffect(() => {
api.files.readText(filePath).then((text) => {
setRows(text.split('\n').map((line) => line.split(',')));
});
}, [filePath]);
return React.createElement(
'table',
{ style: { fontSize: 12, width: '100%' } },
...rows.map((row, i) =>
React.createElement(
'tr',
{ key: i },
...row.map((cell, j) =>
React.createElement(
i === 0 ? 'th' : 'td',
{
key: j,
style: { padding: '4px 8px', borderBottom: '1px solid var(--xp-border, #333)' },
},
cell,
),
),
),
),
);
}
Preview.register({
id: 'csv-preview',
title: 'CSV Preview',
permissions: ['file:read'],
canPreview: (file) => !file.is_dir && file.path.endsWith('.csv'),
priority: 10,
onActivate: (injectedApi) => {
api = injectedApi;
},
render: (props) => {
const files = (props.selectedFiles || []) as Array<{ path: string; name: string }>;
const csvFile = files.find((f) => f.path.endsWith('.csv'));
if (!csvFile) return React.createElement('div', null, 'Select a CSV file');
return React.createElement(CsvViewer, { filePath: csvFile.path });
},
});
Key Points
canPreview(file)returnstruefor files this extension handlesprioritycontrols which extension wins when multiple match (higher = preferred)renderreceivesselectedFiles— find your target file from the list
Command Extension
Commands are actions that can be triggered by keyboard shortcuts or programmatically.
Example: Word Counter
import { Command, type XplorerAPI } from '@xplorer/extension-sdk';
Command.register({
id: 'count-words',
title: 'Count Words in Selection',
shortcut: 'ctrl+shift+w',
action: async (api) => {
const state = (window as any).__xplorer_state__;
const files = state?.selectedFiles || [];
if (files.length === 0) {
api.ui.showMessage('No file selected', 'warning');
return;
}
const text = await api.files.readText(files[0].path);
const words = text.trim().split(/\s+/).filter(Boolean).length;
api.ui.showMessage(`${files[0].name}: ${words} words`, 'info');
},
});
Multiple Commands in One Extension
You can call Command.register() multiple times in the same file:
Command.register({
id: 'json.format',
title: 'Format JSON',
action: async (api) => {
/* ... */
},
});
Command.register({
id: 'json.minify',
title: 'Minify JSON',
action: async (api) => {
/* ... */
},
});
Command.register({
id: 'json.validate',
title: 'Validate JSON',
action: async (api) => {
/* ... */
},
});
Context Menu Extension
Context menu extensions add items to the right-click menu.
Example: File Hasher
import { ContextMenu, type XplorerAPI } from '@xplorer/extension-sdk';
ContextMenu.register({
id: 'sha256-hash',
title: 'Calculate SHA-256',
when: 'singleFileSelected',
action: async (files, api) => {
const data = await api.files.read(files[0].path);
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
const hex = Array.from(new Uint8Array(hashBuffer))
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
api.ui.showMessage(`SHA-256: ${hex}`, 'info');
},
});
when Conditions
| Value | Files shown |
| ------------------------- | -------------------------------------------------- |
| 'always' | Always shown |
| 'singleFileSelected' | Exactly one file selected |
| 'multipleFilesSelected' | Two or more files selected |
| (files) => boolean | Custom function receiving the selected files array |
Sidebar Tab Extension
Sidebar tab extensions add a tab icon to the left sidebar (alongside the built-in Explorer and Search tabs). When clicked, your component renders in the sidebar area. Combine with Editor.register() to also show content in the main area for specific file types.
Example: Google Drive Sidebar
import { Tab, type XplorerAPI } from '@xplorer/extension-sdk';
import { Button, Spinner, Card } from '@xplorer/extension-sdk';
declare const React: typeof import('react');
const { useState, useEffect } = React;
let api: XplorerAPI;
function DriveBrowser({ tabData }: { tabData?: Record<string, any> }) {
const [files, setFiles] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const accountId = tabData?.accountId;
useEffect(() => {
if (!accountId) return;
setLoading(true);
api.gdrive.listFiles(accountId).then((items) => {
setFiles(items);
setLoading(false);
});
}, [accountId]);
if (loading) return React.createElement(Spinner, { size: 24 });
return React.createElement(
'div',
{ style: { padding: 16 } },
...files.map((f) =>
React.createElement(
Card,
{ key: f.id, title: f.name },
React.createElement('span', null, `${(f.size / 1024).toFixed(1)} KB`),
React.createElement(Button, {
label: 'Download',
variant: 'primary',
size: 'sm',
onClick: async () => {
const dest = await api.dialog.pickSaveFile(f.name);
if (dest) await api.gdrive.downloadFile(accountId, f.id, dest);
},
}),
),
),
);
}
SidebarTab.register({
id: 'gdrive-sidebar',
title: 'Google Drive',
icon: 'cloud',
permissions: ['gdrive:access'],
onActivate: (injectedApi) => {
api = injectedApi;
},
render: (props) => React.createElement(DriveBrowser, { currentPath: props.currentPath }),
});
Key Points
SidebarTabadds a clickable icon in the left sidebar tab bar (alongside Explorer and Search)- When clicked, your
rendercomponent replaces the sidebar content - Use
useCurrentPath()hook inside your component for reactive path updates - Combine with
Editor.register()if you also need a main area view for specific file types
Bottom Tab Extension
Bottom tab extensions render in the bottom panel alongside the built-in Terminal tab. Useful for persistent tool panels like version control, build output, or problem lists.
Example: Git Panel
import { BottomTab, type XplorerAPI } from '@xplorer/extension-sdk';
import { Button, Panel } from '@xplorer/extension-sdk';
declare const React: typeof import('react');
const { useState, useEffect } = React;
let api: XplorerAPI;
function GitPanel({ currentPath }: { currentPath?: string }) {
const [status, setStatus] = useState<Array<{ path: string; status: string }>>([]);
const [branch, setBranch] = useState('');
const refresh = async () => {
if (!currentPath) return;
const repo = await api.git.findRepository(currentPath);
if (!repo) return;
const info = await api.git.getGitRepoInfo(repo);
setBranch(info.branch);
const st = await api.git.getGitStatus(repo);
setStatus(st);
};
useEffect(() => {
refresh();
}, [currentPath]);
return React.createElement(
Panel,
{ title: `Git: ${branch}` },
React.createElement(Button, {
label: 'Refresh',
variant: 'ghost',
size: 'sm',
onClick: refresh,
}),
React.createElement(
'ul',
{ style: { fontSize: 12, listStyle: 'none', padding: 0 } },
...status.map((f) =>
React.createElement(
'li',
{ key: f.path, style: { padding: '2px 0' } },
React.createElement(
'span',
{
style: { color: f.status === 'modified' ? 'var(--xp-orange)' : 'var(--xp-green)' },
},
`[${f.status}] `,
),
f.path,
),
),
),
);
}
BottomTab.register({
id: 'git-panel',
title: 'Git',
icon: 'git-branch',
permissions: ['git:read'],
onActivate: (injectedApi) => {
api = injectedApi;
},
render: (props) => React.createElement(GitPanel, { currentPath: props.currentPath }),
});
Key Points
- Bottom tabs appear as clickable tabs in the bottom panel bar
- The panel stays mounted while switching between bottom tabs (state is preserved)
renderreceivescurrentPathandselectedFileslike sidebar panels
Editor Extension
Editor extensions provide custom file editing experiences. When a user opens a file for editing, Xplorer checks registered editors for a match using canEdit.
Example: Markdown Editor
import { Editor, type XplorerAPI } from '@xplorer/extension-sdk';
import { Button, Panel } from '@xplorer/extension-sdk';
declare const React: typeof import('react');
const { useState, useEffect } = React;
let api: XplorerAPI;
function MarkdownEditor({ filePath }: { filePath: string }) {
const [content, setContent] = useState('');
const [dirty, setDirty] = useState(false);
useEffect(() => {
api.files.readText(filePath).then((text) => setContent(text));
}, [filePath]);
const save = async () => {
await api.files.write(filePath, content);
setDirty(false);
api.ui.showMessage('File saved', 'info');
};
return React.createElement(
Panel,
{ title: `Editing: ${filePath.split('/').pop()}` },
React.createElement('textarea', {
value: content,
onChange: (e: any) => {
setContent(e.target.value);
setDirty(true);
},
style: {
width: '100%',
height: '80%',
fontFamily: 'monospace',
fontSize: 14,
background: 'var(--xp-surface)',
color: 'var(--xp-text)',
border: '1px solid var(--xp-border)',
padding: 12,
resize: 'none',
},
}),
React.createElement(Button, {
label: dirty ? 'Save *' : 'Save',
variant: 'primary',
onClick: save,
disabled: !dirty,
}),
);
}
Editor.register({
id: 'markdown-editor',
title: 'Markdown Editor',
icon: 'edit',
permissions: ['file:read', 'file:write'],
canEdit: (file) => !file.is_dir && /\.(md|mdx|markdown)$/.test(file.path),
priority: 10,
onActivate: (injectedApi) => {
api = injectedApi;
},
render: (props) => React.createElement(MarkdownEditor, { filePath: props.filePath }),
});
Key Points
canEdit(file)returnstruefor files this editor handlesprioritycontrols which editor wins when multiple match (higher = preferred)- The editor opens in the main content area as a tab when the user triggers "Open in Editor"
Using SDK Hooks
Inside your extension's React components, you can use hooks to reactively read Xplorer state:
import { useCurrentPath, useSelectedFiles } from '@xplorer/extension-sdk';
function StatusBar() {
const path = useCurrentPath();
const files = useSelectedFiles();
return React.createElement('div', null, `${path} — ${files.length} selected`);
}
These hooks listen for xplorer-state-change CustomEvents dispatched by the host, so they react instantly without polling.
Using SDK UI Components
The SDK provides pre-built components that match the active theme:
import { Button, Input, Card, Spinner, Panel } from '@xplorer/extension-sdk';
function MyPanel() {
return React.createElement(
Panel,
{ title: 'My Tool' },
React.createElement(
Card,
{ title: 'Search' },
React.createElement(Input, { placeholder: 'Search files...', onChange: handleSearch }),
React.createElement(Button, { label: 'Go', variant: 'primary', onClick: doSearch }),
),
loading && React.createElement(Spinner, { size: 20 }),
);
}
Extension Storage
Extensions have access to scoped key-value storage that persists across sessions:
// In onActivate or action callbacks:
await api.settings.set('lastScan', new Date().toISOString());
const lastScan = await api.settings.get<string>('lastScan');
await api.settings.delete('oldKey');
Storage is scoped by extension ID — extensions cannot read each other's data.
Advanced: Class-Based Extensions
For complex extensions that need full lifecycle control, you can use the base classes:
import { Extension, PanelExtension, registerExtension } from '@xplorer/extension-sdk';
class MyExtension extends Extension {
async activate() {
/* ... */
}
async deactivate() {
/* ... */
}
}
registerExtension(
new MyExtension({
id: 'my-ext',
name: 'My Extension',
version: '1.0.0',
author: 'You',
category: 'tool',
}),
);
Or use the factory function:
import { createExtension, registerExtension } from '@xplorer/extension-sdk';
const ext = createExtension({
type: 'panel',
manifest: { id: 'quick', name: 'Quick', version: '1.0.0', author: 'You' },
render: () => React.createElement('div', null, 'Hello!'),
});
registerExtension(ext);
Next Steps
- SDK Reference — complete API documentation
- Manifest Reference —
package.jsonfields - Permissions — what each permission grants