Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2201,7 +2201,7 @@ export class ChatWidget extends Disposable implements IChatWidget {
this.inlineInputPart?.layout(width);
}

this.inputPart.layout(width);
this.inputPart.layout(width, height);

const inputHeight = this.inputPart.height.get();
const chatSuggestNextWidgetHeight = this.chatSuggestNextWidget.height;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { ActionViewItem, BaseActionViewItem, IActionViewItemOptions } from '../.
import * as aria from '../../../../../../base/browser/ui/aria/aria.js';
import { ButtonWithIcon } from '../../../../../../base/browser/ui/button/button.js';
import { createInstantHoverDelegate } from '../../../../../../base/browser/ui/hover/hoverDelegateFactory.js';
import { IHorizontalSashLayoutProvider, Orientation, Sash } from '../../../../../../base/browser/ui/sash/sash.js';
import { renderLabelWithIcons } from '../../../../../../base/browser/ui/iconLabel/iconLabels.js';
import { IAction } from '../../../../../../base/common/actions.js';
import { equals as arraysEqual } from '../../../../../../base/common/arrays.js';
Expand Down Expand Up @@ -123,6 +124,7 @@ import { WorkspacePickerActionItem } from './workspacePickerActionItem.js';
const $ = dom.$;

const INPUT_EDITOR_MAX_HEIGHT = 250;
const INPUT_EDITOR_MIN_HEIGHT = 30;
const CachedLanguageModelsKey = 'chat.cachedLanguageModels.v2';

export interface IChatInputStyles {
Expand Down Expand Up @@ -252,6 +254,11 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
private inputEditorHeight: number = 0;
private container!: HTMLElement;

private _sash: Sash | undefined;
private _sashStartHeight: number | undefined;
private _userSetMinHeight: number | undefined;
private _containerHeight: number | undefined;

private inputSideToolbarContainer?: HTMLElement;

private followupsContainer!: HTMLElement;
Expand Down Expand Up @@ -1726,6 +1733,9 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
// This isolates chatSessionOption.* context keys to this specific chat input instance
this._scopedContextKeyService = this._register(this.contextKeyService.createScoped(this.container));

// Create the resize sash for the input part
this._createSash(this.container);

this.followupsContainer = elements.followupsContainer;
const inputAndSideToolbar = elements.inputAndSideToolbar; // The chat input and toolbar to the right
const inputContainer = elements.inputContainer; // The chat editor, attachments, and toolbars
Expand Down Expand Up @@ -1829,7 +1839,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
}));

