Files
notebook-snapshot-extension/AGENTS.md
T
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

816 lines
29 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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/hello``snapshot/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. **禁用 Monaco**JupyterLab 4 使用 CodeMirror 6,行级 diff 用 `diff`jsdiff)库或 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):
```bash
source .venv/bin/activate
```
## Commands
- **构建**`jlpm build`(改 TS 后必跑)/ `jlpm watch`(自动重建)
- **前端测试**`jlpm test`jest);单文件:`jlpm jest src/__tests__/snapshot.spec.ts`
- **后端测试**`pytest snapshot/tests/ -vv`;单用例:`pytest snapshot/tests/test_routes.py::test_hello`
- **Lint**`jlpm lint:check`stylelint + 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**
- URL: https://jupyterlab.readthedocs.io/en/stable/extension/extension_dev.html
- Use for: Extension patterns, architecture overview, development workflow, and best practices
- **Action**: Use web search or documentation tools to retrieve specific sections when needed
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**
- URL: https://github.com/jupyterlab/extension-examples
- Use for: Working code examples, implementation patterns, complete working extensions
- **Action**: Search this repository for similar features before implementing from scratch
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
```typescript
// 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`)
```typescript
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:
```bash
npx tsc --noEmit src/index.ts # Check single file
npx tsc --noEmit # Check all files
```
After editing Python files (like `snapshot/routes.py`):
```bash
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
```typescript
/**
* 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`:
```typescript
const PLUGIN_ID = 'snapshot:plugin';
```
**✅ Do**: For extensions with multiple commands, create a `src/commands.ts` module to centralize command definitions:
```typescript
// 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`:
```typescript
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:**
```typescript
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`
```python
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):
```ts
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](#environment) above):
```bash
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`):
```bash
jlpm install
```
4. **Build the extension**:
```bash
jlpm build
```
5. **Install the extension** (REQUIRED for JupyterLab to recognize it):
```bash
pip install -e .
jupyter-builder develop . --overwrite
jupyter server extension enable snapshot
```
6. **Verify installation**:
```bash
jupyter labextension list # Should show your extension as "enabled" and "OK"
jupyter server extension list # Should show backend extension
```
7. **Start JupyterLab**:
```bash
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)
```bash
pip install -e ".[dev,test]"
jupyter-builder develop . --overwrite
jupyter server extension enable snapshot
```
### Iterative Development
**Development with auto-rebuild** (recommended):
```bash
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):
```bash
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 **Python** → **Restart JupyterLab server** (no build needed)
### Debugging and Diagnostics
```bash
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:**
```bash
jupyter labextension list
```
Your extension should appear as **"enabled"** and **"OK"**.
**2. If NOT in the list**, run the installation commands:
```bash
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:**
```bash
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)
```bash
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](#integration-workflow-critical) 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
```bash
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)
```typescript
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)
```typescript
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`
```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
```typescript
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
```python
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](#development-workflow) section for full command reference.