Skip to content

Positron Notebooks: Add ghost cell suggestions for AI-powered next step hints#11622

Open
nstrayer wants to merge 34 commits intomainfrom
positron-nb-ghost-suggestions
Open

Positron Notebooks: Add ghost cell suggestions for AI-powered next step hints#11622
nstrayer wants to merge 34 commits intomainfrom
positron-nb-ghost-suggestions

Conversation

@nstrayer
Copy link
Contributor

@nstrayer nstrayer commented Jan 31, 2026

Addresses #11413.

Summary

Adds ghost cell suggestions to Positron Notebooks -- an AI-powered feature that suggests a logical next code cell after successful cell execution. The ghost cell appears at the bottom of the notebook with suggested code that users can accept, dismiss, or regenerate.

Key implementation details:

  • Opt-in by default: Feature is disabled globally; on first cell execution users see an opt-in prompt with "Enable", "Not now", and "Don't ask again" options
  • Two modes: automatic (push) shows suggestions after a configurable delay, on-demand (pull) shows a placeholder that users click to request suggestions
  • Mode toggle directly in the ghost cell header via a SegmentedToggle control
  • Streaming support shows code progressively as the LLM generates it (with blinking cursor indicator)
  • Model picker: Users can select which model to use for suggestions via a QuickPick command, with models grouped by vendor and a "Use Default (Auto-select)" option that matches patterns like haiku/mini
  • Model indicator in ghost cell footer shows which model generated the suggestion; clickable to change model; shows a warning icon when a fallback model was used
  • Visual design: grayed out cell with dashed border, fast transitions (no animation), clearly distinct from real cells
  • Info modal explains the feature, shows current model, and links to settings
  • Per-notebook overrides for both enabled/disabled state and automatic/on-demand mode, managed through the Assistant Panel settings UI
  • Re-enable command (Notebook: Enable Ghost Cell Suggestions for This Notebook) available in the command palette after per-notebook disable
  • Keyboard shortcut Cmd/Ctrl+Shift+G to request suggestion (useful in pull mode or to regenerate)

Screenshots/Demo

next-suggestions.mov

New setting for notebook/global

image

Info Modal

image

Dismiss Options

image

Release Notes

New Features

Bug Fixes

  • N/A

QA Notes

@:positron-notebooks @:assistant

Prerequisites:

  • Ensure an AI model is configured (OpenAI, Anthropic, Azure, Ollama, or Copilot)
  • Ghost cell suggestions are disabled by default -- enable via settings or the opt-in prompt

Test scenarios:

  1. Opt-in prompt (first run):

    • Open a Python or R notebook with ghost cell suggestions disabled (default)
    • Run a cell -- an opt-in prompt should appear with "Enable", "Not now", and "Don't ask again" options
    • Click "Enable" -- the setting should be enabled globally and a suggestion should load
    • Verify "Not now" dismisses the prompt for the session only
    • Verify "Don't ask again" sets the per-notebook disable and no further prompts appear
  2. Basic flow (automatic/push mode - default when enabled):

    • Enable ghost cell suggestions in settings
    • Open a Python or R notebook
    • Run a cell (e.g., import pandas as pd; df = pd.read_csv('data.csv'))
    • After ~2 seconds (configurable), a ghost cell should appear at the bottom with a suggestion
    • Verify loading spinner appears first, then content streams in with a blinking cursor
  3. On-demand/pull mode:

    • Toggle to on-demand mode using the segmented toggle in the ghost cell header, or set positron.assistant.notebook.ghostCellSuggestions.automatic to false
    • Run a cell -- a placeholder ghost cell appears with "Get Suggestion" button
    • Click "Get Suggestion" or press Cmd/Ctrl+Shift+G to request a suggestion
    • Verify suggestion loads and streams in
  4. Mode toggle:

    • In the ghost cell header, toggle between Automatic and On-demand modes using the segmented toggle
    • Verify the setting persists in notebook metadata
    • Verify the toggle state is reflected in the AssistantPanel settings
  5. Model picker:

    • Click the model name in the ghost cell footer to open the model picker
    • Verify models are grouped by vendor
    • Select a specific model and regenerate -- verify the footer updates to show the new model
    • Select "Use Default (Auto-select)" and verify it falls back to pattern matching (haiku/mini)
    • If configured model is unavailable, verify a warning icon appears in the footer
  6. Accept actions:

    • Click "Accept and Run" -- cell should be inserted and executed
    • Use the dropdown to select "Accept" -- cell should be inserted but NOT executed
  7. Dismiss actions:

    • Click "Dismiss" -- ghost cell disappears
    • Use dropdown "Don't suggest for this notebook" -- ghost cell disappears, setting saved to notebook metadata, notification appears with "Re-enable" action
    • Use dropdown "Don't suggest again" -- disables globally
  8. Re-enable for notebook:

    • After disabling for a notebook, run Notebook: Enable Ghost Cell Suggestions for This Notebook from the command palette
    • Verify suggestions work again for that notebook
  9. Regenerate:

    • Click regenerate button or press Cmd/Ctrl+Shift+G -- new suggestion should be generated
  10. Info modal:

    • Click the info (i) button -- modal should explain the feature and show current model name
    • Click the "settings" link -- should open settings filtered to ghost cell settings
  11. AssistantPanel settings:

    • Open the Assistant Panel
    • Verify "Ghost cell suggestions" toggle row appears with Yes/No segmented toggle
    • Toggle "follow global" checkbox -- when unchecked, per-notebook override is active
    • Change the toggle -- verify it persists to notebook metadata
  12. Settings:

    • Enable/disable globally via positron.assistant.notebook.ghostCellSuggestions.enabled
    • Adjust delay via positron.assistant.notebook.ghostCellSuggestions.delay (500-10000ms)
    • Change mode via positron.assistant.notebook.ghostCellSuggestions.automatic (true/false)
    • Change model via positron.assistant.notebook.ghostCellSuggestions.model (array of patterns)
  13. Edge cases:

    • Rapid cell execution should debounce (no flickering)
    • Failed cell execution should NOT trigger suggestion
    • Creating a new cell should dismiss the ghost cell
    • Long explanations should truncate with expand/collapse on hover

