@marimo-team/islands 0.23.14-dev59 → 0.23.14-dev60
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/dist/{chat-ui-D7EyLr6c.js → chat-ui-DinOvbWu.js} +2 -2
- package/dist/{code-visibility-ChrxMIRR.js → code-visibility-DQbr40fP.js} +2 -2
- package/dist/{html-to-image-hevd6UWC.js → html-to-image-_wGfk8V-.js} +23 -22
- package/dist/main.js +5 -5
- package/dist/{process-output-DOO3J9E6.js → process-output-Mh4UrjwM.js} +1 -1
- package/dist/{reveal-component-BLijd8xt.js → reveal-component-B0Zy8cRb.js} +2 -2
- package/package.json +1 -1
- package/src/__tests__/CellStatus.test.tsx +1 -2
- package/src/components/app-config/user-config-form.tsx +26 -0
- package/src/components/editor/cell/CellStatus.tsx +1 -20
- package/src/components/editor/cell/PendingDeleteConfirmation.tsx +1 -1
- package/src/components/tracing/tracing.tsx +2 -1
- package/src/core/codemirror/cells/__tests__/debugger-state.test.ts +89 -4
- package/src/core/codemirror/cells/__tests__/line-timing-decorations.test.ts +126 -0
- package/src/core/codemirror/cells/debugger-state.ts +40 -7
- package/src/core/codemirror/cells/extensions.ts +21 -12
- package/src/core/codemirror/cells/line-timing-decorations.ts +165 -0
- package/src/core/config/feature-flag.tsx +2 -0
- package/src/core/websocket/useMarimoKernelConnection.tsx +3 -4
- package/src/utils/time.ts +20 -0
|
@@ -6,13 +6,13 @@ import { _ as Logger, c as Objects, g as cn, l as useEventListener, t as Button
|
|
|
6
6
|
import { t as require_react } from "./react-DA-nE2FX.js";
|
|
7
7
|
import { t as require_compiler_runtime } from "./compiler-runtime-CEbnTgxf.js";
|
|
8
8
|
import { r as toast } from "./copy-COam1EG7.js";
|
|
9
|
-
import { $t as getRequestClient, Bn as FileText, C as Accordion, Cn as atomWithStorage, E as AccordionTrigger, F as base64ToDataURL, Ft as generateUUID, Gn as CircleX, H as renderHTML, Hn as ExternalLink, Ln as LoaderCircle, Mn as Trash2, Mt as moveToEndOfEditor, O as ChatBubbleIcon, Ot as createVariableInfoElement, Rt as jotaiJsonStorage, S as AIContextRegistry, T as AccordionItem, Tn as Anchor2, Wt as PluralWord, X as notebookAtom, Xt as dataSourceConnectionsAtom, Yt as allTablesAtom, Zt as getTableType, at as parseHtmlContent, b as contextToXml, c as Popover, cn as ZodLocalStorage, d as PopoverTrigger, f as isOutputEmpty, g as formatDatasourceContextForCell, gt as isTracebackMime, h as DatasourceContextProvider, ht as isMarimoErrorsMime, jn as Wrench, mt as isErrorMime, n as Spinner, pn as CellOutputId, r as MarkdownRenderer, rn as singleFacet, t as toPng, u as PopoverContent, v as Boosts, w as AccordionContent, x as AIContextProvider, xt as displayCellName, y as Sections, zn as Info, zt as variablesAtom } from "./html-to-image-
|
|
9
|
+
import { $t as getRequestClient, Bn as FileText, C as Accordion, Cn as atomWithStorage, E as AccordionTrigger, F as base64ToDataURL, Ft as generateUUID, Gn as CircleX, H as renderHTML, Hn as ExternalLink, Ln as LoaderCircle, Mn as Trash2, Mt as moveToEndOfEditor, O as ChatBubbleIcon, Ot as createVariableInfoElement, Rt as jotaiJsonStorage, S as AIContextRegistry, T as AccordionItem, Tn as Anchor2, Wt as PluralWord, X as notebookAtom, Xt as dataSourceConnectionsAtom, Yt as allTablesAtom, Zt as getTableType, at as parseHtmlContent, b as contextToXml, c as Popover, cn as ZodLocalStorage, d as PopoverTrigger, f as isOutputEmpty, g as formatDatasourceContextForCell, gt as isTracebackMime, h as DatasourceContextProvider, ht as isMarimoErrorsMime, jn as Wrench, mt as isErrorMime, n as Spinner, pn as CellOutputId, r as MarkdownRenderer, rn as singleFacet, t as toPng, u as PopoverContent, v as Boosts, w as AccordionContent, x as AIContextProvider, xt as displayCellName, y as Sections, zn as Info, zt as variablesAtom } from "./html-to-image-_wGfk8V-.js";
|
|
10
10
|
import "./chunk-5FQGJX7Z-BbqSm5gU.js";
|
|
11
11
|
import { u as createLucideIcon } from "./dist--2Bqjvs0.js";
|
|
12
12
|
import { C as logNever, I as X, R as ChevronDown, t as Strings } from "./strings-Dq_j3Rxw.js";
|
|
13
13
|
import { C as DropdownMenuTrigger, a as NumberField, f as DropdownMenu, h as DropdownMenuItem, p as DropdownMenuContent, r as Input } from "./input-DtsN7xm-.js";
|
|
14
14
|
import { p as isUrl, v as CircleQuestionMark } from "./toDate-CWNNlFEX.js";
|
|
15
|
-
import { a as MarimoIncomingMessageEvent, d as Square, f as File$1, n as blobToString, t as processOutput, u as deserializeBlob } from "./process-output-
|
|
15
|
+
import { a as MarimoIncomingMessageEvent, d as Square, f as File$1, n as blobToString, t as processOutput, u as deserializeBlob } from "./process-output-Mh4UrjwM.js";
|
|
16
16
|
import "./react-dom-BTJzcVJ9.js";
|
|
17
17
|
import { t as require_jsx_runtime } from "./jsx-runtime-DebpN0FN.js";
|
|
18
18
|
import { A as looseObject, B as union, C as any, D as discriminatedUnion, E as custom, H as safeParseAsync, I as record, L as strictObject, M as never, N as number, O as lazy, P as object$1, R as string, S as _null, T as boolean, V as unknown, W as toJSONSchema, b as _enum, k as literal, w as array$1, x as _instanceof } from "./zod-CijjQh4u.js";
|
|
@@ -6,7 +6,7 @@ import { _ as Logger, c as Objects, g as cn, h as Events, m as useComposedRefs,
|
|
|
6
6
|
import { t as require_react } from "./react-DA-nE2FX.js";
|
|
7
7
|
import { t as require_compiler_runtime } from "./compiler-runtime-CEbnTgxf.js";
|
|
8
8
|
import { n as Copy, r as toast, t as copyToClipboard } from "./copy-COam1EG7.js";
|
|
9
|
-
import { $t as getRequestClient, B as isDataURLString, Bn as FileText, Bt as getFeatureFlag, C as Accordion, Cn as atomWithStorage, E as AccordionTrigger, En as Close$1, Fn as NotebookPen, Gn as CircleX, H as renderHTML, Hn as ExternalLink, In as Minus, It as useChromeActions, Jn as esm_default, Kt as DATA_TYPE_ICON, L as base64ToUint8Array, Ln as LoaderCircle, Lt as adaptForLocalStorage, M as DotFilledIcon, Mn as Trash2, N as PinLeftIcon, Nt as goToCellLine, P as PinRightIcon, Rt as jotaiJsonStorage, T as AccordionItem, Tt as Checkbox, Vn as Eye, Wt as PluralWord, X as notebookAtom, Yn as import_lib, _ as getDatasourceContext, an as extractAllTracebackInfo, bn as jsonParseWithSpecialChar, bt as getCellDomProps, c as Popover, d as PopoverTrigger, dt as viewStateAtom, et as useCellActions, ft as outputIsLoading, hn as SCRATCH_CELL_ID, in as elementContainsMarimoCellFile, j as ChevronRightIcon, jn as Wrench, jt as toggleBreakpoint, k as CheckIcon, l as PopoverClose, ln as filenameAtom, lt as kioskModeAtom, m as useExpandedOutput, mn as HTMLCellId, n as Spinner, nt as useCellNames, on as getTracebackInfo, ot as AnsiUp, pn as CellOutputId, q as getCellEditorView, qn as Braces, qt as getDataTypeColor, r as MarkdownRenderer, t as toPng, tn as useRequestClient, tt as useCellIds, u as PopoverContent, ut as useInstallAllowed, vt as sanitizeHtml, w as AccordionContent, xn as jsonToMarkdown, xt as displayCellName, yt as DATA_CELL_ID, z as extractBase64FromDataURL, zn as Info, __tla as __tla_0 } from "./html-to-image-
|
|
9
|
+
import { $t as getRequestClient, B as isDataURLString, Bn as FileText, Bt as getFeatureFlag, C as Accordion, Cn as atomWithStorage, E as AccordionTrigger, En as Close$1, Fn as NotebookPen, Gn as CircleX, H as renderHTML, Hn as ExternalLink, In as Minus, It as useChromeActions, Jn as esm_default, Kt as DATA_TYPE_ICON, L as base64ToUint8Array, Ln as LoaderCircle, Lt as adaptForLocalStorage, M as DotFilledIcon, Mn as Trash2, N as PinLeftIcon, Nt as goToCellLine, P as PinRightIcon, Rt as jotaiJsonStorage, T as AccordionItem, Tt as Checkbox, Vn as Eye, Wt as PluralWord, X as notebookAtom, Yn as import_lib, _ as getDatasourceContext, an as extractAllTracebackInfo, bn as jsonParseWithSpecialChar, bt as getCellDomProps, c as Popover, d as PopoverTrigger, dt as viewStateAtom, et as useCellActions, ft as outputIsLoading, hn as SCRATCH_CELL_ID, in as elementContainsMarimoCellFile, j as ChevronRightIcon, jn as Wrench, jt as toggleBreakpoint, k as CheckIcon, l as PopoverClose, ln as filenameAtom, lt as kioskModeAtom, m as useExpandedOutput, mn as HTMLCellId, n as Spinner, nt as useCellNames, on as getTracebackInfo, ot as AnsiUp, pn as CellOutputId, q as getCellEditorView, qn as Braces, qt as getDataTypeColor, r as MarkdownRenderer, t as toPng, tn as useRequestClient, tt as useCellIds, u as PopoverContent, ut as useInstallAllowed, vt as sanitizeHtml, w as AccordionContent, xn as jsonToMarkdown, xt as displayCellName, yt as DATA_CELL_ID, z as extractBase64FromDataURL, zn as Info, __tla as __tla_0 } from "./html-to-image-_wGfk8V-.js";
|
|
10
10
|
import { o as useSize, u as createLucideIcon } from "./dist--2Bqjvs0.js";
|
|
11
11
|
import { c as Calendar, i as createReducerAndAtoms, r as Badge } from "./useLifecycle-DegSo0lV.js";
|
|
12
12
|
import { a as ListFilter, i as Table$1, n as $fb18d541ea1ad717$export$ad991b66133851cf, o as ChartPie, r as $5a387cc49350e6db$export$722debc0e56fea39, t as $896ba0a80a8f4d36$export$85fd5fdf27bacc79 } from "./useDateFormatter-CMnRuVmN.js";
|
|
@@ -36209,7 +36209,7 @@ ${d}`,
|
|
|
36209
36209
|
return Logger.warn("Failed to get version from mount config"), null;
|
|
36210
36210
|
}
|
|
36211
36211
|
}
|
|
36212
|
-
marimoVersionAtom = atom(getVersionFromMountConfig() || "0.23.14-
|
|
36212
|
+
marimoVersionAtom = atom(getVersionFromMountConfig() || "0.23.14-dev60");
|
|
36213
36213
|
showCodeInRunModeAtom = atom(true);
|
|
36214
36214
|
atom(null);
|
|
36215
36215
|
var import_compiler_runtime = require_compiler_runtime();
|
|
@@ -19325,7 +19325,8 @@ ${n.sqlString}
|
|
|
19325
19325
|
rtc_v2: false,
|
|
19326
19326
|
cache_panel: false,
|
|
19327
19327
|
external_agents: false,
|
|
19328
|
-
debugger: false
|
|
19328
|
+
debugger: false,
|
|
19329
|
+
line_timing: false
|
|
19329
19330
|
};
|
|
19330
19331
|
getFeatureFlag = function(e) {
|
|
19331
19332
|
var _a2, _b2;
|
|
@@ -20102,7 +20103,26 @@ ${n.sqlString}
|
|
|
20102
20103
|
let n = store.get(breakpointsAtom), r = new Map(n), i = new Set(r.get(e) ?? []);
|
|
20103
20104
|
i.has(t) ? i.delete(t) : i.add(t), i.size > 0 ? r.set(e, i) : r.delete(e), store.set(breakpointsAtom, r), sendBreakpoints(r);
|
|
20104
20105
|
};
|
|
20105
|
-
var
|
|
20106
|
+
var Time = class e {
|
|
20107
|
+
static fromMilliseconds(t) {
|
|
20108
|
+
return t == null ? null : new e(t);
|
|
20109
|
+
}
|
|
20110
|
+
static fromSeconds(t) {
|
|
20111
|
+
return t == null ? null : new e(t * 1e3);
|
|
20112
|
+
}
|
|
20113
|
+
static now() {
|
|
20114
|
+
return new e(Date.now());
|
|
20115
|
+
}
|
|
20116
|
+
constructor(e2) {
|
|
20117
|
+
this.ms = e2;
|
|
20118
|
+
}
|
|
20119
|
+
toMilliseconds() {
|
|
20120
|
+
return this.ms;
|
|
20121
|
+
}
|
|
20122
|
+
toSeconds() {
|
|
20123
|
+
return this.ms / 1e3;
|
|
20124
|
+
}
|
|
20125
|
+
}, MARKDOWN_AUTORUN_USER_EVENTS = [
|
|
20106
20126
|
"input",
|
|
20107
20127
|
"delete",
|
|
20108
20128
|
"undo",
|
|
@@ -24825,26 +24845,7 @@ ${n.sqlString}
|
|
|
24825
24845
|
};
|
|
24826
24846
|
atomWithStorage("marimo:rtc:username", "", jotaiJsonStorage);
|
|
24827
24847
|
const isRtcEnabled = once(() => getFeatureFlag("rtc_v2"));
|
|
24828
|
-
var
|
|
24829
|
-
static fromMilliseconds(t) {
|
|
24830
|
-
return t == null ? null : new e(t);
|
|
24831
|
-
}
|
|
24832
|
-
static fromSeconds(t) {
|
|
24833
|
-
return t == null ? null : new e(t * 1e3);
|
|
24834
|
-
}
|
|
24835
|
-
static now() {
|
|
24836
|
-
return new e(Date.now());
|
|
24837
|
-
}
|
|
24838
|
-
constructor(e2) {
|
|
24839
|
-
this.ms = e2;
|
|
24840
|
-
}
|
|
24841
|
-
toMilliseconds() {
|
|
24842
|
-
return this.ms;
|
|
24843
|
-
}
|
|
24844
|
-
toSeconds() {
|
|
24845
|
-
return this.ms / 1e3;
|
|
24846
|
-
}
|
|
24847
|
-
}, TerminalBuffer = (_c = class {
|
|
24848
|
+
var TerminalBuffer = (_c = class {
|
|
24848
24849
|
constructor() {
|
|
24849
24850
|
__publicField(this, "lines", [
|
|
24850
24851
|
""
|
package/dist/main.js
CHANGED
|
@@ -22,17 +22,17 @@ import { _ as Logger, c as Objects, g as cn, h as Events, i as NOT_SET, l as use
|
|
|
22
22
|
import { t as require_react } from "./react-DA-nE2FX.js";
|
|
23
23
|
import { t as require_compiler_runtime } from "./compiler-runtime-CEbnTgxf.js";
|
|
24
24
|
import { n as Copy, r as toast, t as copyToClipboard } from "./copy-COam1EG7.js";
|
|
25
|
-
import { $ as reducer, $t as getRequestClient, A as ChevronDownIcon, An as Trigger2, At as Paths, Bn as FileText, C as Accordion, Cn as atomWithStorage, Ct as isInternalCellName, D as BorderAllIcon, Dn as Content2, Dt as MarkdownLanguageAdapter, E as AccordionTrigger, Et as customPythonLanguageSupport, Ft as generateUUID, G as hasRunAnyCellAtom, Gt as PluralWords, H as renderHTML, Ht as SQLParser, I as base64ToDataView, J as getCellNames, Jt as require_client, K as createActions, Kn as CircleAlert, Kt as DATA_TYPE_ICON, Ln as LoaderCircle, Mn as Trash2, Nn as Table2, On as Item$1, Pn as PaintRoller, Pt as DeferredRequestRegistry, Q as numColumnsAtom, Qt as convertStatsName, R as dataViewToBase64, Rn as Layers, Rt as jotaiJsonStorage, Sn as atomWithReducer, St as getValidName, T as AccordionItem, Tt as Checkbox, U as getMarimoExportContext, Un as Database, Ut as MarkdownParser, V as safeExtractSetUIElementMessageBuffers, Vn as Eye, Vt as repl, W as hasTrustedExportContext, Wn as Columns2, X as notebookAtom, Y as hasOnlyOneCellAtom, Z as notebookOutline, _n as findCellId, _t as headingToIdentifier, a as useCellFocusActions, bn as jsonParseWithSpecialChar, ct as initialModeAtom, dn as parseDataset, dt as viewStateAtom, en as requestClientAtom, et as useCellActions, f as isOutputEmpty, fn as parseInitialValue, ft as outputIsLoading, gn as UIElementId, i as LazyAnyLanguageCodeMirror, kn as Root2$1, kt as PathBuilder, lt as kioskModeAtom, mn as HTMLCellId, mt as isErrorMime, n as Spinner, nn as isUninstantiated, o as useLastFocusedCellId, p as useExpandedConsoleOutput, pt as outputIsStale, qn as Braces, rt as createCell, s as maybeAddAltairImport, sn as NotebookScopedLocalStorage, st as getInitialAppMode, tn as useRequestClient, tt as useCellIds, un as parseAttrValue, vn as OBJECT_ID_ATTR, w as AccordionContent, wn as selectAtom, wt as normalizeName, yn as RANDOM_ID_ATTR, zn as Info, __tla as __tla_0 } from "./html-to-image-
|
|
25
|
+
import { $ as reducer, $t as getRequestClient, A as ChevronDownIcon, An as Trigger2, At as Paths, Bn as FileText, C as Accordion, Cn as atomWithStorage, Ct as isInternalCellName, D as BorderAllIcon, Dn as Content2, Dt as MarkdownLanguageAdapter, E as AccordionTrigger, Et as customPythonLanguageSupport, Ft as generateUUID, G as hasRunAnyCellAtom, Gt as PluralWords, H as renderHTML, Ht as SQLParser, I as base64ToDataView, J as getCellNames, Jt as require_client, K as createActions, Kn as CircleAlert, Kt as DATA_TYPE_ICON, Ln as LoaderCircle, Mn as Trash2, Nn as Table2, On as Item$1, Pn as PaintRoller, Pt as DeferredRequestRegistry, Q as numColumnsAtom, Qt as convertStatsName, R as dataViewToBase64, Rn as Layers, Rt as jotaiJsonStorage, Sn as atomWithReducer, St as getValidName, T as AccordionItem, Tt as Checkbox, U as getMarimoExportContext, Un as Database, Ut as MarkdownParser, V as safeExtractSetUIElementMessageBuffers, Vn as Eye, Vt as repl, W as hasTrustedExportContext, Wn as Columns2, X as notebookAtom, Y as hasOnlyOneCellAtom, Z as notebookOutline, _n as findCellId, _t as headingToIdentifier, a as useCellFocusActions, bn as jsonParseWithSpecialChar, ct as initialModeAtom, dn as parseDataset, dt as viewStateAtom, en as requestClientAtom, et as useCellActions, f as isOutputEmpty, fn as parseInitialValue, ft as outputIsLoading, gn as UIElementId, i as LazyAnyLanguageCodeMirror, kn as Root2$1, kt as PathBuilder, lt as kioskModeAtom, mn as HTMLCellId, mt as isErrorMime, n as Spinner, nn as isUninstantiated, o as useLastFocusedCellId, p as useExpandedConsoleOutput, pt as outputIsStale, qn as Braces, rt as createCell, s as maybeAddAltairImport, sn as NotebookScopedLocalStorage, st as getInitialAppMode, tn as useRequestClient, tt as useCellIds, un as parseAttrValue, vn as OBJECT_ID_ATTR, w as AccordionContent, wn as selectAtom, wt as normalizeName, yn as RANDOM_ID_ATTR, zn as Info, __tla as __tla_0 } from "./html-to-image-_wGfk8V-.js";
|
|
26
26
|
import { __tla as __tla_1 } from "./chunk-5FQGJX7Z-BbqSm5gU.js";
|
|
27
27
|
import { o as useSize, s as Root$1, u as createLucideIcon } from "./dist--2Bqjvs0.js";
|
|
28
28
|
import { A as SquareFunction, C as DEFAULT_COLOR_SCHEME, D as SCALE_TYPE_DESCRIPTIONS, E as EMPTY_VALUE$1, O as TIME_UNIT_DESCRIPTIONS, S as DEFAULT_AGGREGATION, T as DEFAULT_TIME_UNIT, _ as AGGREGATION_TYPE_DESCRIPTIONS, a as AGGREGATION_FNS$1, b as COLOR_SCHEMES, c as COLOR_BY_FIELDS, d as NONE_VALUE, f as SELECTABLE_DATA_TYPES, g as TIME_UNITS, h as STRING_AGGREGATION_FNS, i as convertDataTypeToSelectable, j as ChartColumn, k as escapeFieldName, l as COMBINED_TIME_UNITS, m as SORT_TYPES, n as createSpecWithoutData, o as BIN_AGGREGATION, p as SINGLE_TIME_UNITS, r as isFieldSet, s as CHART_TYPES, t as augmentSpecWithData, u as ChartType, v as AGGREGATION_TYPE_ICON, w as DEFAULT_MAX_BINS_FACET, x as COUNT_FIELD, y as CHART_TYPE_ICON } from "./spec-Cz-Bj1JI.js";
|
|
29
|
-
import { $ as PANEL_TYPES, $t as hasFunctionProperty, A as prettifyRowCount, At as INDEX_COLUMN_NAME, B as CompactChipRow, Bt as Tabs, C as downloadSizeLimitAtom, Ct as ColumnChartSpecModel, D as ErrorState, Dt as getPageIndexForRow, E as EmptyState, Et as usePrevious$1, F as ContextMenuItem, Ft as isNullishFilter, G as Command, Gt as ChartInfoState, H as DateRangePicker, Ht as TabsList, I as ContextMenuSeparator, It as Maps, J as CommandItem, Jt as useOverflowDetection, K as CommandEmpty, Kt as ChartLoadingState, L as ContextMenuTrigger, Lt as dateToLocalISODate, M as getUserColumnVisibilityCounts, Mt as TOO_MANY_ROWS, N as ContextMenu, Nt as toFieldTypes, O as LoadingState, Ot as loadTableAndRawData, P as ContextMenuContent, Pt as getMimeValues, Q as ContextAwarePanelItem, Qt as EmotionCacheProvider, R as useInternalStateWithSync, Rt as dateToLocalISODateTime, S as Filenames, St as ColumnChartContext, T as ColumnPreviewContainer, Tt as useIntersectionObserver, U as Combobox, Ut as TabsTrigger, V as DatePicker, Vt as TabsContent, W as ComboboxItem, Wt as ChartErrorState, X as CommandSeparator, Xt as Kbd, Y as CommandList, Yt as RenderTextWithLinks, Z as smartMatch, Zt as HtmlOutput, _ as ADD_PRINTING_CLASS, _t as TableRow, an as Funnel, at as slotsController, b as downloadHTMLAsImage, bt as inferFieldTypes, c as Slide, cn as Ellipsis, d as RadioGroupItem, dn as ChevronsUpDown, dt as Provider$1, en as isRecord, et as contextAwarePanelOpen, f as JsonOutput, fn as ChevronsRight, ft as Table, g as InstallPackageButton, gn as ArrowDownWideNarrow, gt as TableHeader, h as DataTable, hn as ChevronLeft, ht as TableHead, in as GripHorizontal, it as SlotNames, j as getColumnCountForDisplay, jt as SELECT_COLUMN_ID, k as prettifyRowColumnCount, kt as loadTableData, l as Switch, ln as Download, m as OutputRenderer, mn as ChevronsDownUp, mt as TableCell, n as marimoVersionAtom, nn as $fae977aafc393c5c$export$6b862160d295c8e, nt as contextAwarePanelType, o as SLIDE_TYPE_OPTIONS_BY_VALUE, on as EyeOff, ot as Toggle, p as OutputArea, pn as ChevronsLeft, pt as TableBody, q as CommandInput, qt as LazyVegaEmbed, r as showCodeInRunModeAtom, rn as TextWrap, rt as isCellAwareAtom, t as useNotebookCodeAvailable, tn as $fae977aafc393c5c$export$588937bcd60ade55, tt as contextAwarePanelOwner, u as RadioGroup, un as Code, ut as Fill, v as downloadBlob, vt as NAMELESS_COLUMN_PREFIX, w as ColumnName, wt as DelayMount, x as Progress, xt as renderCellValue, y as downloadByURL, yt as generateColumns, z as useSelectList, zt as dateToLocalISOTime, __tla as __tla_2 } from "./code-visibility-
|
|
29
|
+
import { $ as PANEL_TYPES, $t as hasFunctionProperty, A as prettifyRowCount, At as INDEX_COLUMN_NAME, B as CompactChipRow, Bt as Tabs, C as downloadSizeLimitAtom, Ct as ColumnChartSpecModel, D as ErrorState, Dt as getPageIndexForRow, E as EmptyState, Et as usePrevious$1, F as ContextMenuItem, Ft as isNullishFilter, G as Command, Gt as ChartInfoState, H as DateRangePicker, Ht as TabsList, I as ContextMenuSeparator, It as Maps, J as CommandItem, Jt as useOverflowDetection, K as CommandEmpty, Kt as ChartLoadingState, L as ContextMenuTrigger, Lt as dateToLocalISODate, M as getUserColumnVisibilityCounts, Mt as TOO_MANY_ROWS, N as ContextMenu, Nt as toFieldTypes, O as LoadingState, Ot as loadTableAndRawData, P as ContextMenuContent, Pt as getMimeValues, Q as ContextAwarePanelItem, Qt as EmotionCacheProvider, R as useInternalStateWithSync, Rt as dateToLocalISODateTime, S as Filenames, St as ColumnChartContext, T as ColumnPreviewContainer, Tt as useIntersectionObserver, U as Combobox, Ut as TabsTrigger, V as DatePicker, Vt as TabsContent, W as ComboboxItem, Wt as ChartErrorState, X as CommandSeparator, Xt as Kbd, Y as CommandList, Yt as RenderTextWithLinks, Z as smartMatch, Zt as HtmlOutput, _ as ADD_PRINTING_CLASS, _t as TableRow, an as Funnel, at as slotsController, b as downloadHTMLAsImage, bt as inferFieldTypes, c as Slide, cn as Ellipsis, d as RadioGroupItem, dn as ChevronsUpDown, dt as Provider$1, en as isRecord, et as contextAwarePanelOpen, f as JsonOutput, fn as ChevronsRight, ft as Table, g as InstallPackageButton, gn as ArrowDownWideNarrow, gt as TableHeader, h as DataTable, hn as ChevronLeft, ht as TableHead, in as GripHorizontal, it as SlotNames, j as getColumnCountForDisplay, jt as SELECT_COLUMN_ID, k as prettifyRowColumnCount, kt as loadTableData, l as Switch, ln as Download, m as OutputRenderer, mn as ChevronsDownUp, mt as TableCell, n as marimoVersionAtom, nn as $fae977aafc393c5c$export$6b862160d295c8e, nt as contextAwarePanelType, o as SLIDE_TYPE_OPTIONS_BY_VALUE, on as EyeOff, ot as Toggle, p as OutputArea, pn as ChevronsLeft, pt as TableBody, q as CommandInput, qt as LazyVegaEmbed, r as showCodeInRunModeAtom, rn as TextWrap, rt as isCellAwareAtom, t as useNotebookCodeAvailable, tn as $fae977aafc393c5c$export$588937bcd60ade55, tt as contextAwarePanelOwner, u as RadioGroup, un as Code, ut as Fill, v as downloadBlob, vt as NAMELESS_COLUMN_PREFIX, w as ColumnName, wt as DelayMount, x as Progress, xt as renderCellValue, y as downloadByURL, yt as generateColumns, z as useSelectList, zt as dateToLocalISOTime, __tla as __tla_2 } from "./code-visibility-DQbr40fP.js";
|
|
30
30
|
import { c as Calendar, i as createReducerAndAtoms, n as useOnUnmount, o as ToggleLeft, t as useOnMount } from "./useLifecycle-DegSo0lV.js";
|
|
31
31
|
import { t as Check } from "./check-C9OoNtR4.js";
|
|
32
32
|
import { A as Icon, C as logNever, D as $18f2051aff69b9bf$export$a54013f0d02a8f82, E as $18f2051aff69b9bf$export$43bb16f9c6d9e3f7, F as createCollection, I as X, M as clamp$2, N as usePrevious$2, P as useDirection, R as ChevronDown, S as assertNever, a as SelectGroup, c as SelectSeparator, d as NativeSelect, i as SelectContent, j as Trigger$1, l as SelectTrigger, n as capitalize, o as SelectItem, r as Select, s as SelectLabel, t as Strings, u as SelectValue, w as $a916eb452884faea$export$b7a616150fdb9f44 } from "./strings-Dq_j3Rxw.js";
|
|
33
33
|
import { B as $64fa3d84918910a7$export$4d86445c2cf5e3, C as DropdownMenuTrigger, Ft as $65484d02dcb7eb3e$export$457c3d6518dd4c6f, It as $3ef42575df84b30b$export$9d1611c77c2fe928, W as $64fa3d84918910a7$export$df3a06d6289f983e, Wt as $ff5963eb1fccf552$export$e08e3b67e392101e, X as useDebouncedCallback, Y as useDebounceControlledState, a as NumberField, d as roundToFractionDigits, f as DropdownMenu, g as DropdownMenuLabel, gn as ChevronRight, h as DropdownMenuItem, hn as Circle, i as OnBlurredInput, l as prettyNumber, m as DropdownMenuGroup, n as DebouncedNumberInput, o as maxFractionDigitsForSteps, ot as $f7dceffc5ad7768b$export$4e328f61c538687f, p as DropdownMenuContent, pt as $6179b936705e76d3$export$ae780daf29e6d456, r as Input, st as $701a24aa0da5b062$export$ea18c227d4417cc3, t as DebouncedInput, u as prettyScientificNumber, v as DropdownMenuSeparator, xt as $458b0a5536c1a7cf$export$40bfa8c7b0832715, y as DropdownMenuShortcut, z as $64fa3d84918910a7$export$29f1550f4b0d4415 } from "./input-DtsN7xm-.js";
|
|
34
34
|
import { _ as isWasm, c as asRemoteURL, d as isStaticNotebook, f as appendQueryParams, g as Deferred, m as require_cuid2, u as getStaticVirtualFiles, v as CircleQuestionMark } from "./toDate-CWNNlFEX.js";
|
|
35
|
-
import { a as MarimoIncomingMessageEvent, c as MarimoValueUpdateEvent, d as Square, f as File, i as PythonIcon, l as createInputEvent, n as blobToString, o as MarimoValueInputEvent, r as filesToBase64, s as MarimoValueReadyEvent, t as processOutput, u as deserializeBlob } from "./process-output-
|
|
35
|
+
import { a as MarimoIncomingMessageEvent, c as MarimoValueUpdateEvent, d as Square, f as File, i as PythonIcon, l as createInputEvent, n as blobToString, o as MarimoValueInputEvent, r as filesToBase64, s as MarimoValueReadyEvent, t as processOutput, u as deserializeBlob } from "./process-output-Mh4UrjwM.js";
|
|
36
36
|
import { n as Trash, r as Pencil, t as BulkEdit } from "./types-C2Ir191_.js";
|
|
37
37
|
import { n as require_prop_types, r as Plus, t as ErrorBoundary } from "./ErrorBoundary-DE6tzZf-.js";
|
|
38
38
|
import { t as require_react_dom } from "./react-dom-BTJzcVJ9.js";
|
|
@@ -8895,7 +8895,7 @@ let __tla = Promise.all([
|
|
|
8895
8895
|
};
|
|
8896
8896
|
}
|
|
8897
8897
|
};
|
|
8898
|
-
var LazyChatbot = import_react.lazy(() => import("./chat-ui-
|
|
8898
|
+
var LazyChatbot = import_react.lazy(() => import("./chat-ui-DinOvbWu.js").then((e) => ({
|
|
8899
8899
|
default: e.Chatbot
|
|
8900
8900
|
}))), messageSchema = array(object({
|
|
8901
8901
|
id: string(),
|
|
@@ -38033,7 +38033,7 @@ ${c}
|
|
|
38033
38033
|
function _temp2$2(e) {
|
|
38034
38034
|
e.target === e.currentTarget && e.key === "Enter" && (e.preventDefault(), e.stopPropagation(), e.currentTarget.click());
|
|
38035
38035
|
}
|
|
38036
|
-
var LazySlidesComponent = import_react.lazy(() => import("./reveal-component-
|
|
38036
|
+
var LazySlidesComponent = import_react.lazy(() => import("./reveal-component-B0Zy8cRb.js"));
|
|
38037
38037
|
const SlidesLayoutRenderer = ({ layout: e, setLayout: r, cells: c, mode: l }) => {
|
|
38038
38038
|
var _a5;
|
|
38039
38039
|
let u = useAtomValue(kioskModeAtom), d = l === "read" || u, f = useAtomValue(numColumnsAtom) > 1, [p, m] = (0, import_react.useState)(null), { slideCells: h, skippedIds: g, noOutputIds: _, slideTypes: v, startCellIndex: y } = (0, import_react.useMemo)(() => computeSlideCellsInfo(c, e), [
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { s as __toESM } from "./chunk-BNovOVIE.js";
|
|
2
2
|
import { t as require_compiler_runtime } from "./compiler-runtime-CEbnTgxf.js";
|
|
3
|
-
import { at as parseHtmlContent, it as ansiToPlainText } from "./html-to-image-
|
|
3
|
+
import { at as parseHtmlContent, it as ansiToPlainText } from "./html-to-image-_wGfk8V-.js";
|
|
4
4
|
import { u as createLucideIcon } from "./dist--2Bqjvs0.js";
|
|
5
5
|
import { t as Strings } from "./strings-Dq_j3Rxw.js";
|
|
6
6
|
import { t as require_jsx_runtime } from "./jsx-runtime-DebpN0FN.js";
|
|
@@ -6,10 +6,10 @@ import { s as __toESM } from "./chunk-BNovOVIE.js";
|
|
|
6
6
|
import { _ as Logger, g as cn, h as Events, l as useEventListener, t as Button } from "./button-BacYv-bE.js";
|
|
7
7
|
import { t as require_react } from "./react-DA-nE2FX.js";
|
|
8
8
|
import { t as require_compiler_runtime } from "./compiler-runtime-CEbnTgxf.js";
|
|
9
|
-
import { lt as kioskModeAtom, pt as outputIsStale } from "./html-to-image-
|
|
9
|
+
import { lt as kioskModeAtom, pt as outputIsStale } from "./html-to-image-_wGfk8V-.js";
|
|
10
10
|
import "./chunk-5FQGJX7Z-BbqSm5gU.js";
|
|
11
11
|
import { u as createLucideIcon } from "./dist--2Bqjvs0.js";
|
|
12
|
-
import { a as DEFAULT_SLIDE_TYPE, c as Slide, ct as PanelGroup, i as DEFAULT_DECK_TRANSITION, lt as PanelResizeHandle, on as EyeOff, s as SlideSidebar, sn as Expand, st as Panel, t as useNotebookCodeAvailable, un as Code } from "./code-visibility-
|
|
12
|
+
import { a as DEFAULT_SLIDE_TYPE, c as Slide, ct as PanelGroup, i as DEFAULT_DECK_TRANSITION, lt as PanelResizeHandle, on as EyeOff, s as SlideSidebar, sn as Expand, st as Panel, t as useNotebookCodeAvailable, un as Code } from "./code-visibility-DQbr40fP.js";
|
|
13
13
|
import { X as useDebouncedCallback } from "./input-DtsN7xm-.js";
|
|
14
14
|
import "./toDate-CWNNlFEX.js";
|
|
15
15
|
import "./react-dom-BTJzcVJ9.js";
|
package/package.json
CHANGED
|
@@ -3,11 +3,10 @@
|
|
|
3
3
|
import { render } from "@testing-library/react";
|
|
4
4
|
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
|
5
5
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
|
6
|
-
import type
|
|
6
|
+
import { formatElapsedTime, type Seconds } from "@/utils/time";
|
|
7
7
|
import {
|
|
8
8
|
CellStatusComponent,
|
|
9
9
|
ElapsedTime,
|
|
10
|
-
formatElapsedTime,
|
|
11
10
|
} from "../components/editor/cell/CellStatus";
|
|
12
11
|
|
|
13
12
|
// Mock date-fns to have consistent date formatting in tests
|
|
@@ -1314,6 +1314,32 @@ export const UserConfigForm: React.FC = () => {
|
|
|
1314
1314
|
</div>
|
|
1315
1315
|
)}
|
|
1316
1316
|
/>
|
|
1317
|
+
<OverriddenFormField
|
|
1318
|
+
control={form.control}
|
|
1319
|
+
name="experimental.line_timing"
|
|
1320
|
+
render={({ field, override }) => (
|
|
1321
|
+
<div className="flex flex-col gap-y-1">
|
|
1322
|
+
<FormItem className={formItemClasses}>
|
|
1323
|
+
<FormLabel className="font-normal">Line Timing</FormLabel>
|
|
1324
|
+
<FormControl>
|
|
1325
|
+
<Checkbox
|
|
1326
|
+
data-testid="line-timing-checkbox"
|
|
1327
|
+
checked={override.value === true}
|
|
1328
|
+
disabled={override.isOverridden}
|
|
1329
|
+
onCheckedChange={field.onChange}
|
|
1330
|
+
/>
|
|
1331
|
+
</FormControl>
|
|
1332
|
+
</FormItem>
|
|
1333
|
+
<IsOverridden override={override} />
|
|
1334
|
+
<FormDescription>
|
|
1335
|
+
Highlight the line a cell is currently executing in green,
|
|
1336
|
+
with an elapsed timer next to long-running lines. Adds
|
|
1337
|
+
tracing overhead to every run while enabled. This change
|
|
1338
|
+
requires a page refresh to take effect.
|
|
1339
|
+
</FormDescription>
|
|
1340
|
+
</div>
|
|
1341
|
+
)}
|
|
1342
|
+
/>
|
|
1317
1343
|
</SettingGroup>
|
|
1318
1344
|
);
|
|
1319
1345
|
}
|
|
@@ -14,7 +14,7 @@ import { Tooltip } from "../../ui/tooltip";
|
|
|
14
14
|
|
|
15
15
|
import "./cell-status.css";
|
|
16
16
|
import { formatDistanceToNow } from "date-fns";
|
|
17
|
-
import { Time } from "@/utils/time";
|
|
17
|
+
import { formatElapsedTime, Time } from "@/utils/time";
|
|
18
18
|
|
|
19
19
|
export interface CellStatusComponentProps extends Pick<
|
|
20
20
|
CellRuntimeState,
|
|
@@ -348,25 +348,6 @@ const LastRanTime = (props: { lastRanTime: number }) => {
|
|
|
348
348
|
);
|
|
349
349
|
};
|
|
350
350
|
|
|
351
|
-
export function formatElapsedTime(elapsedTime: number | null) {
|
|
352
|
-
if (elapsedTime === null) {
|
|
353
|
-
return "";
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
const milliseconds = elapsedTime;
|
|
357
|
-
const seconds = milliseconds / 1000;
|
|
358
|
-
|
|
359
|
-
if (seconds >= 60) {
|
|
360
|
-
const minutes = Math.floor(seconds / 60);
|
|
361
|
-
const remainingSeconds = Math.floor(seconds % 60);
|
|
362
|
-
return `${minutes}m${remainingSeconds}s`;
|
|
363
|
-
}
|
|
364
|
-
if (seconds >= 1) {
|
|
365
|
-
return `${seconds.toFixed(2).toString()}s`;
|
|
366
|
-
}
|
|
367
|
-
return `${milliseconds.toFixed(0).toString()}ms`;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
351
|
const CellTimer = (props: { startTime: Time }) => {
|
|
371
352
|
const time = useElapsedTime(props.startTime.toMilliseconds());
|
|
372
353
|
return <span>{formatElapsedTime(time)}</span>;
|
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
import { AlertTriangleIcon } from "lucide-react";
|
|
4
4
|
import React from "react";
|
|
5
5
|
import { FocusScope } from "react-aria";
|
|
6
|
-
import { formatElapsedTime } from "@/components/editor/cell/CellStatus";
|
|
7
6
|
import { CellLink } from "@/components/editor/links/cell-link";
|
|
8
7
|
import { Button } from "@/components/ui/button";
|
|
9
8
|
import type { CellId } from "@/core/cells/ids";
|
|
10
9
|
import { usePendingDelete } from "@/core/cells/pending-delete-service";
|
|
11
10
|
import { cn } from "@/utils/cn";
|
|
11
|
+
import { formatElapsedTime } from "@/utils/time";
|
|
12
12
|
|
|
13
13
|
export const PendingDeleteConfirmation: React.FC<{ cellId: CellId }> = ({
|
|
14
14
|
cellId,
|
|
@@ -28,9 +28,10 @@ import {
|
|
|
28
28
|
} from "@/core/cells/runs";
|
|
29
29
|
import { type ResolvedTheme, useTheme } from "@/theme/useTheme";
|
|
30
30
|
import { cn } from "@/utils/cn";
|
|
31
|
+
import { formatElapsedTime } from "@/utils/time";
|
|
31
32
|
import { ClearButton } from "../buttons/clear-button";
|
|
32
33
|
import type { SignalListener } from "../charts/types";
|
|
33
|
-
import { ElapsedTime
|
|
34
|
+
import { ElapsedTime } from "../editor/cell/CellStatus";
|
|
34
35
|
import { PanelEmptyState } from "../editor/chrome/panels/empty-state";
|
|
35
36
|
import { usePanelSection } from "../editor/chrome/panels/panel-context";
|
|
36
37
|
import { CellLink } from "../editor/links/cell-link";
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
|
-
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
4
|
import { MockRequestClient } from "@/__mocks__/requests";
|
|
5
5
|
import { cellId } from "@/__tests__/branded";
|
|
6
6
|
import { store } from "@/core/state/jotai";
|
|
7
7
|
import {
|
|
8
|
+
activeLineAtom,
|
|
8
9
|
breakpointsAtom,
|
|
9
10
|
clearCellBreakpoints,
|
|
11
|
+
createActiveLineInfoAtom,
|
|
10
12
|
createCellBreakpointsAtom,
|
|
11
13
|
createDebuggerLineAtom,
|
|
12
|
-
debuggerCurrentLineAtom,
|
|
13
14
|
resyncBreakpoints,
|
|
15
|
+
setActiveLine,
|
|
14
16
|
toggleBreakpoint,
|
|
15
17
|
} from "../debugger-state";
|
|
16
18
|
|
|
@@ -25,7 +27,7 @@ const cell2 = cellId("cell2");
|
|
|
25
27
|
describe("debugger-state", () => {
|
|
26
28
|
beforeEach(() => {
|
|
27
29
|
store.set(breakpointsAtom, new Map());
|
|
28
|
-
store.set(
|
|
30
|
+
store.set(activeLineAtom, null);
|
|
29
31
|
vi.clearAllMocks();
|
|
30
32
|
});
|
|
31
33
|
|
|
@@ -130,10 +132,93 @@ describe("debugger-state", () => {
|
|
|
130
132
|
});
|
|
131
133
|
|
|
132
134
|
it("returns the line only for the matching cell", () => {
|
|
133
|
-
store.set(
|
|
135
|
+
store.set(activeLineAtom, { cellId: cell1, line: 10, startedAtMs: 0 });
|
|
134
136
|
|
|
135
137
|
expect(store.get(createDebuggerLineAtom(cell1))).toBe(10);
|
|
136
138
|
expect(store.get(createDebuggerLineAtom(cell2))).toBeNull();
|
|
137
139
|
});
|
|
138
140
|
});
|
|
141
|
+
|
|
142
|
+
describe("setActiveLine", () => {
|
|
143
|
+
beforeEach(() => {
|
|
144
|
+
vi.useFakeTimers();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
afterEach(() => {
|
|
148
|
+
vi.useRealTimers();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("stamps startedAtMs when the line is first set", () => {
|
|
152
|
+
vi.setSystemTime(1000);
|
|
153
|
+
setActiveLine({ cellId: cell1, line: 3 });
|
|
154
|
+
|
|
155
|
+
expect(store.get(activeLineAtom)).toEqual({
|
|
156
|
+
cellId: cell1,
|
|
157
|
+
line: 3,
|
|
158
|
+
startedAtMs: 1000,
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("preserves startedAtMs when the same line is set again", () => {
|
|
163
|
+
vi.setSystemTime(1000);
|
|
164
|
+
setActiveLine({ cellId: cell1, line: 3 });
|
|
165
|
+
vi.setSystemTime(2000);
|
|
166
|
+
setActiveLine({ cellId: cell1, line: 3 });
|
|
167
|
+
|
|
168
|
+
expect(store.get(activeLineAtom)).toEqual({
|
|
169
|
+
cellId: cell1,
|
|
170
|
+
line: 3,
|
|
171
|
+
startedAtMs: 1000,
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("resets startedAtMs when the line changes", () => {
|
|
176
|
+
vi.setSystemTime(1000);
|
|
177
|
+
setActiveLine({ cellId: cell1, line: 3 });
|
|
178
|
+
vi.setSystemTime(2000);
|
|
179
|
+
setActiveLine({ cellId: cell1, line: 4 });
|
|
180
|
+
|
|
181
|
+
expect(store.get(activeLineAtom)).toEqual({
|
|
182
|
+
cellId: cell1,
|
|
183
|
+
line: 4,
|
|
184
|
+
startedAtMs: 2000,
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("resets startedAtMs when the cell changes", () => {
|
|
189
|
+
vi.setSystemTime(1000);
|
|
190
|
+
setActiveLine({ cellId: cell1, line: 3 });
|
|
191
|
+
vi.setSystemTime(2000);
|
|
192
|
+
setActiveLine({ cellId: cell2, line: 3 });
|
|
193
|
+
|
|
194
|
+
expect(store.get(activeLineAtom)).toEqual({
|
|
195
|
+
cellId: cell2,
|
|
196
|
+
line: 3,
|
|
197
|
+
startedAtMs: 2000,
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("clears the active line on null", () => {
|
|
202
|
+
setActiveLine({ cellId: cell1, line: 3 });
|
|
203
|
+
setActiveLine(null);
|
|
204
|
+
|
|
205
|
+
expect(store.get(activeLineAtom)).toBeNull();
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
describe("createActiveLineInfoAtom", () => {
|
|
210
|
+
it("returns null when no line is set", () => {
|
|
211
|
+
expect(store.get(createActiveLineInfoAtom(cell1))).toBeNull();
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("returns line info only for the matching cell", () => {
|
|
215
|
+
store.set(activeLineAtom, { cellId: cell1, line: 10, startedAtMs: 42 });
|
|
216
|
+
|
|
217
|
+
expect(store.get(createActiveLineInfoAtom(cell1))).toEqual({
|
|
218
|
+
line: 10,
|
|
219
|
+
startedAtMs: 42,
|
|
220
|
+
});
|
|
221
|
+
expect(store.get(createActiveLineInfoAtom(cell2))).toBeNull();
|
|
222
|
+
});
|
|
223
|
+
});
|
|
139
224
|
});
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { python } from "@codemirror/lang-python";
|
|
4
|
+
import { EditorState } from "@codemirror/state";
|
|
5
|
+
import { EditorView } from "@codemirror/view";
|
|
6
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
7
|
+
import { createMockObservable } from "@/core/state/__mocks__/mocks";
|
|
8
|
+
import type { Observable } from "@/core/state/observable";
|
|
9
|
+
import {
|
|
10
|
+
type ActiveLineInfo,
|
|
11
|
+
activeLineTimer,
|
|
12
|
+
} from "../line-timing-decorations";
|
|
13
|
+
|
|
14
|
+
type MockObservable = Observable<ActiveLineInfo | null> & {
|
|
15
|
+
set: (value: ActiveLineInfo | null) => void;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function createEditor(
|
|
19
|
+
content: string,
|
|
20
|
+
infoObservable: Observable<ActiveLineInfo | null>,
|
|
21
|
+
) {
|
|
22
|
+
const state = EditorState.create({
|
|
23
|
+
doc: content,
|
|
24
|
+
extensions: [python(), activeLineTimer(infoObservable)],
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
return new EditorView({
|
|
28
|
+
state,
|
|
29
|
+
parent: document.body,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const CODE = `a = 1
|
|
34
|
+
b = 2
|
|
35
|
+
c = 3`;
|
|
36
|
+
|
|
37
|
+
describe("line-timing-decorations", () => {
|
|
38
|
+
let view: EditorView | null = null;
|
|
39
|
+
let observable: MockObservable;
|
|
40
|
+
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
vi.useFakeTimers();
|
|
43
|
+
vi.setSystemTime(10_000);
|
|
44
|
+
observable = createMockObservable<ActiveLineInfo | null>(null);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
afterEach(() => {
|
|
48
|
+
if (view) {
|
|
49
|
+
view.destroy();
|
|
50
|
+
view = null;
|
|
51
|
+
}
|
|
52
|
+
document.body.innerHTML = "";
|
|
53
|
+
vi.useRealTimers();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("highlights the active line and no other", () => {
|
|
57
|
+
view = createEditor(CODE, observable);
|
|
58
|
+
|
|
59
|
+
expect(view.dom.querySelector(".cm-timing-current-line")).toBeNull();
|
|
60
|
+
|
|
61
|
+
observable.set({ line: 2, startedAtMs: Date.now() });
|
|
62
|
+
|
|
63
|
+
const highlighted = view.dom.querySelectorAll(".cm-timing-current-line");
|
|
64
|
+
expect(highlighted).toHaveLength(1);
|
|
65
|
+
expect(highlighted[0].textContent).toContain("b = 2");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("keeps the timer empty before the threshold and formats after", () => {
|
|
69
|
+
view = createEditor(CODE, observable);
|
|
70
|
+
observable.set({ line: 1, startedAtMs: Date.now() });
|
|
71
|
+
|
|
72
|
+
const timer = view.dom.querySelector(".cm-line-timer");
|
|
73
|
+
expect(timer).not.toBeNull();
|
|
74
|
+
expect(timer?.textContent).toBe("");
|
|
75
|
+
|
|
76
|
+
// Still below the 500ms threshold.
|
|
77
|
+
vi.advanceTimersByTime(250);
|
|
78
|
+
expect(timer?.textContent).toBe("");
|
|
79
|
+
|
|
80
|
+
// Past the threshold: shows the elapsed time and keeps ticking.
|
|
81
|
+
vi.advanceTimersByTime(500);
|
|
82
|
+
expect(timer?.textContent).toBe("750ms");
|
|
83
|
+
|
|
84
|
+
vi.advanceTimersByTime(1000);
|
|
85
|
+
expect(timer?.textContent).toBe("1.75s");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("reuses the widget DOM node when startedAtMs is unchanged", () => {
|
|
89
|
+
view = createEditor(CODE, observable);
|
|
90
|
+
observable.set({ line: 1, startedAtMs: Date.now() });
|
|
91
|
+
|
|
92
|
+
const before = view.dom.querySelector(".cm-line-timer");
|
|
93
|
+
expect(before).not.toBeNull();
|
|
94
|
+
|
|
95
|
+
// A fresh info object for the same line and start time (e.g. a duplicate
|
|
96
|
+
// notification) must not recreate the widget.
|
|
97
|
+
observable.set({ line: 1, startedAtMs: 10_000 });
|
|
98
|
+
|
|
99
|
+
expect(view.dom.querySelector(".cm-line-timer")).toBe(before);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("clears the timer interval when the widget is destroyed", () => {
|
|
103
|
+
const setIntervalSpy = vi.spyOn(window, "setInterval");
|
|
104
|
+
const clearIntervalSpy = vi.spyOn(window, "clearInterval");
|
|
105
|
+
view = createEditor(CODE, observable);
|
|
106
|
+
observable.set({ line: 1, startedAtMs: Date.now() });
|
|
107
|
+
|
|
108
|
+
expect(view.dom.querySelector(".cm-line-timer")).not.toBeNull();
|
|
109
|
+
expect(setIntervalSpy).toHaveBeenCalledTimes(1);
|
|
110
|
+
const intervalId = setIntervalSpy.mock.results[0].value;
|
|
111
|
+
|
|
112
|
+
observable.set(null);
|
|
113
|
+
|
|
114
|
+
expect(view.dom.querySelector(".cm-line-timer")).toBeNull();
|
|
115
|
+
expect(clearIntervalSpy).toHaveBeenCalledWith(intervalId);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("renders nothing for an out-of-range line", () => {
|
|
119
|
+
view = createEditor(CODE, observable);
|
|
120
|
+
|
|
121
|
+
observable.set({ line: 100, startedAtMs: Date.now() });
|
|
122
|
+
|
|
123
|
+
expect(view.dom.querySelector(".cm-timing-current-line")).toBeNull();
|
|
124
|
+
expect(view.dom.querySelector(".cm-line-timer")).toBeNull();
|
|
125
|
+
});
|
|
126
|
+
});
|
|
@@ -2,27 +2,50 @@
|
|
|
2
2
|
|
|
3
3
|
import { atom } from "jotai";
|
|
4
4
|
import type { CellId } from "@/core/cells/ids";
|
|
5
|
+
import type { ActiveLineInfo } from "@/core/codemirror/cells/line-timing-decorations";
|
|
5
6
|
import { getRequestClient } from "@/core/network/requests";
|
|
6
7
|
import { store } from "@/core/state/jotai";
|
|
7
8
|
import { Logger } from "@/utils/Logger";
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
|
-
* State for the experimental live debugger.
|
|
11
|
+
* State for the experimental live debugger and line-timing highlight.
|
|
11
12
|
*
|
|
12
|
-
* - `
|
|
13
|
-
*
|
|
14
|
-
*
|
|
13
|
+
* - `activeLineAtom` holds the cell + line the kernel's frame watcher is
|
|
14
|
+
* currently executing (driven by `active-line` notifications), plus when
|
|
15
|
+
* the frontend first saw execution on that line. Only one cell runs at a
|
|
16
|
+
* time, so a single global value suffices.
|
|
15
17
|
* - `breakpointsAtom` holds the user's gutter breakpoints, session-only. It is
|
|
16
18
|
* the source of truth; mutations are mirrored to the kernel via
|
|
17
19
|
* `sendSetBreakpoints`.
|
|
18
20
|
*/
|
|
19
21
|
|
|
20
|
-
export interface
|
|
22
|
+
export interface ActiveLine {
|
|
21
23
|
cellId: CellId;
|
|
22
24
|
line: number;
|
|
25
|
+
/** When the frontend first saw execution on this (cellId, line). */
|
|
26
|
+
startedAtMs: number;
|
|
23
27
|
}
|
|
24
28
|
|
|
25
|
-
export const
|
|
29
|
+
export const activeLineAtom = atom<ActiveLine | null>(null);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Update the active line, stamping `startedAtMs` only when the (cellId, line)
|
|
33
|
+
* actually changes, so duplicate notifications (e.g. reconnects) never reset
|
|
34
|
+
* the elapsed-time baseline.
|
|
35
|
+
*/
|
|
36
|
+
export function setActiveLine(
|
|
37
|
+
next: { cellId: CellId; line: number } | null,
|
|
38
|
+
): void {
|
|
39
|
+
const prev = store.get(activeLineAtom);
|
|
40
|
+
if (next === null) {
|
|
41
|
+
store.set(activeLineAtom, null);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (prev?.cellId === next.cellId && prev.line === next.line) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
store.set(activeLineAtom, { ...next, startedAtMs: Date.now() });
|
|
48
|
+
}
|
|
26
49
|
|
|
27
50
|
export const breakpointsAtom = atom<ReadonlyMap<CellId, ReadonlySet<number>>>(
|
|
28
51
|
new Map<CellId, ReadonlySet<number>>(),
|
|
@@ -33,11 +56,21 @@ const EMPTY_LINES: ReadonlySet<number> = new Set();
|
|
|
33
56
|
/** Per-cell derived atom: the current debug line for `cellId`, or `null`. */
|
|
34
57
|
export function createDebuggerLineAtom(cellId: CellId) {
|
|
35
58
|
return atom((get) => {
|
|
36
|
-
const current = get(
|
|
59
|
+
const current = get(activeLineAtom);
|
|
37
60
|
return current?.cellId === cellId ? current.line : null;
|
|
38
61
|
});
|
|
39
62
|
}
|
|
40
63
|
|
|
64
|
+
/** Per-cell derived atom: the active line + start time for `cellId`, or `null`. */
|
|
65
|
+
export function createActiveLineInfoAtom(cellId: CellId) {
|
|
66
|
+
return atom((get): ActiveLineInfo | null => {
|
|
67
|
+
const current = get(activeLineAtom);
|
|
68
|
+
return current?.cellId === cellId
|
|
69
|
+
? { line: current.line, startedAtMs: current.startedAtMs }
|
|
70
|
+
: null;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
41
74
|
/** Per-cell derived atom: the breakpoint lines for `cellId`. */
|
|
42
75
|
export function createCellBreakpointsAtom(cellId: CellId) {
|
|
43
76
|
return atom((get) => get(breakpointsAtom).get(cellId) ?? EMPTY_LINES);
|
|
@@ -31,9 +31,11 @@ import {
|
|
|
31
31
|
debuggerLineHighlighter,
|
|
32
32
|
} from "./debugger-decorations";
|
|
33
33
|
import {
|
|
34
|
+
createActiveLineInfoAtom,
|
|
34
35
|
createCellBreakpointsAtom,
|
|
35
36
|
createDebuggerLineAtom,
|
|
36
37
|
} from "./debugger-state";
|
|
38
|
+
import { activeLineTimer } from "./line-timing-decorations";
|
|
37
39
|
import {
|
|
38
40
|
type CodemirrorCellActions,
|
|
39
41
|
cellActionsState,
|
|
@@ -429,6 +431,8 @@ export function cellBundle({
|
|
|
429
431
|
cellActions: CodemirrorCellActions;
|
|
430
432
|
keymapConfig: KeymapConfig;
|
|
431
433
|
}): Extension[] {
|
|
434
|
+
const debuggerOn = getFeatureFlag("debugger");
|
|
435
|
+
const lineTimingOn = getFeatureFlag("line_timing");
|
|
432
436
|
return [
|
|
433
437
|
cellActionsState.of(cellActions),
|
|
434
438
|
cellIdState.of(cellId),
|
|
@@ -437,19 +441,24 @@ export function cellBundle({
|
|
|
437
441
|
errorLineHighlighter(
|
|
438
442
|
createObservable(createTracebackInfoAtom(cellId), store),
|
|
439
443
|
),
|
|
440
|
-
// Experimental live debugger
|
|
441
|
-
//
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
debuggerLineHighlighter(
|
|
449
|
-
createObservable(createDebuggerLineAtom(cellId), store),
|
|
450
|
-
),
|
|
451
|
-
]
|
|
444
|
+
// Experimental live debugger and line-timing highlight. Gated so there is
|
|
445
|
+
// no gutter/overhead when disabled. Both track the same active line; when
|
|
446
|
+
// both flags are on, the green timing highlight replaces the amber one.
|
|
447
|
+
debuggerOn
|
|
448
|
+
? breakpointGutter(
|
|
449
|
+
cellId,
|
|
450
|
+
createObservable(createCellBreakpointsAtom(cellId), store),
|
|
451
|
+
)
|
|
452
452
|
: [],
|
|
453
|
+
lineTimingOn
|
|
454
|
+
? activeLineTimer(
|
|
455
|
+
createObservable(createActiveLineInfoAtom(cellId), store),
|
|
456
|
+
)
|
|
457
|
+
: debuggerOn
|
|
458
|
+
? debuggerLineHighlighter(
|
|
459
|
+
createObservable(createDebuggerLineAtom(cellId), store),
|
|
460
|
+
)
|
|
461
|
+
: [],
|
|
453
462
|
];
|
|
454
463
|
}
|
|
455
464
|
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type EditorState,
|
|
5
|
+
type Extension,
|
|
6
|
+
RangeSetBuilder,
|
|
7
|
+
} from "@codemirror/state";
|
|
8
|
+
import {
|
|
9
|
+
Decoration,
|
|
10
|
+
type DecorationSet,
|
|
11
|
+
EditorView,
|
|
12
|
+
type PluginValue,
|
|
13
|
+
ViewPlugin,
|
|
14
|
+
type ViewUpdate,
|
|
15
|
+
WidgetType,
|
|
16
|
+
} from "@codemirror/view";
|
|
17
|
+
import type { Observable } from "@/core/state/observable";
|
|
18
|
+
import { Logger } from "@/utils/Logger";
|
|
19
|
+
import { formatElapsedTime } from "@/utils/time";
|
|
20
|
+
|
|
21
|
+
/** The line a cell is currently executing and when execution first hit it. */
|
|
22
|
+
export interface ActiveLineInfo {
|
|
23
|
+
line: number;
|
|
24
|
+
startedAtMs: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Only show the timer once a line has been busy this long, so short-lived
|
|
28
|
+
// lines never flash a pill.
|
|
29
|
+
const SHOW_TIMER_AFTER_MS = 500;
|
|
30
|
+
const TICK_INTERVAL_MS = 250;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* End-of-line elapsed-time pill for the currently executing line.
|
|
34
|
+
*
|
|
35
|
+
* Ticking is pure DOM mutation on the widget's own interval; no editor
|
|
36
|
+
* transactions are dispatched while the timer counts.
|
|
37
|
+
*/
|
|
38
|
+
class LineTimerWidget extends WidgetType {
|
|
39
|
+
// Track intervals per DOM node: CodeMirror may reuse the node across
|
|
40
|
+
// decoration remaps (`eq` below), and `destroy` only receives the node.
|
|
41
|
+
private static readonly intervals = new WeakMap<HTMLElement, number>();
|
|
42
|
+
|
|
43
|
+
private readonly startedAtMs: number;
|
|
44
|
+
|
|
45
|
+
constructor(startedAtMs: number) {
|
|
46
|
+
super();
|
|
47
|
+
this.startedAtMs = startedAtMs;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
override eq(other: LineTimerWidget): boolean {
|
|
51
|
+
return other.startedAtMs === this.startedAtMs;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
override toDOM(): HTMLElement {
|
|
55
|
+
const el = document.createElement("span");
|
|
56
|
+
el.className = "cm-line-timer";
|
|
57
|
+
el.setAttribute("aria-hidden", "true");
|
|
58
|
+
const tick = () => {
|
|
59
|
+
const elapsed = Date.now() - this.startedAtMs;
|
|
60
|
+
el.textContent =
|
|
61
|
+
elapsed >= SHOW_TIMER_AFTER_MS ? formatElapsedTime(elapsed) : "";
|
|
62
|
+
};
|
|
63
|
+
tick();
|
|
64
|
+
LineTimerWidget.intervals.set(
|
|
65
|
+
el,
|
|
66
|
+
window.setInterval(tick, TICK_INTERVAL_MS),
|
|
67
|
+
);
|
|
68
|
+
return el;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
override destroy(dom: HTMLElement): void {
|
|
72
|
+
const interval = LineTimerWidget.intervals.get(dom);
|
|
73
|
+
if (interval !== undefined) {
|
|
74
|
+
window.clearInterval(interval);
|
|
75
|
+
LineTimerWidget.intervals.delete(dom);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
override ignoreEvent(): boolean {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function createTimingDecorations(
|
|
85
|
+
state: EditorState,
|
|
86
|
+
info: ActiveLineInfo | null,
|
|
87
|
+
): DecorationSet {
|
|
88
|
+
if (info === null || info.line < 1 || info.line > state.doc.lines) {
|
|
89
|
+
return Decoration.none;
|
|
90
|
+
}
|
|
91
|
+
const builder = new RangeSetBuilder<Decoration>();
|
|
92
|
+
try {
|
|
93
|
+
const line = state.doc.line(info.line);
|
|
94
|
+
builder.add(
|
|
95
|
+
line.from,
|
|
96
|
+
line.from,
|
|
97
|
+
Decoration.line({ class: "cm-timing-current-line" }),
|
|
98
|
+
);
|
|
99
|
+
builder.add(
|
|
100
|
+
line.to,
|
|
101
|
+
line.to,
|
|
102
|
+
Decoration.widget({
|
|
103
|
+
widget: new LineTimerWidget(info.startedAtMs),
|
|
104
|
+
side: 1,
|
|
105
|
+
}),
|
|
106
|
+
);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
Logger.debug("Invalid timing line", { error });
|
|
109
|
+
}
|
|
110
|
+
return builder.finish();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
class ActiveLineTimer implements PluginValue {
|
|
114
|
+
private unsubscribe: () => void;
|
|
115
|
+
decorations: DecorationSet;
|
|
116
|
+
|
|
117
|
+
constructor(
|
|
118
|
+
view: EditorView,
|
|
119
|
+
infoObservable: Observable<ActiveLineInfo | null>,
|
|
120
|
+
) {
|
|
121
|
+
this.decorations = createTimingDecorations(
|
|
122
|
+
view.state,
|
|
123
|
+
infoObservable.get(),
|
|
124
|
+
);
|
|
125
|
+
this.unsubscribe = infoObservable.sub((info) => {
|
|
126
|
+
this.decorations = createTimingDecorations(view.state, info);
|
|
127
|
+
// Force a re-render; the decoration set changed out of band.
|
|
128
|
+
view.dispatch({ userEvent: "marimo.timing-line-update" });
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
update(update: ViewUpdate) {
|
|
133
|
+
this.decorations = this.decorations.map(update.changes);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
destroy() {
|
|
137
|
+
this.unsubscribe();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Highlight the line a cell's frame watcher is currently executing in green,
|
|
143
|
+
* with a dim elapsed-time pill at the end of the line once it has been busy
|
|
144
|
+
* for a while.
|
|
145
|
+
*/
|
|
146
|
+
export function activeLineTimer(
|
|
147
|
+
infoObservable: Observable<ActiveLineInfo | null>,
|
|
148
|
+
): Extension {
|
|
149
|
+
return [
|
|
150
|
+
ViewPlugin.define((view) => new ActiveLineTimer(view, infoObservable), {
|
|
151
|
+
decorations: (plugin) => plugin.decorations,
|
|
152
|
+
}),
|
|
153
|
+
EditorView.theme({
|
|
154
|
+
".cm-timing-current-line": {
|
|
155
|
+
backgroundColor: "color-mix(in srgb, var(--grass-4) 50%, transparent)",
|
|
156
|
+
},
|
|
157
|
+
".cm-line-timer": {
|
|
158
|
+
color: "var(--grass-11)",
|
|
159
|
+
fontSize: "0.85em",
|
|
160
|
+
marginLeft: "1.5ch",
|
|
161
|
+
pointerEvents: "none",
|
|
162
|
+
},
|
|
163
|
+
}),
|
|
164
|
+
];
|
|
165
|
+
}
|
|
@@ -12,6 +12,7 @@ export interface ExperimentalFeatures {
|
|
|
12
12
|
cache_panel: boolean;
|
|
13
13
|
external_agents: boolean;
|
|
14
14
|
debugger: boolean; // Live frame-watching debugger (gutter breakpoints + pdb)
|
|
15
|
+
line_timing: boolean; // Green active-line highlight + per-line elapsed timer
|
|
15
16
|
// Add new feature flags here
|
|
16
17
|
}
|
|
17
18
|
|
|
@@ -22,6 +23,7 @@ const defaultValues: ExperimentalFeatures = {
|
|
|
22
23
|
cache_panel: false,
|
|
23
24
|
external_agents: import.meta.env.DEV,
|
|
24
25
|
debugger: false,
|
|
26
|
+
line_timing: false,
|
|
25
27
|
};
|
|
26
28
|
|
|
27
29
|
export function getFeatureFlag<T extends keyof ExperimentalFeatures>(
|
|
@@ -33,8 +33,8 @@ import { SCRATCH_CELL_ID } from "../cells/ids";
|
|
|
33
33
|
import { useRunsActions } from "../cells/runs";
|
|
34
34
|
import { focusAndScrollCellOutputIntoView } from "../cells/scrollCellIntoView";
|
|
35
35
|
import {
|
|
36
|
-
debuggerCurrentLineAtom,
|
|
37
36
|
resyncBreakpoints,
|
|
37
|
+
setActiveLine,
|
|
38
38
|
} from "../codemirror/cells/debugger-state";
|
|
39
39
|
import type { CellData } from "../cells/types";
|
|
40
40
|
import { capabilitiesAtom } from "../config/capabilities";
|
|
@@ -226,7 +226,6 @@ export function useMarimoKernelConnection(opts: {
|
|
|
226
226
|
const transportType = useAtomValue(connectionTransportTypeAtom);
|
|
227
227
|
const setCacheInfo = useSetAtom(cacheInfoAtom);
|
|
228
228
|
const setKernelStartupError = useSetAtom(kernelStartupErrorAtom);
|
|
229
|
-
const setDebuggerCurrentLine = useSetAtom(debuggerCurrentLineAtom);
|
|
230
229
|
const {
|
|
231
230
|
setNamespaces: setStorageNamespaces,
|
|
232
231
|
filterFromVariables: filterStorageFromVariables,
|
|
@@ -255,7 +254,7 @@ export function useMarimoKernelConnection(opts: {
|
|
|
255
254
|
// A freshly started kernel has no breakpoints of its own; re-push
|
|
256
255
|
// the client's set so they still apply, and clear the stale
|
|
257
256
|
// highlighted line from the previous kernel instance.
|
|
258
|
-
|
|
257
|
+
setActiveLine(null);
|
|
259
258
|
resyncBreakpoints();
|
|
260
259
|
return;
|
|
261
260
|
}
|
|
@@ -429,7 +428,7 @@ export function useMarimoKernelConnection(opts: {
|
|
|
429
428
|
focusAndScrollCellOutputIntoView(msg.data.cell_id);
|
|
430
429
|
return;
|
|
431
430
|
case "active-line":
|
|
432
|
-
|
|
431
|
+
setActiveLine(
|
|
433
432
|
msg.data.line == null
|
|
434
433
|
? null
|
|
435
434
|
: { cellId: msg.data.cell_id, line: msg.data.line },
|
package/src/utils/time.ts
CHANGED
|
@@ -43,3 +43,23 @@ export class Time {
|
|
|
43
43
|
return (this.ms / 1000) as Seconds;
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
|
+
|
|
47
|
+
/** Format a duration in milliseconds, e.g. `500ms`, `1.50s`, `1m30s`. */
|
|
48
|
+
export function formatElapsedTime(elapsedTime: number | null): string {
|
|
49
|
+
if (elapsedTime === null) {
|
|
50
|
+
return "";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const milliseconds = elapsedTime;
|
|
54
|
+
const seconds = milliseconds / 1000;
|
|
55
|
+
|
|
56
|
+
if (seconds >= 60) {
|
|
57
|
+
const minutes = Math.floor(seconds / 60);
|
|
58
|
+
const remainingSeconds = Math.floor(seconds % 60);
|
|
59
|
+
return `${minutes}m${remainingSeconds}s`;
|
|
60
|
+
}
|
|
61
|
+
if (seconds >= 1) {
|
|
62
|
+
return `${seconds.toFixed(2).toString()}s`;
|
|
63
|
+
}
|
|
64
|
+
return `${milliseconds.toFixed(0).toString()}ms`;
|
|
65
|
+
}
|