Getting Started
Create your first Nimbalyst extension step by step, ending with a working custom editor for a new file type built on the extension SDK.
This guide walks you through creating your first Nimbalyst extension, aimed at developers who know some React and TypeScript. By the end, you'll have a working extension that registers a custom editor for .hello files.
Recommended: Create It From Inside Nimbalyst
The intended workflow is to scaffold and iterate on extensions from inside Nimbalyst:
- Enable Extension Dev Tools in Settings > Application > Advanced
- Use
File > New Extension ProjectorDeveloper > New Extension Project - Or ask Claude to run
/new-extension ~/my-first-extension "Hello Editor" *.hello - Ask Claude to build and install it with
extension_buildandextension_install - Use
extension_reloadwhile iterating
The rest of this guide shows the manual scaffold path if you prefer to create the project yourself.
Prerequisites
- Enable Extension Dev Tools: go to Settings > Application > Advanced and enable "Extension Dev Tools"
- Node.js 18+: required for building extensions
Step 1: Create the Project
Create a new directory for your extension:
mkdir my-first-extension
cd my-first-extension
npm init -y
Step 2: Install Dependencies
npm install --save-dev typescript vite @nimbalyst/extension-sdk
npm install react
Step 3: Create the Manifest
Create manifest.json. This tells Nimbalyst about your extension:
{
"id": "com.example.hello-editor",
"name": "Hello Editor",
"version": "1.0.0",
"description": "A simple custom editor for .hello files",
"main": "dist/index.js",
"apiVersion": "1.0.0",
"contributions": {
"customEditors": [
{
"filePatterns": ["*.hello"],
"displayName": "Hello Editor",
"component": "HelloEditor"
}
],
"newFileMenu": [
{
"extension": ".hello",
"displayName": "Hello File",
"icon": "description",
"defaultContent": "Hello, World!"
}
]
}
}
apiVersion is currently optional but recommended.
Step 4: Create the Vite Config
Create vite.config.ts:
import { defineConfig } from 'vite';
import { createExtensionConfig } from '@nimbalyst/extension-sdk/vite';
export default defineConfig(
createExtensionConfig({
entry: './src/index.ts',
})
);
Step 5: Create the TypeScript Config
Create tsconfig.json:
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"declaration": true,
"outDir": "dist",
"rootDir": "src"
},
"include": ["src/**/*"]
}
Step 6: Create the Extension Entry Point
Create src/index.ts:
import type { ExtensionContext } from '@nimbalyst/extension-sdk';
import { HelloEditor } from './HelloEditor';
// Export components that the manifest references
export const components = {
HelloEditor,
};
// Called when the extension is loaded
export function activate(context: ExtensionContext) {
console.log('Hello Editor extension activated!');
}
// Called when the extension is unloaded
export function deactivate() {
console.log('Hello Editor extension deactivated');
}
Step 7: Create the Editor Component
Create src/HelloEditor.tsx:
import React, { useRef, useReducer } from 'react';
import { useEditorLifecycle } from '@nimbalyst/extension-sdk';
import type { EditorHostProps } from '@nimbalyst/extension-sdk';
export function HelloEditor({ host }: EditorHostProps) {
const textRef = useRef('');
const textareaRef = useRef<HTMLTextAreaElement>(null);
const [, forceRender] = useReducer((x) => x + 1, 0);
const { isLoading, error, markDirty } = useEditorLifecycle(host, {
applyContent: (content: string) => {
textRef.current = content;
if (textareaRef.current) {
textareaRef.current.value = content;
}
forceRender();
},
getCurrentContent: () => textRef.current,
});
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
textRef.current = e.target.value;
markDirty();
};
if (error) return <div style={{ padding: '20px' }}>Error: {error.message}</div>;
if (isLoading) return <div style={{ padding: '20px' }}>Loading...</div>;
return (
<div style={{
padding: '20px',
height: '100%',
display: 'flex',
flexDirection: 'column'
}}>
<h2 style={{ marginBottom: '10px' }}>Hello Editor</h2>
<p style={{ color: 'var(--nim-text-muted)', marginBottom: '10px' }}>
Editing: {host.filePath}
</p>
<textarea
ref={textareaRef}
defaultValue={textRef.current}
onChange={handleChange}
style={{
flex: 1,
padding: '10px',
fontSize: '16px',
fontFamily: 'monospace',
backgroundColor: 'var(--nim-bg-secondary)',
color: 'var(--nim-text)',
border: '1px solid var(--nim-border)',
borderRadius: '4px',
resize: 'none'
}}
/>
</div>
);
}
Key points:
useEditorLifecyclehandles loading, saving, file watching, echo detection, and dirty stateapplyContentpushes content into the editor (on load, external changes)getCurrentContentpulls content from the editor (on save)- Call
markDirty()when the user edits; the hook manageshost.setDirty()for you - Content lives in a ref, not React state; the textarea uses
defaultValue
Step 8: Add Build Script
Update your package.json to add a build script:
{
"scripts": {
"build": "vite build"
}
}
Step 9: Build and Install
Now ask Claude to build and install your extension:
"Build and install my extension from ~/my-first-extension"
Claude will use the extension_build and extension_install tools to compile and load your extension.
Step 10: Test It
- Create a new file with the
.helloextension - Your custom editor should appear instead of the default text editor
- Make changes and save; they persist to the file
Next Steps
- Add styling with a
styles.cssfile - Add AI tools so Claude can interact with your editor
- Add a toolbar with actions
- Handle more complex file formats
See the custom-editors.md guide for more advanced editor development.
Troubleshooting
Extension doesn't load
- Check the console for errors (View > Toggle Developer Tools)
- Verify your
manifest.jsonhas a valididfield - Make sure
dist/index.jsexists after building
Editor doesn't appear for file type
- Check that
filePatternsin the manifest matches your file extension - Verify the
componentname matches what you export incomponents
Changes don't appear after editing
Ask Claude to reload the extension:
"Reload my hello-editor extension"
This will rebuild and hot-reload without restarting Nimbalyst.