Show Your Work Extension Link: vscode://nstrayer.show-your-work/open?gist=f863c156bf49e6d314070b51faa86a2a

@github-actions
Copy link

github-actions bot commented Jan 31, 2026

E2E Tests 🚀
This PR will run tests tagged with: @:critical @:positron-notebooks @:assistant

readme  valid tags

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds AI-powered ghost cell suggestions to Positron Notebooks that appear after successful cell execution with suggested next code steps. The feature integrates with existing multi-provider AI infrastructure and includes streaming support, per-notebook settings, and a refined UX with split buttons for accept/dismiss actions.

Changes:

  • Added ghost cell suggestion feature that appears after successful cell execution with AI-generated next step recommendations
  • Created reusable SplitButton component and refactored NotebookCellQuickFix to use it
  • Added per-notebook and global configuration settings for ghost cell suggestions with debounce delay control
  • Updated notebook metadata handling to correctly persist settings in ipynb format via metadata.metadata path

Reviewed changes

Copilot reviewed 18 out of 18 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/vs/workbench/contrib/positronNotebook/common/positronNotebookConfig.ts Added configuration keys for ghost cell suggestions and delay settings
src/vs/workbench/contrib/positronNotebook/common/notebookAssistantMetadata.ts Updated metadata handling to use metadata.metadata path for ipynb persistence and added ghost cell settings support
src/vs/workbench/contrib/positronNotebook/browser/utilityComponents/SplitButton.tsx New reusable split button component with main action and dropdown menu
src/vs/workbench/contrib/positronNotebook/browser/utilityComponents/SplitButton.css Styling for split button component with hover and focus states
src/vs/workbench/contrib/positronNotebook/browser/notebookCells/NotebookCellQuickFix.tsx Refactored to use new SplitButton component, simplified code
src/vs/workbench/contrib/positronNotebook/browser/notebookCells/NotebookCellQuickFix.css Updated CSS to work with SplitButton component
src/vs/workbench/contrib/positronNotebook/browser/notebookCells/GhostCellInfoModalDialog.tsx Info modal explaining ghost cell feature with link to settings
src/vs/workbench/contrib/positronNotebook/browser/notebookCells/GhostCellInfoModalDialog.css Styling for info modal
src/vs/workbench/contrib/positronNotebook/browser/notebookCells/GhostCell.tsx Main ghost cell component with loading, streaming, ready, and error states
src/vs/workbench/contrib/positronNotebook/browser/notebookCells/GhostCell.css Comprehensive styling for ghost cell states and animations with accessibility support
src/vs/workbench/contrib/positronNotebook/browser/PositronNotebookInstance.ts Core logic for ghost cell state management, debouncing, and command integration
src/vs/workbench/contrib/positronNotebook/browser/PositronNotebookComponent.tsx Added GhostCell component to notebook rendering
src/vs/workbench/contrib/positronNotebook/browser/IPositronNotebookInstance.ts Added GhostCellState type and interface methods for ghost cell operations
src/vs/workbench/contrib/positronNotebook/browser/AssistantPanel/AssistantPanel.tsx Added ghost cell suggestions toggle to assistant panel settings
extensions/positron-assistant/src/notebookAssistantMetadata.ts Added ghost cell suggestions metadata handling in extension
extensions/positron-assistant/src/md/prompts/notebook/ghost-cell.md AI prompt template for generating ghost cell suggestions
extensions/positron-assistant/src/ghostCellSuggestions.ts Extension logic for generating ghost cell suggestions with LLM
extensions/positron-assistant/src/extension.ts Command registration for ghost cell suggestion generation

