Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import './CellLeftActionMenu.css';

// Other dependencies.
import { useObservedValue } from '../useObservedValue.js';
import { useDebouncedObservedValue } from '../useObservedValue.js';
import { PositronNotebookCodeCell } from '../PositronNotebookCells/PositronNotebookCodeCell.js';

interface CellLeftActionMenuProps {
Expand All @@ -19,7 +19,7 @@ interface CellLeftActionMenuProps {
*/
export function CellLeftActionMenu({ cell }: CellLeftActionMenuProps) {
// Observed values
const executionOrder = useObservedValue(cell.lastExecutionOrder);
const executionOrder = useDebouncedObservedValue(cell.lastExecutionOrder);

// Determine what to show
const showPending = executionOrder === undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ import { useEffect, useRef, useState } from 'react';
// Other dependencies.
import { localize } from '../../../../../nls.js';
import * as DOM from '../../../../../base/browser/dom.js';
import { useObservedValue } from '../useObservedValue.js';
import { useObservedValue, useDebouncedObservedValue } from '../useObservedValue.js';
import { PositronNotebookCodeCell } from '../PositronNotebookCells/PositronNotebookCodeCell.js';
import { ExecutionStatus } from '../PositronNotebookCells/IPositronNotebookCell.js';
import { formatCellDuration, formatTimestamp, getRelativeTime, isMoreThanOneHourAgo } from './cellExecutionUtils.js';
import { Icon } from '../../../../../platform/positronActionBar/browser/components/icon.js';
import { Codicon } from '../../../../../base/common/codicons.js';
Expand All @@ -23,17 +24,22 @@ interface CodeCellStatusFooterProps {
hasError: boolean;
}

// Defined outside the component so the reference is stable across renders,
// avoiding memoization invalidation inside useDebouncedObservedValue.
const isRunningOrPending = (s: ExecutionStatus) => s === 'running' || s === 'pending';

/**
* Footer component that displays cell execution status information between
* the editor and outputs sections. Shows execution state, duration, and timestamp.
*/
export function CodeCellStatusFooter({ cell, hasError }: CodeCellStatusFooterProps) {
// Observe cell execution state
const executionStatus = useObservedValue(cell.executionStatus);
// Debounce "clearing" transitions to prevent visual flash during fast re-executions.
// Only delay transitions to running/pending/undefined; new values propagate immediately.
const executionStatus = useDebouncedObservedValue(cell.executionStatus, isRunningOrPending);
const executionOrder = useObservedValue(cell.lastExecutionOrder);
const duration = useObservedValue(cell.lastExecutionDuration);
const lastRunEndTime = useObservedValue(cell.lastRunEndTime);
const lastRunSuccess = useObservedValue(cell.lastRunSuccess);
const duration = useDebouncedObservedValue(cell.lastExecutionDuration);
const lastRunEndTime = useDebouncedObservedValue(cell.lastRunEndTime);
const lastRunSuccess = useDebouncedObservedValue(cell.lastRunSuccess);

/**
* `lastRunEndTime` doesn't change after execution completes, which means the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import React, { useMemo } from 'react';
// Other dependencies.
import { NotebookCellOutputs, ParsedTextOutput } from '../PositronNotebookCells/IPositronNotebookCell.js';
import { isParsedTextOutput } from '../getOutputContents.js';
import { useObservedValue } from '../useObservedValue.js';
import { useObservedValue, useDebouncedObservedValue } from '../useObservedValue.js';
import { CellEditorMonacoWidget } from './CellEditorMonacoWidget.js';
import { localize } from '../../../../../nls.js';
import { positronClassNames } from '../../../../../base/common/positronUtilities.js';
Expand Down Expand Up @@ -210,7 +210,16 @@ const CellOutputsSection = React.memo(function CellOutputsSection({ cell, output
});

export const NotebookCodeCell = React.memo(function NotebookCodeCell({ cell }: { cell: PositronNotebookCodeCell }) {
const outputContents = useObservedValue(cell.outputs);
// Debounce transitions to empty only while the cell is executing so
// re-execution doesn't flash. Explicit clears (when idle) propagate
// immediately. We read executionStatus synchronously inside the predicate
// so it reflects the state at the moment outputs change.
const shouldDebounceOutputs = React.useCallback(
(outputs: NotebookCellOutputs[]) =>
outputs.length === 0 && cell.executionStatus.get() !== 'idle',
[cell.executionStatus]
);
const outputContents = useDebouncedObservedValue(cell.outputs, shouldDebounceOutputs);
const hasError = outputContents.some(o => o.parsed.type === 'error');

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
import React from 'react';

// Other dependencies.
import { IObservable, runOnChange } from '../../../../base/common/observable.js';
import { IObservable, debouncedObservable, runOnChange } from '../../../../base/common/observable.js';
import { isUndefinedOrNull } from '../../../../base/common/types.js';

/**
* Automatically updates the component when the observable changes.
Expand Down Expand Up @@ -39,3 +40,38 @@ export function useObservedValue<T>(observable: IObservable<T> | undefined, defa

return value;
}

/**
* Like {@link useObservedValue}, but debounces value transitions where the
* provided `shouldDebounce` predicate returns true. Non-debounced transitions
* propagate immediately.
*
* Useful for suppressing transient UI flashes during cell re-execution without
* delaying meaningful updates.
*
* @param observable The observable to subscribe to.
* @param shouldDebounce Predicate receiving the new value. Return `true` to
* delay propagation by the specified `delayMs`, `false` to propagate
* immediately. Defaults to nullish check which covers `undefined` and `null`
* but not valid falsy values like `0` or `false`. Note that this is used in
* the dependency array of the `useMemo` call that creates the debounced
* observable, so it should be memoized if it is not a stable reference or
* otherwise the memo will be invalidated on every render, defeating the
* purpose of debouncing.
* @param delayMs Optional debounce delay in milliseconds. Defaults to 150ms.
*/

export function useDebouncedObservedValue<T>(
observable: IObservable<T>,
shouldDebounce: (next: T) => boolean = isUndefinedOrNull,
delayMs: number = 150,
): T {
const debounced = React.useMemo(
() => debouncedObservable(observable, (_prev, next) => shouldDebounce(next) ? delayMs : 0),
// We dont need to worry about the delayMs causing invalidation because
// pure number types are compared by value not reference in the
// dependency arrays of hooks.
[observable, shouldDebounce, delayMs]
);
return useObservedValue(debounced);
}
Loading