Files
Obsidian-Pin-Tab-Button/AGENTS.md
Steph Ango ce4fc8c209 First pass
2025-09-04 15:28:25 -07:00

8.9 KiB

Obsidian community plugin

Project overview

  • Target: Obsidian Community Plugin (TypeScript → bundled JavaScript).
  • Entry point: main.ts compiled to main.js and loaded by Obsidian.
  • Required release artifacts: main.js, manifest.json, and optional styles.css.

Environment & tooling

  • Node.js: use current LTS (Node 18+ recommended).
  • Package manager: npm.
  • Bundler: esbuild (preferred). Rollup or webpack are acceptable if they bundle all external dependencies into main.js.
  • Types: obsidian type definitions.

Install

npm install

Dev (watch)

npm run dev

Production build

npm run build

Linting

  • To use eslint install eslint from terminal: npm install -g eslint
  • To use eslint to analyze this project use this command: eslint main.ts
  • eslint will then create a report with suggestions for code improvement by file and line number.
  • If your source code is in a folder, such as src, you can use eslint with this command to analyze all files in that folder: eslint .\src\

File & folder conventions

  • Organize code into multiple files: Split functionality across separate modules rather than putting everything in main.ts.
  • Source lives in src/. Keep main.ts small and focused on plugin lifecycle (loading, unloading, registering commands).
  • Recommended file structure:
    src/
      main.ts           # Plugin entry point, lifecycle management
      settings.ts       # Settings interface and defaults
      commands/         # Command implementations
        command1.ts
        command2.ts
      ui/              # UI components, modals, views
        modal.ts
        view.ts
      utils/           # Utility functions, helpers
        helpers.ts
        constants.ts
      types.ts         # TypeScript interfaces and types
    
  • Do not commit node_modules/ or build artifacts.
  • Keep the plugin small. Avoid large dependencies. Prefer browser-compatible packages.
  • Generated output should be placed at the plugin root or dist/ depending on your build setup. Release artifacts must end up at the top level of the plugin folder in the vault (main.js, manifest.json, styles.css).

Manifest rules (manifest.json)

  • Must include (non-exhaustive):
    • id (plugin ID; for local dev it should match the folder name)
    • name
    • version (Semantic Versioning x.y.z)
    • minAppVersion
    • description
    • isDesktopOnly (boolean)
    • Optional: author, authorUrl, fundingUrl (string or map)
  • Never change id after release. Treat it as stable API.
  • Keep minAppVersion accurate when using newer APIs.

Building & running in Obsidian

  • Manual install for testing: copy main.js, manifest.json, styles.css (if any) to:
    <Vault>/.obsidian/plugins/<plugin-id>/
    
  • Reload Obsidian and enable the plugin in Settings → Community plugins.

Commands & settings

  • Any user-facing commands should be added via this.addCommand(...).
  • If the plugin has configuration, provide a settings tab and sensible defaults.
  • Persist settings using this.loadData() / this.saveData().
  • Use stable command IDs; avoid renaming once released.

Versioning & releases

  • Bump version in manifest.json (SemVer) and update versions.json to map plugin version → minimum app version.
  • Create a GitHub release whose tag exactly matches manifest.json's version (avoid a leading v unless your repo uses it consistently).
  • Attach manifest.json, main.js, and styles.css (if present) to the release as individual assets.
  • After the initial release, follow the process to add/update your plugin in the community catalog as required.

Security, privacy, and compliance

Follow Obsidian's Developer Policies and Plugin Guidelines. In particular:

  • Default to local/offline operation. Only make network requests when essential to the feature.
  • No hidden telemetry. If you collect optional analytics or call third-party services, require explicit opt-in and document clearly in README.md and in settings.
  • Never execute remote code, fetch and eval scripts, or auto-update plugin code outside of normal releases.
  • Minimize scope: read/write only what's necessary inside the vault. Do not access files outside the vault.
  • Clearly disclose any external services used, data sent, and risks.
  • Respect user privacy. Do not collect vault contents, filenames, or personal information unless absolutely necessary and explicitly consented.
  • Avoid deceptive patterns, ads, or spammy notifications.
  • Register and clean up all DOM, app, and interval listeners using the provided register* helpers so the plugin unloads safely.