.ghost-cell-info-button:hover {
color: var(--vscode-foreground);
}

Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The info button (ghost-cell-info-button) has hover styles but is missing focus-visible styles for keyboard accessibility. Users navigating with keyboard should see a clear focus indicator when the button receives focus. Consider adding a :focus-visible rule similar to other focusable elements in the component (e.g., like the regenerate button at lines 144-150).

Suggested change
.ghost-cell-info-button:focus-visible {
color: var(--vscode-foreground);
outline: 1px solid var(--vscode-focusBorder);
outline-offset: 2px;
}

Copilot uses AI. Check for mistakes.
Comment on lines 2374 to 2547
dismissGhostCell(disableForNotebook?: boolean): void {
// Cancel any pending request
if (this._ghostCellDebounceTimer) {
clearTimeout(this._ghostCellDebounceTimer);
this._ghostCellDebounceTimer = undefined;
}

if (this._ghostCellCancellationToken) {
this._ghostCellCancellationToken.cancel();
this._ghostCellCancellationToken.dispose();
this._ghostCellCancellationToken = undefined;
}

// Hide ghost cell
this._ghostCellState.set({ status: 'hidden' }, undefined);

// Optionally disable for this notebook
if (disableForNotebook) {
const textModel = this._textModel.get();
if (textModel) {
const newMetadata = setAssistantSettings({ ...textModel.metadata }, { ghostCellSuggestions: 'disabled' });
textModel.applyEdits([{
editType: CellEditType.DocumentMetadata,
metadata: newMetadata
}], true, undefined, () => undefined, undefined, true);
}
}
}
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description states "Creating a new cell should dismiss the ghost cell," but this behavior is not implemented. When a user manually adds a new cell (not via accepting the ghost cell suggestion), the ghost cell should be dismissed to avoid confusion. Consider adding logic to dismiss the ghost cell in response to cell additions, either by listening to cell change events or by calling dismissGhostCell from relevant methods.

Copilot uses AI. Check for mistakes.
Comment on lines +282 to +620
const dismissActions = React.useMemo((): IAction[] => [
{
id: 'ghost-cell-dismiss',
label: dismissLabel,
tooltip: dismissLabel,
class: undefined,
enabled: true,
run: handleDismiss
},
{
id: 'ghost-cell-disable-for-notebook',
label: dontSuggestInNotebookLabel,
tooltip: dontSuggestInNotebookLabel,
class: undefined,
enabled: true,
run: handleDisableForNotebook
},
{
id: 'ghost-cell-dont-suggest-again',
label: dontSuggestAgainLabel,
tooltip: dontSuggestAgainLabel,
class: undefined,
enabled: true,
run: handleDisableGlobally
}
], [handleDismiss, handleDisableForNotebook, handleDisableGlobally]);
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The dismiss dropdown includes "Dismiss" as the first option, which duplicates the main button's action. This is redundant and may confuse users. Consider removing the "Dismiss" option from the dropdown and only including the "Don't suggest for this notebook" and "Don't suggest again" options. The main button already provides the basic dismiss functionality.

Copilot uses AI. Check for mistakes.
Comment on lines +30 to +89
- After an error: The most likely fix
- After calculations: One way to inspect or visualize the result

## Output Format

You MUST return only valid XML in the output, and nothing else. Use the following structure:

```xml
<suggestion>
<explanation>Brief description of what this code does and why it's a logical next step (1-2 sentences)</explanation>
<code>
# Comment explaining the suggestion
your_code_here()
</code>
</suggestion>
```

## Examples

### Example 1: After loading a DataFrame

Context: User just ran `df = pd.read_csv('data.csv')` and got successful output showing the DataFrame loaded

```xml
<suggestion>
<explanation>Preview the first few rows to see what the data looks like.</explanation>
<code>
df.head()
</code>
</suggestion>
```

### Example 2: After creating a visualization

Context: User just created a scatter plot with `plt.scatter(x, y)`

