Skip to content

Commit dc72785

Browse files
authored
Positron notebook quick-fix errors (#10920)
Addresses #8607. This PR adds "Fix" and "Explain" buttons to error outputs in notebooks, bringing notebook error handling to parity with the console experience. When an error occurs in a notebook cell, users now see quick action buttons directly in the output that allow them to: - **Fix**: Send the error and context to Positron Assistant to get a proposed fix - **Explain**: Get a detailed explanation of what caused the error The implementation integrates with Positron Assistant and strips ANSI codes from errors before sending them to avoid token waste and improve chat clarity. ### Demo https://github.com/user-attachments/assets/e70e378c-21c6-4663-b399-c82a876f584a ### Release Notes #### New Features - N/A #### Bug Fixes - N/A ### QA Notes @:positron-notebooks @:assistant 1. Open a notebook 2. Execute code that produces an error, for example: ```python # Python example x = 1 / 0 ``` 3. Verify that "Fix" and "Explain" buttons appear below the error traceback 4. Click "Fix" and verify: - Positron Assistant opens - Error context is sent to the assistant - Assistant provides a fix suggestion 5. Click "Explain" and verify: - Assistant provides an explanation of the error 6. Test with errors containing ANSI codes to verify they're stripped properly
1 parent d31db5a commit dc72785

File tree

3 files changed

+345
-0
lines changed

3 files changed

+345
-0
lines changed

src/vs/workbench/contrib/positronNotebook/browser/notebookCells/CellTextOutput.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { useNotebookOptions } from '../NotebookInstanceProvider.js';
1717
import { ICommandService } from '../../../../../platform/commands/common/commands.js';
1818
import { NotebookDisplayOptions } from '../../../notebook/browser/notebookOptions.js';
1919
import { usePositronReactServicesContext } from '../../../../../base/browser/positronReactRendererContext.js';
20+
import { NotebookCellQuickFix } from './NotebookCellQuickFix.js';
2021

2122
type LongOutputOptions = Pick<NotebookDisplayOptions, 'outputLineLimit' | 'outputScrolling'>;
2223

@@ -108,6 +109,11 @@ export function CellTextOutput({ content, type }: ParsedTextOutput) {
108109
? <TruncationMessage commandService={services.commandService} truncationResult={truncation} />
109110
: null
110111
}
112+
{
113+
type === 'error'
114+
? <NotebookCellQuickFix errorContent={content} />
115+
: null
116+
}
111117
</>;
112118
}
113119

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (C) 2025 Posit Software, PBC. All rights reserved.
3+
* Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
/* Container for all quick-fix buttons */
7+
.notebook-cell-quick-fix {
8+
display: flex;
9+
flex-direction: row;
10+
justify-content: flex-start;
11+
gap: 8px;
12+
margin-top: 8px;
13+
}
14+
15+
/* Split button container - mimics action-bar-button.border style */
16+
.notebook-cell-quick-fix-split-button {
17+
display: flex;
18+
flex-direction: row;
19+
align-items: center;
20+
border-radius: 4px;
21+
border: 1px solid var(--vscode-positronActionBar-selectBorder);
22+
background: transparent;
23+
}
24+
25+
/* Main action button (left side) */
26+
.notebook-cell-quick-fix .assistant-action-main {
27+
display: flex;
28+
align-items: center;
29+
cursor: pointer;
30+
border-radius: 3px 0 0 3px;
31+
}
32+
33+
.notebook-cell-quick-fix .assistant-action-main:hover {
34+
background: var(--vscode-positronActionBar-hoverBackground);
35+
}
36+
37+
.notebook-cell-quick-fix .assistant-action-main .link-text {
38+
display: flex;
39+
align-items: center;
40+
gap: 4px;
41+
color: var(--vscode-textLink-foreground);
42+
padding: 2px 6px;
43+
font-size: 12px;
44+
cursor: pointer;
45+
user-select: none;
46+
}
47+
48+
.notebook-cell-quick-fix .assistant-action-main .link-text .codicon {
49+
color: var(--vscode-textLink-foreground);
50+
font-size: 12px;
51+
cursor: pointer;
52+
user-select: none;
53+
}
54+
55+
/* Dropdown button (right side) - mimics action-bar-button-drop-down-button */
56+
.notebook-cell-quick-fix .assistant-action-dropdown {
57+
display: flex;
58+
align-items: center;
59+
justify-content: center;
60+
padding: 0 4px;
61+
border-left: 1px solid var(--vscode-positronActionBar-selectBorder);
62+
cursor: pointer;
63+
user-select: none;
64+
align-self: stretch;
65+
border-radius: 0 3px 3px 0;
66+
}
67+
68+
.notebook-cell-quick-fix .assistant-action-dropdown:hover {
69+
background: var(--vscode-positronActionBar-hoverBackground);
70+
}
71+
72+
.notebook-cell-quick-fix .assistant-action-dropdown .codicon {
73+
color: var(--vscode-textLink-foreground);
74+
font-size: 14px;
75+
pointer-events: none;
76+
}
77+
Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (C) 2025 Posit Software, PBC. All rights reserved.
3+
* Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
// CSS.
7+
import './NotebookCellQuickFix.css';
8+
9+
// React.
10+
import React, { useRef } from 'react';
11+
12+
// Other dependencies.
13+
import { localize } from '../../../../../nls.js';
14+
import { PositronButton } from '../../../../../base/browser/ui/positronComponents/button/positronButton.js';
15+
import { usePositronReactServicesContext } from '../../../../../base/browser/positronReactRendererContext.js';
16+
import { usePositronConfiguration, usePositronContextKey } from '../../../../../base/browser/positronReactHooks.js';
17+
import { IAction } from '../../../../../base/common/actions.js';
18+
import { AnchorAlignment, AnchorAxisAlignment } from '../../../../../base/browser/ui/contextview/contextview.js';
19+
import { removeAnsiEscapeCodes } from '../../../../../base/common/strings.js';
20+
import { CHAT_OPEN_ACTION_ID, ACTION_ID_NEW_CHAT } from '../../../chat/browser/actions/chatActions.js';
21+
import { ChatModeKind } from '../../../chat/common/constants.js';
22+
23+
/**
24+
* Props for the NotebookCellQuickFix component.
25+
*/
26+
interface NotebookCellQuickFixProps {
27+
/** The error output content from the cell execution */
28+
errorContent: string;
29+
}
30+
31+
/**
32+
* Quick fix component for notebook cell errors.
33+
* Displays "Fix" and "Explain" split buttons that send the error content to the assistant chat.
34+
* Primary click starts a fresh chat session in agent mode (with tool access to modify notebooks),
35+
* dropdown opens in the existing chat panel to retain conversation context.
36+
*
37+
* @param props Component props containing the error content
38+
* @returns The rendered component, or null if assistant is not enabled
39+
*/
40+
export const NotebookCellQuickFix = (props: NotebookCellQuickFixProps) => {
41+
const fixDropdownRef = useRef<HTMLDivElement>(null);
42+
const explainDropdownRef = useRef<HTMLDivElement>(null);
43+
const services = usePositronReactServicesContext();
44+
const { commandService, contextMenuService } = services;
45+
46+
// Configuration hooks to conditionally show the quick-fix buttons
47+
const enableAssistant = usePositronConfiguration<boolean>('positron.assistant.enable');
48+
const enableNotebookMode = usePositronConfiguration<boolean>('positron.assistant.notebookMode.enable');
49+
const hasChatModels = usePositronContextKey<boolean>('positron-assistant.hasChatModels');
50+
51+
// Only show buttons if assistant is enabled, notebook mode is enabled, and chat models are available
52+
const showQuickFix = enableAssistant && enableNotebookMode && hasChatModels;
53+
54+
/**
55+
* Builds a query string for asking the assistant to fix the erroring cell.
56+
* The assistant already has notebook context including the selected cell,
57+
* so we only need to include the error output.
58+
*
59+
* @returns The formatted fix query string
60+
*/
61+
const buildFixQuery = (): string => {
62+
const cleanError = removeAnsiEscapeCodes(props.errorContent).trim();
63+
return cleanError
64+
? `Fix this cell that produced an error:\n\`\`\`\n${cleanError}\n\`\`\``
65+
: 'Fix this cell that produced an error.';
66+
};
67+
68+
/**
69+
* Builds a query string for asking the assistant to explain the error.
70+
* The assistant already has notebook context including the selected cell,
71+
* so we only need to include the error output.
72+
*
73+
* @returns The formatted explain query string
74+
*/
75+
const buildExplainQuery = (): string => {
76+
const cleanError = removeAnsiEscapeCodes(props.errorContent).trim();
77+
return cleanError
78+
? `Explain why this cell produced an error:\n\`\`\`\n${cleanError}\n\`\`\``
79+
: 'Explain why this cell produced an error.';
80+
};
81+
82+
/**
83+
* Handler for the "Fix" button primary click.
84+
* Starts a fresh chat session and opens the chat panel in agent mode with a fix prompt and error output.
85+
* Agent mode has tool access to modify notebooks.
86+
*/
87+
const pressedFixHandler = async () => {
88+
await commandService.executeCommand(ACTION_ID_NEW_CHAT);
89+
await commandService.executeCommand(CHAT_OPEN_ACTION_ID, {
90+
query: buildFixQuery(),
91+
mode: ChatModeKind.Agent
92+
});
93+
};
94+
95+
/**
96+
* Handler for the "Explain" button primary click.
97+
* Starts a fresh chat session and opens the chat panel in agent mode with an explain prompt and error output.
98+
* Agent mode has tool access to modify notebooks.
99+
*/
100+
const pressedExplainHandler = async () => {
101+
await commandService.executeCommand(ACTION_ID_NEW_CHAT);
102+
await commandService.executeCommand(CHAT_OPEN_ACTION_ID, {
103+
query: buildExplainQuery(),
104+
mode: ChatModeKind.Agent
105+
});
106+
};
107+
108+
/**
109+
* Shows the context menu for the Fix button dropdown.
110+
* Provides option to open in the existing chat panel to retain conversation context.
111+
*
112+
* @param event Event from the dropdown button click or keyboard activation
113+
*/
114+
const showFixDropdownMenu = (event: React.SyntheticEvent<HTMLDivElement>) => {
115+
event.preventDefault();
116+
event.stopPropagation();
117+
118+
const actions: IAction[] = [
119+
{
120+
id: 'continue-in-existing-chat',
121+
label: localize('positronNotebookAssistantFixInCurrentChat', "Ask assistant to fix in current chat"),
122+
tooltip: localize('positronNotebookAssistantFixInCurrentChatTooltip', "Opens in the current chat session to retain conversation context"),
123+
class: undefined,
124+
enabled: true,
125+
run: async () => {
126+
await commandService.executeCommand(CHAT_OPEN_ACTION_ID, {
127+
query: buildFixQuery(),
128+
mode: ChatModeKind.Agent
129+
});
130+
}
131+
}
132+
];
133+
134+
if (!fixDropdownRef.current) {
135+
return;
136+
}
137+
138+
const rect = fixDropdownRef.current.getBoundingClientRect();
139+
contextMenuService.showContextMenu({
140+
getActions: () => actions,
141+
getAnchor: () => ({ x: rect.left, y: rect.bottom }),
142+
anchorAlignment: AnchorAlignment.LEFT,
143+
anchorAxisAlignment: AnchorAxisAlignment.VERTICAL
144+
});
145+
};
146+
147+
/**
148+
* Shows the context menu for the Explain button dropdown.
149+
* Provides option to open in the existing chat panel to retain conversation context.
150+
*
151+
* @param event Event from the dropdown button click or keyboard activation
152+
*/
153+
const showExplainDropdownMenu = (event: React.SyntheticEvent<HTMLDivElement>) => {
154+
event.preventDefault();
155+
event.stopPropagation();
156+
157+
const actions: IAction[] = [
158+
{
159+
id: 'continue-in-existing-chat',
160+
label: localize('positronNotebookAssistantExplainInCurrentChat', "Ask assistant to explain in current chat"),
161+
tooltip: localize('positronNotebookAssistantExplainInCurrentChatTooltip', "Opens in the current chat session to retain conversation context"),
162+
class: undefined,
163+
enabled: true,
164+
run: async () => {
165+
await commandService.executeCommand(CHAT_OPEN_ACTION_ID, {
166+
query: buildExplainQuery(),
167+
mode: ChatModeKind.Agent
168+
});
169+
}
170+
}
171+
];
172+
173+
if (!explainDropdownRef.current) {
174+
return;
175+
}
176+
177+
const rect = explainDropdownRef.current.getBoundingClientRect();
178+
contextMenuService.showContextMenu({
179+
getActions: () => actions,
180+
getAnchor: () => ({ x: rect.left, y: rect.bottom }),
181+
anchorAlignment: AnchorAlignment.LEFT,
182+
anchorAxisAlignment: AnchorAxisAlignment.VERTICAL
183+
});
184+
};
185+
186+
// Don't render if assistant features are not enabled
187+
if (!showQuickFix) {
188+
return null;
189+
}
190+
191+
// Tooltip strings
192+
const fixTooltip = localize('positronNotebookAssistantFixTooltip', "Ask assistant to fix in new chat");
193+
const fixDropdownTooltip = localize('positronNotebookAssistantFixDropdownTooltip', "More fix options");
194+
const explainTooltip = localize('positronNotebookAssistantExplainTooltip', "Ask assistant to explain in new chat");
195+
const explainDropdownTooltip = localize('positronNotebookAssistantExplainDropdownTooltip', "More explain options");
196+
197+
// Render.
198+
return (
199+
<div className='notebook-cell-quick-fix'>
200+
{/* Fix button with split dropdown */}
201+
<div className='notebook-cell-quick-fix-split-button'>
202+
<PositronButton
203+
ariaLabel={fixTooltip}
204+
className='assistant-action assistant-action-main'
205+
onPressed={pressedFixHandler}
206+
>
207+
<div className='link-text' title={fixTooltip}>
208+
<span className='codicon codicon-sparkle' />
209+
{localize('positronNotebookAssistantFix', "Fix")}
210+
</div>
211+
</PositronButton>
212+
<div
213+
ref={fixDropdownRef}
214+
aria-label={fixDropdownTooltip}
215+
className='assistant-action assistant-action-dropdown'
216+
role='button'
217+
tabIndex={0}
218+
title={fixDropdownTooltip}
219+
onKeyDown={(e) => {
220+
if (e.key === 'Enter' || e.key === ' ') {
221+
showFixDropdownMenu(e);
222+
}
223+
}}
224+
onMouseDown={showFixDropdownMenu}
225+
>
226+
<span className='codicon codicon-positron-drop-down-arrow' />
227+
</div>
228+
</div>
229+
230+
{/* Explain button with split dropdown */}
231+
<div className='notebook-cell-quick-fix-split-button'>
232+
<PositronButton
233+
ariaLabel={explainTooltip}
234+
className='assistant-action assistant-action-main'
235+
onPressed={pressedExplainHandler}
236+
>
237+
<div className='link-text' title={explainTooltip}>
238+
<span className='codicon codicon-sparkle' />
239+
{localize('positronNotebookAssistantExplain', "Explain")}
240+
</div>
241+
</PositronButton>
242+
<div
243+
ref={explainDropdownRef}
244+
aria-label={explainDropdownTooltip}
245+
className='assistant-action assistant-action-dropdown'
246+
role='button'
247+
tabIndex={0}
248+
title={explainDropdownTooltip}
249+
onKeyDown={(e) => {
250+
if (e.key === 'Enter' || e.key === ' ') {
251+
showExplainDropdownMenu(e);
252+
}
253+
}}
254+
onMouseDown={showExplainDropdownMenu}
255+
>
256+
<span className='codicon codicon-positron-drop-down-arrow' />
257+
</div>
258+
</div>
259+
</div>
260+
);
261+
};
262+

0 commit comments

Comments
 (0)