## ๐Ÿ“‘ 1. ็ณป็ปŸๆ€ปไฝ“ๆžถๆž„ ้‡‡็”จ **ๅ‰็ซฏ UI ๆณจๅ…ฅ + ๆœฌๅœฐ Server Extension ้€ไผ ** ็š„่ฝป้‡ๅŒๅฑ‚ๆžถๆž„๏ผš ```text โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ JupyterLab / Notebook v7 (ๅ‰็ซฏ TS) โ”‚ โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ Cell Component โ”‚ โ”‚ โ”‚ โ”‚ [CodeEditor] โ”€โ”€โ”€ (ๆŒ‚่ฝฝ) โ”€โ”€โ”€โ–บ [Cell Action Toolbar] โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ (็‚นๅ‡ป ๐Ÿช„ ๆ™บ่ƒฝ็ผ–่พ‘) โ”‚ โ”‚ โ”‚ โ–ผ โ”‚ โ”‚ โ”‚ [Inline Prompt Box] โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ Context Provider ๆ”ถ้›† โ”‚ โ”‚ (Target Cell, Error, Neighbors) โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ REST / SSE Stream โ–ผ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Jupyter Server Extension (ๅŽ็ซฏ Python) โ”‚ โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ OpenCode Bridge Handler โ”‚ โ”‚ โ”‚ โ”‚ - ๆŽฅๆ”ถ Context & Prompt โ”‚ โ”‚ โ”‚ โ”‚ - ๆ‹ผ่ฃ…ๆ ‡ๅ‡† OpenCode API Payload โ”‚ โ”‚ โ”‚ โ”‚ - SSE ๆตๅผ้€ไผ ไปฃ็† (Stream Proxy) โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ Local Socket / HTTP โ–ผ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ OpenCode Serve ่ฟ›็จ‹ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ ``` --- ## ๐ŸŽจ 2. ไบคไบ’ไธŽUIๆต็จ‹่ฎพ่ฎก 1. **ๆ‚ฌๆตฎ Toolbar๏ผˆๅธธ้ฉป๏ผ‰**๏ผšๅœจๆฏไธช `CodeCell` ้กถ้ƒจๆธฒๆŸ“ๅพฎๅž‹ๆ“ไฝœๆ ๏ผš * `โœจ ไผ˜ๅŒ–`๏ผš่‡ชๅŠจๆ็‚ผไปฃ็ ่ง„่ŒƒไธŽๆ€ง่ƒฝใ€‚ * `๐Ÿ› ๆ™บ่ƒฝๆŽ’้”™`๏ผš๏ผˆ**ไป…ๅฝ“ Cell ๆœ‰ Error Output ๆ—ถไบฎ่ตท**๏ผ‰่‡ชๅŠจๆŠ“ๅ– Traceback ๅนถไฟฎๅคใ€‚ * `๐Ÿช„ ๆ™บ่ƒฝ็ผ–่พ‘`๏ผšๅฑ•ๅผ€ๅ†…ๅตŒ Prompt ่พ“ๅ…ฅๆก†ใ€‚ 2. **Inline Prompt ๆก†๏ผˆๆŒ‰้œ€ๅฑ•ๅผ€๏ผ‰**๏ผš * ็”จๆˆท็‚นๅ‡ป `๐Ÿช„ ๆ™บ่ƒฝ็ผ–่พ‘` ๅŽ๏ผŒๅœจๅฝ“ๅ‰ Cell ไธ‹ๆ–นๅนณๆป‘ๅฑ•ๅผ€ไธ€ไธชๅพฎๅž‹่พ“ๅ…ฅๆก†๏ผˆๅธฆ `Textarea` + `๐Ÿš€ ๅ‘้€` ๆŒ‰้’ฎ๏ผ‰ใ€‚ 3. **ๆตๅผๆ›ฟๆข/Diff ้ข„่งˆ**๏ผš * ็‚นๅ‡ปๅ‘้€ๅŽ๏ผŒๆ”ฏๆŒ**ๆ‰“ๅญ—ๆœบๅฎžๆ—ถๆ›ฟๆข**ๆˆ–ๅœจ Cell ไธ‹ๆ–นๅ‡บ็Žฐ `[ Accept ]` / `[ Reject ]` ไธดๆ—ถ Diff ๆฏ”ๅฏน้ขๆฟใ€‚ --- ## ๐Ÿ”Œ 3. ๆŽฅๅฃๅฅ‘็บฆ่ฎพ่ฎก (API Contract) ### ๆŽฅๅฃ๏ผš`POST /opencode/v1/generate` (ๆ”ฏๆŒ SSE ๆตๅผ่ฟ”ๅ›ž) **Request Payload (ๅ‰็ซฏ Context Provider ๆ‰“ๅŒ…ๅ‘็ป™ Server Extension)**: ```json { "action": "custom_edit", // custom_edit | fix_error | optimize "prompt": "ๆŠŠ่ฟ™ๆฎตไปฃ็ ๆ”นไธบ็”จ plotly ็ป˜ๅˆถไบคไบ’ๅผๆŠ˜็บฟๅ›พ", "context": { "notebook_path": "analysis/demo.ipynb", "target_cell": { "index": 3, "code": "import matplotlib.pyplot as plt\nplt.plot([1, 2, 3])", "error_output": "ModuleNotFoundError: No module named 'matplotlib'" }, "surrounding_cells": [ { "index": 2, "code": "import pandas as pd" } ] } } ``` **Response (Server Extension ๆตๅผ้€ไผ  OpenCode Serve ็š„ SSE ๅ“ๅบ”)**: ```text event: chunk data: {"text": "import plotly.express as px\n"} event: chunk data: {"text": "fig = px.line(x=[1, 2, 3], y=[1, 2, 3])\nfig.show()"} event: done data: {"status": "success"} ``` --- ## ๐Ÿ› ๏ธ 4. ๆ ธๅฟƒๅ‰็ซฏๅฎž็Žฐ้€ป่พ‘ (TypeScript) ไปฅไธ‹ๆ˜ฏๅ‰็ซฏๅฎž็Žฐโ€œToolbar ๆณจๅ…ฅโ€ๅ’Œโ€œไธŠไธ‹ๆ–‡ๆŠ“ๅ–โ€็š„ไผชไปฃ็ ้ชจๆžถ๏ผŒๅฏ็›ดๆŽฅไฝœไธบๅผ€ๅ‘ๅ‚่€ƒ๏ผš ### `src/context_provider.ts` (ไธŠไธ‹ๆ–‡ๆ„Ÿ็Ÿฅๆจกๅ—) ```typescript import { Notebook, NotebookTracker } from '@jupyterlab/notebook'; import { Cell } from '@jupyterlab/cells'; export interface CellContext { notebookPath: string; targetCode: string; errorOutput?: string; surroundingCells: string[]; } export function extractCellContext(tracker: NotebookTracker, targetCell: Cell): CellContext | null { const currentWidget = tracker.currentWidget; if (!currentWidget) return null; const notebook: Notebook = currentWidget.content; const cells = notebook.widgets; const targetIndex = cells.findIndex(c => c === targetCell); // 1. ๆŠ“ๅ–ๅฝ“ๅ‰ Cell ็š„้”™่ฏฏ่พ“ๅ‡บ let errorMsg = ''; if (targetCell.model.type === 'code') { const outputs = (targetCell.model as any).outputs; for (let i = 0; i < outputs.length; i++) { const out = outputs.get(i); if (out.type === 'error') { errorMsg += `${out.ename}: ${out.evalue}\n${(out.traceback || []).join('\n')}`; } } } // 2. ๆŠ“ๅ–ๅ‰ไธ€ไธช Cell ็š„ไปฃ็ ๏ผˆไธŠไธ‹ๆ–‡๏ผ‰ const prevCode = targetIndex > 0 ? cells[targetIndex - 1].model.sharedModel.getSource() : ''; return { notebookPath: currentWidget.context.path, targetCode: targetCell.model.sharedModel.getSource(), errorOutput: errorMsg, surroundingCells: [prevCode] }; } ``` ### `src/components/cell_toolbar.ts` (ๅทฅๅ…ทๆ ไธŽๅ†…ๅตŒๆก†ๆณจๅ…ฅ) ```typescript import { Cell } from '@jupyterlab/cells'; export function injectOpenCodeActionBox(cell: Cell, onSendPrompt: (prompt: string) => void) { // ้ฟๅ…้‡ๅคๆณจๅ…ฅ if (cell.node.querySelector('.opencode-toolbar')) return; // 1. ๅˆ›ๅปบ Toolbar ๅฎนๅ™จ const toolbar = document.createElement('div'); toolbar.className = 'opencode-toolbar'; toolbar.innerHTML = ` `; // 2. ็ป‘ๅฎšๅฑ•ๅผ€ไบ‹ไปถ toolbar.querySelector('.edit-btn')!.addEventListener('click', () => { let promptBox = cell.node.querySelector('.opencode-inline-box') as HTMLDivElement; if (promptBox) { promptBox.style.display = promptBox.style.display === 'none' ? 'block' : 'none'; return; } // ๅŠจๆ€ๅˆ›ๅปบ Inline Prompt ่พ“ๅ…ฅๆก† promptBox = document.createElement('div'); promptBox.className = 'opencode-inline-box'; promptBox.innerHTML = `
`; promptBox.querySelector('.submit-btn')!.addEventListener('click', () => { const input = promptBox.querySelector('textarea')!.value; if (input.trim()) onSendPrompt(input); }); cell.node.appendChild(promptBox); }); cell.node.insertBefore(toolbar, cell.node.firstChild); } ``` --- ## ๐Ÿ”— 5. Session ็ฎก็†็ญ–็•ฅ๏ผˆv0.2.1 ไฟฎๆญฃ๏ผ‰ > **2026-07-22 ไฟฎๆญฃ**๏ผšๅŽŸ v0.2 ่ฎพ่ฎก `EditHandler` ้‡‡็”จ"ๆฏ่ฏทๆฑ‚ไธ€ไธช session + finally ๆธ…็†"โ€”โ€”่ฟ™ๆ˜ฏ้”™็š„ใ€‚OpenCode ็š„ session ๆ˜ฏๆœ‰็Šถๆ€็š„ๅฏน่ฏไธŠไธ‹ๆ–‡๏ผŒๆฏไธช notebook ๅฟ…้กปๅค็”จๅŒไธ€ไธช session๏ผŒๅฆๅˆ™็”จๆˆทๅ…ˆไผ˜ๅŒ– cell1ใ€ๅ†็ผ–่พ‘ cell2 ๆ—ถ๏ผŒAI ็œ‹ไธๅˆฐๅ‰ไธ€ๆฌก็š„็ป“ๆžœใ€‚ ### 5.1 ๆ ธๅฟƒ่ง„ๅˆ™ | ็ปดๅบฆ | ๅ†ณ็ญ– | |---|---| | ๆ˜ ๅฐ„ๅ…ณ็ณป | `notebookPath`๏ผˆ1 ไธช๏ผ‰โ†’ OpenCode `sessionID`๏ผˆ1 ไธช๏ผ‰ | | ็”Ÿๅ‘ฝๅ‘จๆœŸ | ้ฆ–ๆฌก `EditHandler` ่ฐƒ็”จๆ—ถ lazy create๏ผ›ๆ˜พๅผ `DELETE /session` ๆ‰้‡Šๆ”พ | | ๅนถๅ‘ | ๅŒ notebook ็š„ๅนถๅ‘่ฏทๆฑ‚็”จ `asyncio.Lock` ไธฒ่กŒๅŒ– session ๅˆ›ๅปบ๏ผ›ๆถˆๆฏๅ‘้€ๅนถๅ‘ๆ— ้”๏ผˆOpenCode ็ซฏ่‡ชๅทฑๆŽ’้˜Ÿ๏ผ‰ | | ๅคฑๆ•ˆๆขๅค | ๅฝ“ `send_message_sync` ๆŠ› `OpenCodeError` ไธ”ๆถˆๆฏๅซ 404/410 โ†’ ่‡ชๅŠจ invalidate ็ผ“ๅญ˜๏ผŒไธ‹ๆฌก่ฏทๆฑ‚้‡ๆ–ฐ create | | ๆธ…็† | **ไธๅš่‡ชๅŠจ้‡Šๆ”พ**ใ€‚ๅ‰็ซฏ notebook ๅ…ณ้—ญๆ—ถ**ไธ**่ฐƒ release๏ผˆ้ฟๅ…ไธคไธช tab ๅ…ฑไบซ session ๆ—ถ่ฏฏๆ€๏ผ‰ใ€‚็”จๆˆทๅฏๆ‰‹ๅŠจ็‚น"้‡็ฝฎๅฏน่ฏ"่งฆๅ‘ release | | ่ทจ้‡ๅฏ | Jupyter server ้‡ๅฏ โ†’ in-memory map ไธขๅคฑ โ†’ OpenCode ็ซฏๅญค็ซ‹ session ๆŽฅๅ—ๆณ„ๆผ๏ผˆv0.4 ๅŠ ๅฏๅŠจๆธ…็†๏ผ‰ | ### 5.2 SessionManager ๆŽฅๅฃ ```python class SessionManager: def __init__(self, client_factory: Callable[[], OpenCodeClient]): ... async def get_or_create(self, notebook_path: str) -> str: """Return existing sessionID, or create one. Async-safe via per-path lock.""" async def release(self, notebook_path: str) -> bool: """Delete session and remove from map. Returns True if session existed.""" def has_session(self, notebook_path: str) -> bool: ... def list_sessions(self) -> list[dict]: """For debug: [{notebookPath, sessionId}, ...]""" ``` **ๅ…ณ้”ฎไธๅ˜้‡**๏ผš - `get_or_create` ๆฐธ่ฟœไธ่ฟ”ๅ›ž None ๆˆ–ๆŠ› "not found" - ้‡ๅค่ฐƒ็”จ `get_or_create(same_path)` โ†’ ๅŒไธ€ sessionID - `release(path)` ๅŽๅ† `get_or_create(path)` โ†’ ๆ–ฐ sessionID ### 5.3 HTTP ็ซฏ็‚น | Method | Path | ไฝœ็”จ | ๅค‡ๆณจ | |---|---|---|---| | `GET` | `/opencode-bridge/sessions` | ๅˆ—ๅ‡บๆ‰€ๆœ‰ active session | debug ็”จ | | `DELETE` | `/opencode-bridge/session?notebook=` | ๆ˜พๅผ้‡Šๆ”พๆŒ‡ๅฎš notebook ็š„ session | URL-encode path | **ไธบไป€ไนˆ็”จ query param ่€Œไธๆ˜ฏ path param**๏ผš - notebook path ๅฏ่ƒฝๅซ `/`ใ€`็ฉบๆ ผ`ใ€`ไธญๆ–‡` - `url_path_join` + regex ๆ•่Žทๅœจๅซ `/` ็š„่ทฏๅพ„ไธŠ่กŒไธบไธๅฏๆŽง - query string ๆ˜ฏ RFC 3986 ๆ ‡ๅ‡†ๅšๆณ•๏ผŒtornado ่‡ชๅŠจ decode ### 5.4 EditHandler ๆ”น้€  **ๆ”น้€ ๅ‰๏ผˆ้”™็š„๏ผ‰**๏ผš ``` create_session โ†’ send_message_sync โ†’ delete_session # ๆฏไธช่ฏทๆฑ‚้ƒฝ่ฟ™ๆ ท ``` **ๆ”น้€ ๅŽ๏ผˆๅฏน็š„๏ผ‰**๏ผš ``` session_id = session_manager.get_or_create(notebook_path) # ้ฆ–ๆฌกๅˆ›ๅปบ๏ผŒๅค็”จๅŽ็ปญ send_message_sync(session_id, ...) # ไธ็ฎก session # ๆฒกๆœ‰ delete โ€” session ็•™็€็ป™ไธ‹ๆฌก ``` ### 5.5 ้”™่ฏฏๆขๅค `send_message_sync` ๅคฑ่ดฅๆ—ถ๏ผš 1. ๆฃ€ๆŸฅ `OpenCodeError` ๆถˆๆฏๆ˜ฏๅฆๅซ "404" ๆˆ– "session not found" 2. ๆ˜ฏ โ†’ `session_manager.invalidate(notebook_path)`๏ผŒไธ‹ๆฌก `get_or_create` ไผš้‡ๆ–ฐๅˆ›ๅปบ 3. ๅฆ โ†’ ้€ไผ ้”™่ฏฏ็ป™ๅ‰็ซฏ v0.2.1 ไธๅฎž็Žฐ่‡ชๅŠจ้‡่ฏ•๏ผŒๅช invalidateใ€‚ๅ‰็ซฏ็œ‹ๅˆฐ 502 ๅŽๅฏ้‡ๅ‘ใ€‚ ### 5.6 ๆœช่งฃๅ†ณ้—ฎ้ข˜๏ผˆv0.4+๏ผ‰ 1. **TTL reaper**๏ผšnotebook ๅ…ณ้—ญๅŽ session ๆฐธ่ฟœไธ้‡Šๆ”พ โ†’ ๆ”น็”จ mtime ่ฟฝ่ธช + 30 ๅˆ†้’Ÿ idle ่‡ชๅŠจ release 2. **ๅคš server ๆจชๅ‘ๆ‰ฉๅฑ•**๏ผšin-memory state ไธ่ƒฝ scale โ†’ ๆ”นๆˆ Redis ๆˆ– sticky session 3. **ๅฏๅŠจๆธ…็†**๏ผšJupyter ้‡ๅฏๅŽ่ฐƒ `GET /session` ๅˆ—ๅ‡บ OpenCode ็ซฏ็š„ๅญคๅ„ฟ๏ผŒ้€ไธช delete 4. **ๅนถๅ‘ LLM ่ฐƒ็”จ**๏ผšๅฝ“ๅ‰ไธคๆฌก `send_message_sync` ๅˆฐๅŒไธ€ session ๆ˜ฏไธฒ่กŒ๏ผˆOpenCode ็ซฏๆŽ’้˜Ÿ๏ผ‰โ€” ๅฏๅŠ  `asyncio.Semaphore(per_session=1)` ๆ˜พๅผๆŽงๅˆถ --- ## ๐ŸŽ› 6. Provider/Model ้€‰ๆ‹ฉ๏ผˆv0.2.2 ๆ–ฐๅขž๏ผ‰ > **2026-07-22**๏ผš็”จๆˆท่ฆๆฑ‚่ƒฝๅœจ settings ้‡Œ้€‰ OpenCode ็š„ provider ๅ’Œ model๏ผŒๅฏ้€‰ๅ€ผไปŽ `GET /config/providers` ๅŠจๆ€่Žทๅ–ใ€‚ ### 6.1 Schema ๆ‰ฉๅฑ• `schema/plugin.json` ๅŠ  2 ไธช string ๅญ—ๆฎต๏ผš ```json "opencodeProvider": { "type": "string", "title": "OpenCode Provider", "description": "Provider id (ๅฆ‚ 'anthropic' / 'openai')ใ€‚ๅฏ้€‰้กน็”ฑๅฏๅŠจๆ—ถไปŽ /opencode-bridge/providers ๆ‹‰ๅ–ๅนถๆ‰“ๅˆฐ console ๅˆ—ๅ‡บใ€‚็•™็ฉบ = ็”จ OpenCode ้ป˜่ฎคใ€‚", "default": "" }, "opencodeModel": { "type": "string", "title": "OpenCode Model", "description": "Model id (ๅฆ‚ 'claude-sonnet-4-20250514')ใ€‚ๅฏ้€‰้กน่ง console ๆ—ฅๅฟ—ๆˆ– /opencode-bridge/providersใ€‚็•™็ฉบ = ็”จ provider ้ป˜่ฎคใ€‚", "default": "" } ``` ### 6.2 ้€‰ๅ€ผๅฆ‚ไฝ•็ป™็”จๆˆท JupyterLab settings ๆ˜ฏ้™ๆ€ JSON schema๏ผŒ**ๆฒกๆœ‰ๅŽŸ็”Ÿ dynamic enum**ใ€‚v0.2.2 ็”จๆœ€็ฎ€ๆ–นๆกˆ๏ผš - ๅฏๅŠจๆ—ถ fetch `/opencode-bridge/providers` โ†’ ๆŠŠ provider/model ๅˆ—่กจ `console.log` ๅ‡บๆฅ - ็”จๆˆทๅœจ Settings Editor ้‡Œๆ‰‹ๅกซ - ๅŽ็ปญ v0.4 ่€ƒ่™‘็”จ QuickPick ๅ‘ฝไปคๅšไบคไบ’ๅผ้€‰ๆ‹ฉ ### 6.3 ๆ•ฐๆฎๆต ``` Settings (JupyterLab) โ”‚ โ”œโ”€โ”€ opencodeProvider = "anthropic" โ””โ”€โ”€ opencodeModel = "claude-sonnet-4-20250514" โ”‚ โ–ผ setOpenCodeRuntime() Frontend (opencode_cell_footer.ts) โ”‚ โ–ผ build request body POST /opencode-bridge/edit { mode, prompt, context, providerId, modelId } โ”‚ โ–ผ send_message_sync OpenCode Serve ๆŽฅๅ— model = { providerID, modelID } ``` ### 6.4 ไธๅ˜้‡ - ็•™็ฉบๅญ—ๆฎต๏ผˆ""๏ผ‰โ†’ ๅฎขๆˆท็ซฏไธๅ‘ `providerId`/`modelId` โ†’ server ไนŸไธๅŠ  `model` ๅญ—ๆฎตๅˆฐ OpenCode ่ฏทๆฑ‚ โ†’ OpenCode ็”จ้ป˜่ฎค - ็”จๆˆทๅœจ settings ้‡Œ่ฎพ็š„ๅ€ผ**ไธไผš่‡ชๅŠจๅ›žๅ†™ๅˆฐ OpenCode**โ€”โ€”ๅชๆ˜ฏ request ็บงๅˆซ็š„่ฆ†็›– - ๆ”น settings ็ซ‹ๅณ็”Ÿๆ•ˆ๏ผˆไธ‹ๆฌก edit ่ฏทๆฑ‚ๅฐฑ็”จๆ–ฐๅ€ผ๏ผ‰๏ผ›ไธ้œ€่ฆ้‡ๅฏ Jupyter