```xml
<suggestion>
<explanation>Add axis labels to make the plot easier to interpret.</explanation>
<code>
plt.xlabel('X Variable')
plt.ylabel('Y Variable')
plt.title('Scatter Plot')
plt.show()
</code>
</suggestion>
```

### Example 3: After an error

Context: User got a KeyError when trying to access a column

```xml
<suggestion>
<explanation>Check the available columns to find the correct column name.</explanation>
<code>
df.columns.tolist()
</code>
</suggestion>
```
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The prompt template mentions "After an error: The most likely fix" (line 30) and includes Example 3 for error scenarios (lines 78-89), but the PositronNotebookInstance.ts implementation only triggers ghost cell suggestions after successful cell execution (when lastRunSuccess === true). This discrepancy means the LLM is being instructed to handle error cases that will never occur in practice. Consider either removing the error-related guidance from the prompt or updating the implementation to also suggest cells after error cases (if that's the desired behavior).

Copilot uses AI. Check for mistakes.
Comment on lines 2315 to 2464
} else {
// No suggestion generated, hide ghost cell
this._ghostCellState.set({ status: 'hidden' }, undefined);
}
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The callback command disposable is not disposed when the suggestion result is null (line 2317). While the disposable will eventually be garbage collected, it should be explicitly disposed to follow consistent cleanup patterns and avoid potential memory leaks. Consider wrapping the promise chain in a finally block or ensuring disposal happens in all paths.

Copilot uses AI. Check for mistakes.
Comment on lines 2333 to 2485
// Auto-dismiss error after 5 seconds
setTimeout(() => {
const currentState = this._ghostCellState.get();
if (currentState.status === 'error') {
this._ghostCellState.set({ status: 'hidden' }, undefined);
}
}, 5000);
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The auto-dismiss error timeout (line 2334) is not cleaned up when the instance is disposed. If the instance is disposed while the 5-second timeout is pending, the callback will still execute and try to access the disposed instance's state. This should be stored as a class field and cleared in the dispose() method, similar to how _ghostCellDebounceTimer is handled.

Suggested change
// Auto-dismiss error after 5 seconds
setTimeout(() => {
const currentState = this._ghostCellState.get();
if (currentState.status === 'error') {
this._ghostCellState.set({ status: 'hidden' }, undefined);
}
}, 5000);
// Auto-dismiss error after 5 seconds. Tie the timeout to the current cancellation token
const errorAutoDismissTimeout = setTimeout(() => {
if (token.isCancellationRequested) {
return;
}
const currentState = this._ghostCellState.get();
if (currentState.status === 'error') {
this._ghostCellState.set({ status: 'hidden' }, undefined);
}
}, 5000);
token.onCancellationRequested(() => {
clearTimeout(errorAutoDismissTimeout);
});

Copilot uses AI. Check for mistakes.
Adds AI-powered ghost cell suggestions that appear after code execution,
suggesting potential next steps based on notebook context. The feature
includes:

- GhostCell component with loading, streaming, and error states
- LLM integration via positron-assistant extension
- Configuration setting to enable/disable the feature
- Keyboard shortcuts (Tab to accept, Escape to dismiss)
- Assistant panel controls for toggling suggestions
Insert accepted ghost cell at end of notebook (where ghost is displayed)
rather than after the executed cell. Also documents known rough edges
including streaming, error checking, and missing tests.
- Add "Single Responsibility" guideline - each suggestion should do ONE thing
- Add "Role Distinction" section explaining ghost cells vs chat pane
- Simplify examples to show focused, bite-sized suggestions
- Remove rigid "under 20 lines" constraint in favor of natural sizing
- Only trigger ghost cell suggestions after successful cell execution
  (skip failed cells to avoid suggesting fixes for errors)
- Register a callback command to receive streaming partial content
  during suggestion generation for responsive UI updates
Introduces a reusable SplitButton component that combines a primary action
button with a dropdown menu for additional options. This pattern is used
in ghost cells and quick fix buttons to provide secondary actions without
cluttering the UI.
- Change ghost cell primary action from "Accept" to "Accept and Run"
- Add split button dropdowns to ghost cell actions with additional options
- Add "Don't suggest again" option to dismiss dropdown that disables
  ghost cell suggestions globally
- Refactor NotebookCellQuickFix to use the new SplitButton component,
  reducing code duplication
Wrap buildFixQuery and buildExplainQuery in useCallback to properly
track dependencies in the useMemo hooks for dropdown actions.
Adds an info button (i icon) next to the sparkle icon in the ghost cell
header. Clicking it opens a modal dialog explaining what ghost cell
suggestions are, how they work, and how to disable them. The dialog
includes a link to open the assistant panel settings.
- Fix metadata access path to use metadata.metadata.positron (matching
  ipynb serializer behavior) instead of metadata.positron
