diff --git a/src/@types/vscode.proposed.chatParticipantPrivate.d.ts b/src/@types/vscode.proposed.chatParticipantPrivate.d.ts index 32d02e5104..7333553509 100644 --- a/src/@types/vscode.proposed.chatParticipantPrivate.d.ts +++ b/src/@types/vscode.proposed.chatParticipantPrivate.d.ts @@ -50,12 +50,6 @@ declare module 'vscode' { constructor(cell: TextDocument); } - export interface ChatRequestSessionGrouping { - readonly id: string; - readonly order: number; - readonly kind?: string; - } - export interface ChatRequest { /** * The id of the chat request. Used to identity an interaction with any of the chat surfaces. @@ -122,11 +116,6 @@ declare module 'vscode' { */ readonly parentRequestId?: string; - /** - * Optional metadata used to group related requests together in the UI. - */ - readonly sessionGrouping?: ChatRequestSessionGrouping; - /** * The permission level for tool auto-approval in this request. * - `'autoApprove'`: Auto-approve all tool calls and retry on errors. @@ -199,10 +188,15 @@ declare module 'vscode' { */ readonly modelId?: string; + /** + * The mode instructions that were active for this request, if any. + */ + readonly modeInstructions2?: ChatRequestModeInstructions; + /** * @hidden */ - constructor(prompt: string, command: string | undefined, references: ChatPromptReference[], participant: string, toolReferences: ChatLanguageModelToolReference[], editedFileEvents: ChatRequestEditedFileEvent[] | undefined, id: string | undefined, modelId: string | undefined); + constructor(prompt: string, command: string | undefined, references: ChatPromptReference[], participant: string, toolReferences: ChatLanguageModelToolReference[], editedFileEvents: ChatRequestEditedFileEvent[] | undefined, id: string | undefined, modelId: string | undefined, modeInstructions2: ChatRequestModeInstructions | undefined); } export class ChatResponseTurn2 { diff --git a/src/test/view/treeNodes/directoryTreeNode.test.ts b/src/test/view/treeNodes/directoryTreeNode.test.ts new file mode 100644 index 0000000000..4677ee22b3 --- /dev/null +++ b/src/test/view/treeNodes/directoryTreeNode.test.ts @@ -0,0 +1,201 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import { default as assert } from 'assert'; +import { DirectoryTreeNode } from '../../../view/treeNodes/directoryTreeNode'; +import { TreeNode, TreeNodeParent } from '../../../view/treeNodes/treeNode'; + +/** + * Minimal mock for a file-like child node that supports checkboxState. + * This is NOT a DirectoryTreeNode so allChildrenViewed() treats it as a leaf file. + */ +class MockFileNode extends TreeNode { + public checkboxState?: { state: vscode.TreeItemCheckboxState; tooltip?: string; accessibilityInformation?: vscode.AccessibilityInformation }; + constructor(parent: TreeNodeParent) { + super(parent); + } + getTreeItem(): vscode.TreeItem { + return this; + } +} + +function createMockParent(): TreeNodeParent { + return { + refresh: () => { }, + reveal: () => Promise.resolve(), + children: undefined, + view: {} as any + } as any; +} + +describe('DirectoryTreeNode', function () { + describe('allChildrenViewed', function () { + it('returns true when all file children are checked', function () { + const dirNode = new DirectoryTreeNode(createMockParent(), 'src'); + + const file1 = new MockFileNode(dirNode); + file1.checkboxState = { state: vscode.TreeItemCheckboxState.Checked }; + const file2 = new MockFileNode(dirNode); + file2.checkboxState = { state: vscode.TreeItemCheckboxState.Checked }; + + (dirNode._children as any[]).push(file1, file2); + + assert.strictEqual(dirNode.allChildrenViewed(), true); + }); + + it('returns false when some file children are unchecked', function () { + const dirNode = new DirectoryTreeNode(createMockParent(), 'src'); + + const file1 = new MockFileNode(dirNode); + file1.checkboxState = { state: vscode.TreeItemCheckboxState.Checked }; + const file2 = new MockFileNode(dirNode); + file2.checkboxState = { state: vscode.TreeItemCheckboxState.Unchecked }; + + (dirNode._children as any[]).push(file1, file2); + + assert.strictEqual(dirNode.allChildrenViewed(), false); + }); + + it('returns false when a file child has no checkboxState', function () { + const dirNode = new DirectoryTreeNode(createMockParent(), 'src'); + + const file1 = new MockFileNode(dirNode); + file1.checkboxState = { state: vscode.TreeItemCheckboxState.Checked }; + const file2 = new MockFileNode(dirNode); + // file2 has no checkboxState + + (dirNode._children as any[]).push(file1, file2); + + assert.strictEqual(dirNode.allChildrenViewed(), false); + }); + + it('returns true when nested directories have all children checked', function () { + const parentDir = new DirectoryTreeNode(createMockParent(), 'src'); + const childDir = new DirectoryTreeNode(parentDir, 'utils'); + + const file1 = new MockFileNode(childDir); + file1.checkboxState = { state: vscode.TreeItemCheckboxState.Checked }; + + (childDir._children as any[]).push(file1); + parentDir._children.push(childDir); + + assert.strictEqual(parentDir.allChildrenViewed(), true); + }); + + it('returns false when nested directories have unchecked children', function () { + const parentDir = new DirectoryTreeNode(createMockParent(), 'src'); + const childDir = new DirectoryTreeNode(parentDir, 'utils'); + + const file1 = new MockFileNode(childDir); + file1.checkboxState = { state: vscode.TreeItemCheckboxState.Unchecked }; + + (childDir._children as any[]).push(file1); + parentDir._children.push(childDir); + + assert.strictEqual(parentDir.allChildrenViewed(), false); + }); + + it('returns true when empty directory has no children', function () { + const dirNode = new DirectoryTreeNode(createMockParent(), 'empty'); + assert.strictEqual(dirNode.allChildrenViewed(), true); + }); + }); + + describe('updateCheckboxFromChildren', function () { + it('sets checkbox to Checked when all children are viewed', function () { + const dirNode = new DirectoryTreeNode(createMockParent(), 'src'); + + const file1 = new MockFileNode(dirNode); + file1.checkboxState = { state: vscode.TreeItemCheckboxState.Checked }; + + (dirNode._children as any[]).push(file1); + + dirNode.updateCheckboxFromChildren(); + + assert.strictEqual(dirNode.checkboxState?.state, vscode.TreeItemCheckboxState.Checked); + }); + + it('sets checkbox to Unchecked when not all children are viewed', function () { + const dirNode = new DirectoryTreeNode(createMockParent(), 'src'); + + const file1 = new MockFileNode(dirNode); + file1.checkboxState = { state: vscode.TreeItemCheckboxState.Checked }; + const file2 = new MockFileNode(dirNode); + file2.checkboxState = { state: vscode.TreeItemCheckboxState.Unchecked }; + + (dirNode._children as any[]).push(file1, file2); + + dirNode.updateCheckboxFromChildren(); + + assert.strictEqual(dirNode.checkboxState?.state, vscode.TreeItemCheckboxState.Unchecked); + }); + + it('propagates state through nested directories', function () { + const parentDir = new DirectoryTreeNode(createMockParent(), 'src'); + const childDir = new DirectoryTreeNode(parentDir, 'utils'); + + const file1 = new MockFileNode(childDir); + file1.checkboxState = { state: vscode.TreeItemCheckboxState.Checked }; + + (childDir._children as any[]).push(file1); + parentDir._children.push(childDir); + + // Update bottom-up (child first, then parent) + childDir.updateCheckboxFromChildren(); + assert.strictEqual(childDir.checkboxState?.state, vscode.TreeItemCheckboxState.Checked); + + parentDir.updateCheckboxFromChildren(); + assert.strictEqual(parentDir.checkboxState?.state, vscode.TreeItemCheckboxState.Checked); + }); + + it('updates parent to Unchecked when a child is unchecked after being checked', function () { + const parentDir = new DirectoryTreeNode(createMockParent(), 'src'); + + const file1 = new MockFileNode(parentDir); + file1.checkboxState = { state: vscode.TreeItemCheckboxState.Checked }; + const file2 = new MockFileNode(parentDir); + file2.checkboxState = { state: vscode.TreeItemCheckboxState.Checked }; + + (parentDir._children as any[]).push(file1, file2); + + // All checked → parent should be Checked + parentDir.updateCheckboxFromChildren(); + assert.strictEqual(parentDir.checkboxState?.state, vscode.TreeItemCheckboxState.Checked); + + // Uncheck one file → parent should be Unchecked + file2.checkboxState = { state: vscode.TreeItemCheckboxState.Unchecked }; + parentDir.updateCheckboxFromChildren(); + assert.strictEqual(parentDir.checkboxState?.state, vscode.TreeItemCheckboxState.Unchecked); + }); + + it('updates correctly with mixed file and directory children', function () { + const rootDir = new DirectoryTreeNode(createMockParent(), 'root'); + const subDir = new DirectoryTreeNode(rootDir, 'sub'); + + const subFile = new MockFileNode(subDir); + subFile.checkboxState = { state: vscode.TreeItemCheckboxState.Checked }; + (subDir._children as any[]).push(subFile); + + const rootFile = new MockFileNode(rootDir); + rootFile.checkboxState = { state: vscode.TreeItemCheckboxState.Checked }; + + (rootDir._children as any[]).push(subDir); + (rootDir._children as any[]).push(rootFile); + + // Update bottom-up + subDir.updateCheckboxFromChildren(); + rootDir.updateCheckboxFromChildren(); + assert.strictEqual(rootDir.checkboxState?.state, vscode.TreeItemCheckboxState.Checked); + + // Uncheck the sub-directory file + subFile.checkboxState = { state: vscode.TreeItemCheckboxState.Unchecked }; + subDir.updateCheckboxFromChildren(); + rootDir.updateCheckboxFromChildren(); + assert.strictEqual(subDir.checkboxState?.state, vscode.TreeItemCheckboxState.Unchecked); + assert.strictEqual(rootDir.checkboxState?.state, vscode.TreeItemCheckboxState.Unchecked); + }); + }); +}); diff --git a/src/view/treeNodes/directoryTreeNode.ts b/src/view/treeNodes/directoryTreeNode.ts index b24e8e9b75..f1926739f5 100644 --- a/src/view/treeNodes/directoryTreeNode.ts +++ b/src/view/treeNodes/directoryTreeNode.ts @@ -131,12 +131,16 @@ export class DirectoryTreeNode extends TreeNode implements vscode.TreeItem { return true; } - private setCheckboxState(isChecked: boolean) { + public setCheckboxState(isChecked: boolean) { this.checkboxState = isChecked ? { state: vscode.TreeItemCheckboxState.Checked, tooltip: vscode.l10n.t('Mark all files unviewed'), accessibilityInformation: { label: vscode.l10n.t('Mark all files in folder {0} as unviewed', this.label!) } } : { state: vscode.TreeItemCheckboxState.Unchecked, tooltip: vscode.l10n.t('Mark all files viewed'), accessibilityInformation: { label: vscode.l10n.t('Mark all files in folder {0} as viewed', this.label!) } }; } + public updateCheckboxFromChildren(): void { + this.setCheckboxState(this.allChildrenViewed()); + } + getTreeItem(): vscode.TreeItem { this.setCheckboxState(this.allChildrenViewed()); return this; diff --git a/src/view/treeNodes/treeUtils.ts b/src/view/treeNodes/treeUtils.ts index 2c318aa7f0..bd3a0fe7da 100644 --- a/src/view/treeNodes/treeUtils.ts +++ b/src/view/treeNodes/treeUtils.ts @@ -46,9 +46,21 @@ export namespace TreeUtils { } } + // Eagerly update ancestor directory checkbox states by walking from each + // affected file up through its parent directories. With manageCheckboxStateManually, + // we must set directory states before refresh rather than relying solely on + // getTreeItem(), since VS Code may not apply checkboxState changes during a refresh. + const allAffected = [...checkedNodes, ...uncheckedNodes]; + for (const node of allAffected) { + let parent = node.getParent(); + while (parent instanceof DirectoryTreeNode) { + parent.updateCheckboxFromChildren(); + parent = parent.getParent(); + } + } + // Refresh the tree so checkbox visual state updates. // Refreshing the topmost affected directory will cascade to all descendants. - const allAffected = [...checkedNodes, ...uncheckedNodes]; const refreshedDirs = new Set(); for (const node of allAffected) { let topDir: DirectoryTreeNode | undefined;