From 4b3ed3e802d6f2ec8ee7caf358af810b7d09f789 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Thu, 5 Mar 2026 16:08:11 -0600 Subject: [PATCH] feat: notebook per-cell execution via open-terminal REST endpoints - Add notebook API functions (createNotebookSession, executeNotebookCell, stopNotebookSession) - Create CellEditor component with CodeMirror for cell editing - Rewrite NotebookView with session-based execution, Run All, Restart, Stop - Kernel status indicator with tooltips - Wire baseUrl/apiKey through FilePreview and FileNav --- src/lib/apis/terminal/index.ts | 78 ++++ src/lib/components/chat/FileNav.svelte | 2 + .../components/chat/FileNav/CellEditor.svelte | 109 ++++++ .../chat/FileNav/FilePreview.svelte | 6 +- .../chat/FileNav/NotebookView.svelte | 361 ++++++++++++++++-- 5 files changed, 523 insertions(+), 33 deletions(-) create mode 100644 src/lib/components/chat/FileNav/CellEditor.svelte diff --git a/src/lib/apis/terminal/index.ts b/src/lib/apis/terminal/index.ts index b1a0e7acf9..748ada33a0 100644 --- a/src/lib/apis/terminal/index.ts +++ b/src/lib/apis/terminal/index.ts @@ -258,3 +258,81 @@ export const getListeningPorts = async ( export const getPortProxyUrl = (baseUrl: string, port: number, path: string = ''): string => { return `${baseUrl.replace(/\/$/, '')}/proxy/${port}/${path}`; }; + +// --------------------------------------------------------------------------- +// Notebook execution +// --------------------------------------------------------------------------- + +export const createNotebookSession = async ( + baseUrl: string, + apiKey: string, + path: string +): Promise<{ id: string; kernel: string; status: string } | { error: string }> => { + const url = `${baseUrl.replace(/\/$/, '')}/notebooks`; + const res = await fetch(url, { + method: 'POST', + headers: { + Authorization: `Bearer ${apiKey}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ path }) + }) + .then(async (res) => { + if (!res.ok) { + const body = await res.json().catch(() => ({})); + return { error: body?.detail ?? `HTTP ${res.status}` }; + } + return res.json(); + }) + .catch((err) => { + console.error('open-terminal createNotebookSession error:', err); + return { error: 'Connection failed' }; + }); + return res; +}; + +export const executeNotebookCell = async ( + baseUrl: string, + apiKey: string, + sessionId: string, + cellIndex: number, + source?: string +): Promise<{ status: string; execution_count?: number; outputs: any[] } | { error: string }> => { + const url = `${baseUrl.replace(/\/$/, '')}/notebooks/${sessionId}/execute`; + const body: Record = { cell_index: cellIndex }; + if (source !== undefined) body.source = source; + + const res = await fetch(url, { + method: 'POST', + headers: { + Authorization: `Bearer ${apiKey}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify(body) + }) + .then(async (res) => { + if (!res.ok) { + const body = await res.json().catch(() => ({})); + return { error: body?.detail ?? `HTTP ${res.status}` }; + } + return res.json(); + }) + .catch((err) => { + console.error('open-terminal executeNotebookCell error:', err); + return { error: 'Connection failed' }; + }); + return res; +}; + +export const stopNotebookSession = async ( + baseUrl: string, + apiKey: string, + sessionId: string +): Promise => { + const url = `${baseUrl.replace(/\/$/, '')}/notebooks/${sessionId}`; + const res = await fetch(url, { + method: 'DELETE', + headers: { Authorization: `Bearer ${apiKey}` } + }).catch(() => null); + return res?.ok ?? false; +}; diff --git a/src/lib/components/chat/FileNav.svelte b/src/lib/components/chat/FileNav.svelte index 7e9e977e67..12d0c9daf3 100644 --- a/src/lib/components/chat/FileNav.svelte +++ b/src/lib/components/chat/FileNav.svelte @@ -845,6 +845,8 @@ const result = await excelToTable(excelWorkbook.Sheets[sheet]); fileOfficeHtml = result.html; }} + baseUrl={selectedTerminal?.url ?? ''} + apiKey={selectedTerminal?.key ?? ''} {overlay} onSave={async (content) => { const terminal = selectedTerminal; diff --git a/src/lib/components/chat/FileNav/CellEditor.svelte b/src/lib/components/chat/FileNav/CellEditor.svelte new file mode 100644 index 0000000000..f39b3bba33 --- /dev/null +++ b/src/lib/components/chat/FileNav/CellEditor.svelte @@ -0,0 +1,109 @@ + + +
+ + diff --git a/src/lib/components/chat/FileNav/FilePreview.svelte b/src/lib/components/chat/FileNav/FilePreview.svelte index c4b73e5108..5b23dc4df2 100644 --- a/src/lib/components/chat/FileNav/FilePreview.svelte +++ b/src/lib/components/chat/FileNav/FilePreview.svelte @@ -25,6 +25,10 @@ export let fileSqliteData: ArrayBuffer | null = null; export let fileContent: string | null = null; + // Terminal connection for notebook execution + export let baseUrl: string = ''; + export let apiKey: string = ''; + // Office preview props export let fileOfficeHtml: string | null = null; export let fileOfficeSlides: string[] | null = null; @@ -409,7 +413,7 @@
{:else if isNotebook && !showRaw && parsedNotebook}
- +
{:else if isJson && !showRaw && parsedJson !== undefined}
diff --git a/src/lib/components/chat/FileNav/NotebookView.svelte b/src/lib/components/chat/FileNav/NotebookView.svelte index 5c88abd0c4..a3ea8279ce 100644 --- a/src/lib/components/chat/FileNav/NotebookView.svelte +++ b/src/lib/components/chat/FileNav/NotebookView.svelte @@ -1,12 +1,23 @@
+ + {#if baseUrl && apiKey && filePath} +
+ + {#if kernelReady} + + + {/if} + +
+ +
+ {#if kernelStarting} + + {:else if runningCell !== null} + + {:else if kernelReady} + + {:else} + + {/if} +
+
+ + {#if kernelError} +
+ {kernelError} +
+ {/if} + {/if} + + {#each cells as cell, i}
{#if cell.cell_type === 'markdown'} -
- {@html renderMarkdown(toStr(cell.source))} -
+ {#if editingCell[i]} + + {:else} + +
startEditing(i)} + > + {@html renderMarkdown(toStr(cell.source))} +
+ {/if} {:else if cell.cell_type === 'code'}
-
- {#if cell.execution_count !== undefined && cell.execution_count !== null} - [{cell.execution_count}] - {:else} - [ ] +
+ {#if runningCell === i} +
+ {:else if baseUrl && apiKey && filePath} + {/if} +
+ {#if cell.execution_count !== undefined && cell.execution_count !== null} + [{cell.execution_count}] + {:else} + [ ] + {/if} +
- {#if highlightedCells[i]} -
- {@html highlightedCells[i]} -
+ {#if editingCell[i]} + { editedSources[i] = e.detail; }} + on:run={() => runCell(i)} + on:cancel={() => cancelEditing(i)} + /> {:else} -
{toStr(cell.source)}
+ +
startEditing(i)} + > + {#if highlightedCells[i]} +
+ {@html highlightedCells[i]} +
+ {:else} +
{toStr(cell.source)}
+ {/if} +
{/if} {#if cell.outputs && cell.outputs.length > 0} @@ -153,9 +363,37 @@