Files
tao.chenandClaude cd1aba6698 dev: initial implementation of notebook-snapshot extension
Manual-commit notebook snapshot extension (JupyterLab 4):

Backend
- Storage adapter layer (Local + S3 via boto3, pip extra)
  - Dumb put/get/list/delete contract; key validation
  - S3Storage: lazy boto3 import, S3ConnectionError with friendly
    messages + connectivity check at startup
- SnapshotStore
  - Strip code outputs/execution_count, MD5 on cleaned JSON
  - Gzipped envelope (id/timestamp/name/description/hash/size/notebook)
  - Append-only manifest.json (rebuildable from version files)
  - SnapshotUnchangedError when new hash matches most recent version
- REST API (commit / list / content) under /snapshot/ namespace
- traitlets config (storage_type, local_root, s3_* + S3_* env fallback)

Frontend
- snapshot:commit command (toolbar button + command palette)
- Dialog for name + description
- SnapshotPanel (left sidebar timeline) + DiffWidget (main area)
- Cell diff: id-match first, LCS fallback; jsdiff for line diff
- Restore via model.fromJSON() + context.save() (no refresh)
- All AGENTS.md hard conventions enforced

Tests: 33 backend pytest passing (storage + store + routes)

Docs: AGENTS.md, design.md, README.md synced with implementation.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-21 17:33:55 +08:00

29 KiB
Raw Permalink Blame History

JupyterLab Extension Development

This guide provides coding standards and best practices for developing JupyterLab extensions. Follow these rules to align with community standards and keep your extension maintainable.

Extension type: frontend-and-server


Project: notebook-snapshot

What This Project Is

Notebook 版本快照扩展:自动保存 .ipynb 历史版本(无 Git 依赖),支持侧边栏查看版本时间轴、Cell 级 diff、无刷新恢复。

design.md 是目标架构文档;当前代码仍处于模板初始状态(src/index.ts 调用 /snapshot/hellosnapshot/routes.py 只有 HelloRouteHandler)。实现新功能一律以 design.md 为准,并把模板代码替换为真实实现。

Architecture (design.md 摘要)

  • 前端:手动提交(notebook 工具栏按钮 / 命令面板 snapshot:commit)→ 对话框填 name + description → POST commit无防抖自动提交,不绑定 Ctrl+S);左侧栏版本时间轴(list API);Cell 级 diffrestore 用 model.fromJSON() + context.save() 无刷新覆盖
  • 后端:路由挂在 /snapshot/ 命名空间;存储 adapterLocal 默认;S3 / MinIO 走 boto3 做成 pip extra基础安装保持零依赖);MD5 去重(hash 命中最近版本则弹 warning 跳过);gzip 压缩持久化;manifest.json 记录版本索引
  • API 契约
    • POST /snapshot/notebook-version/commit — body {path, content, name, description},content 为前端显式提交的 notebook JSON 本体(后端从磁盘读);若 hash 与最近版本相同,响应 {skipped: true, reason: "unchanged", message}(不视为错误),前端弹 warning
    • GET /snapshot/notebook-version/list?path=... — 返回 manifest 版本列表(name / description / 时间戳 / hash / 大小)
    • GET /snapshot/notebook-version/content?path=...&id=... — 解压并返回旧版本 notebook JSON(已剥离 outputs
    • POST /snapshot/notebook-version/restore暂缓,前端 restore 走 fromJSON() + save() 闭环

Hard Conventions (从 design.md 评审钉死,实现时不得违背)

  1. Hash 在清洗之后计算 + 去重:快照存的是剥离全部 outputs / execution_count 后的内容,MD5 基于清洗后 JSON 计算;若新内容 hash 与最近版本相同,跳过保存(不写文件、不更新 manifest),通过 SnapshotUnchangedError 通知前端弹 warning
  2. 禁用 MonacoJupyterLab 4 使用 CodeMirror 6,行级 diff 用 diffjsdiff)库或 CodeMirror merge 视图,不得引入第二个编辑器
  3. 历史 append-only:永不改写 / 删除历史版本;restore 走确认对话框(警告未保存修改将丢失,提示先建快照)→ fromJSON()context.save()——注意手动模式下 restore 不再自动生成备份快照
  4. 快照走命令系统:注册 snapshot:commit 命令(工具栏 + 命令面板);不绑定 Ctrl+S(与 docmanager:save 冲突,Lumino 后注册者会吃掉保存),不拦截 DOM 键盘事件
  5. manifest 只是索引:真相在快照文件的 envelope{id, timestamp, name, description, hash, size, notebook})内;manifest 写入原子化(tmp + rename),损坏 / 丢失时扫描版本文件重建
  6. Cell diff 匹配顺序:先按 nbformat 4.5 的 cell id 匹配,LCS 仅作对齐兜底——纯 LCS 在 cell 重排时会产生噪声 diff