- Refactor setAssistantSettings to iterate over updates dynamically
  instead of individual if-statements per setting
- Extract updateNotebookSettings helper in AssistantPanel to reduce
  code duplication across setting change handlers
- Remove debug console.log statements
Adds a new setting `positron.assistant.notebook.ghostCellSuggestions.delay`
to control how long to wait after cell execution before triggering ghost
cell suggestions. Default is 2000ms (range: 500-10000ms). This replaces
the previously hardcoded 3 second delay.
Change margin-left from 19px to 12px so the ghost cell's dashed border
aligns with regular notebook cell borders.
Add _enabledThisSession flag to bypass config read delays when user
clicks Enable. Change enableGhostCellSuggestions() to sync with
fire-and-forget config updates, matching disableGhostCellSuggestions
pattern.
Rename ghostCellSuggestions to ghostCellSuggestions.enabled for
consistency with ghostCellSuggestions.delay. Update info modal to
show a clickable link that opens the setting directly instead of
an icon button that opened the assistant panel.
Add a "pull mode" option that shows a placeholder after cell execution
instead of automatically requesting suggestions. Users can trigger
suggestions via button or Cmd+Shift+G keyboard shortcut. Includes a
toggle in the ghost cell UI to switch between automatic and on-demand
modes, and supports per-notebook override via metadata.
Change the info modal to use "settings" as link text instead of the raw
setting name, matching the common pattern used elsewhere in the codebase.
Also update the query to show all ghost cell settings, not just enabled.
Add thoughts/ to .gitignore to prevent accidental commits.
Register positron.assistant.notebook.ghostCellSuggestions.model setting
to configure which model generates ghost cell suggestions. The setting
follows the same pattern as other ghost cell settings.

Also display the selected model name in the ghost cell info modal when
a suggestion is ready, so users can see which model generated it.
Copy link
Contributor

@seeM seeM left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really epic PR @nstrayer! 🚀 This worked really smoothly for me in my testing. I have lots of thoughts for improvement further below.

My only blocking issue is that "Don't suggest in this notebook again" worked, but I couldn't figure out how to re-enable it again. I wonder if a setting might work well there so it can be easily edited?

Detailed feedback

I ran a cell that loaded a dataframe, and ~2 seconds later saw an inline prompt to enable suggestions ✅ . BTW I really like the feeling of this inline prompt UI, wonder where else it'd be useful!

Image

I like the difference between normal vs hover styling of the prompt. Currently, the hover transitions over a short delay which differs from the other notebook transitions that are instant. Thoughts on making this consistent - either putting the transition everywhere or removing it from here?

Image

I clicked enable, saw it say "Generating suggestions..." for a split second and then disappear ❌ bug?

I ran the same cell again, and it said "Generating suggestions..." followed by the suggestion cell ✅:

Image

The description text is truncated and I can view the full text by hovering and seeing the tooltip, but a tooltip doesn't feel like a comfortable way to read a medium-sized description. In all of my examples, the description was much longer than the space allowed and truncated. Maybe a collapse/expand approach?

I can toggle the Automatic/On-demand setting.

Double clicking Automatic/On-demand probably shouldn't select the text though.

This might be me being a bit silly, but after toggling Automatic/On-demand a few times I lost track of which visual treatment meant which one was actually selected 😅

Most things in Positron have subtle hover style changes - could we add something subtle for hovering on the Automatic/On-demand toggle?

It also feels a little odd that the Automatic/On-demand button uses blue but the Accept and Run button uses gray and Dismiss white.

Thoughts on moving the About button to the right hand side? The left feels like really valuable real-estate and the About button feels like it'll be infrequently used.

I also wonder if the sparkles icon is necessary, given the dashed border and otherwise pretty distinguished look of the suggestion cell?

The vertical black line feels out of place here:

Image

Switching to another editor and back keeps the suggestion cell ✅ but there is a small animation as it appears.

Thoughts on making the cursor a pointer when over the about button instead of a question mark? Not sure what is preferred but I am generally surprised when the cursor becomes a question mark, maybe because that's not often used.

The blue borders around the Dismiss and Regenerate buttons feel inconsistent with buttons elsewhere in the notebook and in Positron.

Clicking Accept & Run added a cell with the contents and ran it ✅ and followed up with another cell suggestion - that immediate followup was super satisfying BTW 👌

Idea: I felt like I wanted to use the keyboard to navigate to the suggestion as if it were just another cell type, and then use Shift+Enter to accept and run it. Felt natural and surprised that I couldn't. But we might also not want to go this route.

