@webmcp-auto-ui/ui 2.5.38 → 2.5.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/recipe/RecipeCodeBlock.svelte +16 -1
- package/src/widgets/notebook/executors/sql.ts +2 -1
- package/src/widgets/notebook/notebook.svelte +1 -0
- package/src/widgets/notebook/notebook.ts +221 -25
- package/src/widgets/notebook/share-handlers.ts +4 -2
- package/src/widgets/notebook/shared.ts +129 -33
package/package.json
CHANGED
|
@@ -26,6 +26,12 @@
|
|
|
26
26
|
* written back so subsequent blocks can read them. Owner: the host modal.
|
|
27
27
|
*/
|
|
28
28
|
scope?: Record<string, unknown>;
|
|
29
|
+
/**
|
|
30
|
+
* Recipe-level widget hint (from frontmatter `widget:`). When set and
|
|
31
|
+
* lang === 'json', the JSON content is treated as widget params instead
|
|
32
|
+
* of being dispatched as a script.
|
|
33
|
+
*/
|
|
34
|
+
widget?: string;
|
|
29
35
|
}
|
|
30
36
|
|
|
31
37
|
let {
|
|
@@ -34,6 +40,7 @@
|
|
|
34
40
|
actions = undefined,
|
|
35
41
|
onrun,
|
|
36
42
|
scope,
|
|
43
|
+
widget,
|
|
37
44
|
}: Props = $props();
|
|
38
45
|
|
|
39
46
|
let editable = $state('');
|
|
@@ -93,7 +100,15 @@
|
|
|
93
100
|
startTimer(t0);
|
|
94
101
|
|
|
95
102
|
const multi = canvas.multiClient as McpMultiClient | undefined;
|
|
96
|
-
|
|
103
|
+
// For widget recipes (frontmatter `widget:`), JSON fences carry params —
|
|
104
|
+
// wrap as widget_display(...) so runCode renders the widget instead of
|
|
105
|
+
// dispatching the JSON to run_script.
|
|
106
|
+
const isJsonWidgetParams = widget && /^json\b/i.test(lang);
|
|
107
|
+
const runCodeStr = isJsonWidgetParams
|
|
108
|
+
? `widget_display({name: ${JSON.stringify(widget)}, params: ${editable}})`
|
|
109
|
+
: editable;
|
|
110
|
+
const runLang = isJsonWidgetParams ? 'text' : lang;
|
|
111
|
+
const result = await runCode(runCodeStr, runLang, multi, scope);
|
|
97
112
|
|
|
98
113
|
stopTimer();
|
|
99
114
|
lastDuration = result.durationMs;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { canvas } from '@webmcp-auto-ui/sdk/canvas';
|
|
2
2
|
import { findCodeParamName, buildToolArgs } from '@webmcp-auto-ui/sdk';
|
|
3
|
+
import { stripSqlPreamble } from '../shared.js';
|
|
3
4
|
import type { CellExecutor, CellExecContext, CellResult, DataServerDescriptor, DataServerTool } from '../shared.js';
|
|
4
5
|
|
|
5
6
|
const PATTERN_PRIMARY = /^.*query_sql$/i;
|
|
@@ -24,7 +25,7 @@ export function createSqlExecutor(getServers: () => DataServerDescriptor[]): Cel
|
|
|
24
25
|
return { ok: false, error: 'No SQL tool available on connected servers.', errorKind: 'schema', durationMs: Date.now() - startedAt };
|
|
25
26
|
}
|
|
26
27
|
const { srv, tool } = hit;
|
|
27
|
-
const sql = (ctx.cell.content ?? '').trim();
|
|
28
|
+
const sql = stripSqlPreamble((ctx.cell.content ?? '').trim());
|
|
28
29
|
if (!sql) return { ok: true, kind: 'empty', durationMs: Date.now() - startedAt };
|
|
29
30
|
|
|
30
31
|
// Build args from the tool's inputSchema:
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
renderCellLogs, uid, defaultCellContent,
|
|
14
14
|
createPublishControls, autoConnectFrontmatterServers,
|
|
15
15
|
createRuntimeOverlay, effectiveResult, cellRuntimeStatus,
|
|
16
|
-
lastRefreshedAt, bootstrapLiveRefresh, fmtRelTime, preserveScrollAround,
|
|
16
|
+
lastRefreshedAt, bootstrapLiveRefresh, fmtRelTime, preserveScrollAround, startRun,
|
|
17
17
|
type NotebookState, type NotebookCell, type CellResult, type CellExecContext,
|
|
18
18
|
type RuntimeOverlay,
|
|
19
19
|
} from './shared.js';
|
|
@@ -79,8 +79,11 @@ export async function render(container: HTMLElement, data: Record<string, unknow
|
|
|
79
79
|
hidePublishBadge: (data as any).hidePublishBadge === true,
|
|
80
80
|
} as any);
|
|
81
81
|
|
|
82
|
-
// Default src visibility per mode (toggle remains usable for per-cell override).
|
|
83
|
-
for (const c of state.cells)
|
|
82
|
+
// Default src + logs visibility per mode (toggle remains usable for per-cell override).
|
|
83
|
+
for (const c of state.cells) {
|
|
84
|
+
c.hideSource = state.mode === 'view';
|
|
85
|
+
if (state.mode === 'view' && c.hideLogs === undefined) c.hideLogs = true;
|
|
86
|
+
}
|
|
84
87
|
|
|
85
88
|
// Live mode runtime overlay (created lazily). Never mutates state.
|
|
86
89
|
let overlay: RuntimeOverlay | null = null;
|
|
@@ -282,7 +285,7 @@ export async function render(container: HTMLElement, data: Record<string, unknow
|
|
|
282
285
|
const viewBtn = shell.querySelector('.nb-mode-view') as HTMLElement;
|
|
283
286
|
editBtn.addEventListener('click', () => {
|
|
284
287
|
state.mode = 'edit';
|
|
285
|
-
for (const c of state.cells) c.hideSource = false;
|
|
288
|
+
for (const c of state.cells) { c.hideSource = false; c.hideLogs = false; c.chatOpen = false; }
|
|
286
289
|
container.classList.remove('nb-view-mode');
|
|
287
290
|
editBtn.classList.add('nb-on'); viewBtn.classList.remove('nb-on');
|
|
288
291
|
// Leaving view: stop live refresh and clear overlay so frozen snapshots show.
|
|
@@ -291,7 +294,7 @@ export async function render(container: HTMLElement, data: Record<string, unknow
|
|
|
291
294
|
});
|
|
292
295
|
viewBtn.addEventListener('click', () => {
|
|
293
296
|
state.mode = 'view';
|
|
294
|
-
for (const c of state.cells) c.hideSource = true;
|
|
297
|
+
for (const c of state.cells) { c.hideSource = true; c.hideLogs = true; }
|
|
295
298
|
container.classList.add('nb-view-mode');
|
|
296
299
|
viewBtn.classList.add('nb-on'); editBtn.classList.remove('nb-on');
|
|
297
300
|
if (state.autoRun === true) bootstrapLive();
|
|
@@ -336,12 +339,28 @@ export async function render(container: HTMLElement, data: Record<string, unknow
|
|
|
336
339
|
rerender();
|
|
337
340
|
}
|
|
338
341
|
|
|
342
|
+
// Real-time elapsed timer for running cells. Updates every 200ms in-place
|
|
343
|
+
// (no full rerender) by reading data-running-since attributes.
|
|
344
|
+
const tickRunningTimers = () => {
|
|
345
|
+
const els = container.querySelectorAll<HTMLElement>('.nbe-running-time[data-running-since]');
|
|
346
|
+
if (els.length === 0) return;
|
|
347
|
+
const now = Date.now();
|
|
348
|
+
els.forEach((el) => {
|
|
349
|
+
const since = Number(el.getAttribute('data-running-since'));
|
|
350
|
+
if (!since) return;
|
|
351
|
+
const ms = now - since;
|
|
352
|
+
el.textContent = ms < 1000 ? `running ${ms}ms` : `running ${(ms / 1000).toFixed(1)}s`;
|
|
353
|
+
});
|
|
354
|
+
};
|
|
355
|
+
const runningTimerHandle = setInterval(tickRunningTimers, 200);
|
|
356
|
+
|
|
339
357
|
return () => {
|
|
340
358
|
unsubHistory();
|
|
341
359
|
canvasUnsub?.();
|
|
342
360
|
pane.destroy();
|
|
343
361
|
publishCleanup();
|
|
344
362
|
liveCleanup?.();
|
|
363
|
+
clearInterval(runningTimerHandle);
|
|
345
364
|
};
|
|
346
365
|
}
|
|
347
366
|
|
|
@@ -447,24 +466,34 @@ function renderCell(cell: NotebookCell, state: NotebookState, overlay: RuntimeOv
|
|
|
447
466
|
let liveBadge = '';
|
|
448
467
|
if (showLive) {
|
|
449
468
|
if (rtStatus === 'running') {
|
|
450
|
-
|
|
469
|
+
const startedAt = overlay?.cellStartedAt.get(cell.id) ?? Date.now();
|
|
470
|
+
liveBadge = `<span class="nbe-cell-badge nbe-cell-running" title="re-executing"><span class="nbe-spinner"></span><span class="nbe-running-time" data-running-since="${startedAt}">running</span></span>`;
|
|
451
471
|
} else if (rtStatus === 'stale') {
|
|
452
472
|
liveBadge = `<span class="nbe-cell-badge nbe-cell-stale" title="last live refresh failed">stale</span>`;
|
|
453
473
|
} else if (rtStatus === 'frozen') {
|
|
454
474
|
liveBadge = `<span class="nbe-cell-badge nbe-cell-frozen" title="JS cells are not re-executed in live mode">frozen</span>`;
|
|
455
475
|
}
|
|
456
476
|
}
|
|
477
|
+
const lastRes = effectiveResult(cell, overlay) ?? cell.lastResult;
|
|
478
|
+
const logCount = (lastRes?.logs as string[] | undefined)?.length ?? 0;
|
|
479
|
+
const logsToggle = logCount > 0
|
|
480
|
+
? `<button class="nb-icon-btn nb-toggle-logs" title="toggle console">${cell.hideLogs ? '▸' : '▾'} ${logCount}</button>`
|
|
481
|
+
: '';
|
|
457
482
|
head.innerHTML = `
|
|
458
483
|
<span class="nbe-run-controls"></span>
|
|
459
484
|
<span class="nbe-type-${cell.type}">${cell.type}</span>
|
|
460
485
|
<span class="nbe-meta-info">${escapeHtml(metaInfoFor(cell, overlay))}</span>
|
|
461
486
|
${liveBadge}
|
|
462
487
|
<div class="nbe-actions">
|
|
488
|
+
${logsToggle}
|
|
463
489
|
<button class="nb-icon-btn nb-toggle-src">${cell.hideSource ? '▸ src' : '◂ src'}</button>
|
|
464
490
|
<button class="nb-icon-btn nb-toggle-res">${cell.hideResult ? '▸ res' : '◂ res'}</button>
|
|
491
|
+
${state.mode === 'view' ? `<span class="nbe-actions-sep">│</span><button class="nb-icon-btn nb-toggle-chat">${cell.chatOpen ? '◂ chat' : '▸ chat'}</button>` : ''}
|
|
465
492
|
</div>`;
|
|
466
493
|
codeCell.appendChild(head);
|
|
467
494
|
mountRunControls(head.querySelector('.nbe-run-controls') as HTMLElement, cell, wrap, state, rerender);
|
|
495
|
+
const logsBtn = head.querySelector('.nb-toggle-logs') as HTMLElement | null;
|
|
496
|
+
if (logsBtn) logsBtn.addEventListener('click', () => { cell.hideLogs = !cell.hideLogs; rerender(); });
|
|
468
497
|
|
|
469
498
|
const body = document.createElement('div');
|
|
470
499
|
body.className = 'nbe-code-body' + (cell.hideSource ? ' nbe-hidden' : '');
|
|
@@ -499,6 +528,13 @@ function renderCell(cell: NotebookCell, state: NotebookState, overlay: RuntimeOv
|
|
|
499
528
|
|
|
500
529
|
(head.querySelector('.nb-toggle-src') as HTMLElement).addEventListener('click', () => { cell.hideSource = !cell.hideSource; rerender(); });
|
|
501
530
|
(head.querySelector('.nb-toggle-res') as HTMLElement).addEventListener('click', () => { cell.hideResult = !cell.hideResult; rerender(); });
|
|
531
|
+
const chatBtn = head.querySelector('.nb-toggle-chat') as HTMLElement | null;
|
|
532
|
+
if (chatBtn) chatBtn.addEventListener('click', () => { cell.chatOpen = !cell.chatOpen; rerender(); });
|
|
533
|
+
|
|
534
|
+
if (state.mode === 'view' && cell.chatOpen) {
|
|
535
|
+
wrap.classList.add('nb-cell-chat-active');
|
|
536
|
+
mountAgentBarAbove(codeCell, state, cell, rerender, overlay);
|
|
537
|
+
}
|
|
502
538
|
|
|
503
539
|
if (state.mode !== 'view') wrap.appendChild(renderCellActionBar(state, cell, rerender));
|
|
504
540
|
|
|
@@ -666,27 +702,77 @@ function toggleAgentBar(host: HTMLElement, state: NotebookState, cell: NotebookC
|
|
|
666
702
|
});
|
|
667
703
|
}
|
|
668
704
|
|
|
705
|
+
/**
|
|
706
|
+
* View-mode variant — renders the agent bar ABOVE the code cell (beforebegin)
|
|
707
|
+
* and persists open state via cell.chatOpen so the bar survives the rerender
|
|
708
|
+
* triggered by `update_cell`. Reuses runAgentForCell unchanged.
|
|
709
|
+
*/
|
|
710
|
+
function mountAgentBarAbove(codeCell: HTMLElement, state: NotebookState, cell: NotebookCell, rerender: () => void, overlay: RuntimeOverlay | null): void {
|
|
711
|
+
const bar = document.createElement('div');
|
|
712
|
+
bar.className = 'nbe-agent-bar';
|
|
713
|
+
bar.innerHTML = `
|
|
714
|
+
<auto-chat-input placeholder="ask agent — e.g. 'filter rows where votes > 50' / 'add a sankey of this'"></auto-chat-input>
|
|
715
|
+
<div class="nbe-agent-progress" hidden>
|
|
716
|
+
<span class="nbe-progress-track"><span class="nbe-progress-fill"></span></span>
|
|
717
|
+
<span class="nbe-progress-label"></span>
|
|
718
|
+
</div>`;
|
|
719
|
+
codeCell.insertAdjacentElement('beforebegin', bar);
|
|
720
|
+
const input = bar.querySelector('auto-chat-input') as HTMLElement & { disabled?: boolean };
|
|
721
|
+
const progress = bar.querySelector('.nbe-agent-progress') as HTMLElement;
|
|
722
|
+
const fill = bar.querySelector('.nbe-progress-fill') as HTMLElement;
|
|
723
|
+
const label = bar.querySelector('.nbe-progress-label') as HTMLElement;
|
|
724
|
+
let aborter: AbortController | null = null;
|
|
725
|
+
input.addEventListener('widget:interact', (e: Event) => {
|
|
726
|
+
const detail = (e as CustomEvent).detail ?? {};
|
|
727
|
+
const action = (detail as { action?: string }).action;
|
|
728
|
+
if (action === 'stop') { e.stopPropagation(); aborter?.abort(); return; }
|
|
729
|
+
if (action !== 'submit') return;
|
|
730
|
+
e.stopPropagation();
|
|
731
|
+
const text = ((detail as { payload?: { text?: string } }).payload?.text ?? '').trim();
|
|
732
|
+
if (!text) return;
|
|
733
|
+
void runAgentForCell(text, state, cell, rerender, progress, fill, label, input, () => aborter ??= new AbortController(), overlay);
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
|
|
669
737
|
async function runAgentForCell(
|
|
670
738
|
prompt: string,
|
|
671
739
|
state: NotebookState,
|
|
672
740
|
cell: NotebookCell,
|
|
673
741
|
rerender: () => void,
|
|
674
|
-
|
|
742
|
+
progress: HTMLElement,
|
|
743
|
+
fill: HTMLElement,
|
|
744
|
+
label: HTMLElement,
|
|
675
745
|
input: HTMLElement & { disabled?: boolean },
|
|
676
746
|
getAborter: () => AbortController,
|
|
747
|
+
overlay: RuntimeOverlay | null,
|
|
677
748
|
): Promise<void> {
|
|
678
|
-
|
|
679
|
-
|
|
749
|
+
progress.hidden = false;
|
|
750
|
+
progress.classList.remove('nbe-progress-error', 'nbe-progress-done');
|
|
751
|
+
let currentTool = '…';
|
|
752
|
+
const startedAt = Date.now();
|
|
753
|
+
// Indeterminate slide: a 3-block fill traverses the 8-block track.
|
|
754
|
+
const TRACK = 8;
|
|
755
|
+
const FILL = 3;
|
|
756
|
+
let pos = 0;
|
|
757
|
+
const tick = () => {
|
|
758
|
+
const cells: string[] = [];
|
|
759
|
+
for (let i = 0; i < TRACK; i++) {
|
|
760
|
+
const inFill = i >= pos && i < pos + FILL;
|
|
761
|
+
cells.push(inFill ? '▰' : '▱');
|
|
762
|
+
}
|
|
763
|
+
fill.textContent = cells.join('');
|
|
764
|
+
label.textContent = `${currentTool} · ${((Date.now() - startedAt) / 1000).toFixed(1)}s`;
|
|
765
|
+
pos = (pos + 1) % (TRACK - FILL + 1 + 2); // small pause at each end
|
|
766
|
+
};
|
|
767
|
+
tick();
|
|
768
|
+
const interval = setInterval(tick, 120);
|
|
680
769
|
input.disabled = true;
|
|
681
770
|
const aborter = getAborter();
|
|
682
771
|
try {
|
|
683
|
-
// Lazy import to keep notebook bundle slim when agent is unused.
|
|
684
772
|
const { RemoteLLMProvider, runAgentLoop } = await import('@webmcp-auto-ui/agent');
|
|
685
|
-
// Host injects chatApiBase via data (flex serves under /flex, notebook-viewer
|
|
686
|
-
// at root). Default '/api/chat' kept for hosts mounting at the root.
|
|
687
773
|
const proxyUrl = (state as { chatApiBase?: string }).chatApiBase ?? '/api/chat';
|
|
688
774
|
const provider = new RemoteLLMProvider({ proxyUrl, model: 'haiku' });
|
|
689
|
-
const layer = buildAgentLayerForCell(state, cell, rerender);
|
|
775
|
+
const layer = buildAgentLayerForCell(state, cell, rerender, overlay);
|
|
690
776
|
const systemPrompt = buildAgentSystemPromptForCell(state, cell);
|
|
691
777
|
await runAgentLoop(prompt, {
|
|
692
778
|
provider,
|
|
@@ -695,12 +781,20 @@ async function runAgentForCell(
|
|
|
695
781
|
maxIterations: 6,
|
|
696
782
|
signal: aborter.signal,
|
|
697
783
|
callbacks: {
|
|
698
|
-
onToolCall: (call: { name?: string }) => {
|
|
784
|
+
onToolCall: (call: { name?: string }) => { currentTool = call?.name ?? '?'; },
|
|
699
785
|
},
|
|
700
786
|
});
|
|
701
|
-
|
|
787
|
+
clearInterval(interval);
|
|
788
|
+
progress.classList.add('nbe-progress-done');
|
|
789
|
+
fill.textContent = '▰▰▰▰▰▰▰▰';
|
|
790
|
+
label.textContent = `done · ${((Date.now() - startedAt) / 1000).toFixed(1)}s`;
|
|
791
|
+
// Auto-close happens earlier inside update_cell.execute (avoids the lag of
|
|
792
|
+
// Haiku's trailing assistant turn). Nothing to do here.
|
|
702
793
|
} catch (err) {
|
|
703
|
-
|
|
794
|
+
clearInterval(interval);
|
|
795
|
+
progress.classList.add('nbe-progress-error');
|
|
796
|
+
fill.textContent = '▱▱▱▱▱▱▱▱';
|
|
797
|
+
label.textContent = 'error: ' + String((err as { message?: unknown })?.message ?? err);
|
|
704
798
|
} finally {
|
|
705
799
|
input.disabled = false;
|
|
706
800
|
}
|
|
@@ -711,8 +805,12 @@ async function runAgentForCell(
|
|
|
711
805
|
* shared helpers (addImportedCells, logHistory) so revert via ⟲ history works
|
|
712
806
|
* exactly like a manual edit.
|
|
713
807
|
*/
|
|
714
|
-
function buildAgentLayerForCell(state: NotebookState, cell: NotebookCell, rerender: () => void) {
|
|
808
|
+
function buildAgentLayerForCell(state: NotebookState, cell: NotebookCell, rerender: () => void, overlay: RuntimeOverlay | null) {
|
|
715
809
|
const idx = () => state.cells.findIndex((c) => c.id === cell.id);
|
|
810
|
+
// View-mode chat is per-cell and meant to TRANSFORM the targeted cell.
|
|
811
|
+
// Exception: when the cell is `sql`, the natural follow-up is "add a chart on
|
|
812
|
+
// top of these rows", so we keep insert_cell_after + list_widgets available.
|
|
813
|
+
const transformOnly = state.mode === 'view' && cell.type !== 'sql';
|
|
716
814
|
const tools = [
|
|
717
815
|
{
|
|
718
816
|
name: 'get_current_cell',
|
|
@@ -758,7 +856,20 @@ function buildAgentLayerForCell(state: NotebookState, cell: NotebookCell, rerend
|
|
|
758
856
|
snapshot: { cellId: cell.id, before, after: next },
|
|
759
857
|
} as never);
|
|
760
858
|
} catch { /* ignore — best-effort */ }
|
|
859
|
+
// Drop the live-overlay cache for this cell. effectiveResult prefers
|
|
860
|
+
// overlay.outputs over cell.lastResult, so without this the pre-edit
|
|
861
|
+
// render lingers until the next bootstrapLive (= view-edit-view).
|
|
862
|
+
overlay?.outputs.delete(cell.id);
|
|
863
|
+
overlay?.status.delete(cell.id);
|
|
864
|
+
// Close the chat bar immediately on success in view mode — waiting for
|
|
865
|
+
// runAgentLoop to fully resolve adds a 1-2s lag from Haiku's trailing
|
|
866
|
+
// assistant turn ("Done!") after the tool result.
|
|
867
|
+
if (state.mode === 'view') cell.chatOpen = false;
|
|
761
868
|
rerender();
|
|
869
|
+
// Re-run so the result panel reflects the new content.
|
|
870
|
+
if (cell.type !== 'md' && state.executors?.[cell.type]) {
|
|
871
|
+
startRun(cell, state, rerender);
|
|
872
|
+
}
|
|
762
873
|
return { ok: true };
|
|
763
874
|
},
|
|
764
875
|
},
|
|
@@ -790,21 +901,65 @@ function buildAgentLayerForCell(state: NotebookState, cell: NotebookCell, rerend
|
|
|
790
901
|
},
|
|
791
902
|
{
|
|
792
903
|
name: 'list_widgets',
|
|
793
|
-
description: 'List widgets exposed by the connected WebMCP servers
|
|
904
|
+
description: 'List widgets exposed by the connected WebMCP servers, with one usage example each. Pickable in JS cells via widget(name, params).',
|
|
794
905
|
inputSchema: { type: 'object', properties: {} },
|
|
795
906
|
execute: async () => {
|
|
796
|
-
const out: Array<{ server: string; name: string; description?: string }> = [];
|
|
907
|
+
const out: Array<{ server: string; name: string; description?: string; example?: string }> = [];
|
|
797
908
|
for (const s of state.webmcpServers ?? []) {
|
|
798
|
-
for (const r of s.layer().recipes ?? [])
|
|
909
|
+
for (const r of s.layer().recipes ?? []) {
|
|
910
|
+
const body = (r as { body?: string }).body ?? '';
|
|
911
|
+
// First fenced block in the recipe body is the canonical example,
|
|
912
|
+
// showing the exact param keys the renderer expects.
|
|
913
|
+
const m = /```[a-zA-Z]*\n([\s\S]*?)\n```/.exec(body);
|
|
914
|
+
out.push({
|
|
915
|
+
server: s.name,
|
|
916
|
+
name: r.name,
|
|
917
|
+
description: r.description,
|
|
918
|
+
...(m ? { example: m[1] } : {}),
|
|
919
|
+
});
|
|
920
|
+
}
|
|
799
921
|
}
|
|
800
922
|
return out;
|
|
801
923
|
},
|
|
802
924
|
},
|
|
803
925
|
];
|
|
804
|
-
|
|
926
|
+
// list_widgets is read-only and essential — without it, the agent hallucinates
|
|
927
|
+
// widget names. Only insert_cell_after is dropped in transformOnly.
|
|
928
|
+
const filtered = transformOnly
|
|
929
|
+
? tools.filter((t) => t.name !== 'insert_cell_after')
|
|
930
|
+
: tools;
|
|
931
|
+
return { protocol: 'webmcp' as const, serverName: 'notebook-editor', tools: filtered, recipes: [] };
|
|
805
932
|
}
|
|
806
933
|
|
|
807
|
-
function buildAgentSystemPromptForCell(
|
|
934
|
+
function buildAgentSystemPromptForCell(state: NotebookState, cell: NotebookCell): string {
|
|
935
|
+
if (state.mode === 'view') {
|
|
936
|
+
if (cell.type === 'sql') {
|
|
937
|
+
return [
|
|
938
|
+
'You are an in-notebook editing assistant. The user invoked you on a SQL cell.',
|
|
939
|
+
'Two valid actions:',
|
|
940
|
+
' - TRANSFORM: rewrite this SQL cell. Call get_current_cell, then update_cell.',
|
|
941
|
+
' - FOLLOW-UP (chart/viz/widget): insert a JS cell after this one that renders a widget on top of the rows.',
|
|
942
|
+
'For a follow-up widget:',
|
|
943
|
+
' 1. Call get_current_cell to read this cell\'s `varname` (the rows of this SQL are bound to that name in cross-cell scope).',
|
|
944
|
+
' 2. Call list_widgets to discover the exact widget name + the example showing the required param keys. NEVER invent a widget name.',
|
|
945
|
+
' 3. Call insert_cell_after with type="js" and content like: `return widget("<name>", { ...params per the example, with rows substituted for <varname> });`',
|
|
946
|
+
'For JS cells: use the `widget(name, params)` helper. `widget_display` is not available as a global.',
|
|
947
|
+
'Never write a JS cell that re-issues the SQL — reuse the varname from cross-cell scope.',
|
|
948
|
+
'Be terse. Two or three tool calls is enough. Do not explain at length.',
|
|
949
|
+
].join('\n');
|
|
950
|
+
}
|
|
951
|
+
return [
|
|
952
|
+
'You are an in-notebook editing assistant. The user invoked you on a specific cell to TRANSFORM it.',
|
|
953
|
+
`The current cell is of type "${cell.type}".`,
|
|
954
|
+
'Workflow:',
|
|
955
|
+
' 1. Call get_current_cell to read the cell content.',
|
|
956
|
+
' 2. If the user asks for a chart/widget/viz, call list_widgets to learn the exact widget names + the example showing the required param keys. NEVER invent a widget name.',
|
|
957
|
+
' 3. Call update_cell with the rewritten content.',
|
|
958
|
+
'Always rewrite the existing cell. Never insert new cells.',
|
|
959
|
+
'For JS cells: use the `widget(name, params)` helper. `widget_display` is not available as a global. Call widget once with the final result; do NOT also call widget_display after.',
|
|
960
|
+
'Be terse. Two tool calls is usually enough. Do not explain at length.',
|
|
961
|
+
].join('\n');
|
|
962
|
+
}
|
|
808
963
|
return [
|
|
809
964
|
'You are an in-notebook editing assistant. The user invoked you on a specific cell.',
|
|
810
965
|
'Your job: rewrite that cell, OR insert a follow-up cell after it, based on the user\'s prompt.',
|
|
@@ -847,12 +1002,25 @@ function renderResultInto(el: HTMLElement, cell: NotebookCell, overlay: RuntimeO
|
|
|
847
1002
|
const r = effectiveResult(cell, overlay) ?? cell.lastResult;
|
|
848
1003
|
el.innerHTML = '';
|
|
849
1004
|
if (!r) {
|
|
850
|
-
|
|
1005
|
+
const isView = stateRef?.mode === 'view';
|
|
1006
|
+
const rtStatus = cellRuntimeStatus(cell, overlay);
|
|
1007
|
+
const startedAt = overlay?.cellStartedAt.get(cell.id);
|
|
1008
|
+
if (rtStatus === 'running' && startedAt != null) {
|
|
1009
|
+
el.innerHTML = `<div class="nbe-result-running"><span class="nbe-spinner"></span> <span class="nbe-running-time" data-running-since="${startedAt}">running</span></div>`;
|
|
1010
|
+
} else if (isView && (rtStatus === 'pending' || rtStatus === 'idle')) {
|
|
1011
|
+
// Queued: cell will run sequentially after upstream cells finish.
|
|
1012
|
+
// Spinner without timer — there's nothing to count yet.
|
|
1013
|
+
el.innerHTML = `<div class="nbe-result-running"><span class="nbe-spinner"></span> queued</div>`;
|
|
1014
|
+
} else {
|
|
1015
|
+
el.innerHTML = `<div class="nbe-result-empty">press ▶ to run</div>`;
|
|
1016
|
+
}
|
|
851
1017
|
return;
|
|
852
1018
|
}
|
|
853
1019
|
// Logs panel (shared across all widgets), prepended above the main result
|
|
854
|
-
|
|
855
|
-
|
|
1020
|
+
if (!cell.hideLogs) {
|
|
1021
|
+
const logsEl = renderCellLogs(r);
|
|
1022
|
+
if (logsEl) el.appendChild(logsEl);
|
|
1023
|
+
}
|
|
856
1024
|
if (!r.ok) {
|
|
857
1025
|
const err = document.createElement('div');
|
|
858
1026
|
err.className = 'nbe-result-error';
|
|
@@ -1080,6 +1248,30 @@ function injectLayoutStyles(): void {
|
|
|
1080
1248
|
.nbe-code-body { padding: 14px 16px; }
|
|
1081
1249
|
.nbe-hidden { display: none !important; }
|
|
1082
1250
|
|
|
1251
|
+
/* View-mode Option B: chrome hidden by default, revealed on hover in top-right corner. */
|
|
1252
|
+
.nb-root.nb-view-mode .nbe-code-cell { position: relative; min-height: 28px; }
|
|
1253
|
+
.nb-root.nb-view-mode .nbe-cell-head {
|
|
1254
|
+
position: absolute; top: 0; right: 0; z-index: 2;
|
|
1255
|
+
padding: 4px 8px; gap: 6px;
|
|
1256
|
+
background: var(--color-surface2);
|
|
1257
|
+
border: 1px solid var(--color-border);
|
|
1258
|
+
border-top: none; border-right: none;
|
|
1259
|
+
border-bottom-left-radius: 6px;
|
|
1260
|
+
opacity: 0; pointer-events: none;
|
|
1261
|
+
transition: opacity 120ms ease;
|
|
1262
|
+
font-size: 9.5px;
|
|
1263
|
+
}
|
|
1264
|
+
.nb-root.nb-view-mode .nbe-code-cell:hover .nbe-cell-head,
|
|
1265
|
+
.nb-root.nb-view-mode .nbe-cell-head:focus-within {
|
|
1266
|
+
opacity: 0.95; pointer-events: auto;
|
|
1267
|
+
}
|
|
1268
|
+
.nb-root.nb-view-mode .nbe-cell-head .nbe-meta-info,
|
|
1269
|
+
.nb-root.nb-view-mode .nbe-cell-head .nbe-type-sql,
|
|
1270
|
+
.nb-root.nb-view-mode .nbe-cell-head .nbe-type-js,
|
|
1271
|
+
.nb-root.nb-view-mode .nbe-cell-head .nbe-type-md { display: none; }
|
|
1272
|
+
.nb-root.nb-view-mode .nbe-code-body { padding: 10px 14px; }
|
|
1273
|
+
.nb-root.nb-view-mode .nbe-result { padding: 8px 14px; }
|
|
1274
|
+
|
|
1083
1275
|
.nbe-result {
|
|
1084
1276
|
background: var(--color-bg);
|
|
1085
1277
|
border-top: 1px solid var(--color-border);
|
|
@@ -1090,6 +1282,10 @@ function injectLayoutStyles(): void {
|
|
|
1090
1282
|
.nbe-result-empty {
|
|
1091
1283
|
color: var(--color-text2); font-style: italic; font-size: 11.5px;
|
|
1092
1284
|
}
|
|
1285
|
+
.nbe-result-running {
|
|
1286
|
+
display: inline-flex; align-items: center; gap: 6px;
|
|
1287
|
+
color: #2ea043; font-size: 11.5px;
|
|
1288
|
+
}
|
|
1093
1289
|
.nbe-result-error {
|
|
1094
1290
|
color: var(--color-accent2); white-space: pre-wrap; font-size: 12px;
|
|
1095
1291
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// 4 formats: JSON, Markdown, Hyperskill link (+ short), PNG snapshot.
|
|
5
5
|
// ---------------------------------------------------------------------------
|
|
6
6
|
|
|
7
|
-
import { encode, buildShortUrl } from '@webmcp-auto-ui/sdk';
|
|
7
|
+
import { encode, buildShortUrl, pickFence } from '@webmcp-auto-ui/sdk';
|
|
8
8
|
import { canvasVanilla } from '@webmcp-auto-ui/sdk/canvas-vanilla';
|
|
9
9
|
import type { NotebookState, NotebookCell } from './shared.js';
|
|
10
10
|
|
|
@@ -56,7 +56,9 @@ export function serializeToMarkdown(state: NotebookState): string {
|
|
|
56
56
|
const metaLine = cell.args && Object.keys(cell.args).length > 0
|
|
57
57
|
? `${commentPrefix} @meta ${JSON.stringify(cell.args)}\n`
|
|
58
58
|
: '';
|
|
59
|
-
|
|
59
|
+
const body = metaLine + cell.content.trim();
|
|
60
|
+
const fence = pickFence(body);
|
|
61
|
+
parts.push(fence + lang + varname, body, fence, '');
|
|
60
62
|
}
|
|
61
63
|
}
|
|
62
64
|
return parts.join('\n').trim() + '\n';
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// ---------------------------------------------------------------------------
|
|
6
6
|
|
|
7
7
|
import { serializeToMarkdown } from './share-handlers.js';
|
|
8
|
+
import { extractTopLevelDecls, hasIdentifierReference, findCodeParamName, buildToolArgs } from '@webmcp-auto-ui/sdk';
|
|
8
9
|
|
|
9
10
|
export const NB_PUBLISH_HOST: string = (() => {
|
|
10
11
|
try {
|
|
@@ -47,6 +48,10 @@ export interface NotebookCell {
|
|
|
47
48
|
varname?: string; // named output (compact)
|
|
48
49
|
hideSource?: boolean;
|
|
49
50
|
hideResult?: boolean;
|
|
51
|
+
/** view-mode: agent chat bar opened above this cell (persists across rerender). */
|
|
52
|
+
chatOpen?: boolean;
|
|
53
|
+
/** view-mode: collapse the console logs panel under the head bar. Default true in view mode. */
|
|
54
|
+
hideLogs?: boolean;
|
|
50
55
|
runState?: RunState;
|
|
51
56
|
lastMs?: number;
|
|
52
57
|
status?: 'fresh' | 'stale';
|
|
@@ -95,6 +100,25 @@ export interface HistoryEntry {
|
|
|
95
100
|
// Utilities
|
|
96
101
|
// ---------------------------------------------------------------------------
|
|
97
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Strip leading SQL preamble — line comments and block comments,
|
|
105
|
+
* BOM, and whitespace — so the first non-trivial token is the real keyword.
|
|
106
|
+
* Some upstream MCP SQL servers (Tricoteuses) enforce a strict
|
|
107
|
+
* `^(SELECT|WITH)` regex on the raw string; a comment header would fail.
|
|
108
|
+
*/
|
|
109
|
+
export function stripSqlPreamble(sql: string): string {
|
|
110
|
+
let s = sql.replace(/^/, '');
|
|
111
|
+
// Repeat: there can be a mix of line + block comments + blank lines.
|
|
112
|
+
for (;;) {
|
|
113
|
+
const before = s;
|
|
114
|
+
s = s.replace(/^[\s]+/, '');
|
|
115
|
+
s = s.replace(/^--[^\n]*\n?/, '');
|
|
116
|
+
s = s.replace(/^\/\*[\s\S]*?\*\//, '');
|
|
117
|
+
if (s === before) break;
|
|
118
|
+
}
|
|
119
|
+
return s;
|
|
120
|
+
}
|
|
121
|
+
|
|
98
122
|
export function uid(): string {
|
|
99
123
|
return 'c_' + Math.random().toString(36).slice(2, 9);
|
|
100
124
|
}
|
|
@@ -245,6 +269,8 @@ export interface RuntimeOverlay {
|
|
|
245
269
|
outputs: Map<string, { result: CellResult; refreshedAt: number }>;
|
|
246
270
|
/** Per-cell status during/after the refresh cycle. */
|
|
247
271
|
status: Map<string, CellRuntimeStatus>;
|
|
272
|
+
/** Per-cell start timestamp — set when status flips to 'running', cleared on completion. */
|
|
273
|
+
cellStartedAt: Map<string, number>;
|
|
248
274
|
startedAt: number | null;
|
|
249
275
|
finishedAt: number | null;
|
|
250
276
|
/** Last fatal reason (e.g. "no reachable server"). */
|
|
@@ -255,6 +281,7 @@ export function createRuntimeOverlay(): RuntimeOverlay {
|
|
|
255
281
|
return {
|
|
256
282
|
outputs: new Map(),
|
|
257
283
|
status: new Map(),
|
|
284
|
+
cellStartedAt: new Map(),
|
|
258
285
|
startedAt: null,
|
|
259
286
|
finishedAt: null,
|
|
260
287
|
error: null,
|
|
@@ -324,34 +351,77 @@ export async function runAutoRefresh(opts: AutoRefreshOptions): Promise<AutoRefr
|
|
|
324
351
|
const summary: AutoRefreshSummary = { rerun: 0, frozen: 0, stale: 0, failed: 0 };
|
|
325
352
|
const sharedSignal = signal ?? new AbortController().signal;
|
|
326
353
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
354
|
+
// Build dependency graph: a JS cell depends on the most recent prior cell
|
|
355
|
+
// that declares a top-level identifier referenced in this cell's body.
|
|
356
|
+
// Cells with no inter-cell deps run in parallel; dependent cells await
|
|
357
|
+
// their providers via a Promise map (no batching, no sequential bottleneck).
|
|
358
|
+
const declToProvider = new Map<string, string>(); // identifier → cell id of latest declarer
|
|
359
|
+
const cellDeps = new Map<string, string[]>(); // cell id → [provider cell ids]
|
|
360
|
+
for (const cell of state.cells) {
|
|
361
|
+
if (!isReRunnable(cell)) continue;
|
|
362
|
+
const deps = new Set<string>();
|
|
363
|
+
if (cell.type === 'js') {
|
|
364
|
+
for (const [name, providerId] of declToProvider) {
|
|
365
|
+
if (providerId === cell.id) continue;
|
|
366
|
+
if (hasIdentifierReference(cell.content, name)) deps.add(providerId);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
cellDeps.set(cell.id, [...deps]);
|
|
370
|
+
// Register this cell's provided names AFTER computing its deps so a cell
|
|
371
|
+
// doesn't depend on itself when it both reads and re-declares a name.
|
|
372
|
+
if (cell.type === 'js') {
|
|
373
|
+
for (const decl of extractTopLevelDecls(cell.content)) {
|
|
374
|
+
declToProvider.set(decl, cell.id);
|
|
375
|
+
}
|
|
376
|
+
} else if (cell.type === 'sql' && cell.varname) {
|
|
377
|
+
declToProvider.set(cell.varname, cell.id);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const cellPromises = new Map<string, Promise<void>>();
|
|
382
|
+
for (const cell of state.cells) {
|
|
383
|
+
cellPromises.set(cell.id, (async () => {
|
|
384
|
+
if (sharedSignal.aborted) return;
|
|
385
|
+
if (!isReRunnable(cell)) { summary.frozen++; return; }
|
|
330
386
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
387
|
+
// Wait for dependencies before starting.
|
|
388
|
+
const deps = cellDeps.get(cell.id) ?? [];
|
|
389
|
+
if (deps.length > 0) {
|
|
390
|
+
await Promise.all(deps.map((d) => cellPromises.get(d)).filter(Boolean) as Promise<void>[]);
|
|
391
|
+
}
|
|
392
|
+
if (sharedSignal.aborted) return;
|
|
334
393
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
394
|
+
overlay.status.set(cell.id, 'running');
|
|
395
|
+
overlay.cellStartedAt.set(cell.id, Date.now());
|
|
396
|
+
onCellChange?.(cell.id);
|
|
397
|
+
onTick?.(overlay);
|
|
398
|
+
|
|
399
|
+
try {
|
|
400
|
+
const result = await runner(cell, sharedSignal);
|
|
338
401
|
overlay.outputs.set(cell.id, { result, refreshedAt: Date.now() });
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
402
|
+
if (result.ok) {
|
|
403
|
+
overlay.status.set(cell.id, 'fresh');
|
|
404
|
+
summary.rerun++;
|
|
405
|
+
} else {
|
|
406
|
+
overlay.status.set(cell.id, 'stale');
|
|
407
|
+
summary.stale++;
|
|
408
|
+
}
|
|
409
|
+
} catch (err) {
|
|
410
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
411
|
+
overlay.outputs.set(cell.id, {
|
|
412
|
+
result: { ok: false, error: message, errorKind: 'runtime', durationMs: 0 },
|
|
413
|
+
refreshedAt: Date.now(),
|
|
414
|
+
});
|
|
342
415
|
overlay.status.set(cell.id, 'stale');
|
|
343
|
-
summary.
|
|
416
|
+
summary.failed++;
|
|
417
|
+
if (!overlay.error) overlay.error = message;
|
|
344
418
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
onTick?.(overlay);
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
await Promise.all(tasks);
|
|
419
|
+
overlay.cellStartedAt.delete(cell.id);
|
|
420
|
+
onCellChange?.(cell.id);
|
|
421
|
+
onTick?.(overlay);
|
|
422
|
+
})());
|
|
423
|
+
}
|
|
424
|
+
await Promise.all([...cellPromises.values()]);
|
|
355
425
|
|
|
356
426
|
overlay.finishedAt = Date.now();
|
|
357
427
|
onTick?.(overlay);
|
|
@@ -381,15 +451,15 @@ export function createBridgeSqlRunner(
|
|
|
381
451
|
const PATTERN_PRIMARY = /^.*query_sql$/i;
|
|
382
452
|
const PATTERN_FALLBACK = /^(query|run|execute)(_sql)?$/i;
|
|
383
453
|
|
|
384
|
-
function findSqlTool(servers: DataServerDescriptor[]): { serverName: string; toolName: string } | null {
|
|
454
|
+
function findSqlTool(servers: DataServerDescriptor[]): { serverName: string; toolName: string; tool: DataServerTool } | null {
|
|
385
455
|
for (const srv of servers) {
|
|
386
456
|
for (const t of srv.tools ?? []) {
|
|
387
|
-
if (PATTERN_PRIMARY.test(t.name)) return { serverName: srv.name, toolName: t.name };
|
|
457
|
+
if (PATTERN_PRIMARY.test(t.name)) return { serverName: srv.name, toolName: t.name, tool: t };
|
|
388
458
|
}
|
|
389
459
|
}
|
|
390
460
|
for (const srv of servers) {
|
|
391
461
|
for (const t of srv.tools ?? []) {
|
|
392
|
-
if (PATTERN_FALLBACK.test(t.name)) return { serverName: srv.name, toolName: t.name };
|
|
462
|
+
if (PATTERN_FALLBACK.test(t.name)) return { serverName: srv.name, toolName: t.name, tool: t };
|
|
393
463
|
}
|
|
394
464
|
}
|
|
395
465
|
return null;
|
|
@@ -437,7 +507,11 @@ export function createBridgeSqlRunner(
|
|
|
437
507
|
if (!hit) {
|
|
438
508
|
return { ok: false, error: 'No SQL tool exposed by reachable servers', errorKind: 'schema', durationMs: 0 };
|
|
439
509
|
}
|
|
440
|
-
const
|
|
510
|
+
const sql = stripSqlPreamble((cell.content ?? '').trim());
|
|
511
|
+
const codeParam = findCodeParamName(hit.tool.inputSchema) ?? 'sql';
|
|
512
|
+
const args: Record<string, unknown> = { ...buildToolArgs(hit.tool.inputSchema, codeParam, sql, 'sql'), ...(cell.args ?? {}) };
|
|
513
|
+
args[codeParam] = sql;
|
|
514
|
+
const raw = await callTool(hit.serverName, hit.toolName, args);
|
|
441
515
|
return parseResult(raw, startedAt);
|
|
442
516
|
};
|
|
443
517
|
}
|
|
@@ -464,12 +538,21 @@ interface CanvasLike {
|
|
|
464
538
|
callTool: (name: string, toolName: string, args: unknown) => Promise<unknown>;
|
|
465
539
|
}
|
|
466
540
|
|
|
467
|
-
async function waitForEnabledServers(
|
|
541
|
+
async function waitForEnabledServers(
|
|
542
|
+
canvas: CanvasLike,
|
|
543
|
+
timeoutMs: number,
|
|
544
|
+
expectedCount: number,
|
|
545
|
+
): Promise<void> {
|
|
546
|
+
// expectedCount = number of servers declared by the notebook frontmatter.
|
|
547
|
+
// Without this, an empty canvas.dataServers (autoConnect lost a race against
|
|
548
|
+
// canvas.__canvasVanilla being set, or a notify loop hasn't fired yet)
|
|
549
|
+
// would return immediately and the runner would execute against zero servers.
|
|
468
550
|
const deadline = Date.now() + Math.max(0, timeoutMs);
|
|
469
551
|
while (Date.now() < deadline) {
|
|
470
552
|
const enabled = (canvas.dataServers ?? []).filter((s) => s.enabled !== false);
|
|
471
|
-
|
|
472
|
-
if (enabled.
|
|
553
|
+
// Notebook declared no servers — nothing to wait for.
|
|
554
|
+
if (expectedCount === 0 && enabled.length === 0) return;
|
|
555
|
+
if (enabled.length >= expectedCount && enabled.every((s) => s.connected)) return;
|
|
473
556
|
await new Promise((r) => setTimeout(r, 100));
|
|
474
557
|
}
|
|
475
558
|
}
|
|
@@ -489,7 +572,10 @@ export function bootstrapLiveRefresh(opts: BootstrapLiveRefreshOptions): () => v
|
|
|
489
572
|
onTick?.(overlay);
|
|
490
573
|
return;
|
|
491
574
|
}
|
|
492
|
-
|
|
575
|
+
const declaredServers = Array.isArray((data as { servers?: unknown }).servers)
|
|
576
|
+
? (data as { servers: unknown[] }).servers.length
|
|
577
|
+
: 0;
|
|
578
|
+
await waitForEnabledServers(canvas, timeoutMs ?? 10_000, declaredServers);
|
|
493
579
|
|
|
494
580
|
const sqlRunner = createBridgeSqlRunner(
|
|
495
581
|
canvas.callTool.bind(canvas),
|
|
@@ -1248,8 +1334,6 @@ const NOTEBOOK_STYLES = `
|
|
|
1248
1334
|
|
|
1249
1335
|
.nb-root.nb-view-mode .nb-drag-handle,
|
|
1250
1336
|
.nb-root.nb-view-mode .nb-icon-btn.nb-danger,
|
|
1251
|
-
.nb-root.nb-view-mode .nb-toggle-src,
|
|
1252
|
-
.nb-root.nb-view-mode .nb-toggle-res,
|
|
1253
1337
|
.nb-root.nb-view-mode .nb-add-cell,
|
|
1254
1338
|
.nb-root.nb-view-mode .nbe-cell-actionbar { display: none !important; }
|
|
1255
1339
|
|
|
@@ -1318,11 +1402,23 @@ const NOTEBOOK_STYLES = `
|
|
|
1318
1402
|
display: flex; flex-direction: column; gap: 6px;
|
|
1319
1403
|
}
|
|
1320
1404
|
.nbe-agent-bar auto-chat-input { display: block; }
|
|
1405
|
+
.nbe-actions-sep { color: var(--color-border, #c8c2b4); margin: 0 4px; user-select: none; }
|
|
1406
|
+
.nb-cell-chat-active { outline: 2px solid var(--color-accent, #1d6f5f); outline-offset: 4px; border-radius: 6px; }
|
|
1321
1407
|
.nbe-agent-status {
|
|
1322
1408
|
font-family: var(--font-mono, monospace); font-size: 11px;
|
|
1323
1409
|
color: var(--color-text2, #645d4f);
|
|
1324
1410
|
padding: 0 4px;
|
|
1325
1411
|
}
|
|
1412
|
+
.nbe-agent-progress {
|
|
1413
|
+
display: flex; align-items: center; gap: 10px;
|
|
1414
|
+
font-family: var(--font-mono, monospace); font-size: 11px;
|
|
1415
|
+
color: var(--color-text2, #645d4f);
|
|
1416
|
+
padding: 0 4px;
|
|
1417
|
+
}
|
|
1418
|
+
.nbe-progress-track { letter-spacing: 1px; color: var(--color-accent, #1d6f5f); }
|
|
1419
|
+
.nbe-progress-fill { display: inline-block; }
|
|
1420
|
+
.nbe-agent-progress.nbe-progress-done .nbe-progress-track { color: var(--color-accent, #1d6f5f); }
|
|
1421
|
+
.nbe-agent-progress.nbe-progress-error .nbe-progress-track { color: #b54141; }
|
|
1326
1422
|
/* Run controls (.nb-ctl-pill) remain active in view mode — users can execute cells
|
|
1327
1423
|
* even when the notebook is read-only; only editing the source is locked. */
|
|
1328
1424
|
.nb-root.nb-view-mode textarea,
|