Environment

本仓库使用 uv/venv(非 conda):

source .venv/bin/activate

Commands

  • 构建jlpm build(改 TS 后必跑)/ jlpm watch(自动重建)
  • 前端测试jlpm testjest);单文件:jlpm jest src/__tests__/snapshot.spec.ts
  • 后端测试pytest snapshot/tests/ -vv;单用例:pytest snapshot/tests/test_routes.py::test_hello
  • Lintjlpm lint:checkstylelint + prettier + eslint
  • 安装注册pip install -e ".[dev,test]" && jupyter-builder develop . --overwrite && jupyter server extension enable snapshot
  • 集成测试ui-tests/Playwright + Galata,见 ui-tests/README.md

External Documentation and Resources

PRIORITY RESOURCE USAGE

When you encounter uncertainty, incomplete information, or need implementation examples, you MUST consult these external resources FIRST before attempting to implement features.

Use your available tools (web search, documentation search) to access and retrieve content from these resources when:

  • You're unsure about API usage, method signatures, or interface requirements
  • You need to verify the correct approach for a feature or pattern
  • You're looking for existing implementation examples or best practices
  • You're debugging unexpected behavior and need official documentation
  • You're implementing a feature that likely exists in core JupyterLab or other extensions

Required External Resources

These resources are PRIORITY references. Always check them when you need external information:

  1. JupyterLab Extension Developer Guide

  2. JupyterLab API Reference (Frontend)

    • URL: https://jupyterlab.readthedocs.io/en/latest/api/index.html
    • Use for: Complete API reference for all JupyterLab frontend packages, interfaces, classes, and methods
    • Action: Search for specific APIs when you need method signatures, interface definitions, or class documentation. For example, search "JupyterLab IRenderMime.IRenderer" or "JupyterLab ICommandPalette"
  3. JupyterLab Extension Examples Repository

  4. JupyterLab Core Repository

    • URL: https://github.com/jupyterlab/jupyterlab
    • Use for: Reference implementations in packages/ directory - all core packages are extensions themselves
    • Action: When implementing complex features, search this repo for how core extensions solve similar problems
  5. Jupyter Server API Documentation

    • URL: https://jupyter-server.readthedocs.io/
    • Use for: Server-side API handlers, route setup, backend integration patterns
    • Action: Consult when working on backend routes or server extension configuration
  6. Project-Specific Documentation

    • Locations: README.md, RELEASE.md in project root; check for docs/ directory
    • Use for: Project requirements, specific configuration, custom conventions
    • Action: Read these files at the start of work and reference when making architectural decisions

When to Use These Resources

ALWAYS consult external documentation when:

  • You're about to implement a feature without knowing if there's an established pattern
  • An API call or method isn't working as expected
  • You need to understand the correct lifecycle methods or hooks
  • You're uncertain about type definitions or interfaces
  • You're implementing something that seems like it should be a common pattern

HOW to access these resources:

  • 🔍 Use web search tools with specific queries like: "JupyterLab IRenderMime.IRenderer interface documentation"
  • 🔍 Search GitHub repositories for code examples: "JupyterLab extension examples widget"
  • 🔍 Retrieve documentation pages to read API specifications and usage guidelines
  • 🔍 Look for working code in the extension-examples repository before writing custom implementations

Remember: These resources contain the authoritative information. Don't guess at API usage - look it up!

Code Quality Rules

Logging and Debugging

Don't: Use console.log() Do: Use structured logging or user-facing notifications

// In TypeScript files like src/index.ts
import { INotification } from '@jupyterlab/apputils';
app.commands.notifyCommandChanged();

Do: Use console.error() to log low-level error details that should not be presented to users in the UI Do: Use console.warn() to log non-optimal conditions, e.g. an unexpected response from an external API that's been successfully handled.

Type Safety

Do: Define explicit interfaces (see example patterns in src/index.ts)

interface PluginConfig {
  enabled: boolean;
  apiEndpoint: string;
}

Don't: Use the any type in TypeScript files Do: Prefer typeguards over type casts

File-Scoped Validation

After editing TypeScript files, run:

