@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.
@@ -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-hevd6UWC.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-_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-DOO3J9E6.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-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-hevd6UWC.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-_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-dev59");
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 MARKDOWN_AUTORUN_USER_EVENTS = [
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 Time = class e {
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-hevd6UWC.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-_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-ChrxMIRR.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-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-DOO3J9E6.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-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-D7EyLr6c.js").then((e) => ({
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-BLijd8xt.js"));
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-hevd6UWC.js";
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-hevd6UWC.js";
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-ChrxMIRR.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-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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marimo-team/islands",
3
- "version": "0.23.14-dev59",
3
+ "version": "0.23.14-dev60",
4
4
  "main": "dist/main.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -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 { Seconds } from "@/utils/time";
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, formatElapsedTime } from "../editor/cell/CellStatus";
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(debuggerCurrentLineAtom, null);
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(debuggerCurrentLineAtom, { cellId: cell1, line: 10 });
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
- * - `debuggerCurrentLineAtom` holds the cell + line the kernel's frame watcher
13
- * is currently executing (driven by `active-line` notifications). Only one
14
- * cell runs at a time, so a single global value suffices.
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 DebuggerLine {
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 debuggerCurrentLineAtom = atom<DebuggerLine | null>(null);
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(debuggerCurrentLineAtom);
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: clickable breakpoint gutter + current-line
441
- // highlight. Gated so there is no gutter/overhead when disabled.
442
- getFeatureFlag("debugger")
443
- ? [
444
- breakpointGutter(
445
- cellId,
446
- createObservable(createCellBreakpointsAtom(cellId), store),
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
- setDebuggerCurrentLine(null);
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
- setDebuggerCurrentLine(
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
+ }