inline chat input: clamp widget to viewport when scrolling with content (#291390)

When the input has content, the widget now stays visible and clamps to viewport edges (respecting sticky scroll height) instead of hiding when the anchor line scrolls out of view.

Fixes https://github.com/microsoft/vscode/issues/291075
This commit is contained in:
Johannes Rieken
2026-01-28 20:23:10 +00:00
committed by GitHub
parent 76749211a7
commit bddac8596e
2 changed files with 28 additions and 8 deletions
@@ -51,15 +51,15 @@ export class InlineChatInputWidget extends Disposable {
private readonly _input: IActiveCodeEditor;
private readonly _position = observableValue<IOverlayWidgetPosition | null>(this, null);
readonly position: IObservable<IOverlayWidgetPosition | null> = this._position;
readonly minContentWidthInPx = constObservable(0);
private readonly _showStore = this._store.add(new DisposableStore());
private readonly _stickyScrollHeight: IObservable<number>;
private _inlineStartAction: IAction | undefined;
private _anchorLineNumber: number = 0;
private _anchorLeft: number = 0;
private _anchorAbove: boolean = false;
readonly allowEditorOverflow = true;
constructor(
private readonly _editorObs: ObservableCodeEditor,
@@ -108,6 +108,10 @@ export class InlineChatInputWidget extends Disposable {
this._input.setModel(model);
this._input.layout({ width: 200, height: 18 });
// Initialize sticky scroll height observable
const stickyScrollController = StickyScrollController.get(this._editorObs.editor);
this._stickyScrollHeight = stickyScrollController ? observableFromEvent(stickyScrollController.onDidChangeStickyScrollHeight, () => stickyScrollController.stickyScrollWidgetHeight) : constObservable(0);
// Update placeholder based on selection state
this._store.add(autorun(r => {
const selection = this._editorObs.cursorSelection.read(r);
@@ -216,8 +220,8 @@ export class InlineChatInputWidget extends Disposable {
this._showStore.add(this._editorObs.createOverlayWidget({
domNode: this._domNode,
position: this._position,
minContentWidthInPx: this.minContentWidthInPx,
allowEditorOverflow: this.allowEditorOverflow,
minContentWidthInPx: constObservable(0),
allowEditorOverflow: true,
}));
// If anchoring above, adjust position after render to account for widget height
@@ -225,13 +229,14 @@ export class InlineChatInputWidget extends Disposable {
this._updatePosition();
}
// Update position on scroll, hide if anchor line is out of view
// Update position on scroll, hide if anchor line is out of view (only when input is empty)
this._showStore.add(this._editorObs.editor.onDidScrollChange(() => {
const visibleRanges = this._editorObs.editor.getVisibleRanges();
const isLineVisible = visibleRanges.some(range =>
this._anchorLineNumber >= range.startLineNumber && this._anchorLineNumber <= range.endLineNumber
);
if (!isLineVisible) {
const hasContent = !!this._input.getModel().getValue();
if (!isLineVisible && !hasContent) {
this._hide();
} else {
this._updatePosition();
@@ -244,6 +249,7 @@ export class InlineChatInputWidget extends Disposable {
private _updatePosition(): void {
const editor = this._editorObs.editor;
const lineHeight = editor.getOption(EditorOption.lineHeight);
const top = editor.getTopForLineNumber(this._anchorLineNumber) - editor.getScrollTop();
let adjustedTop = top;
@@ -251,12 +257,22 @@ export class InlineChatInputWidget extends Disposable {
const widgetHeight = this._domNode.offsetHeight;
adjustedTop = top - widgetHeight;
} else {
const lineHeight = editor.getOption(EditorOption.lineHeight);
adjustedTop = top + lineHeight;
}
// Clamp to viewport bounds when anchor line is out of view
const stickyScrollHeight = this._stickyScrollHeight.get();
const layoutInfo = editor.getLayoutInfo();
const widgetHeight = this._domNode.offsetHeight;
const minTop = stickyScrollHeight;
const maxTop = layoutInfo.height - widgetHeight;
const clampedTop = Math.max(minTop, Math.min(adjustedTop, maxTop));
const isClamped = clampedTop !== adjustedTop;
this._domNode.classList.toggle('clamped', isClamped);
this._position.set({
preference: { top: adjustedTop, left: this._anchorLeft },
preference: { top: clampedTop, left: this._anchorLeft },
stackOrdinal: 10000,
}, undefined);
}
@@ -3,6 +3,10 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.inline-chat-gutter-menu.clamped {
transition: top 100ms;
}
.inline-chat-gutter-menu .input .monaco-editor-background {
background-color: var(--vscode-menu-background);
}