I didn't change the settings but was surprised to see these in my settings file Can we avoid adding settings manually and instead changing our defaults?

  "positron.assistant.notebook.ghostCellSuggestions.hasOptedIn": true,
  "positron.assistant.notebook.ghostCellSuggestions.mode": "push",
  "positron.assistant.notebook.ghostCellSuggestions.enabled": true,

Dismiss button worked ✅

Can we streamline the UI for on-demand mode? I feel we could do without "AI suggestion available on request", and perhaps we could move the buttons next to the Automatic/on-demand toggle so it can be a slim single line.

Image

Future: Might be nice to get syntax highlighting in the suggestions source since it makes it easier to review the suggestion.

Comment on lines +5 to +8
1. **Single Responsibility**: Each suggestion should do ONE thing. If you're tempted to chain multiple operations, pick the most valuable one.
2. **Be Actionable**: The suggested code should run immediately without modification
3. **Be Obvious**: Suggest the natural, low-friction next step - not a multi-step analysis pipeline
4. **Be Contextual**: Base your suggestion on what the user just executed and its results
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These seemed to be followed well in my testing

Adds a quick pick UI for selecting which model to use for ghost cell
suggestions. Users can access the picker via:
- Command palette: "Select Ghost Cell Suggestion Model"
- Settings: Click "Select a model" link in the setting description
- Ghost cell footer: Click the model name after a suggestion is shown

Also adds fallback warning indicator when configured model is unavailable.
@nstrayer nstrayer force-pushed the positron-nb-ghost-suggestions branch from 0eabbd7 to da22b87 Compare February 5, 2026 14:29
When users click "Don't suggest in this notebook again", they previously
had no discoverable way to re-enable suggestions. This adds:

- New command "Enable Ghost Cell Suggestions for This Notebook" in the
  command palette (searchable via F1)
- Info notification with "Re-enable" button shown when disabling
- enableGhostCellSuggestionsForNotebook() method that clears the
  per-notebook disabled metadata and triggers a suggestion
When clicking Enable, the workbench fires async config updates then
immediately triggers a suggestion. The extension reads the stale config
and returns null, causing the ghost cell to disappear.

Added skipConfigCheck parameter to bypass the extension-side config
check when the workbench has already verified the suggestion should
proceed (e.g., user just clicked Enable).
- Add user-select: none to prevent text selection on double-click
- Add hover effect for non-selected toggle buttons
- Update sizing to match ActionBarToggle (22px container, 20px buttons)
- Update border-radius to match ActionBarToggle (6px)
Refactor suggestion mode from string enum ('push'/'pull') to boolean
(automatic). Remove separate hasOptedIn setting - now detect user choice
by inspecting whether enabled setting has been explicitly set.

UI improvements:
- Add expand/collapse for truncated explanations
- Simplify awaiting-request layout to single row
- Move info button to footer alongside model indicator
…us styles

Remove fade-in animation that replayed when switching editors, change
transitions from 200ms to 0.1s to match notebook patterns, use
:focus-visible for button outlines, and change info button cursor to
pointer. Update PR feedback tracking with completed items.
These internal development documents were accidentally committed
and should not be part of the feature PR.
Create a standalone SegmentedToggle component in positronComponents that
encapsulates the two-option segmented toggle pattern previously duplicated
across AssistantPanel and GhostCell. Refactor both consumers to use it.

Also extract SettingToggleRow in AssistantPanel to reduce repetition across
the three settings rows, and consolidate renderGhostCellState parameters
into a context object for readability.
Move all ghost cell state, config, actions, and logic out of
PositronNotebookInstance into a self-contained GhostCellController
contribution, following the same pattern as contrib/find. This keeps
the notebook instance focused on core notebook concerns and makes the
ghost cell feature independently maintainable.
Remove noise from the branch diff: trailing blank line in
IPositronNotebookInstance, empty ghost-cell region comment block,
unused INotificationService injection, and extra blank line between
_register() calls.
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 28 out of 28 changed files in this pull request and generated 12 comments.

const positron = metadata?.positron as Record<string, unknown> | undefined;
// Access inner metadata (this is what gets serialized to ipynb file)
const innerMetadata = metadata?.metadata as Record<string, unknown> | undefined;
const positron = innerMetadata?.positron as Record<string, unknown> | undefined;
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same breaking change exists in the extension version of this file. Existing notebooks with assistant settings stored at metadata.positron will have those settings ignored after this change. Add migration logic to check the old path (metadata.positron) as a fallback when the new path (metadata.metadata.positron) is not found.