UX & copy guidelines (for UI text, commands, settings)

  • Prefer sentence case for headings, buttons, and titles.
  • Use clear, action-oriented imperatives in step-by-step copy.
  • Use bold to indicate literal UI labels. Prefer "select" for interactions.
  • Use arrow notation for navigation: Settings → Community plugins.
  • Keep in-app strings short, consistent, and free of jargon.

Performance

  • Keep startup light. Defer heavy work until needed.
  • Avoid long-running tasks during onload; use lazy initialization.
  • Batch disk access and avoid excessive vault scans.
  • Debounce/throttle expensive operations in response to file system events.

Coding conventions

  • TypeScript with "strict": true preferred.
  • Keep main.ts minimal: Focus only on plugin lifecycle (onload, onunload, addCommand calls). Delegate all feature logic to separate modules.
  • Split large files: If any file exceeds ~200-300 lines, consider breaking it into smaller, focused modules.
  • Use clear module boundaries: Each file should have a single, well-defined responsibility.
  • Bundle everything into main.js (no unbundled runtime deps).
  • Avoid Node/Electron APIs if you want mobile compatibility; set isDesktopOnly accordingly.
  • Prefer async/await over promise chains; handle errors gracefully.

Mobile

  • Where feasible, test on iOS and Android.
  • Don't assume desktop-only behavior unless isDesktopOnly is true.
  • Avoid large in-memory structures; be mindful of memory and storage constraints.

Agent do/don't

Do

  • Add commands with stable IDs (don't rename once released).
  • Provide defaults and validation in settings.
  • Write idempotent code paths so reload/unload doesn't leak listeners or intervals.
  • Use this.register* helpers for everything that needs cleanup.

Don't

  • Introduce network calls without an obvious user-facing reason and documentation.
  • Ship features that require cloud services without clear disclosure and explicit opt-in.
  • Store or transmit vault contents unless essential and consented.

Common tasks

Organize code across multiple files

main.ts (minimal, lifecycle only):

import { Plugin } from "obsidian";
import { MySettings, DEFAULT_SETTINGS } from "./settings";
import { registerCommands } from "./commands";

export default class MyPlugin extends Plugin {
  settings: MySettings;

  async onload() {
    this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
    registerCommands(this);
  }
}

settings.ts:

export interface MySettings {
  enabled: boolean;
  apiKey: string;
}

export const DEFAULT_SETTINGS: MySettings = {
  enabled: true,
  apiKey: "",
};

commands/index.ts:

import { Plugin } from "obsidian";
import { doSomething } from "./my-command";

export function registerCommands(plugin: Plugin) {
  plugin.addCommand({
    id: "do-something",
    name: "Do something",
    callback: () => doSomething(plugin),
  });
}

Add a command

this.addCommand({
  id: "your-command-id",
  name: "Do the thing",
  callback: () => this.doTheThing(),
});

Persist settings

interface MySettings { enabled: boolean }
const DEFAULT_SETTINGS: MySettings = { enabled: true };

async onload() {
  this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
  await this.saveData(this.settings);
}

Register listeners safely

this.registerEvent(this.app.workspace.on("file-open", f => { /* ... */ }));
this.registerDomEvent(window, "resize", () => { /* ... */ });
this.registerInterval(window.setInterval(() => { /* ... */ }, 1000));

Troubleshooting

  • Plugin doesn't load after build: ensure main.js and manifest.json are at the top level of the plugin folder under <Vault>/.obsidian/plugins/<plugin-id>/.
  • Commands not appearing: verify addCommand runs after onload and IDs are unique.
  • Settings not persisting: ensure loadData/saveData are awaited and you re-render the UI after changes.
  • Mobile-only issues: confirm you're not using desktop-only APIs; check isDesktopOnly and adjust.

References