this._register(this._inputEditor.onDidChangeModelContent(() => {
const currentHeight = Math.min(this._inputEditor.getContentHeight(), this.inputEditorMaxHeight);
const contentHeight = Math.min(this._inputEditor.getContentHeight(), this._getMaxAllowedHeight());
const currentHeight = Math.max(contentHeight, this._getEffectiveMinHeight());
Comment on lines +1842 to +1843
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

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

The height calculation logic needs to be consistent. While the onDidChangeModelContent handler now correctly applies constraints using _getMaxAllowedHeight() and _getEffectiveMinHeight(), the onDidContentSizeChange handler at line 1860 (outside this diff) sets height to e.contentHeight directly without these constraints. This could cause inconsistent behavior between the two handlers.

Copilot uses AI. Check for mistakes.
if (currentHeight !== this.inputEditorHeight) {
this.inputEditorHeight = currentHeight;
// Directly update editor layout - ResizeObserver will notify parent about height change
Expand Down Expand Up @@ -2647,9 +2658,14 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
/**
* Layout the input part with the given width. Height is intrinsic - determined by content
* and detected via ResizeObserver, which updates `inputPartHeight` for the parent to observe.
* @param width The width of the input part
* @param containerHeight The total height available in the chat widget container, used to constrain sash resizing
*/
layout(width: number) {
layout(width: number, containerHeight?: number) {
this.cachedWidth = width;
if (containerHeight !== undefined) {
this._containerHeight = containerHeight;
}

return this._layout(width);
}
Expand All @@ -2663,7 +2679,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge

const initialEditorScrollWidth = this._inputEditor.getScrollWidth();
const newEditorWidth = width - data.inputPartHorizontalPadding - data.editorBorder - data.inputPartHorizontalPaddingInside - data.toolbarsWidth - data.sideToolbarWidth;
const inputEditorHeight = Math.min(this._inputEditor.getContentHeight(), this.inputEditorMaxHeight);
const contentHeight = Math.min(this._inputEditor.getContentHeight(), this._getMaxAllowedHeight());
const inputEditorHeight = Math.max(contentHeight, this._getEffectiveMinHeight());
const newDimension = { width: newEditorWidth, height: inputEditorHeight };
if (!this.previousInputEditorDimension || (this.previousInputEditorDimension.width !== newDimension.width || this.previousInputEditorDimension.height !== newDimension.height)) {
// This layout call has side-effects that are hard to understand. eg if we are calling this inside a onDidChangeContent handler, this can trigger the next onDidChangeContent handler
Expand Down Expand Up @@ -2708,6 +2725,65 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
sideToolbarWidth: inputSideToolbarWidth > 0 ? inputSideToolbarWidth + 4 /*gap*/ : 0,
};
}

private _createSash(container: HTMLElement): void {
const layoutProvider: IHorizontalSashLayoutProvider = {
getHorizontalSashTop: () => 0
};

this._sash = this._register(new Sash(container, layoutProvider, { orientation: Orientation.HORIZONTAL }));

this._register(this._sash.onDidStart(() => {
this._sashStartHeight = this._getEffectiveMinHeight();
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

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

The sash start height should be set to the current editor height, not the effective minimum height. When dragging starts, the calculation should be based on where the editor currently is, not just the user-set minimum. This should be this._sashStartHeight = this.inputEditorHeight; to match the test logic and expected behavior.

Suggested change
this._sashStartHeight = this._getEffectiveMinHeight();
this._sashStartHeight = this.inputEditorHeight;

Copilot uses AI. Check for mistakes.
}));

this._register(this._sash.onDidChange((e) => {
if (this._sashStartHeight === undefined) {
return;
}

// Dragging up (currentY decreasing) should increase the editor height
const delta = e.startY - e.currentY;
const newHeight = Math.max(
INPUT_EDITOR_MIN_HEIGHT,
Math.min(this._sashStartHeight + delta, this._getMaxAllowedHeight())
);

this._userSetMinHeight = newHeight;

// Re-layout to apply the new height
if (this.cachedWidth) {
this._layout(this.cachedWidth);
}
}));

this._register(this._sash.onDidEnd(() => {
this._sashStartHeight = undefined;
}));

this._register(this._sash.onDidReset(() => {
// Double-click resets to default height
this._userSetMinHeight = undefined;

// Re-layout to apply the default height
if (this.cachedWidth) {
this._layout(this.cachedWidth);
}
}));
}

private _getEffectiveMinHeight(): number {
return this._userSetMinHeight ?? 0;
}

private _getMaxAllowedHeight(): number {
if (this._containerHeight !== undefined) {
return this._containerHeight;
}

// Fallback to a sensible fixed maximum
return this.inputEditorMaxHeight * 3;
}
}


Expand Down
5 changes: 5 additions & 0 deletions src/vs/workbench/contrib/chat/browser/widget/media/chat.css
Original file line number Diff line number Diff line change
Expand Up @@ -1509,6 +1509,11 @@ have to be updated for changes to the rules above, or to support more deeply nes
position: relative;
}

.interactive-session .interactive-input-part > .monaco-sash {
/* Position the sash at the top of the input part */
top: 0;
}

.interactive-session .interactive-input-part.compact {
margin: 0;
padding: 8px 0 0 0
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import assert from 'assert';
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js';

/**
* Unit tests for the sash resize logic used in ChatInputPart.
* These tests verify the mathematical constraints for editor height resizing.
*
* The actual sash creation and DOM integration is tested through manual testing
* since ChatInputPart has many dependencies that make integration testing complex.
*/
suite('ChatInputPart Sash Resize Logic', () => {

ensureNoDisposablesAreLeakedInTestSuite();

const INPUT_EDITOR_MAX_HEIGHT = 250;
const INPUT_EDITOR_MIN_HEIGHT = 50;

/**
* Simulates the height calculation logic used in the sash's onDidChange handler.
*/
function calculateNewHeight(startHeight: number, startY: number, currentY: number, maxAllowed: number): number {
const delta = startY - currentY;
return Math.max(
INPUT_EDITOR_MIN_HEIGHT,
Math.min(startHeight + delta, maxAllowed)
);
}

test('dragging up increases editor height', () => {
const startHeight = INPUT_EDITOR_MAX_HEIGHT;
const startY = 200;
const currentY = 150; // Drag up by 50px
const maxAllowed = 750;

const newHeight = calculateNewHeight(startHeight, startY, currentY, maxAllowed);
assert.strictEqual(newHeight, 300, 'Dragging up by 50px should increase height by 50');
});

test('dragging down decreases editor height', () => {
const startHeight = INPUT_EDITOR_MAX_HEIGHT;
const startY = 200;
const currentY = 230; // Drag down by 30px
const maxAllowed = 750;

const newHeight = calculateNewHeight(startHeight, startY, currentY, maxAllowed);
assert.strictEqual(newHeight, 220, 'Dragging down by 30px should decrease height by 30');
});

test('height respects minimum constraint', () => {
const startHeight = INPUT_EDITOR_MAX_HEIGHT;
const startY = 200;
const currentY = 500; // Drag down a lot
const maxAllowed = 750;

const newHeight = calculateNewHeight(startHeight, startY, currentY, maxAllowed);
assert.strictEqual(newHeight, INPUT_EDITOR_MIN_HEIGHT, 'Height should not go below minimum');
});

test('height respects maximum constraint', () => {
const startHeight = INPUT_EDITOR_MAX_HEIGHT;
const startY = 200;
const currentY = -400; // Drag up a lot
const maxAllowed = 500;

const newHeight = calculateNewHeight(startHeight, startY, currentY, maxAllowed);
assert.strictEqual(newHeight, maxAllowed, 'Height should not exceed maximum allowed');
});

test('no change when currentY equals startY', () => {
const startHeight = INPUT_EDITOR_MAX_HEIGHT;
const startY = 200;
const currentY = 200; // No movement
const maxAllowed = 750;

const newHeight = calculateNewHeight(startHeight, startY, currentY, maxAllowed);
assert.strictEqual(newHeight, startHeight, 'Height should remain unchanged when no drag occurs');
});

/**
* Simulates the editor height calculation used in _layout.
* The editor height should be the content height (capped at max allowed),
* but at least the user-set minimum height from sash dragging.
*/
function calculateEditorHeight(contentHeight: number, maxAllowed: number, userSetMinHeight: number | undefined): number {
const cappedContent = Math.min(contentHeight, maxAllowed);
const effectiveMin = userSetMinHeight ?? 0;
return Math.max(cappedContent, effectiveMin);
}

test('editor height respects user-set minimum from sash', () => {
const contentHeight = 100; // Small content
const maxAllowed = 750;
const userSetMinHeight = 300; // User dragged sash to make it taller

const editorHeight = calculateEditorHeight(contentHeight, maxAllowed, userSetMinHeight);
assert.strictEqual(editorHeight, userSetMinHeight, 'Editor should be at least as tall as user-set minimum');
});

test('editor height grows with content beyond user-set minimum', () => {
const contentHeight = 400; // Large content
const maxAllowed = 750;
const userSetMinHeight = 200; // User set a smaller minimum

const editorHeight = calculateEditorHeight(contentHeight, maxAllowed, userSetMinHeight);
assert.strictEqual(editorHeight, contentHeight, 'Editor should grow with content when larger than minimum');
});

test('editor height respects max allowed even with large content', () => {
const contentHeight = 1000; // Very large content
const maxAllowed = 750;
const userSetMinHeight = 200;

const editorHeight = calculateEditorHeight(contentHeight, maxAllowed, userSetMinHeight);
assert.strictEqual(editorHeight, maxAllowed, 'Editor should not exceed max allowed');
});
});
Loading