From c8148f66ef5f4d50454bdc1f9450477b5adce414 Mon Sep 17 00:00:00 2001 From: charlie Date: Wed, 7 Jan 2026 20:43:45 +0800 Subject: [PATCH] chore(libs): add Logseq plugin development agent guide --- libs/development-notes/AGENTS.md | 627 +++++++++++++++++++++++++++++++ 1 file changed, 627 insertions(+) create mode 100644 libs/development-notes/AGENTS.md diff --git a/libs/development-notes/AGENTS.md b/libs/development-notes/AGENTS.md new file mode 100644 index 0000000000..918fe0eeea --- /dev/null +++ b/libs/development-notes/AGENTS.md @@ -0,0 +1,627 @@ +# AGENTS.md - Logseq Plugin Development Guide + +## Overview + +This document provides guidance for AI agents (Copilot, coding assistants, etc.) to help develop Logseq plugins using +the `@logseq/libs` SDK. + +Logseq is a privacy-first, open-source knowledge management and note-taking application built on top of local plain-text +Markdown and Org-mode files. The plugin system extends Logseq's functionality through a JavaScript/TypeScript SDK. + +## Repository Structure + +### Core SDK (`logseq/logseq` - `libs/` folder) + +The official SDK source code is located in the `libs/` directory of the main Logseq repository: + +``` +libs/ +├── src/ +│ ├── LSPlugin.ts # Main type definitions & interfaces +│ ├── LSPlugin.user.ts # User-facing plugin API implementation +│ ├── LSPlugin.core.ts # Core plugin system +│ ├── LSPlugin. caller.ts # IPC message caller +│ ├── LSPlugin.shadow.ts # Shadow DOM support +│ ├── helpers.ts # Utility functions +│ ├── modules/ # Additional modules (Experiments, Storage, Request) +│ └── postmate/ # Cross-frame communication +├── package.json # NPM package config (@logseq/libs) +├── README.md # SDK documentation +└── CHANGELOG.md # Version history +``` + +### Plugin Samples (`logseq/logseq-plugin-samples`) + +Official sample plugins demonstrating various SDK features: + +| Sample | Description | Key APIs | +|-------------------------------|----------------------------------|-------------------------------------------------| +| `logseq-slash-commands` | Basic slash command registration | `Editor.registerSlashCommand` | +| `logseq-pomodoro-timer` | Macro renderer & slot UI | `App.onMacroRendererSlotted`, `provideUI` | +| `logseq-a-translator` | Selection hooks & float UI | `Editor.onInputSelectionEnd`, `provideUI` | +| `logseq-awesome-fonts` | Custom styles & settings | `provideStyle`, `useSettingsSchema` | +| `logseq-bujo-themes` | Custom theme development | `provideTheme` | +| `logseq-journals-calendar` | Calendar integration | `App.getCurrentGraph`, `Editor.getPage` | +| `logseq-emoji-picker` | UI overlay with 3rd party libs | `showMainUI`, `hideMainUI` | +| `logseq-reddit-hot-news` | Batch block insertion | `Editor.insertBatchBlock`, `App.registerUIItem` | +| `logseq-imdb-top250-importer` | DB graph data import | DB-specific APIs | + +## Plugin Architecture + +### Entry Point Pattern + +Every Logseq plugin follows this basic pattern: + +```typescript +import '@logseq/libs' + +async function main() { + // Plugin initialization code + console.log('Plugin loaded!') +} + +// Bootstrap +logseq.ready(main).catch(console.error) +``` + +### Plugin Modes + +Plugins can run in two modes defined in `package.json`: + +- **`iframe`** (default): Plugin runs in an isolated iframe sandbox +- **`shadow`** (still draft): Plugin runs in a Shadow DOM container (faster, less isolation) + +> **⚠️ Sandbox Environment:** All Logseq plugins run in an isolated iframe sandbox. +> This provides security isolation between plugins and the +> main application. Communication between the plugin iframe and +> Logseq happens through a postMessage-based RPC mechanism. + +### package.json Configuration + +## Package. json Configuration + +### Basic Structure + +```json +{ + "name": "logseq-my-plugin", + "version": "0.1.0", + "description": "A brief description of your plugin", + "author": "Your Name", + "license": "MIT", + "main": "dist/index.html", + "scripts": { + "dev": "parcel ./index.html --public-url ./", + "build": "parcel build --public-url . --no-source-maps index.html" + }, + "devDependencies": { + "@logseq/libs": "^0.0.17", + "parcel": "^2.0.0" + }, + "logseq": { + "id": "my-unique-plugin-id", + "main": "dist/index.html", + "icon": "./icon.png" + } +} +``` + +### Logseq Configuration Block + +The `logseq` field in `package.json` defines plugin metadata and behavior: + +```json +{ + "logseq": { + "id": "my-unique-plugin-id", + "main": "dist/index.html", + "icon": "./icon. png", + "title": "My Plugin Display Name", + "effect": true, + "themes": [ + { + "name": "My Dark Theme", + "url": "./css/dark.css", + "mode": "dark", + "description": "A beautiful dark theme" + }, + { + "name": "My Light Theme", + "url": "./css/light.css", + "mode": "light", + "description": "A clean light theme" + } + ] + } +} +``` + +### Configuration Fields + +| Field | Type | Required | Description | +|------------|-----------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `id` | `string` | Yes | Unique plugin identifier. Must be unique across all plugins. Use lowercase with hyphens. | +| `main` | `string` | Yes | Entry point HTML file path (relative to package.json). | +| `entry` | `string` | No | Alias for `main`. | +| `icon` | `string` | No | Plugin icon path. Displayed in plugin marketplace and settings. | +| `title` | `string` | No | Display name shown in UI. Falls back to package `name` if not set. | +| `effect` | `boolean` | No | When `true`, the plugin runs as a background effect without user interaction. Useful for plugins that only inject styles/themes or run automated tasks. Default: `false`. | +| `themes` | `Theme[]` | No | Array of theme definitions this plugin provides. | +| `devEntry` | `string` | No | Alternative entry point for development (e.g., with HMR). | + +### Theme Configuration + +For theme-only plugins, define themes in the `themes` array: + +```json +{ + "logseq": { + "id": "my-theme-pack", + "main": "dist/index.html", + "effect": true, + "themes": [ + { + "name": "Nord Dark", + "url": "./themes/nord-dark.css", + "mode": "dark", + "description": "Nord-inspired dark color scheme" + }, + { + "name": "Nord Light", + "url": "./themes/nord-light.css", + "mode": "light", + "description": "Nord-inspired light color scheme" + } + ] + } +} +``` + +**Theme Object Fields:** + +| Field | Type | Required | Description | +|---------------|----------|----------|--------------------------------------------------| +| `name` | `string` | Yes | Theme display name shown in theme selector. | +| `url` | `string` | Yes | Path to the CSS file (relative to package.json). | +| `mode` | `light` | `dark` | Yes | Theme mode. Determines when theme appears in selector. | +| `description` | `string` | No | Brief description of the theme. | + +## Core API Namespaces + +The SDK exposes the following primary namespaces through the global `logseq` object: + +### `logseq. App` - Application APIs + +Application-level operations and hooks: + +```typescript +// Get app/user information +await logseq.App.getInfo() +await logseq.App.getUserConfigs() +await logseq.App.getCurrentGraph() + +// Graph operations +await logseq.App.getCurrentGraphFavorites() +await logseq.App.getCurrentGraphRecent() + +// Navigation +logseq.App.pushState('page', { name: 'my-page' }) + +// UI Registration +logseq.App.registerUIItem('toolbar', { + key: 'my-button', + template: `Click` +}) + +// Command Registration +logseq.App.registerCommandPalette({ + key: 'my-command', + label: 'My Command', + keybinding: { binding: 'mod+shift+m' } +}, () => { /* action */ }) + +// Event Hooks +logseq.App.onCurrentGraphChanged(() => {}) +logseq.App.onThemeModeChanged(({ mode }) => {}) +logseq.App.onRouteChanged(({ path }) => {}) +logseq.App.onMacroRendererSlotted(({ slot, payload }) => {}) +``` + +### `logseq.Editor` - Editor APIs + +Block and page manipulation: + +```typescript +// Slash Commands +logseq.Editor.registerSlashCommand('My Command', async ({ uuid }) => { + await logseq.Editor.insertAtEditingCursor('Hello!') +}) + +// Block Context Menu +logseq.Editor.registerBlockContextMenuItem('My Action', async ({ uuid }) => { + const block = await logseq.Editor.getBlock(uuid) +}) + +// Block Operations +await logseq.Editor.getBlock(uuid) +await logseq.Editor.insertBlock(uuid, 'content', { sibling: true }) +await logseq.Editor.updateBlock(uuid, 'new content') +await logseq.Editor.removeBlock(uuid) +await logseq.Editor.insertBatchBlock(uuid, [ + { content: 'Block 1', children: [{ content: 'Child' }] } +]) + +// Page Operations +await logseq.Editor.getPage('page-name') +await logseq.Editor.createPage('new-page', { prop: 'value' }) +await logseq.Editor.deletePage('page-name') +await logseq.Editor.getCurrentPageBlocksTree() + +// Cursor & Selection +await logseq.Editor.checkEditing() +await logseq.Editor.getEditingCursorPosition() +await logseq.Editor.insertAtEditingCursor('text') +await logseq.Editor.exitEditingMode() + +// Properties (DB graphs) +await logseq.Editor.upsertBlockProperty(uuid, 'key', 'value') +await logseq.Editor.getBlockProperties(uuid) +``` + +### `logseq.DB` - Database APIs + +Query and data operations: + +```typescript +// DSL Query (Logseq query language) +const results = await logseq.DB.q('[[my-page]]') + +// Datascript Query +const results1 = await logseq.DB.datascriptQuery(` + [: find (pull ?b [*]) + :where [?b :block/marker "TODO"]] +`) + +// Watch for changes +logseq.DB.onChanged(({ blocks, txData }) => { + console.log('Database changed:', blocks) +}) + +// Watch specific block +logseq.DB.onBlockChanged(uuid, (block, txData) => { + console.log('Block changed:', block) +}) + +// File operations +await logseq.DB.getFileContent('logseq/custom.css') +await logseq.DB.setFileContent('logseq/custom.js', 'console.log("hi")') +``` + +### `logseq.UI` - UI Utilities + +```typescript +// Toast messages +await logseq.UI.showMsg('Success! ', 'success') +await logseq.UI.showMsg('Warning!', 'warning', { timeout: 5000 }) +logseq.UI.closeMsg(key) + +// DOM queries +const rect = await logseq.UI.queryElementRect('.my-selector') +const exists = await logseq.UI.queryElementById('my-id') +``` + +### `logseq.Git` - Git Operations + +```typescript +const result = await logseq.Git.execCommand(['status']) +const ignoreContent = await logseq.Git.loadIgnoreFile() +await logseq.Git.saveIgnoreFile('. DS_Store\nnode_modules') +``` + +### `logseq.Assets` - Asset Management + +```typescript +const files = await logseq.Assets.listFilesOfCurrentGraph(['png', 'jpg']) +const storage = logseq.Assets.makeSandboxStorage() +const url = await logseq.Assets.makeUrl('path/to/file') +await logseq.Assets.builtInOpen('path/to/asset. pdf') +``` + +## UI Injection Patterns + +### provideUI - Inject Custom UI + +```typescript +logseq.provideUI({ + key: 'my-ui', + path: '#my-target', // or use `slot` for macro renderers + template: `
Click me
`, + style: { backgroundColor: 'white', padding: '10px' }, + attrs: { title: 'My UI' }, + close: 'outside', // close when clicking outside +}) +``` + +### provideStyle - Inject CSS + +```typescript +logseq.provideStyle(` + .my-class { + color: var(--ls-primary-text-color); + background: var(--ls-primary-background-color); + } +`) + +// Or with a key for updates +logseq.provideStyle({ + key: 'my-styles', + style: `.my-class { color: red; }` +}) +``` + +### provideModel - Event Handlers + +```typescript +logseq.provideModel({ + myHandler(e) { + console.log('Clicked!', e.dataset) + }, + async asyncHandler(e) { + const block = await logseq.Editor.getBlock(e.dataset.blockUuid) + } +}) +``` + +### Main UI (Full-screen Overlay) + +```typescript +// Show plugin's main UI +logseq.showMainUI({ autoFocus: true }) + +// Hide it +logseq.hideMainUI({ restoreEditingCursor: true }) + +// Toggle +logseq.toggleMainUI() + +// Style the main UI container +logseq.setMainUIInlineStyle({ + position: 'fixed', + top: '100px', + left: '50%', + zIndex: 11, +}) +``` + +## Settings Schema + +Define user-configurable settings: + +```typescript +logseq.useSettingsSchema([ + { + key: 'apiKey', + type: 'string', + default: '', + title: 'API Key', + description: 'Enter your API key' + }, + { + key: 'enableFeature', + type: 'boolean', + default: true, + title: 'Enable Feature', + description: 'Toggle this feature on/off' + }, + { + key: 'theme', + type: 'enum', + enumChoices: ['light', 'dark', 'auto'], + enumPicker: 'select', + default: 'auto', + title: 'Theme', + description: 'Select theme mode' + }, + { + key: 'fontSize', + type: 'number', + default: 14, + title: 'Font Size', + description: 'Customize font size', + inputAs: 'range' + } +]) + +// Access settings +const apiKey = logseq.settings?.apiKey + +// Update settings +logseq.updateSettings({ apiKey: 'new-key' }) + +// Listen for changes +logseq.onSettingsChanged((newSettings, oldSettings) => { + console.log('Settings changed:', newSettings) +}) +``` + +## Macro Renderer Pattern + +Create custom block renderers using `{{renderer : type, arg1, arg2}}`: + +```typescript +logseq.App.onMacroRendererSlotted(({ slot, payload }) => { + const [type, ...args] = payload.arguments + + if (type !== ': my-renderer') return + + logseq.provideUI({ + key: `my-renderer-${payload.uuid}`, + slot, + template: ` +
+ ${args.join(', ')} +
+ `, + }) +}) + +// Register slash command to insert the macro +logseq.Editor.registerSlashCommand('Insert My Renderer', async () => { + await logseq.Editor.insertAtEditingCursor( + `{{renderer :my-renderer, arg1, arg2}}` + ) +}) +``` + +## Key Types Reference + +### Block Entity + +```typescript +interface BlockEntity { + id: number // Database entity ID + uuid: string // Block UUID + title: string // Block content + content?: string // @deprecated, use title + format: 'markdown' | 'org' + parent: { id: number } + page: { id: number } + properties?: Record + children?: BlockEntity[] + 'collapsed? ': boolean + createdAt: number + updatedAt: number +} +``` + +### Page Entity + +```typescript +interface PageEntity { + id: number + uuid: string + name: string + type: 'page' | 'journal' | 'whiteboard' | 'class' | 'property' | 'hidden' + format: 'markdown' | 'org' + 'journal? ': boolean + journalDay?: number // YYYYMMDD format for journals + properties?: Record + createdAt: number + updatedAt: number +} +``` + +## Development Workflow + +### Setup + +```bash +# Clone sample repository +git clone https://github.com/logseq/logseq-plugin-samples +cd logseq-plugin-samples/logseq-slash-commands + +# Install dependencies +npm install # or yarn + +# Build +npm run build +``` + +### Loading in Logseq + +1. Open Logseq Desktop +2. Enable **Developer mode** in Settings +3. Press `t p` to open Plugins dashboard +4. Click **Load unpacked plugin** +5. Select your plugin directory + +### Debugging + +- Use browser DevTools (Ctrl/Cmd + Shift + I) +- Check console for `@logseq/libs` debug output +- Use `logseq.UI.showMsg()` for quick debugging + +## Best Practices + +### Performance + +1. **Debounce frequent operations** - especially DB queries and UI updates +2. **Use `onBlockChanged` sparingly** - it fires on every edit +3. **Batch block operations** - use `insertBatchBlock` instead of multiple `insertBlock` calls +4. **Clean up listeners** - store and call off-hooks in `beforeunload` + +```typescript +const offHooks: (() => void)[] = [] + +offHooks.push( + logseq.DB.onChanged(() => { /* ... */ }) +) + +logseq.beforeunload(async () => { + offHooks.forEach(off => off()) +}) +``` + +### Error Handling + +```typescript +try { + const block = await logseq.Editor.getBlock(uuid) + if (!block) { + await logseq.UI.showMsg('Block not found', 'warning') + return + } + // ... proceed +} catch (error) { + console.error('Plugin error:', error) + await logseq.UI.showMsg('An error occurred', 'error') +} +``` + +### CSS Variables + +Use Logseq's CSS custom properties for consistent theming: + +```css +.my-plugin-ui { + color: var(--ls-primary-text-color); + background: var(--ls-primary-background-color); + border: 1px solid var(--ls-border-color); +} +``` + +### DB vs File Graphs + +Check graph type before using DB-specific features: + +```typescript +const isDbGraph = await logseq.App.checkCurrentIsDbGraph() +if (isDbGraph) { + // Use DB-specific APIs + await logseq.Editor.upsertProperty('my-prop', { type: 'string' }) +} +``` + +## Resources + +- **API Documentation**: https://plugins-doc.logseq.com +- **Plugin Samples**: https://github.com/logseq/logseq-plugin-samples +- **SDK Source**: https://github.com/logseq/logseq/tree/master/libs +- **NPM Package**: https://www.npmjs.com/package/@logseq/libs +- **Discord Community**: https://discord.gg/KpN4eHY +- **Community Templates**: + - ClojureScript: https://github.com/logseq/cljs-plugin-example + - TypeScript: https://github.com/YU000jp/logseq-plugin-sample-kit-typescript + +## Version Compatibility + +Always check the minimum Logseq version required for specific APIs. Keep `@logseq/libs` updated to the latest version +for best compatibility and performance. + +```bash +# Check current version +npm info @logseq/libs version + +# Update +npm update @logseq/libs +``` \ No newline at end of file