npx tsc --noEmit src/index.ts  # Check single file
npx tsc --noEmit               # Check all files

After editing Python files (like snapshot/routes.py):

python -m py_compile snapshot/__init__.py  # Check single file for syntax errors

Coding Standards

Naming Conventions

Python (in snapshot/*.py files):

  • Do: Use PEP 8 style with 4-space indentation
    • Classes: DataProcessor, UserDataRouteHandler
    • Functions/methods: setup_route_handlers(), process_request()
    • Private: _internal_method()
  • Don't: Use camelCase for Python or mix styles

TypeScript/JavaScript (in src/*.ts files):

  • Do: Use consistent casing
    • Classes/interfaces: MyPanelWidget, PluginConfig
    • Functions/variables: activatePlugin(), buttonCount
    • Constants: PLUGIN_ID, COMMAND_ID
  • Do: Use 2-space indentation (Prettier default)
  • Don't: Use lowercase_snake_case or inconsistent formatting

Documentation

Do: Add JSDoc for TypeScript and docstrings for Python

/**
 * Activates the extension plugin.
 * @param app - JupyterLab application instance
 */
function activate(app: JupyterFrontEnd): void {}

Don't: Leave complex logic undocumented or use vague names like MyRouteHandler — prefer DataUploadRouteHandler

Code Organization

Do: Keep backend and frontend logic separate

  • Backend processing in snapshot/routes.py
  • Frontend calls in src/request.ts using requestAPI()

Don't: Duplicate business logic across TypeScript and Python

Do: Implement features completely or not at all. Notify the prompter if you're unable to completely implement a feature.

Don't: Leave TODO comments or dead code in committed files

Project Structure and Naming

Package Naming

Python package (directory name and imports):

  • Do: snapshot/ with underscores, all lowercase
  • Don't: Use dashes in any Python file or directory names

PyPI distribution name (in pyproject.toml):

  • Do: Use dashes instead of underscores, like jupyterlab-myext
  • Do: Match it to the npm package name for consistency

NPM package (in package.json):

  • Do: Use lowercase with dashes: "jupyterlab-myext" or scoped "@org/myext"
  • Don't: Mix naming styles between package.json and pyproject.toml

Plugin and Command IDs

Do: Define plugin ID in src/index.ts:

const PLUGIN_ID = 'snapshot:plugin';

Do: For extensions with multiple commands, create a src/commands.ts module to centralize command definitions:

// src/commands.ts
import { JupyterFrontEnd } from '@jupyterlab/application';
import { ReadonlyPartialJSONObject } from '@lumino/coreutils';

// Command IDs
export namespace CommandIDs {
  export const openPanel = 'snapshot:open-panel';
  export const refreshData = 'snapshot:refresh-data';
}

// Command argument types
export namespace CommandArguments {
  export interface IOpenPanel {
    filePath?: string;
  }

  export interface IRefreshData {
    force?: boolean;
  }
}

/**
 * Register all commands with the application command registry.
 * Call this function in your plugin's activate function.
 */
export function registerCommands(app: JupyterFrontEnd): void {
  // Register the openPanel command
  app.commands.addCommand(CommandIDs.openPanel, {
    label: 'Open Panel',
    caption: 'Open the extension panel',
    execute: (args: ReadonlyPartialJSONObject) => {
      const typedArgs = args as CommandArguments.IOpenPanel;
      // Implementation using typedArgs.filePath
    }
  });

  // Register the refreshData command
  app.commands.addCommand(CommandIDs.refreshData, {
    label: 'Refresh Data',
    execute: (args: ReadonlyPartialJSONObject) => {
      const typedArgs = args as CommandArguments.IRefreshData;
      // Implementation using typedArgs.force
    }
  });
}

Then in src/index.ts:

import {
  JupyterFrontEnd,
  JupyterFrontEndPlugin
} from '@jupyterlab/application';
import { registerCommands, CommandIDs, CommandArguments } from './commands';

const plugin: JupyterFrontEndPlugin<void> = {
  id: 'snapshot:plugin',
  autoStart: true,
  activate: (app: JupyterFrontEnd) => {
    // Register all commands with JupyterLab's command registry
    registerCommands(app);

    // Commands are now registered and can be executed anywhere:
    // - From the command palette
    // - From menus
    // - Programmatically via app.commands.execute()

    // ... rest of activation (e.g., add to palette, create widgets, etc.)
  }
};

export default plugin;

Executing commands with typed arguments:

import { CommandIDs, CommandArguments } from './commands';

// Execute with typed arguments
await app.commands.execute(CommandIDs.openPanel, {
  filePath: '/path/to/file'
} as CommandArguments.IOpenPanel);

// Execute without arguments
await app.commands.execute(CommandIDs.refreshData);

Notes:

  • Accept ReadonlyPartialJSONObject in the execute function signature (required by Lumino)
  • Cast to your typed interface inside the function for type safety
  • Use namespaces (CommandIDs, CommandArguments) to organize related constants and types
  • This pattern matches how popular extensions like jupyterlab-git handle commands

Do: For simple extensions with 1-2 commands, you can define them directly in src/index.ts

Don't: Use generic IDs like 'mycommand' or mix casing styles

File Organization

Do: Organize related files into directories and name by their purpose

  • Widget components: src/widgets/DataPanel.tsx (class DataPanel)
  • Command definitions (for multiple commands): src/commands.ts with COMMANDS mapping
  • API utilities: src/api.ts (not src/utils.ts)
  • Backend routes: snapshot/routes.py (class DataRouteHandler)
  • Frontend logic: src/ directory
  • Python package: snapshot/ directory

Don't: Create catch-all files or directories like utils.ts or helpers.py or handlers.py — partition by feature instead

BackendFrontend Integration

Integration Workflow (Critical!)

When connecting frontend and backend, ALWAYS follow this order:

  1. Read the backend first — Check snapshot/routes.py to understand the existing API contract
  2. Write frontend to match — Create TypeScript interfaces in src/api.ts that match backend responses exactly
  3. Or modify backend intentionally — If changing the backend, update it first, then write matching frontend code

Why this matters: Writing frontend code based on assumptions leads to field name mismatches (e.g., expecting message when backend returns data), causing empty widgets and debugging cycles. Always verify the actual backend response format first.

Backend Routes

Create RESTful endpoints in snapshot/routes.py:

Do: Extend APIHandler from jupyter_server.base.handlers

from jupyter_server.base.handlers import APIHandler
from jupyter_server.utils import url_path_join

class DataRouteHandler(APIHandler):
    def get(self):
        """Handle GET requests."""
        result = {"status": "success", "data": "Hello"}
        self.finish(result)

    def post(self):
        """Handle POST requests."""
        body = self.get_json_body()
        # Process body...
        self.finish({"status": "success"})

def setup_route_handlers(web_app):
    base_url = web_app.settings.get("base_url", "/")
    data_route = url_path_join(base_url, "snapshot", "data")
    web_app.add_handlers(r".*$", [(data_route, DataRouteHandler)])

Do: Include error handling in route handlers

Don't:

  • Hardcode URL paths — always use url_path_join()
  • Use plain tornado.web.RequestHandler — instead, use APIHandler from jupyter_server.base.handlers

Frontend API Calls

Do: Call backend endpoints from typed API functions in src/api.ts (not directly in widgets):

import { ServerConnection } from '@jupyterlab/services';
import { requestAPI } from './request';

interface DataResponse {
  status: 'success' | 'error';
  data: string;
}

export async function fetchData(
  serverSettings: ServerConnection.ISettings
): Promise<string> {
  try {
    const response = await requestAPI<DataResponse>('data', serverSettings, {
      method: 'GET'
    });
    if (response.status === 'error') {
      throw new Error('Server returned error');
    }
    return response.data;
  } catch (err) {
    // Extract detailed error information from ResponseError
    if (err instanceof ServerConnection.ResponseError) {
      const status = err.response.status;
      let detail = err.message;

      // Truncate HTML responses for cleaner error messages
      if (
        typeof detail === 'string' &&
        (detail.includes('<!DOCTYPE') || detail.includes('<html'))
      ) {
        detail = `HTML error page (${detail.substring(0, 100)}...)`;
      }

      throw new Error(`API request failed (${status}): ${detail}`);
    }

    const msg = err instanceof Error ? err.message : 'Unknown error';
    throw new Error(`API request failed: ${msg}`);
  }
}

Do:

  • Always wrap API calls in try-catch blocks with proper error handling
  • Check for ServerConnection.ResponseError to extract HTTP status codes and response details
  • Handle HTML error responses gracefully by truncating them (they're often unhelpful error pages)
  • Include response status codes in error messages for better debugging
  • Use matching response types between Python and TypeScript
  • Create typed API wrapper functions in src/api.ts instead of calling requestAPI() directly from widgets

API Sync and Naming

Do: Keep backend and frontend in sync

  • Match JSON keys: {"result": ...} in Python → response.result in TypeScript
  • Update TypeScript interfaces when changing Python responses
  • Define matching endpoint path strings (e.g., "hello", "get-data") in both snapshot/routes.py and src/api.ts to ensure routes sync between backend and frontend

Don't:

  • Create unused routes or orphaned API calls
  • Use inconsistent field naming across languages

Development Workflow

Environment Activation (CRITICAL)

Before ANY command, ensure you're in the correct environment. This repository uses a uv-managed venv at .venv/ (see Environment above):

source .venv/bin/activate  # macOS/Linux (this repo)

All jlpm, pip, and jupyter commands MUST run within the activated environment.

Symptoms of running outside the environment:

  • jlpm: command not found
  • Extension not appearing after build
  • jupyter: command not found

Do: Always activate your environment first Don't: Run commands in your base/system environment


Complete Development Workflow Checklist

When implementing a new feature from scratch, follow this complete sequence:

  1. Activate environment (see above — required first!)
  2. Write the code (TypeScript in src/, styles in style/, Python in snapshot/)
  3. Install dependencies (if you added any to package.json):
    jlpm install
    
  4. Build the extension:
    jlpm build
    
  5. Install the extension (REQUIRED for JupyterLab to recognize it):
    pip install -e .
    jupyter-builder develop . --overwrite
    jupyter server extension enable snapshot
    
  6. Verify installation:
    jupyter labextension list  # Should show your extension as "enabled" and "OK"
    jupyter server extension list  # Should show backend extension
    
  7. Start JupyterLab:
    jupyter lab
    
  8. Test the feature in your browser

Critical: Steps 5-7 are REQUIRED after building. Building alone is not enough!


Understanding Build vs Install

Many issues arise from confusing these two steps:

jlpm build — Compiles the Extension. Do this every time you change TypeScript code.

  • What it does: Compiles TypeScript → JavaScript, bundles the extension
  • Output: Creates files in lib/ and snapshot/labextension/
  • What it does NOT do: Register the extension with JupyterLab

pip install -e . + jupyter-builder develop . — Registers the Extension. Do this once as a setup step.

  • What it does: Tells JupyterLab where to find your extension
  • Output: Creates symlinks so changes are reflected
  • Note: Also installs the Python package in editable mode
  • Result: Extension appears in JupyterLab

You need BOTH steps! Building prepares the code; installing registers it with JupyterLab.

Common mistake: Running only jlpm build and expecting the extension to appear. It won't show up until you also run the installation commands.


Initial Setup (run once)

pip install -e ".[dev,test]"
jupyter-builder develop . --overwrite
jupyter server extension enable snapshot

Iterative Development

Development with auto-rebuild (recommended):

jlpm run watch                      # Auto-rebuild on file changes (keep running)
# In another terminal:
jupyter lab

After editing TypeScript (files in src/):

  • If using jlpm run watch: Just refresh your browser (Cmd+R / Ctrl+R)
  • If not using watch: Run jlpm build, then refresh your browser

Quick TypeScript validation (optional, for fast feedback):

npx tsc --noEmit src/index.ts       # Check single file

After editing Python (files in snapshot/):

  • Restart the JupyterLab server (Ctrl+C in terminal, then jupyter lab again)
  • No rebuild needed!
  • Only run pip install -e . if you changed package structure (renamed package directory, or modified entry points in pyproject.toml)

Memory aid: "What did you change? Restart that!"

  • Changed JavaScript → Build (or auto-builds with watch) → Refresh browser
  • Changed PythonRestart JupyterLab server (no build needed)

Debugging and Diagnostics

jupyter labextension list           # Check if extension is installed
jupyter server extension list        # Check backend extension
jlpm run lint                # Lint frontend code

Browser console (ask user to check):

  • Request user to open browser console (F12 or Cmd+Option+I)
  • Ask user to report any JavaScript errors
  • Ask user to check for failed network requests to backend endpoints
  • Ask user if the extension appears to be loaded

Server logs (terminal running jupyter lab):

  • Check for Python errors or exceptions
  • Verify backend routes are registered
  • Look for HTTP request logs

Troubleshooting: Extension Not Appearing

If your extension doesn't appear in JupyterLab after building:

1. Check if the extension is installed:

jupyter labextension list

Your extension should appear as "enabled" and "OK".

2. If NOT in the list, run the installation commands:

pip install -e .
jupyter-builder develop . --overwrite
jupyter server extension enable snapshot

3. Did you restart JupyterLab?

  • Changes require a full restart (Ctrl+C in terminal, then jupyter lab again)
  • Simply refreshing the browser is NOT enough for new extensions

4. Ask user to check the browser console (F12 or Cmd+Option+I):

  • Request user to look for JavaScript errors that might prevent extension activation
  • Ask user to search for the extension ID (snapshot) to see if it loaded
  • Ask user to report any error messages or warnings

5. Verify the build output:

ls -la lib/                          # Should contain compiled .js files
ls -la snapshot/labextension/  # Should contain bundled extension

6. If still not working, try a clean rebuild following the reset instructions below

Common causes:

  • Only ran jlpm build without installation commands
  • Forgot to restart JupyterLab after installation
  • Running commands outside the activated environment
  • Build errors that were missed (check terminal output)

Reset (if build state is broken)

jlpm clean:all       # Clean build artifacts
# git clean -fdX     # (Optional) Remove all ignored files including node_modules
jlpm install         # Only needed if you used 'git clean -fdX'
jlpm build
pip install -e ".[dev,test]"
jupyter-builder develop . --overwrite
jupyter server extension enable snapshot

Environment Notes

Do: Use a virtual environment (conda/mamba/micromamba/venv) Do: Use jlpm exclusively Don't: Mix package managers (npm, yarn) with jlpm Don't: Mix lockfiles — keep only yarn.lock, not package-lock.json

Best Practices

Project Structure Alignment

Do: Follow the template structure

  • Keep configuration files in project root: package.json, pyproject.toml, tsconfig.json
  • Backend routes: snapshot/routes.py
  • Server extension config: jupyter-config/server-config/snapshot.json
  • Frontend code: src/index.ts and other src/ files
  • Styles: style/index.css
  • Settings schema: schema/plugin.json

Don't: Rename or move core files without updating all references in configuration

Version Management

Do: Update version in package.json only

  • The package.json version is the source of truth
  • pyproject.toml automatically syncs from package.json via hatch-nodejs-version
  • Follow semantic versioning: MAJOR.MINOR.PATCH

Don't: Manually edit version in pyproject.toml — it's dynamically sourced from package.json

Note: Releases are handled by GitHub Actions, not manually. AI agents should only update versions when explicitly requested by the user.

Development Approach

Do: Start simple and iterate

  • Begin with minimal functionality (e.g., a single command or widget)
  • When integrating backend/frontend: See Integration Workflow for the correct order
  • Add backend routes or verbs only when frontend needs them
  • Test in running JupyterLab frequently
  • Ask user to check browser console and review terminal logs for errors

Don't: Build complex features without incremental testing

Don't: Write frontend interfaces without first checking the backend API contract in snapshot/routes.py

Common Pitfalls

Package Management

Do: Use jlpm consistently

jlpm install
jlpm build

Don't: Mix package managers or lockfiles

  • Don't use package-lock.json (this project uses yarn.lock)
  • Don't run npm install

Path Handling

Do: Use relative imports in TypeScript (src/ files)

import { MyWidget } from './widgets/MyWidget';

Don't: Use absolute paths or assume specific directory structures

Error Handling

Do: Wrap async operations in try-catch (in src/api.ts, widget code)

try {
  const data = await fetchData();
} catch (err) {
  showErrorMessage('Failed to fetch data');
}

Don't: Let errors propagate silently or crash the extension

CSS and Styling

Do: Namespace all CSS in style/index.css

.jp-snapshot-widget {
  padding: 8px;
}

Don't: Use generic class names like .widget or .button

Resource Cleanup

Do: Dispose resources in widget dispose() methods

dispose(): void {
  this._signal.disconnect();
  super.dispose();
}

Don't: Leave event listeners or signal connections active after disposal

Backend Integration

Do: Use relative imports within your package

from .routes import setup_route_handlers

Don't: Use absolute imports like from snapshot.routes import ...

Quick Reference

Key Identifiers

Use these patterns consistently throughout your code:

  • Plugin ID (in src/index.ts): 'snapshot:plugin'
  • Command IDs (in src/commands.ts or src/index.ts): 'snapshot:command-name'
    • For multiple commands, create src/commands.ts with a centralized COMMANDS mapping
    • For 1-2 commands, define directly in src/index.ts
  • CSS classes (in style/index.css): .jp-snapshot-ClassName
  • API routes (in snapshot/routes.py): url_path_join(base_url, "snapshot", "endpoint")

Essential Commands

See Development Workflow section for full command reference.