Suggested change
const positron = innerMetadata?.positron as Record<string, unknown> | undefined;
const positronFromInner = innerMetadata?.positron as Record<string, unknown> | undefined;
// Migration fallback: older notebooks stored settings at metadata.positron.assistant
const positronFromRoot = metadata?.positron as Record<string, unknown> | undefined;
const positron = positronFromInner ?? positronFromRoot;

Copilot uses AI. Check for mistakes.

// State for ghost cell suggestions setting
const [ghostCellSuggestionsOverride, setGhostCellSuggestionsOverride] = useState<GhostCellSuggestionsOverride>(undefined);
const globalGhostCellSuggestions = configurationService.getValue<boolean>(POSITRON_NOTEBOOK_GHOST_CELL_SUGGESTIONS_KEY) ?? true;
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default value for globalGhostCellSuggestions is set to true, but according to the config file (config.ts line 38), the actual default is false. This mismatch means the UI will show an incorrect state when the user hasn't explicitly set a value. Change the default to false to match the configuration default.

Suggested change
const globalGhostCellSuggestions = configurationService.getValue<boolean>(POSITRON_NOTEBOOK_GHOST_CELL_SUGGESTIONS_KEY) ?? true;
const globalGhostCellSuggestions = configurationService.getValue<boolean>(POSITRON_NOTEBOOK_GHOST_CELL_SUGGESTIONS_KEY) ?? false;

Copilot uses AI. Check for mistakes.
Comment on lines +377 to +378
// Setting enabled to false marks the user's explicit choice
// Use undefined to remove when setting matches default (false)
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says "Setting enabled to false marks the user's explicit choice" but the code actually sets the value to undefined (line 381). The comment is misleading. Update the comment to accurately reflect that setting to undefined removes the user override and falls back to the default (false).

Suggested change
// Setting enabled to false marks the user's explicit choice
// Use undefined to remove when setting matches default (false)
// Remove user override so the setting falls back to its default (false/disabled)

Copilot uses AI. Check for mistakes.
Comment on lines +94 to +101
const resizeObserver = new ResizeObserver(checkTruncation);
if (spanRef.current) {
resizeObserver.observe(spanRef.current);
}

return () => resizeObserver.disconnect();
}, [text, isExpanded]);

Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ResizeObserver created on line 94 is not properly cleaned up when the element changes. If spanRef.current changes between renders, the observer will continue observing the old element. Save the observed element in the effect and only disconnect/observe when the element reference actually changes. Also, the effect dependencies should include spanRef.current to handle ref changes.

Suggested change
const resizeObserver = new ResizeObserver(checkTruncation);
if (spanRef.current) {
resizeObserver.observe(spanRef.current);
}
return () => resizeObserver.disconnect();
}, [text, isExpanded]);
const currentSpan = spanRef.current;
if (!currentSpan) {
return;
}
const resizeObserver = new ResizeObserver(checkTruncation);
resizeObserver.observe(currentSpan);
return () => {
resizeObserver.unobserve(currentSpan);
resizeObserver.disconnect();
};
}, [text, isExpanded, spanRef.current]);

