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/positronIPyWidgets/test/browser/positronIPyWidgetsService.test.ts b/src/vs/workbench/contrib/positronIPyWidgets/test/browser/positronIPyWidgetsService.test.ts
index fb14cf1b1be8..49d3360d58d3 100644
--- a/src/vs/workbench/contrib/positronIPyWidgets/test/browser/positronIPyWidgetsService.test.ts
+++ b/src/vs/workbench/contrib/positronIPyWidgets/test/browser/positronIPyWidgetsService.test.ts
@@ -100,8 +100,8 @@ 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),
+ execution_id: '',
session_id: session.sessionId,
code: '',
output_id: message.output_id,
diff --git a/src/vs/workbench/contrib/positronPlots/browser/components/actionBars.tsx b/src/vs/workbench/contrib/positronPlots/browser/components/actionBars.tsx
index 86363f78e6d1..6911cfaa4e47 100644
--- a/src/vs/workbench/contrib/positronPlots/browser/components/actionBars.tsx
+++ b/src/vs/workbench/contrib/positronPlots/browser/components/actionBars.tsx
@@ -28,6 +28,7 @@ import { OpenInEditorMenuButton } from './openInEditorMenuButton.js';
import { DarkFilterMenuButton } from './darkFilterMenuButton.js';
import { ThemeIcon } from '../../../../../base/common/themables.js';
import { usePositronReactServicesContext } from '../../../../../base/browser/positronReactRendererContext.js';
+import { PlotCodeMenuButton } from './plotCodeMenuButton.js';
// Constants.
const kPaddingLeft = 14;
@@ -93,6 +94,9 @@ export const ActionBars = (props: PropsWithChildren) => {
&& (selectedPlot instanceof PlotClientInstance
|| selectedPlot instanceof StaticPlotClient);
+ // Enable code actions when the plot has code metadata
+ const enableCodeActions = hasPlots && !!selectedPlot?.metadata.code;
+
// Only show the "Open in editor" button when in the main window
const showOpenInEditorButton = enableEditorPlot
&& props.displayLocation === PlotsDisplayLocation.MainWindow;
@@ -209,6 +213,9 @@ export const ActionBars = (props: PropsWithChildren) => {
tooltip={openInEditorTab}
/>
}
+ {enableCodeActions &&
+
+ }
{enableDarkFilter &&
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 ;
+ return ;
} else {
return ;
}
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;
}
diff --git a/src/vs/workbench/contrib/positronPlots/browser/components/plotCodeMenuButton.tsx b/src/vs/workbench/contrib/positronPlots/browser/components/plotCodeMenuButton.tsx
new file mode 100644
index 000000000000..18fff433acef
--- /dev/null
+++ b/src/vs/workbench/contrib/positronPlots/browser/components/plotCodeMenuButton.tsx
@@ -0,0 +1,124 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (C) 2025 Posit Software, PBC. All rights reserved.
+ * Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+// React.
+import React, { useEffect, useState } from 'react';
+
+// Other dependencies.
+import { localize } from '../../../../../nls.js';
+import { IAction } from '../../../../../base/common/actions.js';
+import { ThemeIcon } from '../../../../../base/common/themables.js';
+import { ActionBarMenuButton } from '../../../../../platform/positronActionBar/browser/components/actionBarMenuButton.js';
+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");
+const copyCode = localize('positronPlots.copyCode', "Copy Code");
+const revealInConsole = localize('positronPlots.revealInConsole', "Reveal Code in Console");
+const runCodeAgain = localize('positronPlots.runCodeAgain', "Run Code Again");
+
+/**
+ * PlotCodeMenuButtonProps interface.
+ */
+interface PlotCodeMenuButtonProps {
+ readonly plotClient: IPositronPlotClient;
+}
+
+/**
+ * PlotCodeMenuButton component.
+ * @param props A PlotCodeMenuButtonProps that contains the component properties.
+ * @returns The rendered component.
+ */
+export const PlotCodeMenuButton = (props: PlotCodeMenuButtonProps) => {
+ // Context hooks.
+ const services = usePositronReactServicesContext();
+
+ // 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[] => {
+ return [
+ {
+ id: 'copyCode',
+ label: copyCode,
+ tooltip: '',
+ class: 'codicon codicon-copy',
+ enabled: !!plotCode,
+ run: () => {
+ 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)
+ );
+ }
+ },
+ {
+ id: 'revealInConsole',
+ label: revealInConsole,
+ tooltip: '',
+ class: 'codicon codicon-go-to-file',
+ enabled: !!executionId && !!sessionId,
+ run: () => {
+ 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.")
+ );
+ }
+ }
+ },
+ {
+ id: 'runCodeAgain',
+ label: runCodeAgain,
+ tooltip: '',
+ class: 'codicon codicon-run',
+ enabled: !!plotCode && !!sessionId && !!languageId,
+ run: async () => {
+ await services.positronConsoleService.executeCode(
+ languageId!,
+ sessionId,
+ plotCode,
+ { source: CodeAttributionSource.Interactive },
+ true
+ );
+ }
+ }
+ ];
+ };
+
+ return (
+
+ );
+};
diff --git a/src/vs/workbench/contrib/positronPlots/browser/components/plotGalleryThumbnail.css b/src/vs/workbench/contrib/positronPlots/browser/components/plotGalleryThumbnail.css
new file mode 100644
index 000000000000..d640232eaccd
--- /dev/null
+++ b/src/vs/workbench/contrib/positronPlots/browser/components/plotGalleryThumbnail.css
@@ -0,0 +1,70 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (C) 2025 Posit Software, PBC. All rights reserved.
+ * Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+/**
+ * Plot thumbnail name label - positioned at the bottom of the thumbnail.
+ */
+.plot-thumbnail-name {
+ padding: 1px 4px;
+ background-color: var(--vscode-list-inactiveSelectionBackground);
+ color: var(--vscode-list-inactiveSelectionForeground);
+ border: 1px solid var(--vscode-editorWidget-border);
+ border-radius: 3px;
+ font-size: 9px;
+ line-height: 1.2;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ z-index: 1;
+ flex-shrink: 0;
+ margin-top: 2px;
+}
+
+.plot-thumbnail.selected .plot-thumbnail-name {
+ background-color: var(--vscode-list-activeSelectionBackground);
+ color: var(--vscode-list-activeSelectionForeground);
+ border-color: var(--vscode-focusBorder);
+}
+
+.plot-thumbnail-name-text {
+ display: block;
+ max-width: 72px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.plot-thumbnail-button {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 4px;
+ 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;
+}
diff --git a/src/vs/workbench/contrib/positronPlots/browser/components/plotGalleryThumbnail.tsx b/src/vs/workbench/contrib/positronPlots/browser/components/plotGalleryThumbnail.tsx
index 857208f2fccd..b1e32e9c1b20 100644
--- a/src/vs/workbench/contrib/positronPlots/browser/components/plotGalleryThumbnail.tsx
+++ b/src/vs/workbench/contrib/positronPlots/browser/components/plotGalleryThumbnail.tsx
@@ -3,12 +3,16 @@
* Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
*--------------------------------------------------------------------------------------------*/
+// CSS.
+import './plotGalleryThumbnail.css';
+
// React.
-import React, { PropsWithChildren, useRef } from 'react';
+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.
@@ -20,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.
@@ -41,12 +47,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) {
@@ -66,25 +75,34 @@ export const PlotGalleryThumbnail = (props: PropsWithChildren {
+ return props.plotClient.metadata.name;
+ }, [props.plotClient.metadata.name]);
+
return (
-
+
diff --git a/src/vs/workbench/contrib/positronPlots/browser/components/plotsContainer.css b/src/vs/workbench/contrib/positronPlots/browser/components/plotsContainer.css
index d827d50107e1..7b2bde831ed9 100644
--- a/src/vs/workbench/contrib/positronPlots/browser/components/plotsContainer.css
+++ b/src/vs/workbench/contrib/positronPlots/browser/components/plotsContainer.css
@@ -13,3 +13,87 @@
.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 4px;
+ flex-shrink: 0;
+ overflow: hidden;
+ background-color: var(--vscode-editor-background);
+}
+
+.plots-container .plot-info-header .plot-info-text {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ width: 100%;
+ white-space: nowrap;
+ overflow: hidden;
+}
+
+.plots-container .plot-info-header .plot-session-name {
+ font-size: 11px;
+ color: var(--vscode-foreground);
+ opacity: 0.8;
+ 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 {
+ font-size: 12px;
+ color: var(--vscode-foreground);
+ opacity: 0.8;
+ margin-right: 5px;
+}
+
+.plots-container .plot-info-header .plot-code-button {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ margin-left: auto;
+ padding: 2px 4px;
+ background: none;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ 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;
+ 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 .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 6538ca2f2228..b793069e8cd5 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';
@@ -45,7 +45,12 @@ 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.
+ */
+export const PlotInfoHeaderPx = 30;
/**
* PlotContainer component; holds the plot instances.
@@ -67,8 +72,67 @@ 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;
- const plotWidth = historyBottom || props.width <= 0 ? props.width : props.width - historyPx;
+ // 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 + 1);
+
+ // 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);
+
+ // State to track session name updates and trigger re-renders
+ const [sessionNameVersion, setSessionNameVersion] = 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]);
+
+ // Listen for session name updates to update the displayed session name
+ useEffect(() => {
+ const disposable = services.runtimeSessionService.onDidUpdateSessionName((session) => {
+ // Only trigger re-render if the updated session is the current plot's session
+ if (currentPlotInstance && session.sessionId === currentPlotInstance.metadata.session_id) {
+ setSessionNameVersion(v => v + 1);
+ }
+ });
+ return () => disposable.dispose();
+ }, [services.runtimeSessionService, currentPlotInstance]);
+
+ // 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) {
+ // Use dynState.sessionName to get the current (possibly renamed) session name
+ return session.dynState.sessionName;
+ }
+ // 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, sessionNameVersion]);
+
+ // 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(() => {
@@ -85,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.
@@ -254,7 +318,7 @@ export const PlotsContainer = (props: PlotContainerProps) => {
return;
}
const plotThumbnailElement = plotHistory.querySelector(
- `.plot-thumbnail[data-plot-id="${plotId}"]`
+ `.plot-thumbnail-button[data-plot-id="${plotId}"]`
) as HTMLButtonElement;
if (plotThumbnailElement) {
plotThumbnailElement.focus();
@@ -297,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
@@ -341,17 +426,41 @@ export const PlotsContainer = (props: PlotContainerProps) => {
;
};
+ // Render the plot info header showing the session name and plot name.
+ const renderPlotInfoHeader = () => {
+ if (!currentPlotInstance) {
+ return null;
+ }
+
+ // If no info to display, show a placeholder to maintain consistent height
+ if (!sessionName && !plotName) {
+ return