From aaea0a17dc797668b7c5af8760193068e44875d2 Mon Sep 17 00:00:00 2001 From: Jonathan McPherson Date: Wed, 17 Dec 2025 15:44:05 -0800 Subject: [PATCH 01/32] fetch and display plot metadata --- .../python_files/posit/positron/plot_comm.py | 45 +++++++++++ positron/comms/plot-backend-openrpc.json | 38 +++++++++ .../browser/positronIPyWidgetsService.test.ts | 1 - .../browser/components/plotsContainer.css | 23 ++++++ .../browser/components/plotsContainer.tsx | 77 ++++++++++++++++++- .../browser/positronPlotsService.ts | 56 +++++++++++++- .../common/languageRuntimePlotClient.ts | 29 ++++++- .../common/positronPlotComm.ts | 39 ++++++++++ .../common/positronPlotCommProxy.ts | 9 +++ .../common/positronPlotRenderQueue.ts | 42 +++++++++- .../positronPlots/common/positronPlots.ts | 11 +++ .../test/common/testPositronPlotClient.ts | 1 - .../test/common/testPositronPlotsService.ts | 11 +++ 13 files changed, 369 insertions(+), 13 deletions(-) diff --git a/extensions/positron-python/python_files/posit/positron/plot_comm.py b/extensions/positron-python/python_files/posit/positron/plot_comm.py index 76d54164bb9d..924f87f7f94f 100644 --- a/extensions/positron-python/python_files/posit/positron/plot_comm.py +++ b/extensions/positron-python/python_files/posit/positron/plot_comm.py @@ -68,6 +68,28 @@ class IntrinsicSize(BaseModel): ) +class PlotMetadata(BaseModel): + """ + The plot's metadata + """ + + name: StrictStr = Field( + description="A human-readable name for the plot", + ) + + kind: StrictStr = Field( + description="The kind of plot e.g. 'Matplotlib', 'ggplot2', etc.", + ) + + execution_id: StrictStr = Field( + description="The ID of the code fragment that produced the plot", + ) + + code: StrictStr = Field( + description="The code fragment that produced the plot", + ) + + class PlotResult(BaseModel): """ A rendered plot @@ -128,6 +150,9 @@ class PlotBackendRequest(str, enum.Enum): # Get the intrinsic size of a plot, if known. GetIntrinsicSize = "get_intrinsic_size" + # Get metadata for the plot + GetMetadata = "get_metadata" + # Render a plot Render = "render" @@ -148,6 +173,21 @@ class GetIntrinsicSizeRequest(BaseModel): ) +class GetMetadataRequest(BaseModel): + """ + Get metadata for the plot + """ + + method: Literal[PlotBackendRequest.GetMetadata] = Field( + description="The JSON-RPC method name (get_metadata)", + ) + + jsonrpc: str = Field( + default="2.0", + description="The JSON-RPC version specifier", + ) + + class RenderParams(BaseModel): """ Requests a plot to be rendered. The plot data is returned in a @@ -192,6 +232,7 @@ class PlotBackendMessageContent(BaseModel): comm_id: str data: Union[ GetIntrinsicSizeRequest, + GetMetadataRequest, RenderRequest, ] = Field(..., discriminator="method") @@ -221,6 +262,8 @@ class UpdateParams(BaseModel): IntrinsicSize.update_forward_refs() +PlotMetadata.update_forward_refs() + PlotResult.update_forward_refs() PlotSize.update_forward_refs() @@ -229,6 +272,8 @@ class UpdateParams(BaseModel): GetIntrinsicSizeRequest.update_forward_refs() +GetMetadataRequest.update_forward_refs() + RenderParams.update_forward_refs() RenderRequest.update_forward_refs() diff --git a/positron/comms/plot-backend-openrpc.json b/positron/comms/plot-backend-openrpc.json index c4dc011b38ff..9d8acc73e73b 100644 --- a/positron/comms/plot-backend-openrpc.json +++ b/positron/comms/plot-backend-openrpc.json @@ -43,6 +43,44 @@ } } }, + { + "name": "get_metadata", + "summary": "Get metadata for the plot", + "description": "Get metadata for the plot", + "params": [], + "result": { + "required": true, + "schema": { + "name": "plot_metadata", + "description": "The plot's metadata", + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "A unique, human-readable name for the plot" + }, + "kind": { + "type": "string", + "description": "The kind of plot e.g. 'Matplotlib', 'ggplot2', etc." + }, + "execution_id": { + "type": "string", + "description": "The ID of the code fragment that produced the plot" + }, + "code": { + "description": "The code fragment that produced the plot", + "type": "string" + } + }, + "required": [ + "name", + "kind", + "execution_id", + "code" + ] + } + } + }, { "name": "render", "summary": "Render a plot", diff --git a/src/vs/workbench/contrib/positronIPyWidgets/test/browser/positronIPyWidgetsService.test.ts b/src/vs/workbench/contrib/positronIPyWidgets/test/browser/positronIPyWidgetsService.test.ts index fb14cf1b1be8..e90b1e10bd86 100644 --- a/src/vs/workbench/contrib/positronIPyWidgets/test/browser/positronIPyWidgetsService.test.ts +++ b/src/vs/workbench/contrib/positronIPyWidgets/test/browser/positronIPyWidgetsService.test.ts @@ -100,7 +100,6 @@ suite('Positron - PositronIPyWidgetsService', () => { assert.strictEqual(plotClient.id, message.id); assert.deepStrictEqual(plotClient.metadata, { id: message.id, - parent_id: message.parent_id, created: Date.parse(message.when), session_id: session.sessionId, code: '', diff --git a/src/vs/workbench/contrib/positronPlots/browser/components/plotsContainer.css b/src/vs/workbench/contrib/positronPlots/browser/components/plotsContainer.css index d827d50107e1..e6f2c141b575 100644 --- a/src/vs/workbench/contrib/positronPlots/browser/components/plotsContainer.css +++ b/src/vs/workbench/contrib/positronPlots/browser/components/plotsContainer.css @@ -13,3 +13,26 @@ .vs-dark .dark-filter-auto .plot-thumbnail img.plot { filter: invert(1) hue-rotate(180deg); } + +/** + * Plot info header showing session name and plot name. + */ +.plots-container .plot-info-header { + display: flex; + align-items: center; + padding: 0 8px; + min-height: 24px; + flex-shrink: 0; + overflow: hidden; + background-color: var(--vscode-editor-background); + border-bottom: 1px solid var(--vscode-panel-border); +} + +.plots-container .plot-info-header .plot-info-text { + font-size: 12px; + color: var(--vscode-foreground); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + opacity: 0.8; +} diff --git a/src/vs/workbench/contrib/positronPlots/browser/components/plotsContainer.tsx b/src/vs/workbench/contrib/positronPlots/browser/components/plotsContainer.tsx index 6538ca2f2228..8f1aafa398ff 100644 --- a/src/vs/workbench/contrib/positronPlots/browser/components/plotsContainer.tsx +++ b/src/vs/workbench/contrib/positronPlots/browser/components/plotsContainer.tsx @@ -7,7 +7,7 @@ import './plotsContainer.css'; // React. -import React, { useEffect, useRef } from 'react'; +import React, { useEffect, useMemo, useRef } from 'react'; // Other dependencies. import * as DOM from '../../../../../base/browser/dom.js'; @@ -47,6 +47,11 @@ interface PlotContainerProps { */ export const HistoryPx = 100; +/** + * The number of pixels (height) to use for the plot info header row. + */ +export const PlotInfoHeaderPx = 24; + /** * PlotContainer component; holds the plot instances. * @@ -67,9 +72,53 @@ export const PlotsContainer = (props: PlotContainerProps) => { const historyPx = props.showHistory ? HistoryPx : 0; const historyEdge = historyBottom ? 'history-bottom' : 'history-right'; - const plotHeight = historyBottom && props.height > 0 ? props.height - historyPx : props.height; + // Account for the plot info header when calculating plot dimensions + const plotHeight = historyBottom && props.height > 0 ? props.height - historyPx - PlotInfoHeaderPx : props.height - PlotInfoHeaderPx; const plotWidth = historyBottom || props.width <= 0 ? props.width : props.width - historyPx; + // Get the current plot instance + const currentPlotInstance = useMemo(() => + positronPlotsContext.positronPlotInstances.find( + (plotInstance) => plotInstance.id === positronPlotsContext.selectedInstanceId + ), + [positronPlotsContext.positronPlotInstances, positronPlotsContext.selectedInstanceId] + ); + + // State to track metadata updates and trigger re-renders + const [metadataVersion, setMetadataVersion] = React.useState(0); + + // Listen for metadata updates from the plots service (service-level event) + useEffect(() => { + const disposable = services.positronPlotsService.onDidUpdatePlotMetadata((plotId) => { + // Only trigger re-render if the updated plot is the current one + if (plotId === positronPlotsContext.selectedInstanceId) { + setMetadataVersion(v => v + 1); + } + }); + return () => disposable.dispose(); + }, [services.positronPlotsService, positronPlotsContext.selectedInstanceId]); + + // Get the session name for the current plot + const sessionName = useMemo(() => { + if (!currentPlotInstance) { + return undefined; + } + const sessionId = currentPlotInstance.metadata.session_id; + const session = services.runtimeSessionService.getSession(sessionId); + if (session) { + return session.runtimeMetadata.runtimeName; + } + // Fallback to the language name from metadata if session is not found + return currentPlotInstance.metadata.language; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [currentPlotInstance, services.runtimeSessionService, metadataVersion]); + + // Get the plot name from metadata (reactive to metadata updates) + const plotName = useMemo(() => { + return currentPlotInstance?.metadata.name; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [currentPlotInstance, metadataVersion]); + // Plot history useEffect to handle scrolling, mouse wheel events, and keyboard navigation. useEffect(() => { // Get the current plot history and container. If the plot history is not rendered, @@ -341,10 +390,34 @@ export const PlotsContainer = (props: PlotContainerProps) => { ; }; + // Render the plot info header showing the session name and plot name. + const renderPlotInfoHeader = () => { + if (!currentPlotInstance) { + return null; + } + + const separator = sessionName && plotName ? ' : ' : ''; + + // Show session name and plot name if available + const displayText = `${sessionName ?? ''}${separator}${plotName ?? ''}`; + + // If no info to display, show a placeholder to maintain consistent height + if (!displayText) { + return
+   +
; + } + + return
+ {displayText} +
; + }; + // If there are no plot instances, show a placeholder; otherwise, show the // most recently generated plot. return (
+ {positronPlotsContext.positronPlotInstances.length > 0 && renderPlotInfoHeader()}
{positronPlotsContext.positronPlotInstances.length === 0 &&
} diff --git a/src/vs/workbench/contrib/positronPlots/browser/positronPlotsService.ts b/src/vs/workbench/contrib/positronPlots/browser/positronPlotsService.ts index 33e5d3397c4b..4fc680a37cd1 100644 --- a/src/vs/workbench/contrib/positronPlots/browser/positronPlotsService.ts +++ b/src/vs/workbench/contrib/positronPlots/browser/positronPlotsService.ts @@ -132,6 +132,9 @@ export class PositronPlotsService extends Disposable implements IPositronPlotsSe /** The emitter for the onDidRemovePlot event */ private readonly _onDidRemovePlot = new Emitter(); + /** The emitter for the onDidUpdatePlotMetadata event */ + private readonly _onDidUpdatePlotMetadata = new Emitter(); + /** The emitter for the onDidChangePlotsRenderSettings event */ private readonly _onDidChangePlotsRenderSettings = new Emitter(); @@ -754,7 +757,6 @@ export class PositronPlotsService extends Disposable implements IPositronPlotsSe created: Date.now(), id: client.getClientId(), session_id: session.sessionId, - parent_id: '', code: '', location: PlotClientLocation.View, suggested_file_name: createSuggestedFileNameForPlot(this._storageService), @@ -762,7 +764,10 @@ export class PositronPlotsService extends Disposable implements IPositronPlotsSe zoom_level: ZoomLevel.Fit, }; const commProxy = this.createCommProxy(client, metadata); - plotClients.push(this.createRuntimePlotClient(commProxy, metadata)); + const plotClient = this.createRuntimePlotClient(commProxy, metadata); + // Fetch metadata from the backend to populate kind, name, execution_id, and code + this.fetchAndUpdateMetadata(commProxy, metadata, plotClient); + plotClients.push(plotClient); } } else { console.warn( @@ -837,7 +842,6 @@ export class PositronPlotsService extends Disposable implements IPositronPlotsSe created: Date.parse(event.message.when), id: clientId, session_id: session.sessionId, - parent_id: event.message.parent_id, code, pre_render: data?.pre_render, suggested_file_name: createSuggestedFileNameForPlot(this._storageService), @@ -848,6 +852,8 @@ export class PositronPlotsService extends Disposable implements IPositronPlotsSe // Register the plot client const commProxy = this.createCommProxy(event.client, metadata); const plotClient = this.createRuntimePlotClient(commProxy, metadata); + // Fetch metadata from the backend to populate kind, name, execution_id, and code + this.fetchAndUpdateMetadata(commProxy, metadata, plotClient); this.registerPlotClient(plotClient, true); // Raise the Plots pane so the plot is visible. @@ -1069,6 +1075,7 @@ export class PositronPlotsService extends Disposable implements IPositronPlotsSe onDidEmitPlot: Event = this._onDidEmitPlot.event; onDidSelectPlot: Event = this._onDidSelectPlot.event; onDidRemovePlot: Event = this._onDidRemovePlot.event; + onDidUpdatePlotMetadata: Event = this._onDidUpdatePlotMetadata.event; onDidReplacePlots: Event = this._onDidReplacePlots.event; onDidChangeHistoryPolicy: Event = this._onDidChangeHistoryPolicy.event; onDidChangeDarkFilterMode: Event = this._onDidChangeDarkFilterMode.event; @@ -1537,6 +1544,49 @@ export class PositronPlotsService extends Disposable implements IPositronPlotsSe plotClient.dispose(); } + /** + * Fetches metadata from the backend and updates the metadata object. + * The metadata object is updated in place with the fetched values. + * + * @param commProxy The comm proxy to use for fetching metadata + * @param metadata The metadata object to update + * @param plotClient Optional plot client to notify when metadata is updated + */ + private async fetchAndUpdateMetadata( + commProxy: PositronPlotCommProxy, + metadata: IPositronPlotMetadata, + plotClient?: PlotClientInstance + ): Promise { + try { + const backendMetadata = await commProxy.getMetadata(); + // Update the metadata with the fetched values + metadata.kind = backendMetadata.kind; + metadata.name = backendMetadata.name; + metadata.execution_id = backendMetadata.execution_id; + // Only update code if we don't already have it from recent executions + if (!metadata.code) { + metadata.code = backendMetadata.code; + } + // Store the updated metadata + this.storePlotMetadata(metadata); + // Notify the plot client if provided - also update its metadata copy + if (plotClient) { + plotClient.metadata.kind = backendMetadata.kind; + plotClient.metadata.name = backendMetadata.name; + plotClient.metadata.execution_id = backendMetadata.execution_id; + if (!plotClient.metadata.code) { + plotClient.metadata.code = backendMetadata.code; + } + plotClient.notifyMetadataUpdated(); + } + // Fire the service-level event to notify UI components + this._onDidUpdatePlotMetadata.fire(metadata.id); + } catch (err) { + // Log the error but don't fail - we can still use the plot with partial metadata + this._logService.warn(`Failed to fetch metadata for plot ${metadata.id}: ${err}`); + } + } + /** * Creates a new communication proxy for the given client and metadata. * diff --git a/src/vs/workbench/services/languageRuntime/common/languageRuntimePlotClient.ts b/src/vs/workbench/services/languageRuntime/common/languageRuntimePlotClient.ts index efe17e61c46b..04836c7d7101 100644 --- a/src/vs/workbench/services/languageRuntime/common/languageRuntimePlotClient.ts +++ b/src/vs/workbench/services/languageRuntime/common/languageRuntimePlotClient.ts @@ -52,12 +52,18 @@ export interface IPositronPlotMetadata { /** The plot's moment of creation, in milliseconds since the Epoch */ created: number; + /** The kind of the plot (e.g. 'Matplotlib', 'ggplot2', etc.) */ + kind?: string; + + /** A unique, human-readable name for the plot */ + name?: string; + + /** The execution ID that created the plot */ + execution_id?: string; + /** The code that created the plot, if known. */ code: string; - /** The plot's parent message ID; useful for jumping to associated spot in the console */ - parent_id: string; - /** The ID of the runtime session that created the plot */ session_id: string; @@ -173,6 +179,12 @@ export class PlotClientInstance extends Disposable implements IPositronPlotClien onDidChangeZoomLevel: Event; private readonly _zoomLevelEmitter = new Emitter(); + /** + * Event that fires when the plot's metadata is updated. + */ + onDidUpdateMetadata: Event; + private readonly _updateMetadataEmitter = new Emitter(); + /** * Creates a new plot client instance. * @@ -238,6 +250,9 @@ export class PlotClientInstance extends Disposable implements IPositronPlotClien // Connect the zoom level emitter event this.onDidChangeZoomLevel = this._zoomLevelEmitter.event; + // Connect the metadata update emitter event + this.onDidUpdateMetadata = this._updateMetadataEmitter.event; + // Listen to our own state changes this._register(this.onDidChangeState((state) => { this._state = state; @@ -308,6 +323,14 @@ export class PlotClientInstance extends Disposable implements IPositronPlotClien return this._commProxy.getIntrinsicSize(); } + /** + * Notifies listeners that the plot's metadata has been updated. + * This should be called after the metadata object has been modified. + */ + public notifyMetadataUpdated(): void { + this._updateMetadataEmitter.fire(this.metadata); + } + get sizingPolicy() { return this._sizingPolicy; } diff --git a/src/vs/workbench/services/languageRuntime/common/positronPlotComm.ts b/src/vs/workbench/services/languageRuntime/common/positronPlotComm.ts index e6e1a689ed0c..8e637d4b0281 100644 --- a/src/vs/workbench/services/languageRuntime/common/positronPlotComm.ts +++ b/src/vs/workbench/services/languageRuntime/common/positronPlotComm.ts @@ -37,6 +37,32 @@ export interface IntrinsicSize { } +/** + * The plot's metadata + */ +export interface PlotMetadata { + /** + * A human-readable name for the plot + */ + name: string; + + /** + * The kind of plot e.g. 'Matplotlib', 'ggplot2', etc. + */ + kind: string; + + /** + * The ID of the code fragment that produced the plot + */ + execution_id: string; + + /** + * The code fragment that produced the plot + */ + code: string; + +} + /** * A rendered plot */ @@ -169,6 +195,7 @@ export enum PlotFrontendEvent { export enum PlotBackendRequest { GetIntrinsicSize = 'get_intrinsic_size', + GetMetadata = 'get_metadata', Render = 'render' } @@ -195,6 +222,18 @@ export class PositronPlotComm extends PositronBaseComm { return super.performRpc('get_intrinsic_size', [], []); } + /** + * Get metadata for the plot + * + * Get metadata for the plot + * + * + * @returns The plot's metadata + */ + getMetadata(): Promise { + return super.performRpc('get_metadata', [], []); + } + /** * Render a plot * diff --git a/src/vs/workbench/services/languageRuntime/common/positronPlotCommProxy.ts b/src/vs/workbench/services/languageRuntime/common/positronPlotCommProxy.ts index f217a30f4ba6..8928eac56b6e 100644 --- a/src/vs/workbench/services/languageRuntime/common/positronPlotCommProxy.ts +++ b/src/vs/workbench/services/languageRuntime/common/positronPlotCommProxy.ts @@ -156,6 +156,15 @@ export class PositronPlotCommProxy extends Disposable { return this._currentIntrinsicSize; } + /** + * Get metadata for the plot. + * + * @returns A promise that resolves to the plot metadata. + */ + public getMetadata(): ReturnType { + return this._sessionRenderQueue.queueMetadataRequest(this._comm); + } + /** * Renders a plot. The request is queued if a render is already in progress. * diff --git a/src/vs/workbench/services/languageRuntime/common/positronPlotRenderQueue.ts b/src/vs/workbench/services/languageRuntime/common/positronPlotRenderQueue.ts index 8566f7a49e21..c8671bf57118 100644 --- a/src/vs/workbench/services/languageRuntime/common/positronPlotRenderQueue.ts +++ b/src/vs/workbench/services/languageRuntime/common/positronPlotRenderQueue.ts @@ -9,7 +9,7 @@ import { ILogService } from '../../../../platform/log/common/log.js'; import { IPlotSize } from '../../positronPlots/common/sizingPolicy.js'; import { ILanguageRuntimeSession } from '../../runtimeSession/common/runtimeSessionService.js'; import { RuntimeState } from './languageRuntimeService.js'; -import { PlotRenderFormat, PositronPlotComm, IntrinsicSize } from './positronPlotComm.js'; +import { PlotRenderFormat, PositronPlotComm, IntrinsicSize, PlotMetadata } from './positronPlotComm.js'; import { padBase64 } from './utils.js'; /** @@ -17,13 +17,14 @@ import { padBase64 } from './utils.js'; */ export enum OperationType { Render = 'render', - GetIntrinsicSize = 'get_intrinsic_size' + GetIntrinsicSize = 'get_intrinsic_size', + GetMetadata = 'get_metadata' } /** * The result of a plot operation. */ -export type PlotOperationResult = IRenderedPlot | IntrinsicSize | undefined; +export type PlotOperationResult = IRenderedPlot | IntrinsicSize | PlotMetadata | undefined; /** * A rendered plot. @@ -318,6 +319,29 @@ export class PositronPlotRenderQueue extends Disposable { }); } + /** + * Queue a metadata request. + * + * @param comm The comm to use for the operation + */ + public queueMetadataRequest(comm: PositronPlotComm): Promise { + // Cancel any existing metadata requests for the same plot + this.cancelExistingOperations(comm, OperationType.GetMetadata); + + const operationRequest: PlotOperationRequest = { + type: OperationType.GetMetadata + }; + + const deferredOperation = this.queueOperation(operationRequest, comm); + return deferredOperation.promise.then((result) => { + if (result && typeof result === 'object' && 'name' in result && 'kind' in result) { + return result as PlotMetadata; + } else { + throw new Error('Invalid metadata result'); + } + }); + } + /** * Cancel existing operations in the queue for the same plot and operation * type. Used to avoid unnecessary work, e.g. when a new render request is @@ -416,6 +440,18 @@ export class PositronPlotRenderQueue extends Disposable { this._isProcessing = false; this.processQueue(); }); + } else if (operationRequest.type === OperationType.GetMetadata) { + // Handle metadata operation + queuedOperation.comm.getMetadata().then((metadata) => { + queuedOperation.operation.complete(metadata); + }).catch((err) => { + // Handle the error + queuedOperation.operation.error(err); + }).finally(() => { + // Mark processing as complete and process the next item in the queue + this._isProcessing = false; + this.processQueue(); + }); } else { // Unknown operation type queuedOperation.operation.error(new Error(`Unknown operation type: ${operationRequest.type}`)); diff --git a/src/vs/workbench/services/positronPlots/common/positronPlots.ts b/src/vs/workbench/services/positronPlots/common/positronPlots.ts index 1521481e43ac..d0cbc0a8eec7 100644 --- a/src/vs/workbench/services/positronPlots/common/positronPlots.ts +++ b/src/vs/workbench/services/positronPlots/common/positronPlots.ts @@ -29,6 +29,11 @@ export const IPositronPlotsService = createDecorator(POSI export interface IPositronPlotClient extends IDisposable { readonly id: string; readonly metadata: IPositronPlotMetadata; + + /** + * Event that fires when the plot's metadata changes. + */ + readonly onDidUpdateMetadata?: Event; } export interface IZoomablePlotClient { @@ -182,6 +187,12 @@ export interface IPositronPlotsService { */ readonly onDidRemovePlot: Event; + /** + * Notifies subscribers when a plot's metadata has been updated. The ID + * of the plot is the event payload. + */ + readonly onDidUpdatePlotMetadata: Event; + /** * Notifies subscribers when the list of Positron plot instances is replaced * with a new list. The new list of plots is the event paylod. This event is diff --git a/src/vs/workbench/services/positronPlots/test/common/testPositronPlotClient.ts b/src/vs/workbench/services/positronPlots/test/common/testPositronPlotClient.ts index 80c67d189329..20db593008e4 100644 --- a/src/vs/workbench/services/positronPlots/test/common/testPositronPlotClient.ts +++ b/src/vs/workbench/services/positronPlots/test/common/testPositronPlotClient.ts @@ -23,7 +23,6 @@ export class TestPositronPlotClient extends Disposable implements IPositronPlotC id: generateUuid(), session_id: 'test-session', created: Date.now(), - parent_id: '', code: 'test code', zoom_level: ZoomLevel.Fit, } diff --git a/src/vs/workbench/services/positronPlots/test/common/testPositronPlotsService.ts b/src/vs/workbench/services/positronPlots/test/common/testPositronPlotsService.ts index 444d8a89798b..284614ab55bb 100644 --- a/src/vs/workbench/services/positronPlots/test/common/testPositronPlotsService.ts +++ b/src/vs/workbench/services/positronPlots/test/common/testPositronPlotsService.ts @@ -76,6 +76,12 @@ export class TestPositronPlotsService extends Disposable implements IPositronPlo private readonly _onDidRemovePlotEmitter = this._register(new Emitter()); + /** + * The onDidUpdatePlotMetadata event emitter. + */ + private readonly _onDidUpdatePlotMetadataEmitter = + this._register(new Emitter()); + /** * The onDidReplacePlots event emitter. */ @@ -201,6 +207,11 @@ export class TestPositronPlotsService extends Disposable implements IPositronPlo */ readonly onDidRemovePlot = this._onDidRemovePlotEmitter.event; + /** + * The onDidUpdatePlotMetadata event. + */ + readonly onDidUpdatePlotMetadata = this._onDidUpdatePlotMetadataEmitter.event; + /** * The onDidReplacePlots event. */ From 6800c54ad4370e9b0a9ba8cb151cbb97be30c3ad Mon Sep 17 00:00:00 2001 From: Jonathan McPherson Date: Thu, 18 Dec 2025 13:02:40 -0800 Subject: [PATCH 02/32] adapt to vertical history display --- .../browser/components/plotsContainer.tsx | 18 ++++++++++-------- .../positronPlots/browser/positronPlots.css | 7 +++++++ .../test/common/testPlotsServiceHelper.ts | 4 ++-- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/vs/workbench/contrib/positronPlots/browser/components/plotsContainer.tsx b/src/vs/workbench/contrib/positronPlots/browser/components/plotsContainer.tsx index 8f1aafa398ff..77168280180f 100644 --- a/src/vs/workbench/contrib/positronPlots/browser/components/plotsContainer.tsx +++ b/src/vs/workbench/contrib/positronPlots/browser/components/plotsContainer.tsx @@ -417,14 +417,16 @@ export const PlotsContainer = (props: PlotContainerProps) => { // most recently generated plot. return (
- {positronPlotsContext.positronPlotInstances.length > 0 && renderPlotInfoHeader()} -
- {positronPlotsContext.positronPlotInstances.length === 0 && -
} - {positronPlotsContext.positronPlotInstances.map((plotInstance, index) => ( - plotInstance.id === positronPlotsContext.selectedInstanceId && - render(plotInstance) - ))} +
+ {positronPlotsContext.positronPlotInstances.length > 0 && renderPlotInfoHeader()} +
+ {positronPlotsContext.positronPlotInstances.length === 0 && +
} + {positronPlotsContext.positronPlotInstances.map((plotInstance, index) => ( + plotInstance.id === positronPlotsContext.selectedInstanceId && + render(plotInstance) + ))} +
{props.showHistory && renderHistory()}
diff --git a/src/vs/workbench/contrib/positronPlots/browser/positronPlots.css b/src/vs/workbench/contrib/positronPlots/browser/positronPlots.css index e1f26110303f..4fc6cda965a6 100644 --- a/src/vs/workbench/contrib/positronPlots/browser/positronPlots.css +++ b/src/vs/workbench/contrib/positronPlots/browser/positronPlots.css @@ -14,6 +14,13 @@ flex-direction: row; } +.plots-container .plot-content { + display: flex; + flex-direction: column; + flex: 1; + overflow: hidden; +} + .plots-container.history-bottom { flex-direction: column; } diff --git a/src/vs/workbench/services/positronPlots/test/common/testPlotsServiceHelper.ts b/src/vs/workbench/services/positronPlots/test/common/testPlotsServiceHelper.ts index cdfd3c565832..db7fb7a02a60 100644 --- a/src/vs/workbench/services/positronPlots/test/common/testPlotsServiceHelper.ts +++ b/src/vs/workbench/services/positronPlots/test/common/testPlotsServiceHelper.ts @@ -19,7 +19,7 @@ export function createTestPlotsServiceWithPlots(): TestPositronPlotsService { id: 'test-plot-1', session_id: 'test-session', created: Date.now(), - parent_id: '', + execution_id: '', code: 'plot(1:10)', zoom_level: ZoomLevel.Fit, }); @@ -28,7 +28,7 @@ export function createTestPlotsServiceWithPlots(): TestPositronPlotsService { id: 'test-plot-2', session_id: 'test-session', created: Date.now() + 1000, // Created later - parent_id: '', + execution_id: '', code: 'hist(rnorm(100))', zoom_level: ZoomLevel.Fit, }); From 0b30707a583734c5271d23911195f9fc0b26eef8 Mon Sep 17 00:00:00 2001 From: Jonathan McPherson Date: Thu, 18 Dec 2025 13:58:53 -0800 Subject: [PATCH 03/32] plot code button --- .../browser/components/plotsContainer.css | 34 +++++++++++++++++++ .../browser/components/plotsContainer.tsx | 16 +++++++-- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/positronPlots/browser/components/plotsContainer.css b/src/vs/workbench/contrib/positronPlots/browser/components/plotsContainer.css index e6f2c141b575..8f6029351492 100644 --- a/src/vs/workbench/contrib/positronPlots/browser/components/plotsContainer.css +++ b/src/vs/workbench/contrib/positronPlots/browser/components/plotsContainer.css @@ -36,3 +36,37 @@ text-overflow: ellipsis; opacity: 0.8; } + +.plots-container .plot-info-header .plot-code-button { + display: flex; + align-items: center; + gap: 4px; + margin-left: auto; + padding: 0 4px; + background: none; + border: none; + cursor: pointer; + max-width: 150px; + overflow: hidden; +} + +.plots-container .plot-info-header .plot-code-button .plot-code-text { + font-family: var(--monaco-monospace-font); + font-size: 11px; + color: var(--vscode-foreground); + opacity: 0.8; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.plots-container .plot-info-header .plot-code-button .codicon { + flex-shrink: 0; + color: var(--vscode-foreground); + opacity: 0.8; +} + +.plots-container .plot-info-header .plot-code-button:hover .plot-code-text, +.plots-container .plot-info-header .plot-code-button:hover .codicon { + opacity: 1; +} diff --git a/src/vs/workbench/contrib/positronPlots/browser/components/plotsContainer.tsx b/src/vs/workbench/contrib/positronPlots/browser/components/plotsContainer.tsx index 77168280180f..a67d5617c04e 100644 --- a/src/vs/workbench/contrib/positronPlots/browser/components/plotsContainer.tsx +++ b/src/vs/workbench/contrib/positronPlots/browser/components/plotsContainer.tsx @@ -119,6 +119,12 @@ export const PlotsContainer = (props: PlotContainerProps) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [currentPlotInstance, metadataVersion]); + // Get the plot code from metadata (reactive to metadata updates) + const plotCode = useMemo(() => { + return currentPlotInstance?.metadata.code; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [currentPlotInstance, metadataVersion]); + // Plot history useEffect to handle scrolling, mouse wheel events, and keyboard navigation. useEffect(() => { // Get the current plot history and container. If the plot history is not rendered, @@ -402,14 +408,20 @@ export const PlotsContainer = (props: PlotContainerProps) => { const displayText = `${sessionName ?? ''}${separator}${plotName ?? ''}`; // If no info to display, show a placeholder to maintain consistent height - if (!displayText) { + if (!displayText && !plotCode) { return
 
; } return
- {displayText} + {displayText && {displayText}} + {plotCode && ( + + )}
; }; From 42ab978f207e29098c29c91da05778e7b8990b98 Mon Sep 17 00:00:00 2001 From: Jonathan McPherson Date: Thu, 18 Dec 2025 14:08:04 -0800 Subject: [PATCH 04/32] add copy button --- .../browser/components/plotsContainer.css | 14 ++++++- .../browser/components/plotsContainer.tsx | 40 ++++++++++++++++++- 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/positronPlots/browser/components/plotsContainer.css b/src/vs/workbench/contrib/positronPlots/browser/components/plotsContainer.css index 8f6029351492..5944cf3d5205 100644 --- a/src/vs/workbench/contrib/positronPlots/browser/components/plotsContainer.css +++ b/src/vs/workbench/contrib/positronPlots/browser/components/plotsContainer.css @@ -42,14 +42,19 @@ align-items: center; gap: 4px; margin-left: auto; - padding: 0 4px; + padding: 2px 4px; background: none; border: none; + border-radius: 4px; cursor: pointer; - max-width: 150px; + max-width: 200px; overflow: hidden; } +.plots-container .plot-info-header .plot-code-button:hover { + background-color: var(--vscode-toolbar-hoverBackground); +} + .plots-container .plot-info-header .plot-code-button .plot-code-text { font-family: var(--monaco-monospace-font); font-size: 11px; @@ -66,6 +71,11 @@ opacity: 0.8; } +.plots-container .plot-info-header .plot-code-button .codicon-chevron-down { + font-size: 10px; + margin-left: 2px; +} + .plots-container .plot-info-header .plot-code-button:hover .plot-code-text, .plots-container .plot-info-header .plot-code-button:hover .codicon { opacity: 1; diff --git a/src/vs/workbench/contrib/positronPlots/browser/components/plotsContainer.tsx b/src/vs/workbench/contrib/positronPlots/browser/components/plotsContainer.tsx index a67d5617c04e..b091fd75bc32 100644 --- a/src/vs/workbench/contrib/positronPlots/browser/components/plotsContainer.tsx +++ b/src/vs/workbench/contrib/positronPlots/browser/components/plotsContainer.tsx @@ -11,6 +11,7 @@ import React, { useEffect, useMemo, useRef } from 'react'; // Other dependencies. import * as DOM from '../../../../../base/browser/dom.js'; +import { localize } from '../../../../../nls.js'; import { DynamicPlotInstance } from './dynamicPlotInstance.js'; import { DynamicPlotThumbnail } from './dynamicPlotThumbnail.js'; import { PlotGalleryThumbnail } from './plotGalleryThumbnail.js'; @@ -27,6 +28,8 @@ import { PlotSizingPolicyIntrinsic } from '../../../../services/positronPlots/co import { PlotSizingPolicyAuto } from '../../../../services/positronPlots/common/sizingPolicyAuto.js'; import { DisposableStore } from '../../../../../base/common/lifecycle.js'; import { usePositronReactServicesContext } from '../../../../../base/browser/positronReactRendererContext.js'; +import { showCustomContextMenu } from '../../../../browser/positronComponents/customContextMenu/customContextMenu.js'; +import { CustomContextMenuItem } from '../../../../browser/positronComponents/customContextMenu/customContextMenuItem.js'; /** * PlotContainerProps interface. @@ -414,12 +417,45 @@ export const PlotsContainer = (props: PlotContainerProps) => {
; } + /** + * Handles the click on the plot code button to show a dropdown menu. + * @param e The mouse event. + */ + const handlePlotCodeClick = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + + const targetElement = e.currentTarget as HTMLElement; + + showCustomContextMenu({ + anchorElement: targetElement, + popupPosition: 'bottom', + popupAlignment: 'left', + entries: [ + new CustomContextMenuItem({ + label: localize('positronPlots.copyCode', "Copy"), + icon: 'copy', + onSelected: () => { + if (plotCode) { + services.clipboardService.writeText(plotCode); + } + } + }) + ] + }); + }; + return
{displayText && {displayText}} {plotCode && ( - )}
; From d433448883d22847f1d95e558f2d1b917c0f8c43 Mon Sep 17 00:00:00 2001 From: Jonathan McPherson Date: Thu, 18 Dec 2025 14:53:42 -0800 Subject: [PATCH 05/32] add 'reveal execution' --- .../browser/components/activityInput.css | 13 +++++ .../browser/components/consoleInstance.tsx | 28 ++++++++++ .../browser/components/runtimeActivity.tsx | 2 +- .../browser/components/plotsContainer.tsx | 12 +++++ .../positronPlots/browser/htmlPlotClient.ts | 2 +- .../browser/notebookMultiMessagePlotClient.ts | 2 +- .../browser/notebookOutputPlotClient.ts | 2 +- .../interfaces/positronConsoleService.ts | 25 +++++++++ .../browser/positronConsoleService.ts | 51 +++++++++++++++++++ .../browser/testPositronConsoleService.ts | 28 ++++++++++ 10 files changed, 161 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/positronConsole/browser/components/activityInput.css b/src/vs/workbench/contrib/positronConsole/browser/components/activityInput.css index f28644afb240..ec9c03eeef0d 100644 --- a/src/vs/workbench/contrib/positronConsole/browser/components/activityInput.css +++ b/src/vs/workbench/contrib/positronConsole/browser/components/activityInput.css @@ -20,12 +20,25 @@ 100% { opacity: 1; } } +@keyframes positronActivityInput-revealFadeInOut { + 0% { opacity: 0; } + 15% { opacity: 1; } + 85% { opacity: 1; } + 100% { opacity: 0; } +} + .activity-input.executing .progress-bar { background-color: var(--vscode-positronConsole-ansiGreen); opacity: 0; animation: positronActivityInput-fadeIn 0.25s ease-in 0.25s 1 forwards; } +.activity-input.revealed .progress-bar { + background-color: var(--vscode-focusBorder); + opacity: 0; + animation: positronActivityInput-revealFadeInOut 2s ease-in-out 1 forwards; +} + .activity-input .prompt { text-align: right; display: inline-block; diff --git a/src/vs/workbench/contrib/positronConsole/browser/components/consoleInstance.tsx b/src/vs/workbench/contrib/positronConsole/browser/components/consoleInstance.tsx index 2cd07b2157f2..43e855907721 100644 --- a/src/vs/workbench/contrib/positronConsole/browser/components/consoleInstance.tsx +++ b/src/vs/workbench/contrib/positronConsole/browser/components/consoleInstance.tsx @@ -292,6 +292,34 @@ export const ConsoleInstance = (props: ConsoleInstanceProps) => { setRuntimeAttached(!!runtime); })); + // Add the onDidRequestRevealExecution event handler. + disposableStore.add(props.positronConsoleInstance.onDidRequestRevealExecution((executionId) => { + // Find the element with the matching data-execution-id attribute. + const element = consoleInstanceRef.current.querySelector( + `[data-execution-id="${executionId}"]` + ); + if (!element) { + return; + } + + // Find the activity-input element within this activity. + const activityInput = element.querySelector('.activity-input'); + if (!activityInput) { + return; + } + + // Scroll the element into view. + element.scrollIntoView({ behavior: 'smooth', block: 'center' }); + + // Add the 'revealed' class to trigger the highlight animation. + activityInput.classList.add('revealed'); + + // Remove the 'revealed' class after the animation completes (2 seconds). + disposableTimeout(() => { + activityInput.classList.remove('revealed'); + }, 2000, disposableStore); + })); + // Return the cleanup function that will dispose of the event handlers. return () => disposableStore.dispose(); }, [positronConsoleContext.activePositronConsoleInstance?.attachedRuntimeSession, positronConsoleContext.activePositronConsoleInstance, services.configurationService, services.positronPlotsService, services.runtimeSessionService, services.viewsService, props.positronConsoleInstance, props.reactComponentContainer, scrollToBottom]); diff --git a/src/vs/workbench/contrib/positronConsole/browser/components/runtimeActivity.tsx b/src/vs/workbench/contrib/positronConsole/browser/components/runtimeActivity.tsx index 7b709628f36c..98e2f36cd92b 100644 --- a/src/vs/workbench/contrib/positronConsole/browser/components/runtimeActivity.tsx +++ b/src/vs/workbench/contrib/positronConsole/browser/components/runtimeActivity.tsx @@ -44,7 +44,7 @@ export interface RuntimeActivityProps { export const RuntimeActivity = (props: RuntimeActivityProps) => { // Render. return ( -
+
{props.runtimeItemActivity.activityItems.filter(activityItem => !activityItem.isHidden).map(activityItem => { if (activityItem instanceof ActivityItemInput) { diff --git a/src/vs/workbench/contrib/positronPlots/browser/components/plotsContainer.tsx b/src/vs/workbench/contrib/positronPlots/browser/components/plotsContainer.tsx index b091fd75bc32..1a0c741785cf 100644 --- a/src/vs/workbench/contrib/positronPlots/browser/components/plotsContainer.tsx +++ b/src/vs/workbench/contrib/positronPlots/browser/components/plotsContainer.tsx @@ -426,6 +426,8 @@ export const PlotsContainer = (props: PlotContainerProps) => { e.stopPropagation(); const targetElement = e.currentTarget as HTMLElement; + const executionId = currentPlotInstance?.metadata.execution_id; + const sessionId = currentPlotInstance?.metadata.session_id; showCustomContextMenu({ anchorElement: targetElement, @@ -440,6 +442,16 @@ export const PlotsContainer = (props: PlotContainerProps) => { services.clipboardService.writeText(plotCode); } } + }), + new CustomContextMenuItem({ + label: localize('positronPlots.revealInConsole', "Reveal in Console"), + icon: 'go-to-file', + disabled: !executionId || !sessionId, + onSelected: () => { + if (executionId && sessionId) { + services.positronConsoleService.revealExecution(sessionId, executionId); + } + } }) ] }); diff --git a/src/vs/workbench/contrib/positronPlots/browser/htmlPlotClient.ts b/src/vs/workbench/contrib/positronPlots/browser/htmlPlotClient.ts index 3c2151658cdc..7b737f58674f 100644 --- a/src/vs/workbench/contrib/positronPlots/browser/htmlPlotClient.ts +++ b/src/vs/workbench/contrib/positronPlots/browser/htmlPlotClient.ts @@ -40,7 +40,7 @@ export class HtmlPlotClient extends WebviewPlotClient { // Create the metadata for the plot. super({ id: `plot-${HtmlPlotClient._nextId++}`, - parent_id: '', + execution_id: '', created: Date.now(), session_id: _session.sessionId, code: '', diff --git a/src/vs/workbench/contrib/positronPlots/browser/notebookMultiMessagePlotClient.ts b/src/vs/workbench/contrib/positronPlots/browser/notebookMultiMessagePlotClient.ts index 334f17e4ac2b..26979c9ed02f 100644 --- a/src/vs/workbench/contrib/positronPlots/browser/notebookMultiMessagePlotClient.ts +++ b/src/vs/workbench/contrib/positronPlots/browser/notebookMultiMessagePlotClient.ts @@ -40,7 +40,7 @@ export class NotebookMultiMessagePlotClient extends WebviewPlotClient { // Create the metadata for the plot. super({ id: _displayMessage.id, - parent_id: _displayMessage.parent_id, + execution_id: _displayMessage.parent_id, created: Date.parse(_displayMessage.when), session_id: _session.sessionId, code: code ? code : '', diff --git a/src/vs/workbench/contrib/positronPlots/browser/notebookOutputPlotClient.ts b/src/vs/workbench/contrib/positronPlots/browser/notebookOutputPlotClient.ts index 7882587b1ce3..7eccce906afd 100644 --- a/src/vs/workbench/contrib/positronPlots/browser/notebookOutputPlotClient.ts +++ b/src/vs/workbench/contrib/positronPlots/browser/notebookOutputPlotClient.ts @@ -37,7 +37,7 @@ export class NotebookOutputPlotClient extends WebviewPlotClient { // Create the metadata for the plot. super({ id: _message.id, - parent_id: _message.parent_id, + execution_id: _message.parent_id, created: Date.parse(_message.when), session_id: _session.sessionId, code: code ? code : '', diff --git a/src/vs/workbench/services/positronConsole/browser/interfaces/positronConsoleService.ts b/src/vs/workbench/services/positronConsole/browser/interfaces/positronConsoleService.ts index 75a1326312ad..39bad511538f 100644 --- a/src/vs/workbench/services/positronConsole/browser/interfaces/positronConsoleService.ts +++ b/src/vs/workbench/services/positronConsole/browser/interfaces/positronConsoleService.ts @@ -153,6 +153,16 @@ export interface IPositronConsoleService { * @param focus A value which indicates whether to focus the console. */ showNotebookConsole(notebookUri: URI, focus: boolean): void; + + /** + * Reveals and highlights the console input associated with the given execution ID. + * This will activate the console instance for the given session, scroll to the input, + * and briefly highlight it to draw attention to it. + * + * @param sessionId The session ID of the console instance. + * @param executionId The execution ID of the input to reveal. + */ + revealExecution(sessionId: string, executionId: string): void; } /** @@ -336,6 +346,12 @@ export interface IPositronConsoleInstance { */ readonly onDidAttachSession: Event; + /** + * The onDidRequestRevealExecution event. Fired when an execution should be + * revealed and highlighted in the console. + */ + readonly onDidRequestRevealExecution: Event; + /** * The onDidChangeWidthInChars event. */ @@ -480,4 +496,13 @@ export interface IPositronConsoleInstance { * Gets the currently attached runtime, or undefined if none. */ attachedRuntimeSession: ILanguageRuntimeSession | undefined; + + /** + * Reveals and highlights the console input associated with the given execution ID. + * This will scroll to the input and briefly highlight it. + * + * @param executionId The execution ID of the input to reveal. + * @returns `true` if the execution was found and revealed, `false` otherwise. + */ + revealExecution(executionId: string): boolean; } diff --git a/src/vs/workbench/services/positronConsole/browser/positronConsoleService.ts b/src/vs/workbench/services/positronConsole/browser/positronConsoleService.ts index 5a3e80a372c5..230f7a1fd9d9 100644 --- a/src/vs/workbench/services/positronConsole/browser/positronConsoleService.ts +++ b/src/vs/workbench/services/positronConsole/browser/positronConsoleService.ts @@ -951,6 +951,29 @@ export class PositronConsoleService extends Disposable implements IPositronConso this._onDidDeletePositronConsoleInstanceEmitter.fire(consoleInstance); } + /** + * Reveals and highlights the console input associated with the given execution ID. + * + * @param sessionId The session ID of the console instance. + * @param executionId The execution ID of the input to reveal. + */ + revealExecution(sessionId: string, executionId: string): void { + // Find the console instance with the given session ID. + const consoleInstance = this._positronConsoleInstancesBySessionId.get(sessionId); + if (!consoleInstance) { + return; + } + + // Open the console view to ensure it's visible. + this._viewsService.openView(POSITRON_CONSOLE_VIEW_ID, false); + + // Set this console instance as active. + this.setActivePositronConsoleInstance(consoleInstance); + + // Ask the console instance to reveal the execution. + consoleInstance.revealExecution(executionId); + } + /** * Sets the active Positron console instance. * @param positronConsoleInstance @@ -1171,6 +1194,11 @@ class PositronConsoleInstance extends Disposable implements IPositronConsoleInst private readonly _onDidAttachRuntime = this._register( new Emitter); + /** + * The onDidRequestRevealExecution event emitter. + */ + private readonly _onDidRequestRevealExecutionEmitter = this._register(new Emitter); + /** * Provides access to the code editor, if it's available. Note that we generally prefer to * interact with this editor indirectly, since its state is managed by React. @@ -1467,6 +1495,11 @@ class PositronConsoleInstance extends Disposable implements IPositronConsoleInst */ readonly onDidAttachSession = this._onDidAttachRuntime.event; + /** + * onDidRequestRevealExecution event. + */ + readonly onDidRequestRevealExecution = this._onDidRequestRevealExecutionEmitter.event; + /** * Emitted when the width of the console changes. */ @@ -1832,6 +1865,24 @@ class PositronConsoleInstance extends Disposable implements IPositronConsoleInst ); } + /** + * Reveals and highlights the console input associated with the given execution ID. + * + * @param executionId The execution ID of the input to reveal. + * @returns `true` if the execution was found and revealed, `false` otherwise. + */ + revealExecution(executionId: string): boolean { + // Try to find the activity item for this execution ID. + const activity = this._runtimeItemActivities.get(executionId); + if (!activity) { + return false; + } + + // Fire the event to request the UI to reveal and highlight this execution. + this._onDidRequestRevealExecutionEmitter.fire(executionId); + return true; + } + //#endregion IPositronConsoleInstance Implementation //#region Public Methods diff --git a/src/vs/workbench/services/positronConsole/test/browser/testPositronConsoleService.ts b/src/vs/workbench/services/positronConsole/test/browser/testPositronConsoleService.ts index f3ce0b8b5c04..ecb98bcca377 100644 --- a/src/vs/workbench/services/positronConsole/test/browser/testPositronConsoleService.ts +++ b/src/vs/workbench/services/positronConsole/test/browser/testPositronConsoleService.ts @@ -211,6 +211,19 @@ export class TestPositronConsoleService implements IPositronConsoleService { // Doesn't do anything } + /** + * Reveals and highlights the console input associated with the given execution ID. + * @param sessionId The session ID of the console instance. + * @param executionId The execution ID of the input to reveal. + */ + revealExecution(sessionId: string, executionId: string): void { + const instance = this._positronConsoleInstances.find(instance => instance.sessionId === sessionId); + if (instance) { + this.setActivePositronConsoleSession(sessionId); + instance.revealExecution(executionId); + } + } + /** * Creates a test code execution event. */ @@ -279,6 +292,7 @@ export class TestPositronConsoleInstance implements IPositronConsoleInstance { private readonly _onDidRequestRestartEmitter = new Emitter(); private readonly _onDidAttachSessionEmitter = new Emitter(); private readonly _onDidChangeWidthInCharsEmitter = new Emitter(); + private readonly _onDidRequestRevealExecutionEmitter = new Emitter(); private _state: PositronConsoleState = PositronConsoleState.Ready; private _trace: boolean = false; @@ -362,6 +376,10 @@ export class TestPositronConsoleInstance implements IPositronConsoleInstance { return this._onDidAttachSessionEmitter.event; } + get onDidRequestRevealExecution(): Event { + return this._onDidRequestRevealExecutionEmitter.event; + } + get onDidChangeWidthInChars(): Event { return this._onDidChangeWidthInCharsEmitter.event; } @@ -582,6 +600,16 @@ export class TestPositronConsoleInstance implements IPositronConsoleInstance { return this._attachedRuntimeSession; } + /** + * Reveals and highlights the console input associated with the given execution ID. + * @param executionId The execution ID of the input to reveal. + * @returns `true` if the execution was found and revealed, `false` otherwise. + */ + revealExecution(executionId: string): boolean { + this._onDidRequestRevealExecutionEmitter.fire(executionId); + return true; + } + /** * Attach a runtime session to this console instance. * @param session The session to attach. From 215a0deaa3c451fa59bfab0b6002c360d0598a91 Mon Sep 17 00:00:00 2001 From: Jonathan McPherson Date: Thu, 18 Dec 2025 14:57:50 -0800 Subject: [PATCH 06/32] always show the progress bar --- .../positronConsole/browser/components/activityInput.css | 1 + .../positronConsole/browser/components/activityInput.tsx | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/positronConsole/browser/components/activityInput.css b/src/vs/workbench/contrib/positronConsole/browser/components/activityInput.css index ec9c03eeef0d..ecb962b7716c 100644 --- a/src/vs/workbench/contrib/positronConsole/browser/components/activityInput.css +++ b/src/vs/workbench/contrib/positronConsole/browser/components/activityInput.css @@ -13,6 +13,7 @@ position: absolute; top: 0; left: -10px; + opacity: 0; } @keyframes positronActivityInput-fadeIn { diff --git a/src/vs/workbench/contrib/positronConsole/browser/components/activityInput.tsx b/src/vs/workbench/contrib/positronConsole/browser/components/activityInput.tsx index b9540b3f0e36..1221f96348c3 100644 --- a/src/vs/workbench/contrib/positronConsole/browser/components/activityInput.tsx +++ b/src/vs/workbench/contrib/positronConsole/browser/components/activityInput.tsx @@ -220,7 +220,7 @@ export const ActivityInput = (props: ActivityInputProps) => { // Render colorized lines. return (
- {state === ActivityItemInputState.Executing &&
} +
{colorizedOutputLines.map((outputLine, index) =>
@@ -236,7 +236,7 @@ export const ActivityInput = (props: ActivityInputProps) => { // Render non-colorized lines. return (
- {state === ActivityItemInputState.Executing &&
} +
{props.activityItemInput.codeOutputLines.map((outputLine, index) =>
From 566a20e933b7b471f88669914c5cbe00edf25bba Mon Sep 17 00:00:00 2001 From: Jonathan McPherson Date: Thu, 18 Dec 2025 15:22:31 -0800 Subject: [PATCH 07/32] run code again --- .../browser/components/plotsContainer.css | 24 ++++++++++-- .../browser/components/plotsContainer.tsx | 38 ++++++++++++++----- 2 files changed, 49 insertions(+), 13 deletions(-) diff --git a/src/vs/workbench/contrib/positronPlots/browser/components/plotsContainer.css b/src/vs/workbench/contrib/positronPlots/browser/components/plotsContainer.css index 5944cf3d5205..70921764ac95 100644 --- a/src/vs/workbench/contrib/positronPlots/browser/components/plotsContainer.css +++ b/src/vs/workbench/contrib/positronPlots/browser/components/plotsContainer.css @@ -20,20 +20,36 @@ .plots-container .plot-info-header { display: flex; align-items: center; - padding: 0 8px; + padding: 0 4px; min-height: 24px; flex-shrink: 0; overflow: hidden; background-color: var(--vscode-editor-background); - border-bottom: 1px solid var(--vscode-panel-border); } .plots-container .plot-info-header .plot-info-text { - font-size: 12px; - color: var(--vscode-foreground); + display: flex; + align-items: center; + gap: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; +} + +.plots-container .plot-info-header .plot-session-name { + font-size: 11px; + color: var(--vscode-foreground); + opacity: 0.8; + padding: 0px 5px; + border-radius: 5px; + border: 1px solid var(--vscode-panel-border); + margin-top: 3px; + margin-right: 4px; +} + +.plots-container .plot-info-header .plot-name { + font-size: 12px; + color: var(--vscode-foreground); opacity: 0.8; } diff --git a/src/vs/workbench/contrib/positronPlots/browser/components/plotsContainer.tsx b/src/vs/workbench/contrib/positronPlots/browser/components/plotsContainer.tsx index 1a0c741785cf..3f87adc7220d 100644 --- a/src/vs/workbench/contrib/positronPlots/browser/components/plotsContainer.tsx +++ b/src/vs/workbench/contrib/positronPlots/browser/components/plotsContainer.tsx @@ -30,6 +30,7 @@ import { DisposableStore } from '../../../../../base/common/lifecycle.js'; import { usePositronReactServicesContext } from '../../../../../base/browser/positronReactRendererContext.js'; import { showCustomContextMenu } from '../../../../browser/positronComponents/customContextMenu/customContextMenu.js'; import { CustomContextMenuItem } from '../../../../browser/positronComponents/customContextMenu/customContextMenuItem.js'; +import { CodeAttributionSource } from '../../../../services/positronConsole/common/positronConsoleCodeExecution.js'; /** * PlotContainerProps interface. @@ -405,13 +406,8 @@ export const PlotsContainer = (props: PlotContainerProps) => { return null; } - const separator = sessionName && plotName ? ' : ' : ''; - - // Show session name and plot name if available - const displayText = `${sessionName ?? ''}${separator}${plotName ?? ''}`; - // If no info to display, show a placeholder to maintain consistent height - if (!displayText && !plotCode) { + if (!sessionName && !plotName && !plotCode) { return
 
; @@ -435,7 +431,7 @@ export const PlotsContainer = (props: PlotContainerProps) => { popupAlignment: 'left', entries: [ new CustomContextMenuItem({ - label: localize('positronPlots.copyCode', "Copy"), + label: localize('positronPlots.copyCode', "Copy Code"), icon: 'copy', onSelected: () => { if (plotCode) { @@ -444,7 +440,7 @@ export const PlotsContainer = (props: PlotContainerProps) => { } }), new CustomContextMenuItem({ - label: localize('positronPlots.revealInConsole', "Reveal in Console"), + label: localize('positronPlots.revealInConsole', "Reveal Code in Console"), icon: 'go-to-file', disabled: !executionId || !sessionId, onSelected: () => { @@ -452,13 +448,37 @@ export const PlotsContainer = (props: PlotContainerProps) => { services.positronConsoleService.revealExecution(sessionId, executionId); } } + }), + new CustomContextMenuItem({ + label: localize('positronPlots.runCodeAgain', "Run Code Again"), + icon: 'run', + disabled: !plotCode || !sessionId, + onSelected: async () => { + if (plotCode && sessionId) { + const languageId = currentPlotInstance?.metadata.language; + if (languageId) { + await services.positronConsoleService.executeCode( + languageId, + sessionId, + plotCode, + { source: CodeAttributionSource.Interactive }, + true + ); + } + } + } }) ] }); }; return
- {displayText && {displayText}} + {(sessionName || plotName) && ( + + {sessionName && {sessionName}} + {plotName && {plotName}} + + )} {plotCode && ( -
+ ); }; diff --git a/src/vs/workbench/contrib/positronPlots/browser/positronPlots.css b/src/vs/workbench/contrib/positronPlots/browser/positronPlots.css index 5a62108ae2d5..91ef7786f4f6 100644 --- a/src/vs/workbench/contrib/positronPlots/browser/positronPlots.css +++ b/src/vs/workbench/contrib/positronPlots/browser/positronPlots.css @@ -88,7 +88,7 @@ .history-bottom .plot-thumbnail { margin-left: 5px; margin-right: 5px; - margin-bottom: 10px; + width: 80px; } .selected-plot { @@ -106,25 +106,8 @@ transition: opacity 0.2s ease-in-out; } -.plot-thumbnail button { - cursor: pointer; - overflow: hidden; - margin: 0; - padding: 0; -} - -.plot-thumbnail .plot-thumbnail-button { - position: relative; - width: 100%; - height: 100%; - background: transparent; - border: none; - overflow: visible; -} - -.plot-thumbnail .plot-thumbnail-button .image-wrapper { - width: 100%; - height: 100%; +.plot-thumbnail-button{ + border: 1px solid transparent; } .plot-thumbnail button:active img, @@ -157,9 +140,7 @@ } .plot-thumbnail.selected .image-wrapper { - border-radius: 3px; - outline: 1px solid var(--vscode-focusBorder); - outline-offset: -1px; + border: none; } /** @@ -187,8 +168,7 @@ position: relative; } -.plot-instance .image-wrapper img, -.plot-thumbnail .image-wrapper img { +.plot-instance .image-wrapper img { display: block; max-width: 100%; max-height: 100%; @@ -199,6 +179,11 @@ transform: translate(-50%, -50%); } +.plot-thumbnail .image-wrapper img { + max-width: 100%; + max-height: 100%; +} + .plot-instance { height: 100%; width: 100%; From fa38c60393d7705f8f25ac6317e4088331b0c654 Mon Sep 17 00:00:00 2001 From: Jonathan McPherson Date: Fri, 19 Dec 2025 13:04:52 -0800 Subject: [PATCH 14/32] further simplify css --- .../browser/components/plotGalleryThumbnail.css | 2 -- .../positronPlots/browser/positronPlots.css | 16 ++++------------ 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/src/vs/workbench/contrib/positronPlots/browser/components/plotGalleryThumbnail.css b/src/vs/workbench/contrib/positronPlots/browser/components/plotGalleryThumbnail.css index 109089063325..153a109bcffd 100644 --- a/src/vs/workbench/contrib/positronPlots/browser/components/plotGalleryThumbnail.css +++ b/src/vs/workbench/contrib/positronPlots/browser/components/plotGalleryThumbnail.css @@ -42,6 +42,4 @@ border: none; background: none; cursor: pointer; - width: 100%; - height: 100%; } diff --git a/src/vs/workbench/contrib/positronPlots/browser/positronPlots.css b/src/vs/workbench/contrib/positronPlots/browser/positronPlots.css index 91ef7786f4f6..698e98a96e50 100644 --- a/src/vs/workbench/contrib/positronPlots/browser/positronPlots.css +++ b/src/vs/workbench/contrib/positronPlots/browser/positronPlots.css @@ -59,7 +59,6 @@ .history-right .plot-thumbnail { margin-top: 5px; - margin-bottom: 15px; } /* @@ -82,13 +81,10 @@ width: fit-content; margin-right: 10px; margin-left: 10px; - padding-bottom: 10px; } .history-bottom .plot-thumbnail { - margin-left: 5px; - margin-right: 5px; - width: 80px; + width: 100px; } .selected-plot { @@ -99,8 +95,9 @@ } .plot-thumbnail { - height: 80px; - width: 80px; + height: 90px; + width: 90px; + margin: 5px; padding: 0; opacity: 0.75; transition: opacity 0.2s ease-in-out; @@ -134,11 +131,6 @@ border: 1px dotted var(--vscode-editorWidget-border); } -.plot-thumbnail.selected img { - border-radius: 3px; - border: 1px solid transparent; -} - .plot-thumbnail.selected .image-wrapper { border: none; } From 4e747c2153ba3ec657aad8e1cfadf6eae636bbca Mon Sep 17 00:00:00 2001 From: Jonathan McPherson Date: Fri, 19 Dec 2025 13:37:16 -0800 Subject: [PATCH 15/32] properly constrain thumbnail --- .../components/plotGalleryThumbnail.css | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/positronPlots/browser/components/plotGalleryThumbnail.css b/src/vs/workbench/contrib/positronPlots/browser/components/plotGalleryThumbnail.css index 153a109bcffd..d640232eaccd 100644 --- a/src/vs/workbench/contrib/positronPlots/browser/components/plotGalleryThumbnail.css +++ b/src/vs/workbench/contrib/positronPlots/browser/components/plotGalleryThumbnail.css @@ -4,8 +4,7 @@ *--------------------------------------------------------------------------------------------*/ /** - * Plot thumbnail name label - positioned to overlap the bottom of the thumbnail - * with half above and half below the thumbnail image. + * Plot thumbnail name label - positioned at the bottom of the thumbnail. */ .plot-thumbnail-name { padding: 1px 4px; @@ -19,6 +18,8 @@ overflow: hidden; text-overflow: ellipsis; z-index: 1; + flex-shrink: 0; + margin-top: 2px; } .plot-thumbnail.selected .plot-thumbnail-name { @@ -42,4 +43,28 @@ border: none; background: none; cursor: pointer; + height: 100%; + width: 100%; + box-sizing: border-box; + overflow: hidden; +} + +.plot-thumbnail-button .image-wrapper { + display: flex; + align-items: center; + justify-content: center; + flex: 1; + min-height: 0; + overflow: hidden; +} + +.plot-thumbnail-button .image-wrapper img { + max-width: 100%; + max-height: 64px; + object-fit: contain; +} + +/* When no name is present, image can use more vertical space */ +.plot-thumbnail-button:not(:has(.plot-thumbnail-name)) .image-wrapper img { + max-height: 82px; } From 6f98670e9fd8e33fc2ea89f2990ba07afecedfed Mon Sep 17 00:00:00 2001 From: Jonathan McPherson Date: Fri, 19 Dec 2025 13:52:43 -0800 Subject: [PATCH 16/32] tighten margins --- .../contrib/positronPlots/browser/positronPlots.css | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/positronPlots/browser/positronPlots.css b/src/vs/workbench/contrib/positronPlots/browser/positronPlots.css index 698e98a96e50..b4c9a644b244 100644 --- a/src/vs/workbench/contrib/positronPlots/browser/positronPlots.css +++ b/src/vs/workbench/contrib/positronPlots/browser/positronPlots.css @@ -97,12 +97,17 @@ .plot-thumbnail { height: 90px; width: 90px; - margin: 5px; padding: 0; opacity: 0.75; transition: opacity 0.2s ease-in-out; } +.history-bottom .plot-thumbnail, +.history-right .plot-thumbnail { + margin-top: 5px; + margin-bottom: 5px; +} + .plot-thumbnail-button{ border: 1px solid transparent; } From 71d19903a0c067868f6faef2730ac276edd9bdfe Mon Sep 17 00:00:00 2001 From: Jonathan McPherson Date: Fri, 19 Dec 2025 14:09:07 -0800 Subject: [PATCH 17/32] use smaller placeholder --- .../browser/components/placeholderThumbnail.css | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/positronPlots/browser/components/placeholderThumbnail.css b/src/vs/workbench/contrib/positronPlots/browser/components/placeholderThumbnail.css index ad42f4634d05..280dfbdb2b42 100644 --- a/src/vs/workbench/contrib/positronPlots/browser/components/placeholderThumbnail.css +++ b/src/vs/workbench/contrib/positronPlots/browser/components/placeholderThumbnail.css @@ -1,5 +1,5 @@ /*--------------------------------------------------------------------------------------------- - * Copyright (C) 2023 Posit Software, PBC. All rights reserved. + * Copyright (C) 2023-2025 Posit Software, PBC. All rights reserved. * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. *--------------------------------------------------------------------------------------------*/ @@ -7,8 +7,8 @@ display: flex; align-items: center; justify-content: center; - height: 75px; - width: 75px; + height: 64px; + width: 64px; background-color: var(--vscode-input-background); cursor: pointer; } From 18695c061c03abeb59b159a885531eb7065e150a Mon Sep 17 00:00:00 2001 From: Jonathan McPherson Date: Fri, 19 Dec 2025 16:10:56 -0800 Subject: [PATCH 18/32] make the close button work again --- .../components/plotGalleryThumbnail.tsx | 8 ++++---- .../positronPlots/browser/positronPlots.css | 19 ++++++++++++++++--- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/positronPlots/browser/components/plotGalleryThumbnail.tsx b/src/vs/workbench/contrib/positronPlots/browser/components/plotGalleryThumbnail.tsx index 1d69a9c0b83e..de87175c0c37 100644 --- a/src/vs/workbench/contrib/positronPlots/browser/components/plotGalleryThumbnail.tsx +++ b/src/vs/workbench/contrib/positronPlots/browser/components/plotGalleryThumbnail.tsx @@ -75,12 +75,12 @@ export const PlotGalleryThumbnail = (props: PropsWithChildren +
- +
); }; diff --git a/src/vs/workbench/contrib/positronPlots/browser/positronPlots.css b/src/vs/workbench/contrib/positronPlots/browser/positronPlots.css index b4c9a644b244..3c3cdc53afbd 100644 --- a/src/vs/workbench/contrib/positronPlots/browser/positronPlots.css +++ b/src/vs/workbench/contrib/positronPlots/browser/positronPlots.css @@ -149,7 +149,7 @@ } .plot-thumbnail:hover .plot-close { - opacity: 1; + opacity: 0.75; transition: opacity 0.2s ease-in-out; } @@ -199,11 +199,24 @@ .plot-close { position: absolute; - top: 0; - right: 0; + top: 3px; + right: 3px; padding: 2px; cursor: pointer; opacity: 0; + transition: opacity 0.2s ease-in-out; + border: 1px solid; + border-radius: 3px; +} + +.plot-history .plot-thumbnail .plot-close { + color: var(--vscode-button-foreground); + background-color: var(--vscode-button-background); + border-color: var(--vscode-button-border); +} + +.plot-history .plot-thumbnail .plot-close:hover { + opacity: 1; } .monaco-pane-view .pane .plots-container .monaco-progress-container { From ab4575edea652d6434a2b5e0818db72b1a291de5 Mon Sep 17 00:00:00 2001 From: Jonathan McPherson Date: Fri, 19 Dec 2025 16:28:37 -0800 Subject: [PATCH 19/32] lowercase python libs to match r --- .../python_files/posit/positron/matplotlib_backend.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/extensions/positron-python/python_files/posit/positron/matplotlib_backend.py b/extensions/positron-python/python_files/posit/positron/matplotlib_backend.py index 7faec4b2a80d..9031ceee6fbc 100644 --- a/extensions/positron-python/python_files/posit/positron/matplotlib_backend.py +++ b/extensions/positron-python/python_files/posit/positron/matplotlib_backend.py @@ -51,15 +51,15 @@ def _detect_plotting_library() -> str: # Note: We don't include pandas here because pandas.plotting is auto-loaded # when pandas is imported, even if not used for plotting. library_priority = [ - ("seaborn", "Seaborn"), - ("plotnine", "plotnine"), + "seaborn" + "plotnine" ] - for module_name, display_name in library_priority: + for module_name in library_priority: if module_name in sys.modules: - return display_name + return module_name - return "Matplotlib" + return "matplotlib" class FigureManagerPositron(FigureManagerBase): From 192816485ce4f8c20884135f435687444f7eb864 Mon Sep 17 00:00:00 2001 From: Jonathan McPherson Date: Fri, 19 Dec 2025 17:02:50 -0800 Subject: [PATCH 20/32] improve keyboard accessibility --- .../browser/components/plotGalleryThumbnail.tsx | 8 ++++++-- .../positronPlots/browser/components/plotsContainer.tsx | 2 +- .../contrib/positronPlots/browser/htmlPlotClient.ts | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/positronPlots/browser/components/plotGalleryThumbnail.tsx b/src/vs/workbench/contrib/positronPlots/browser/components/plotGalleryThumbnail.tsx index de87175c0c37..d29dd88e6502 100644 --- a/src/vs/workbench/contrib/positronPlots/browser/components/plotGalleryThumbnail.tsx +++ b/src/vs/workbench/contrib/positronPlots/browser/components/plotGalleryThumbnail.tsx @@ -44,12 +44,15 @@ export const PlotGalleryThumbnail = (props: PropsWithChildren { - if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') { + if (e.key === 'ArrowLeft' || e.key === 'ArrowUp' || e.key == 'k' || e.key == 'h') { e.preventDefault(); props.focusPreviousPlotThumbnail(props.plotClient.id); - } else if (e.key === 'ArrowRight' || e.key === 'ArrowDown') { + } else if (e.key === 'ArrowRight' || e.key === 'ArrowDown' || e.key == 'j' || e.key == 'l') { e.preventDefault(); props.focusNextPlotThumbnail(props.plotClient.id); + } else if (e.key === 'Delete' || e.key === 'Backspace' || e.key === 'x') { + e.preventDefault(); + removePlot(); } else if (e.key === 'Enter' || e.key === ' ') { // if the focus is on the remove button, call the removePlot function if (e.target === plotRemoveButtonRef.current) { @@ -78,6 +81,7 @@ export const PlotGalleryThumbnail = (props: PropsWithChildren - )} + + {sessionName && {sessionName}} + {plotName && {plotName}} +
; }; From 64740efe7caf6c66042fb20bcf92645a9fc80d46 Mon Sep 17 00:00:00 2001 From: Jonathan McPherson Date: Fri, 19 Dec 2025 17:39:07 -0800 Subject: [PATCH 22/32] better layout for 2nd level toolbar --- .../positronPlots/browser/components/plotsContainer.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/positronPlots/browser/components/plotsContainer.css b/src/vs/workbench/contrib/positronPlots/browser/components/plotsContainer.css index 70921764ac95..cc5b452b6731 100644 --- a/src/vs/workbench/contrib/positronPlots/browser/components/plotsContainer.css +++ b/src/vs/workbench/contrib/positronPlots/browser/components/plotsContainer.css @@ -30,10 +30,10 @@ .plots-container .plot-info-header .plot-info-text { display: flex; align-items: center; - gap: 4px; + justify-content: space-between; + width: 100%; white-space: nowrap; overflow: hidden; - text-overflow: ellipsis; } .plots-container .plot-info-header .plot-session-name { From 4892c5974dd34eb438e3064ef02fa420e19363f7 Mon Sep 17 00:00:00 2001 From: Jonathan McPherson Date: Mon, 22 Dec 2025 09:27:57 -0800 Subject: [PATCH 23/32] refine spacing --- .../positronPlots/browser/components/plotsContainer.css | 3 +-- .../positronPlots/browser/components/plotsContainer.tsx | 2 +- .../contrib/positronPlots/browser/positronPlots.css | 9 ++++----- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/contrib/positronPlots/browser/components/plotsContainer.css b/src/vs/workbench/contrib/positronPlots/browser/components/plotsContainer.css index cc5b452b6731..daffafae86c5 100644 --- a/src/vs/workbench/contrib/positronPlots/browser/components/plotsContainer.css +++ b/src/vs/workbench/contrib/positronPlots/browser/components/plotsContainer.css @@ -21,7 +21,6 @@ display: flex; align-items: center; padding: 0 4px; - min-height: 24px; flex-shrink: 0; overflow: hidden; background-color: var(--vscode-editor-background); @@ -43,7 +42,6 @@ padding: 0px 5px; border-radius: 5px; border: 1px solid var(--vscode-panel-border); - margin-top: 3px; margin-right: 4px; } @@ -51,6 +49,7 @@ font-size: 12px; color: var(--vscode-foreground); opacity: 0.8; + margin-right: 5px; } .plots-container .plot-info-header .plot-code-button { diff --git a/src/vs/workbench/contrib/positronPlots/browser/components/plotsContainer.tsx b/src/vs/workbench/contrib/positronPlots/browser/components/plotsContainer.tsx index 0d8046750e3c..0a73f045e256 100644 --- a/src/vs/workbench/contrib/positronPlots/browser/components/plotsContainer.tsx +++ b/src/vs/workbench/contrib/positronPlots/browser/components/plotsContainer.tsx @@ -50,7 +50,7 @@ export const HistoryPx = 100; /** * The number of pixels (height) to use for the plot info header row. */ -export const PlotInfoHeaderPx = 24; +export const PlotInfoHeaderPx = 30; /** * PlotContainer component; holds the plot instances. diff --git a/src/vs/workbench/contrib/positronPlots/browser/positronPlots.css b/src/vs/workbench/contrib/positronPlots/browser/positronPlots.css index 3c3cdc53afbd..148c9ca9d25b 100644 --- a/src/vs/workbench/contrib/positronPlots/browser/positronPlots.css +++ b/src/vs/workbench/contrib/positronPlots/browser/positronPlots.css @@ -46,7 +46,7 @@ .history-right .plot-history-scroller { border-left: 1px solid var(--vscode-statusBar-border); - width: 100px; + width: 110px; } .history-right .plot-history { @@ -72,8 +72,8 @@ .history-bottom .plot-history-scroller { border-top: 1px solid var(--vscode-statusBar-border); - min-height: 100px; - height: 100px; + height: 110px; + min-width: 0; } .history-bottom .plot-history { @@ -81,6 +81,7 @@ width: fit-content; margin-right: 10px; margin-left: 10px; + flex-shrink: 0; } .history-bottom .plot-thumbnail { @@ -95,8 +96,6 @@ } .plot-thumbnail { - height: 90px; - width: 90px; padding: 0; opacity: 0.75; transition: opacity 0.2s ease-in-out; From 6c2ca6570ce3c67f449bf9f8aae6f224f5ac340c Mon Sep 17 00:00:00 2001 From: Jonathan McPherson Date: Mon, 22 Dec 2025 09:33:33 -0800 Subject: [PATCH 24/32] a11y for close button --- .../positronPlots/browser/components/plotGalleryThumbnail.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/vs/workbench/contrib/positronPlots/browser/components/plotGalleryThumbnail.tsx b/src/vs/workbench/contrib/positronPlots/browser/components/plotGalleryThumbnail.tsx index d29dd88e6502..b1e32e9c1b20 100644 --- a/src/vs/workbench/contrib/positronPlots/browser/components/plotGalleryThumbnail.tsx +++ b/src/vs/workbench/contrib/positronPlots/browser/components/plotGalleryThumbnail.tsx @@ -12,6 +12,7 @@ import React, { PropsWithChildren, useMemo, useRef } from 'react'; // Other dependencies. import { IPositronPlotClient } from '../../../../services/positronPlots/common/positronPlots.js'; import { usePositronReactServicesContext } from '../../../../../base/browser/positronReactRendererContext.js'; +import { localize } from '../../../../../nls.js'; /** * PlotGalleryThumbnailProps interface. @@ -23,6 +24,8 @@ interface PlotGalleryThumbnailProps { focusNextPlotThumbnail: (currentPlotId: string) => void; } +const removePlotTitle = localize('positronRemovePlot', "Remove plot"); + /** * PlotGalleryThumbnail component. This component renders a thumbnail of a plot * instance as a child component, and is used as a wrapper for all plot thumbnails. @@ -99,6 +102,7 @@ export const PlotGalleryThumbnail = (props: PropsWithChildren From eccafe4df5bfc1265b3a4f99252889e70cb72e4c Mon Sep 17 00:00:00 2001 From: Jonathan McPherson Date: Mon, 22 Dec 2025 10:06:12 -0800 Subject: [PATCH 25/32] make the session name navigable --- .../browser/components/plotsContainer.css | 4 ++- .../browser/components/plotsContainer.tsx | 27 ++++++++++++++++--- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/positronPlots/browser/components/plotsContainer.css b/src/vs/workbench/contrib/positronPlots/browser/components/plotsContainer.css index daffafae86c5..7b2bde831ed9 100644 --- a/src/vs/workbench/contrib/positronPlots/browser/components/plotsContainer.css +++ b/src/vs/workbench/contrib/positronPlots/browser/components/plotsContainer.css @@ -39,10 +39,12 @@ font-size: 11px; color: var(--vscode-foreground); opacity: 0.8; - padding: 0px 5px; + padding: 3px 5px; border-radius: 5px; border: 1px solid var(--vscode-panel-border); margin-right: 4px; + background: none; + cursor: pointer; } .plots-container .plot-info-header .plot-name { diff --git a/src/vs/workbench/contrib/positronPlots/browser/components/plotsContainer.tsx b/src/vs/workbench/contrib/positronPlots/browser/components/plotsContainer.tsx index 0a73f045e256..588ecb58f8dc 100644 --- a/src/vs/workbench/contrib/positronPlots/browser/components/plotsContainer.tsx +++ b/src/vs/workbench/contrib/positronPlots/browser/components/plotsContainer.tsx @@ -45,7 +45,7 @@ interface PlotContainerProps { * The number of pixels (height or width) to use for the history portion of the * plots container. */ -export const HistoryPx = 100; +export const HistoryPx = 110; /** * The number of pixels (height) to use for the plot info header row. @@ -74,7 +74,7 @@ export const PlotsContainer = (props: PlotContainerProps) => { const historyEdge = historyBottom ? 'history-bottom' : 'history-right'; // Account for the plot info header when calculating plot dimensions const plotHeight = historyBottom && props.height > 0 ? props.height - historyPx - PlotInfoHeaderPx : props.height - PlotInfoHeaderPx; - const plotWidth = historyBottom || props.width <= 0 ? props.width : props.width - historyPx; + const plotWidth = historyBottom || props.width <= 0 ? props.width : props.width - (historyPx + 1); // Get the current plot instance const currentPlotInstance = useMemo(() => @@ -361,6 +361,27 @@ export const PlotsContainer = (props: PlotContainerProps) => { focusPlotThumbnail(nextPlotInstance.id); } + /** + * Navigates to the code that generated the current plot. + */ + const navigateToCode = () => { + if (!currentPlotInstance) { + return; + } + // If we have an execution ID, reveal the execution in the Positron + // Console; otherwise, just activate the session. + if (currentPlotInstance.metadata.execution_id) { + services.positronConsoleService.revealExecution( + currentPlotInstance.metadata.session_id, + currentPlotInstance.metadata.execution_id + ); + } else { + services.positronConsoleService.setActivePositronConsoleSession( + currentPlotInstance.metadata.session_id + ); + } + }; + /** * Renders a thumbnail of either a DynamicPlotInstance (resizable plot), a * StaticPlotInstance (static plot image), or a WebviewPlotInstance @@ -420,7 +441,7 @@ export const PlotsContainer = (props: PlotContainerProps) => { return
- {sessionName && {sessionName}} + {sessionName && } {plotName && {plotName}}
; From 64842dd5aa989ceb217d95146c5f8c2cb71e6fb9 Mon Sep 17 00:00:00 2001 From: Jonathan McPherson Date: Mon, 22 Dec 2025 10:46:37 -0800 Subject: [PATCH 26/32] use plot name as more descriptive alt text --- .../positronPlots/browser/components/dynamicPlotInstance.tsx | 5 ++--- .../browser/components/dynamicPlotThumbnail.tsx | 2 +- .../positronPlots/browser/components/plotsContainer.tsx | 2 +- .../positronPlots/browser/components/staticPlotInstance.tsx | 3 ++- .../positronPlots/browser/components/staticPlotThumbnail.tsx | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/positronPlots/browser/components/dynamicPlotInstance.tsx b/src/vs/workbench/contrib/positronPlots/browser/components/dynamicPlotInstance.tsx index 5c2679967a61..74e8d5dc863e 100644 --- a/src/vs/workbench/contrib/positronPlots/browser/components/dynamicPlotInstance.tsx +++ b/src/vs/workbench/contrib/positronPlots/browser/components/dynamicPlotInstance.tsx @@ -46,6 +46,7 @@ export const DynamicPlotInstance = (props: DynamicPlotInstanceProps) => { const [uri, setUri] = useState(''); const [error, setError] = useState(''); const progressRef = React.useRef(null); + const plotName = props.plotClient.metadata.name ? props.plotClient.metadata.name : 'Plot ' + props.plotClient.id; useEffect(() => { const ratio = DOM.getActiveWindow().devicePixelRatio; @@ -176,9 +177,7 @@ export const DynamicPlotInstance = (props: DynamicPlotInstanceProps) => { // Render method for the plot image. const renderedImage = () => { return { // Consider: we probably want a more explicit loading state; as written we // will show the old URI until the new one is ready. if (uri) { - return {'Plot; + return {props.plotClient.metadata.name; } else { return ; } diff --git a/src/vs/workbench/contrib/positronPlots/browser/components/plotsContainer.tsx b/src/vs/workbench/contrib/positronPlots/browser/components/plotsContainer.tsx index 588ecb58f8dc..b793069e8cd5 100644 --- a/src/vs/workbench/contrib/positronPlots/browser/components/plotsContainer.tsx +++ b/src/vs/workbench/contrib/positronPlots/browser/components/plotsContainer.tsx @@ -149,7 +149,7 @@ export const PlotsContainer = (props: PlotContainerProps) => { const selectedPlot = plotHistory.querySelector('.selected'); if (selectedPlot) { // If there is a selected plot, scroll it into view. - selectedPlot.scrollIntoView({ behavior: 'smooth' }); + selectedPlot.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' }); } else { // If there isn't a selected plot, scroll the history to the end to // show the most recently generated plot. diff --git a/src/vs/workbench/contrib/positronPlots/browser/components/staticPlotInstance.tsx b/src/vs/workbench/contrib/positronPlots/browser/components/staticPlotInstance.tsx index d0d39dc0c36a..800153aacf7c 100644 --- a/src/vs/workbench/contrib/positronPlots/browser/components/staticPlotInstance.tsx +++ b/src/vs/workbench/contrib/positronPlots/browser/components/staticPlotInstance.tsx @@ -34,6 +34,7 @@ export const StaticPlotInstance = (props: StaticPlotInstanceProps) => { const [width, setWidth] = useState(1); const [height, setHeight] = useState(1); const resizeObserver = useRef(null!); + const plotName = props.plotClient.metadata.name ? props.plotClient.metadata.name : 'Plot ' + props.plotClient.id; useEffect(() => { resizeObserver.current = new ResizeObserver((entries: ResizeObserverEntry[]) => { @@ -55,7 +56,7 @@ export const StaticPlotInstance = (props: StaticPlotInstanceProps) => { return (
{ - return {'Plot; + return {props.plotClient.metadata.name; }; From d7726cd183c2b00e4174987de1fe94416a4d099e Mon Sep 17 00:00:00 2001 From: Jonathan McPherson Date: Mon, 22 Dec 2025 12:53:42 -0800 Subject: [PATCH 27/32] make code commands more responsive --- .../browser/components/plotCodeMenuButton.tsx | 41 ++++++++++++------- .../interfaces/positronConsoleService.ts | 1 + .../browser/positronConsoleService.ts | 6 ++- 3 files changed, 31 insertions(+), 17 deletions(-) diff --git a/src/vs/workbench/contrib/positronPlots/browser/components/plotCodeMenuButton.tsx b/src/vs/workbench/contrib/positronPlots/browser/components/plotCodeMenuButton.tsx index 6c1410aee8e8..74d6a09ab3d0 100644 --- a/src/vs/workbench/contrib/positronPlots/browser/components/plotCodeMenuButton.tsx +++ b/src/vs/workbench/contrib/positronPlots/browser/components/plotCodeMenuButton.tsx @@ -53,9 +53,11 @@ export const PlotCodeMenuButton = (props: PlotCodeMenuButtonProps) => { class: 'codicon codicon-copy', enabled: !!plotCode, run: () => { - if (plotCode) { - services.clipboardService.writeText(plotCode); - } + services.clipboardService.writeText(plotCode); + const trimmedCode = plotCode.substring(0, 20) + (plotCode.length > 20 ? '...' : ''); + services.notificationService.info( + localize('positronPlots.copyCodeInfo', "Plot code copied to clipboard: {0}", trimmedCode) + ); } }, { @@ -65,8 +67,19 @@ export const PlotCodeMenuButton = (props: PlotCodeMenuButtonProps) => { class: 'codicon codicon-go-to-file', enabled: !!executionId && !!sessionId, run: () => { - if (executionId && sessionId) { - services.positronConsoleService.revealExecution(sessionId, executionId); + try { + services.positronConsoleService.revealExecution(sessionId!, executionId!); + } catch (error) { + // It's very possible that the code that generated this + // plot has been removed from the console (e.g. if the + // console was cleared). In that case, just log a + // warning and show a notification. + if (error instanceof Error) { + services.logService.warn(error.message); + } + services.notificationService.warn( + localize('positronPlots.revealInConsoleError', "The code that generated this plot is no longer present in the console.") + ); } } }, @@ -75,17 +88,15 @@ export const PlotCodeMenuButton = (props: PlotCodeMenuButtonProps) => { label: runCodeAgain, tooltip: '', class: 'codicon codicon-run', - enabled: !!plotCode && !!sessionId, + enabled: !!plotCode && !!sessionId && !!languageId, run: async () => { - if (plotCode && sessionId && languageId) { - await services.positronConsoleService.executeCode( - languageId, - sessionId, - plotCode, - { source: CodeAttributionSource.Interactive }, - true - ); - } + await services.positronConsoleService.executeCode( + languageId!, + sessionId, + plotCode, + { source: CodeAttributionSource.Interactive }, + true + ); } } ]; diff --git a/src/vs/workbench/services/positronConsole/browser/interfaces/positronConsoleService.ts b/src/vs/workbench/services/positronConsole/browser/interfaces/positronConsoleService.ts index 39bad511538f..9c8a0f50959b 100644 --- a/src/vs/workbench/services/positronConsole/browser/interfaces/positronConsoleService.ts +++ b/src/vs/workbench/services/positronConsole/browser/interfaces/positronConsoleService.ts @@ -161,6 +161,7 @@ export interface IPositronConsoleService { * * @param sessionId The session ID of the console instance. * @param executionId The execution ID of the input to reveal. + * @throws an error if the console instance or execution cannot be found. */ revealExecution(sessionId: string, executionId: string): void; } diff --git a/src/vs/workbench/services/positronConsole/browser/positronConsoleService.ts b/src/vs/workbench/services/positronConsole/browser/positronConsoleService.ts index 230f7a1fd9d9..4b741d8032aa 100644 --- a/src/vs/workbench/services/positronConsole/browser/positronConsoleService.ts +++ b/src/vs/workbench/services/positronConsole/browser/positronConsoleService.ts @@ -961,7 +961,7 @@ export class PositronConsoleService extends Disposable implements IPositronConso // Find the console instance with the given session ID. const consoleInstance = this._positronConsoleInstancesBySessionId.get(sessionId); if (!consoleInstance) { - return; + throw new Error(`Cannot reveal execution: no Positron console instance found for session ID ${sessionId}.`); } // Open the console view to ensure it's visible. @@ -971,7 +971,9 @@ export class PositronConsoleService extends Disposable implements IPositronConso this.setActivePositronConsoleInstance(consoleInstance); // Ask the console instance to reveal the execution. - consoleInstance.revealExecution(executionId); + if (!consoleInstance.revealExecution(executionId)) { + throw new Error(`Cannot reveal execution: execution ID ${executionId} not found in session ID ${sessionId}.`); + } } /** From ca4eb9cdb56fc093d515ce876b9cdb5a62200a19 Mon Sep 17 00:00:00 2001 From: Jonathan McPherson Date: Mon, 22 Dec 2025 13:22:02 -0800 Subject: [PATCH 28/32] update after metadata changes --- .../browser/components/plotCodeMenuButton.tsx | 24 ++++++++++++++----- .../browser/positronPlotsState.tsx | 12 +++++++++- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/positronPlots/browser/components/plotCodeMenuButton.tsx b/src/vs/workbench/contrib/positronPlots/browser/components/plotCodeMenuButton.tsx index 74d6a09ab3d0..18fff433acef 100644 --- a/src/vs/workbench/contrib/positronPlots/browser/components/plotCodeMenuButton.tsx +++ b/src/vs/workbench/contrib/positronPlots/browser/components/plotCodeMenuButton.tsx @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ // React. -import React from 'react'; +import React, { useEffect, useState } from 'react'; // Other dependencies. import { localize } from '../../../../../nls.js'; @@ -14,6 +14,7 @@ import { ActionBarMenuButton } from '../../../../../platform/positronActionBar/b import { usePositronReactServicesContext } from '../../../../../base/browser/positronReactRendererContext.js'; import { IPositronPlotClient } from '../../../../services/positronPlots/common/positronPlots.js'; import { CodeAttributionSource } from '../../../../services/positronConsole/common/positronConsoleCodeExecution.js'; +import { IPositronPlotMetadata } from '../../../../services/languageRuntime/common/languageRuntimePlotClient.js'; // Localized strings. const plotCodeActionsTooltip = localize('positronPlotCodeActions', "Plot code actions"); @@ -37,11 +38,22 @@ export const PlotCodeMenuButton = (props: PlotCodeMenuButtonProps) => { // Context hooks. const services = usePositronReactServicesContext(); - // Get metadata from the plot client. - const plotCode = props.plotClient.metadata.code; - const executionId = props.plotClient.metadata.execution_id; - const sessionId = props.plotClient.metadata.session_id; - const languageId = props.plotClient.metadata.language; + // State to track metadata changes. + const [metadata, setMetadata] = useState(props.plotClient.metadata); + + // Subscribe to metadata updates. + useEffect(() => { + const disposable = props.plotClient.onDidUpdateMetadata?.(newMetadata => { + setMetadata(newMetadata); + }); + return () => disposable?.dispose(); + }, [props.plotClient]); + + // Get metadata from state. + const plotCode = metadata.code; + const executionId = metadata.execution_id; + const sessionId = metadata.session_id; + const languageId = metadata.language; // Builds the actions. const actions = (): IAction[] => { diff --git a/src/vs/workbench/contrib/positronPlots/browser/positronPlotsState.tsx b/src/vs/workbench/contrib/positronPlots/browser/positronPlotsState.tsx index 84208526f2ee..178e6374eb33 100644 --- a/src/vs/workbench/contrib/positronPlots/browser/positronPlotsState.tsx +++ b/src/vs/workbench/contrib/positronPlots/browser/positronPlotsState.tsx @@ -16,6 +16,8 @@ export interface PositronPlotsState { readonly positronPlotInstances: IPositronPlotClient[]; selectedInstanceId: string; selectedInstanceIndex: number; + /** Counter that increments when any plot's metadata changes, used to trigger re-renders. */ + metadataVersion: number; } /** @@ -39,6 +41,9 @@ export const usePositronPlotsState = (): PositronPlotsState => { (p => p.id === initialSelectedId); const [selectedInstanceIndex, setSelectedInstanceIndex] = useState(initialSelectedIndex); + // Counter to trigger re-renders when metadata changes. + const [metadataVersion, setMetadataVersion] = useState(0); + // Add event handlers. useEffect(() => { const disposableStore = new DisposableStore(); @@ -87,9 +92,14 @@ export const usePositronPlotsState = (): PositronPlotsState => { setPositronPlotInstances(plots); })); + // Listen for metadata updates. + disposableStore.add(services.positronPlotsService.onDidUpdatePlotMetadata(() => { + setMetadataVersion(v => v + 1); + })); + // Return the clean up for our event handlers. return () => disposableStore.dispose(); }, [services.positronPlotsService]); - return { positronPlotInstances, selectedInstanceId, selectedInstanceIndex }; + return { positronPlotInstances, selectedInstanceId, selectedInstanceIndex, metadataVersion }; }; From 905fd11b682d10d90e67148a2767279f750b88c1 Mon Sep 17 00:00:00 2001 From: Jonathan McPherson Date: Mon, 22 Dec 2025 15:23:14 -0800 Subject: [PATCH 29/32] update python types --- .../posit/positron/matplotlib_backend.py | 15 ++++++--------- .../posit/positron/tests/test_plots.py | 6 +++--- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/extensions/positron-python/python_files/posit/positron/matplotlib_backend.py b/extensions/positron-python/python_files/posit/positron/matplotlib_backend.py index 9031ceee6fbc..c4d7fa1587e8 100644 --- a/extensions/positron-python/python_files/posit/positron/matplotlib_backend.py +++ b/extensions/positron-python/python_files/posit/positron/matplotlib_backend.py @@ -20,7 +20,7 @@ import io import logging import sys -from typing import TYPE_CHECKING, cast +from typing import TYPE_CHECKING, Any, cast import matplotlib from matplotlib.backend_bases import FigureManagerBase @@ -50,10 +50,7 @@ def _detect_plotting_library() -> str: # Order matters - check more specific libraries first # Note: We don't include pandas here because pandas.plotting is auto-loaded # when pandas is imported, even if not used for plotting. - library_priority = [ - "seaborn" - "plotnine" - ] + library_priority = ["seaborn", "plotnine"] for module_name in library_priority: if module_name in sys.modules: @@ -90,10 +87,10 @@ def __init__(self, canvas: FigureCanvasPositron, num: int | str): # Get the execution context from the current shell message parent = kernel.get_parent("shell") - header = parent.get("header", {}) - content = parent.get("content", {}) - execution_id = header.get("msg_id", "") - code = content.get("code", "") + header: dict[str, Any] = parent.get("header", {}) + content: dict[str, Any] = parent.get("content", {}) + execution_id: str = header.get("msg_id", "") + code: str = content.get("code", "") # Detect which plotting library was used kind = _detect_plotting_library() diff --git a/extensions/positron-python/python_files/posit/positron/tests/test_plots.py b/extensions/positron-python/python_files/posit/positron/tests/test_plots.py index 23ca6ca295c7..0fab1af9020a 100644 --- a/extensions/positron-python/python_files/posit/positron/tests/test_plots.py +++ b/extensions/positron-python/python_files/posit/positron/tests/test_plots.py @@ -132,7 +132,7 @@ def test_mpl_get_intrinsic_size(shell: PositronShell, plots_service: PlotsServic "width": intrinsic_size[0], "height": intrinsic_size[1], "unit": PlotUnit.Inches.value, - "source": "Matplotlib", + "source": "matplotlib", } ) ] @@ -152,8 +152,8 @@ def test_mpl_get_metadata(shell: PositronShell, plots_service: PlotsService) -> result = response["data"]["result"] # Verify the metadata structure - assert result["kind"] == "Matplotlib" - assert result["name"] == "Matplotlib Figure 1" + assert result["kind"] == "matplotlib" + assert result["name"] == "matplotlib Figure 1" # execution_id and code may be empty in test context since there's no real execute_request assert "execution_id" in result assert "code" in result From e7e0ac858702b1c87d3088edd30935ac9a44467e Mon Sep 17 00:00:00 2001 From: Jonathan McPherson Date: Mon, 22 Dec 2025 15:35:43 -0800 Subject: [PATCH 30/32] a few more for python --- .../python_files/posit/positron/matplotlib_backend.py | 4 ++-- .../python_files/posit/positron/tests/test_plots.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/extensions/positron-python/python_files/posit/positron/matplotlib_backend.py b/extensions/positron-python/python_files/posit/positron/matplotlib_backend.py index c4d7fa1587e8..2567b24bf511 100644 --- a/extensions/positron-python/python_files/posit/positron/matplotlib_backend.py +++ b/extensions/positron-python/python_files/posit/positron/matplotlib_backend.py @@ -87,8 +87,8 @@ def __init__(self, canvas: FigureCanvasPositron, num: int | str): # Get the execution context from the current shell message parent = kernel.get_parent("shell") - header: dict[str, Any] = parent.get("header", {}) - content: dict[str, Any] = parent.get("content", {}) + header: dict[str, Any] = cast(dict[str, Any], parent.get("header", {})) + content: dict[str, Any] = cast(dict[str, Any], parent.get("content", {})) execution_id: str = header.get("msg_id", "") code: str = content.get("code", "") diff --git a/extensions/positron-python/python_files/posit/positron/tests/test_plots.py b/extensions/positron-python/python_files/posit/positron/tests/test_plots.py index 0fab1af9020a..e662fab18158 100644 --- a/extensions/positron-python/python_files/posit/positron/tests/test_plots.py +++ b/extensions/positron-python/python_files/posit/positron/tests/test_plots.py @@ -153,7 +153,7 @@ def test_mpl_get_metadata(shell: PositronShell, plots_service: PlotsService) -> # Verify the metadata structure assert result["kind"] == "matplotlib" - assert result["name"] == "matplotlib Figure 1" + assert result["name"] == "matplotlib 1" # execution_id and code may be empty in test context since there's no real execute_request assert "execution_id" in result assert "code" in result From 9b393250c7da465ea45b671aabb335993943cac7 Mon Sep 17 00:00:00 2001 From: Jonathan McPherson Date: Mon, 22 Dec 2025 15:41:34 -0800 Subject: [PATCH 31/32] make ruff happy --- .../python_files/posit/positron/matplotlib_backend.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/positron-python/python_files/posit/positron/matplotlib_backend.py b/extensions/positron-python/python_files/posit/positron/matplotlib_backend.py index 2567b24bf511..ad03f88bea5c 100644 --- a/extensions/positron-python/python_files/posit/positron/matplotlib_backend.py +++ b/extensions/positron-python/python_files/posit/positron/matplotlib_backend.py @@ -87,8 +87,8 @@ def __init__(self, canvas: FigureCanvasPositron, num: int | str): # Get the execution context from the current shell message parent = kernel.get_parent("shell") - header: dict[str, Any] = cast(dict[str, Any], parent.get("header", {})) - content: dict[str, Any] = cast(dict[str, Any], parent.get("content", {})) + header: dict[str, Any] = cast("dict[str, Any]", parent.get("header", {})) + content: dict[str, Any] = cast("dict[str, Any]", parent.get("content", {})) execution_id: str = header.get("msg_id", "") code: str = content.get("code", "") From 6cbe66c9e21bc553ba686e8f1d65ab6b483f7dcb Mon Sep 17 00:00:00 2001 From: Jonathan McPherson Date: Mon, 22 Dec 2025 16:49:23 -0800 Subject: [PATCH 32/32] update tests to expect new execution_id --- .../test/browser/positronIPyWidgetsService.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/workbench/contrib/positronIPyWidgets/test/browser/positronIPyWidgetsService.test.ts b/src/vs/workbench/contrib/positronIPyWidgets/test/browser/positronIPyWidgetsService.test.ts index e90b1e10bd86..49d3360d58d3 100644 --- a/src/vs/workbench/contrib/positronIPyWidgets/test/browser/positronIPyWidgetsService.test.ts +++ b/src/vs/workbench/contrib/positronIPyWidgets/test/browser/positronIPyWidgetsService.test.ts @@ -101,6 +101,7 @@ suite('Positron - PositronIPyWidgetsService', () => { assert.deepStrictEqual(plotClient.metadata, { id: message.id, created: Date.parse(message.when), + execution_id: '', session_id: session.sessionId, code: '', output_id: message.output_id,