A JupyterLab extension that bridges the cell UI to a local OpenCode Serve process. The extension is a dual package: a Python server extension exposed under /opencode-bridge/*, plus a TypeScript frontend that registers per-cell toolbars. Backend (Python, tornado) - Slice 1: config + auth + OpenCode HTTP client (tornado.httpclient, no aiohttp). 4 settings in schema/plugin.json (url, user, password, request timeout). - Slice 2: handlers for /hello, /health, /providers, /edit. - Slice 2.1 (correction): SessionManager with 1 notebook = 1 session mapping, async-safe via per-path locks, 404 recovery via invalidate(). Two new endpoints: GET /sessions, DELETE /session?notebook=<path>. - 32 pytest tests pass. Frontend (TypeScript, JupyterLab 4.6) - src/types.ts: CellContext, OpenCodeRequest/Response, OpenCodeSettings. - src/context/cell_context.ts: extract CellContext from a CodeCell + its parent NotebookPanel, structured error collection. - src/api/opencode_client.ts: callOpenCodeEdit, callOpenCodeProviders. - src/components/opencode_cell_footer.ts: OpenCodeCellFooter Widget implementing ICellFooter with 3 buttons (optimize / fix / edit), resolved via this.parent instanceof CodeCell. NOT cellToolbar (does not exist in JL 4.6) and NOT Widget.findParent (removed in @lumino/widgets 2.x). - src/components/opencode_cell_factory.ts: Cell.ContentFactory subclass returning the OpenCodeCellFooter. - src/components/opencode_installer.ts: installOpenCodeEverywhere patches every notebook (existing + new) to use the custom factory. - src/index.ts: registers the factory, loads settings, fetches /providers on activation and logs the list to the console. - 23 jest tests pass (mocked JupyterLab boundary, pnpm path safe). Settings - 6 fields: 3 auth (url/user/password) + 1 timeout + 2 model selection (provider/model). Provider list is fetched at startup from /opencode-bridge/providers and printed to the browser console so users can copy values into Settings Editor. Docs - design.md: 6 sections covering architecture, UI flow, API contract, TS skeletons, session management (v0.2.1 correction), and provider/model selection (v0.2.2 addition). - CLAUDE.md: agent guidance for working in this repo. - TODO.md: remaining work for Slices 4-7 + v0.4+ backlog. CI - Gitea release workflow at .github/workflows/build.yml. - Bark notification helper (non-fatal on failure). Generated artefacts ignored: opencode_bridge/labextension/, _version.py, *.tsbuildinfo, junit.xml, test.ipynb scratch notebook.
82 lines
2.2 KiB
TypeScript
82 lines
2.2 KiB
TypeScript
/**
|
|
* Extract a CellContext snapshot from a CodeCell + its parent NotebookPanel.
|
|
*
|
|
* Pure functions, no DOM, no signals — testable with plain object mocks.
|
|
*/
|
|
import type { CodeCell, ICodeCellModel } from '@jupyterlab/cells';
|
|
import type { NotebookPanel } from '@jupyterlab/notebook';
|
|
|
|
import type { CellContext, ErrorOutput } from '../types';
|
|
|
|
/**
|
|
* Find the first error output in a code cell's outputs.
|
|
* Returns null if the cell has no error output or is not a code cell.
|
|
*/
|
|
export function collectErrorOutputs(model: ICodeCellModel): ErrorOutput | null {
|
|
const outputs = model.outputs;
|
|
for (let i = 0; i < outputs.length; i++) {
|
|
const out = outputs.get(i);
|
|
if (out && out.type === 'error') {
|
|
const e = out as {
|
|
ename?: unknown;
|
|
evalue?: unknown;
|
|
traceback?: unknown;
|
|
};
|
|
return {
|
|
ename: String(e.ename ?? ''),
|
|
evalue: String(e.evalue ?? ''),
|
|
traceback: Array.isArray(e.traceback) ? (e.traceback as string[]) : [],
|
|
};
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Build a CellContext from a CodeCell and its parent NotebookPanel.
|
|
* Returns null if essential data is missing.
|
|
*
|
|
* @param cell The code cell to inspect.
|
|
* @param panel The notebook panel that contains the cell.
|
|
*/
|
|
export function extractCellContext(
|
|
cell: CodeCell,
|
|
panel: NotebookPanel
|
|
): CellContext | null {
|
|
if (!cell || !panel) {
|
|
return null;
|
|
}
|
|
|
|
const cells = panel.content.widgets;
|
|
const cellIndex = cells ? cells.indexOf(cell) : -1;
|
|
if (cellIndex < 0) {
|
|
return null;
|
|
}
|
|
|
|
const totalCells = cells ? cells.length : 0;
|
|
const previousCell = cellIndex > 0 ? cells[cellIndex - 1] : null;
|
|
const previousCode = previousCell
|
|
? previousCell.model.sharedModel.getSource()
|
|
: null;
|
|
|
|
// Language: use 'python' as default for code cells.
|
|
// Future improvement: read from kernel info_reply.
|
|
const language = 'python';
|
|
|
|
let error: ErrorOutput | null = null;
|
|
if (cell.model.type === 'code') {
|
|
error = collectErrorOutputs(cell.model as ICodeCellModel);
|
|
}
|
|
|
|
return {
|
|
notebookPath: panel.context.path,
|
|
cellId: cell.model.id,
|
|
language,
|
|
cellIndex,
|
|
totalCells,
|
|
source: cell.model.sharedModel.getSource(),
|
|
previousCode,
|
|
error,
|
|
};
|
|
}
|