Expert guidance for building extensions for Bike Outliner. Handles app context commands, DOM components with React, styling, queries, and runtime debugging via AppleScript evaluation.
Expert guidance for building extensions for Bike Outliner, a Mac outlining application. This skill provides comprehensive knowledge of the Bike Extension Kit architecture, APIs, build system, and debugging workflows.
Provides step-by-step assistance for:
1. **App Context** (`app/main.ts`): Main application logic, commands, keybindings, sidebar items. Access via `bike` global object.
2. **DOM Context** (`dom/*.ts|tsx`): Custom UI components using React. Access via `window.React`, `window.ReactDOM`, `window.ReactDOMClient`.
3. **Style Context** (`style/main.ts`): Custom styling and visual enhancements.
Extensions live in `src/` with each `.bkext` folder containing:
```
extension.bkext/
├── manifest.json # Extension metadata, permissions, install config
├── app/main.ts # App context entry point (optional)
├── dom/ # DOM context files (optional, multiple allowed)
├── style/main.ts # Style context entry point (optional)
├── theme/*.bktheme # Theme files (optional)
└── README.md # Extension documentation
```
When the user requests a new extension:
1. Run `npm run new` to create from template
2. Navigate to `src/[extension-name].bkext/`
3. Edit `manifest.json` to set metadata, permissions, and `"install": true` for auto-install
4. Implement contexts as needed (app, DOM, style)
Use these commands:
The build system automatically:
**Key patterns:**
```typescript
// Access editor and outline
const editor = bike.frontmostOutlineEditor;
const outline = editor.outline;
const selection = editor.selection;
// Add commands
bike.commands.addCommands({
'my-extension:myCommand': {
title: 'My Command',
perform: (context: CommandContext) => {
const editor = context.outlineEditor;
// Command logic here
}
}
});
// Add keybindings
bike.keybindings.addKeybindings({
'Cmd-Shift-K': 'my-extension:myCommand'
});
// Add sidebar items
window.sidebar.addItem({
symbol: 'sparkles',
title: 'My Item',
action: () => { /* ... */ }
});
```
**CRITICAL**: Bike's query syntax is XPath-like but uses DIFFERENT syntax. Do NOT use XPath brackets or syntax.
**Common patterns:**
```javascript
// Get all rows
outline.query('//*').value
// Get by type
outline.query('//task').value
outline.query('//heading').value
// Filter by attributes (NO BRACKETS!)
outline.query('//@done').value // Correct
outline.query('//[@done]').value // WRONG - XPath syntax doesn't work
// Combine type and attribute (space optional but preferred)
outline.query('//task @done').value
// Multiple predicates
outline.query('//task @done and @priority=high').value
// Text matching (case-insensitive by default)
outline.query('//@text contains "hello"').value
outline.query('//@text contains[s] "hello"').value // Case-sensitive
// Slicing (1-based indexing)
outline.query('//task[1]').value // First task
outline.query('//task[-1]').value // Last task
```
**Query result structure:**
All queries return `{type, value}` - always access results via `.value`:
```javascript
const result = outline.query('//task');
// result = { type: 'elements', value: [Row, Row, ...] }
const tasks = result.value;
```
**Debug queries:**
```javascript
console.log(outline.explainQuery('//task @done'));
```
**Always use transactions:**
```typescript
editor.transaction({}, () => {
// Insert rows using RowTemplate objects
outline.insertRows([
{ text: 'Item 1' },
{ text: 'Item 2', type: 'heading' },
{ text: 'Item 3', attributes: { done: 'true' } }
], parent, before?);
// Move or remove rows
outline.moveRows(rows, parent, before?);
outline.removeRows(rows);
});
```
**Access row properties:**
```javascript
row.text.string // Plain text (NOT bodyText!)
row.type // 'body', 'heading', 'task', etc.
row.children // Direct children
row.descendants // All descendants
row.getAttribute(name)
row.setAttribute(name, value)
```
Extension code runs inside Bike.app's JSContexts. Use AppleScript's `evaluate` command to test APIs:
**Simple evaluation:**
```bash
osascript -l JavaScript -e '
Application("Bike").evaluate({ script: "bike.version" })
'
```
**Function with parameter:**
```bash
osascript -l JavaScript -e '
Application("Bike").evaluate({
input: "Hello!",
script: "(input) => { return bike.version + \" says: \" + input }"
})
'
```
**Using classes (require needed for evaluate):**
```bash
osascript -l JavaScript -e '
Application("Bike").evaluate({
script: `
const { Outline } = require("bike/app");
const outline = new Outline(["Item 1", "Item 2"]);
JSON.stringify({ count: outline.root.children.length });
`
})
'
```
**Critical patterns for evaluate:**
1. **Use `var` for persistence** - `const`/`let` do NOT persist between calls
2. **Promises DO resolve** - Store results in `var` variables
3. **DOM scripts use `extensionExports` pattern** (not ES6 `export`)
4. **String escaping** - Use unicode escapes: `\u0027` for `'`, `\u0022` for `"`
**Bidirectional messaging (app ↔ DOM):**
```javascript
// App → DOM
var domCode = "var extensionExports = { activate: async function(context) { context.element.textContent = 'Hello'; } };";
var p = bike.frontmostWindow.presentSheet(domCode, { width: 400, height: 300 });
p.then(function(handle) {
myHandle = handle;
handle.postMessage({ type: 'greet', data: 'Hello DOM!' });
handle.onmessage = function(msg) { console.log('From DOM:', msg); };
});
// DOM → App (in DOM script)
context.postMessage({ type: 'response', data: 'Hello App!' });
```
Outlines support two metadata types:
```typescript
// Runtime metadata (not saved)
outline.runtimeMetadata.set('tempState', { foo: 'bar' });
const value = outline.runtimeMetadata.get('tempState');
// Persistent metadata (saved to file)
outline.persistentMetadata.set('author', 'Claude');
outline.persistentMetadata.set('version', 1);
outline.persistentMetadata.set('tags', ['test', 'metadata']);
// Delete keys
meta.delete('key'); // or meta.set('key', undefined);
```
**DOM context React component:**
```typescript
// dom/my-component.tsx
type Context = {
element: HTMLElement;
postMessage: (message: any) => void;
};
var extensionExports = {
activate: async function(context: Context) {
const root = window.ReactDOMClient.createRoot(context.element);
root.render(
window.React.createElement(MyComponent, { context })
);
}
};
function MyComponent({ context }: { context: Context }) {
return window.React.createElement('div', null, 'Hello from React!');
}
```
**Present from app context:**
```typescript
bike.frontmostWindow?.presentSheet('./dom/my-component.js', {
width: 400,
height: 300,
modal: false
});
```
1. **Always consult TypeScript API definitions** in `api/` directory - don't guess property names
2. **NO XPath bracket syntax** - Use Bike's query syntax correctly
3. **Queries return objects** - Always access via `.value`
4. **Use transactions** - All outline modifications must be in `transaction()` blocks
5. **Use `var` in evaluate** - `const`/`let` don't persist across AppleScript calls
6. **External modules in build** - `bike/app`, `bike/dom`, `bike/style`, `react`, `react-dom` are external
7. **Row text property** - Use `row.text.string` NOT `row.bodyText`
**Query and export tasks:**
```typescript
import { Outline } from 'bike/app';
bike.commands.addCommands({
'my-ext:exportTasks': {
title: 'Export Tasks',
perform: (context) => {
const result = context.outlineEditor.outline.query('//task @done');
const temp = new Outline(result);
const text = temp.archive('plaintext');
bike.clipboard.writeText(text.data);
}
}
});
```
**Add heading command:**
```typescript
bike.commands.addCommands({
'my-ext:makeHeading': {
title: 'Make Heading',
perform: (context) => {
const editor = context.outlineEditor;
const selection = editor.selection;
editor.transaction({}, () => {
selection.rows.forEach(row => {
row.type = 'heading';
});
});
}
}
});
```
Use this skill when:
Leave a review
No reviews yet. Be the first to review this skill!
# Download SKILL.md from killerskills.ai/api/skills/bike-outliner-extension-development/raw