Copilot uses AI. Check for mistakes.
tabIndex={disabled ? -1 : 0}
title={dropdownTooltip}
onKeyDown={(e) => {
if ((e.key === 'Enter' || e.key === ' ') && !disabled) {
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The dropdown button handles keyboard events for Enter and Space keys but doesn't prevent the default browser behavior. For Space key specifically, this could cause the page to scroll when activating the dropdown. Add e.preventDefault() inside the keyboard handler to prevent unwanted default behaviors.

Suggested change
if ((e.key === 'Enter' || e.key === ' ') && !disabled) {
if ((e.key === 'Enter' || e.key === ' ') && !disabled) {
e.preventDefault();

Copilot uses AI. Check for mistakes.
Comment on lines 78 to 79
const assistant = positron?.assistant as Record<string, unknown> | undefined;

Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The metadata access path has changed from metadata?.positron to metadata?.metadata?.positron. This is a breaking change that affects how metadata is read from notebooks. The comment explains this is to match ipynb serialization, but existing notebooks with settings stored at the old path (metadata.positron) will lose their assistant settings after this change. Consider adding migration logic to read from the old path as a fallback to preserve existing user settings.

Suggested change
const assistant = positron?.assistant as Record<string, unknown> | undefined;
let assistant = positron?.assistant as Record<string, unknown> | undefined;
// Fallback for legacy metadata location: metadata.positron.assistant
if (!assistant && metadata) {
const legacyRoot = metadata as unknown as Record<string, unknown>;
const legacyPositron = legacyRoot.positron as Record<string, unknown> | undefined;
assistant = legacyPositron?.assistant as Record<string, unknown> | undefined;
}

Copilot uses AI. Check for mistakes.
metadata: newMetadata
}], true, undefined, () => undefined, undefined, true);

commandService.executeCommand('workbench.action.files.save');
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The updateNotebookSettings helper automatically saves the file after updating metadata (line 520), but this happens synchronously without waiting for the save to complete or handling potential save failures. The fire-and-forget executeCommand call could fail silently. Consider awaiting the save command and handling errors, or at least logging if the save fails to give users feedback when their settings changes don't persist.

Copilot uses AI. Check for mistakes.
Comment on lines +102 to +106
// Type narrowing for cell execution events
const cellEvent = event;
if (cellEvent.type !== NotebookExecutionType.cell) {
return;
}
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type narrowing check on line 104 is redundant since line 98 already checks that event.type !== NotebookExecutionType.cell and returns early. The additional check on line 104 can never be true due to the earlier guard. Remove this redundant type check.

Suggested change
// Type narrowing for cell execution events
const cellEvent = event;
if (cellEvent.type !== NotebookExecutionType.cell) {
return;
}
// Type narrowing for cell execution events (already ensured by the guard above)
const cellEvent = event;

Copilot uses AI. Check for mistakes.
Comment on lines +124 to +125
item.mime === 'application/vnd.code.notebook.error' ||
item.mime === 'application/vnd.code.notebook.stderr'
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error detection logic treats stderr output as an error (line 125), but stderr is commonly used for warnings, progress bars, or informational messages in data science workflows, not just errors. This could lead to inappropriate ghost cell suggestions after non-error stderr output. Consider checking for actual error mime types only (application/vnd.code.notebook.error) or examining the execution result status more carefully.

Suggested change
item.mime === 'application/vnd.code.notebook.error' ||
item.mime === 'application/vnd.code.notebook.stderr'
item.mime === 'application/vnd.code.notebook.error'

Copilot uses AI. Check for mistakes.
Comment on lines +148 to +153
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => reject(new Error('Ghost cell suggestion timed out')), timeoutMs);
});

const responsePromise = model.sendRequest([systemMessage, userMessage], {}, token);
const response = await Promise.race([responsePromise, timeoutPromise]);
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The timeout mechanism creates a Promise that rejects after 30 seconds but doesn't clear the setTimeout timer if the race completes early. This means the timer continues running unnecessarily and the error handler closure stays in memory. Store the timeout ID and clear it in a finally block or when the race completes.

Suggested change
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => reject(new Error('Ghost cell suggestion timed out')), timeoutMs);
});
const responsePromise = model.sendRequest([systemMessage, userMessage], {}, token);
const response = await Promise.race([responsePromise, timeoutPromise]);
let timeoutId: ReturnType<typeof setTimeout>;
const timeoutPromise = new Promise<never>((_, reject) => {
timeoutId = setTimeout(() => reject(new Error('Ghost cell suggestion timed out')), timeoutMs);
});
const responsePromise = model.sendRequest([systemMessage, userMessage], {}, token);
const response = await (async () => {
try {
return await Promise.race([responsePromise, timeoutPromise]);
} finally {
clearTimeout(timeoutId);
}
})();

Copilot uses AI. Check for mistakes.
Copy link
Contributor

@seeM seeM left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes are looking good! This seems good to merge but still have a few suggestions from the previous batch and some new:

  1. Top padding of description and buttons shouldn't shift down when expanding description
  2. Not sure how to re-enable for notebook after "Don't suggest in this notebook again"
  3. Thoughts on removing the hover animation - make instant?
  4. Add hover state to the selected (white) toggle button
  5. Remove Dismiss border on hover
  6. Lighter color for Accept button vertical separator
  7. 12px font for model indicator

Future:

  1. Add a way to accept & run via keyboard
  2. Can it make suggestions at any point in the notebook - not only the end?
  3. Could we update the notebooks rule with a short instruction about notebook editor extensions/contributions?

* Hook to access the GhostCellController contribution from the current notebook instance.
* Must be used within a NotebookInstanceProvider.
*/
export function useGhostCellController(): GhostCellController {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah this is neat

@@ -0,0 +1,677 @@
/*---------------------------------------------------------------------------------------------
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thoughts on moving this to contrib/ghostCell as well?

@@ -30,8 +30,10 @@ import { IConfigurationService } from '../../../../../platform/configuration/com
import { IChatEditingService, IModifiedFileEntry, ModifiedFileEntryState } from '../../../chat/common/chatEditingService.js';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could also move to assistant notebook contribution?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants