diff --git a/.github/hooks/hooks.json b/.github/hooks/hooks.json new file mode 100644 index 0000000000000..59c170e420e01 --- /dev/null +++ b/.github/hooks/hooks.json @@ -0,0 +1,29 @@ +{ + "version": 1, + "hooks": { + "sessionStart": [ + { + "type": "command", + "bash": "if [ -f ~/.vscode-worktree-setup ]; then nohup npm ci > /tmp/npm-ci-$(date +%Y-%m-%d_%H-%M-%S).log 2>&1 & fi" + } + ], + "userPromptSubmitted": [ + { + "type": "command", + "bash": "" + } + ], + "preToolUse": [ + { + "type": "command", + "bash": "" + } + ], + "postToolUse": [ + { + "type": "command", + "bash": "" + } + ] + } +} \ No newline at end of file diff --git a/src/vs/sessions/contrib/aiCustomizationTreeView/browser/aiCustomizationOverviewView.ts b/src/vs/sessions/contrib/aiCustomizationTreeView/browser/aiCustomizationOverviewView.ts index 7e335ef732616..bf959abffad05 100644 --- a/src/vs/sessions/contrib/aiCustomizationTreeView/browser/aiCustomizationOverviewView.ts +++ b/src/vs/sessions/contrib/aiCustomizationTreeView/browser/aiCustomizationOverviewView.ts @@ -27,7 +27,7 @@ import { AICustomizationManagementEditor } from '../../../../workbench/contrib/c import { agentIcon, instructionsIcon, promptIcon, skillIcon } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationIcons.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; import { IAICustomizationWorkspaceService } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js'; -import { IEditorService } from '../../../../workbench/services/editor/common/editorService.js'; +import { IEditorService, MODAL_GROUP } from '../../../../workbench/services/editor/common/editorService.js'; const $ = DOM.$; @@ -187,7 +187,7 @@ export class AICustomizationOverviewView extends ViewPane { private async openSection(sectionId: AICustomizationManagementSection): Promise { const input = AICustomizationManagementEditorInput.getOrCreate(); - const editor = await this.editorService.openEditor(input, { pinned: true }); + const editor = await this.editorService.openEditor(input, { pinned: true }, MODAL_GROUP); // Deep-link to the section if (editor instanceof AICustomizationManagementEditor) { diff --git a/src/vs/sessions/contrib/sessions/browser/customizationsToolbar.contribution.ts b/src/vs/sessions/contrib/sessions/browser/customizationsToolbar.contribution.ts index c5c913e179726..382c1b38051df 100644 --- a/src/vs/sessions/contrib/sessions/browser/customizationsToolbar.contribution.ts +++ b/src/vs/sessions/contrib/sessions/browser/customizationsToolbar.contribution.ts @@ -30,7 +30,7 @@ import { ISessionsManagementService } from './sessionsManagementService.js'; import { Button } from '../../../../base/browser/ui/button/button.js'; import { defaultButtonStyles } from '../../../../platform/theme/browser/defaultStyles.js'; import { getSourceCounts, getSourceCountsTotal, ISourceCounts } from './customizationCounts.js'; -import { IEditorService } from '../../../../workbench/services/editor/common/editorService.js'; +import { IEditorService, MODAL_GROUP } from '../../../../workbench/services/editor/common/editorService.js'; import { IAICustomizationWorkspaceService } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js'; interface ICustomizationItemConfig { @@ -154,18 +154,28 @@ class CustomizationLinkViewItem extends ActionViewItem { this._updateCounts(); } + private _updateCountsRequestId = 0; + private async _updateCounts(): Promise { if (!this._countContainer) { return; } + const requestId = ++this._updateCountsRequestId; + if (this._config.promptType) { const type = this._config.promptType; const filter = this._workspaceService.getStorageSourceFilter(type); const counts = await getSourceCounts(this._promptsService, type, filter, this._workspaceContextService, this._workspaceService); + if (requestId !== this._updateCountsRequestId) { + return; + } this._renderSourceCounts(this._countContainer, counts); } else if (this._config.getCount) { const count = await this._config.getCount(this._languageModelsService, this._mcpService); + if (requestId !== this._updateCountsRequestId) { + return; + } this._renderSimpleCount(this._countContainer, count); } } @@ -244,7 +254,7 @@ class CustomizationsToolbarContribution extends Disposable implements IWorkbench async run(accessor: ServicesAccessor): Promise { const editorService = accessor.get(IEditorService); const input = AICustomizationManagementEditorInput.getOrCreate(); - const editor = await editorService.openEditor(input, { pinned: true }); + const editor = await editorService.openEditor(input, { pinned: true }, MODAL_GROUP); if (editor instanceof AICustomizationManagementEditor) { editor.selectSectionById(config.section); } diff --git a/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts b/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts index d53188c77bd1a..eb76bcf42f222 100644 --- a/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts +++ b/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts @@ -249,6 +249,10 @@ export class AgenticSessionsViewPane extends ViewPane { this.mcpService.servers.read(reader); updateHeaderTotalCount(); })); + this._register(autorun(reader => { + this.workspaceService.activeProjectRoot.read(reader); + updateHeaderTotalCount(); + })); updateHeaderTotalCount(); // Toggle collapse on header click diff --git a/src/vs/sessions/test/ai-customizations.test.md b/src/vs/sessions/test/ai-customizations.test.md index acc5ec8d364bf..75fa33d55de82 100644 --- a/src/vs/sessions/test/ai-customizations.test.md +++ b/src/vs/sessions/test/ai-customizations.test.md @@ -10,6 +10,10 @@ The following test plan outlines the scenarios and specifications for the AI Cus ### Scenario 1: Empty state — no session, no customizations +#### Description + +This tests the baseline empty state before any session or workspace is active. The 'new AI developer' state - who doesn't have any customizations on their machine yet. + #### Preconditions - On 'New Session' screen @@ -21,146 +25,183 @@ The following test plan outlines the scenarios and specifications for the AI Cus 1. Open the sidebar customizations section 2. Observe no sidebar counts are shown for any section (Agents, Skills, Instructions, Prompts, Hooks) 3. Open the management editor by clicking on one of the sections (e.g., "Instructions") -4. Observe the empty state message and -4. Click through each section in the sidebar +4. Observe the empty state messages +5. Click through each section in the sidebar +6. Run Developer: Customizations Debug and read the report #### Expected Results -- All sidebar counts show 0 (no badges visible) -- Management editor shows empty state for each section with "No X yet" message and create -- "Add" button is disabled or hidden (no active workspace to create in) -- User storage group is empty (no `~/.copilot/` or `~/.claude/` files) -- Extension storage group may show built-in extension items if Copilot extension contributes any +- All sidebar counts are hidden (no badges visible) +- Management editor shows empty state for each section with "No X yet" message +- Create button for **user** customizations is visible but disabled until a workspace folder or repository is selected (Hooks should also show a disabled button, since there is no 'user' scoped hooks) #### Notes -- This tests the baseline empty state before any session or workspace is active -- The `isSessionsWindow` flag should be `true`, verified via Developer: Customizations Debug +- The `Window: Sessions` should be verified by running `Developer: Customizations Debug` +- No workspace root should be active, verified via `Developer: Customizations Debug` (active root = none) --- -### Scenario 2: Active session with workspace customizations +### Scenario 2: Active workspace selected from new session state + +#### Description + +This tests the transition from the empty state to having an active workspace selected, but before a worktree is checked out (i.e., before starting a task). This is the 'new session' state where the user has selected a repository but hasn't started working in a specific branch or worktree yet. Customizations should be loaded from the repository root, not a worktree, and counts should reflect that. #### Preconditions -- Active session with a repository that has customizations (e.g., the `vscode` repo which has `.github/agents/`, `.github/skills/`, `.github/instructions/`, `.github/prompts/`) -- Session has a worktree checked out +- On 'New Session' screen (Scenario 1 completed) +- A git repository, cloned on the machine, is available to select + - For this test use `microsoft/vscode` cloned to a test folder #### Actions -1. Observe sidebar counts update after session becomes active -2. Open management editor, select "Instructions" section -3. Verify items listed match the workspace files -4. Run Developer: Customizations Debug and compare Stage 1 (raw data) with Stage 3 (widget state) -5. Select "Agents" section and verify agent count -6. Select "Prompts" section and verify skill-type commands are excluded +1. From the new session screen, select a workspace folder +2. Observe the sidebar customization counts update +3. Open the management editor by clicking on "Instructions" +4. Observe items appear in the "Workspace" group +5. Note the workspace item count in the group header +6. Compare the sidebar badge count with the editor's workspace item count — they should match +7. Click "Agents" in the sidebar +8. Observe agent items listed with parsed friendly names (not raw filenames) and a description +9. Click "Skills" in the sidebar +10. Observe skills listed with names derived from folder names +11. Click "Prompts" in the sidebar +12. Observe only prompt-type items (no skills mixed in, although note there may be similarly named items) +13. Click "Hooks" in the sidebar +14. Observe only workspace-scoped hook files (no user-level `~/.claude/settings.json`) +15. Run Developer: Customizations Debug and read the report #### Expected Results -- Sidebar counts match editor list counts for every section -- Instructions section includes AGENTS.md, CLAUDE.md, copilot-instructions.md (from `listAgentInstructions`) classified as "Workspace" -- Instructions section includes `.instructions.md` files from `.github/instructions/` -- Agents section uses `getCustomAgents()` — parsed names, not raw filenames (README.md excluded) -- Prompts section shows only prompt-type commands, not skill-type (skills have their own section) -- Hooks section shows only workspace-local hook files (user hooks filtered out by `sources: [local]`) -- Debug report shows `Window: Sessions`, `Active root: /path/to/worktree` -- Extension and plugin groups are not shown (sessions filter: `sources: [local, user]`) +- Sidebar counts update from 0 to reflect the selected workspace's customizations +- Sidebar badge count matches editor list count for every section +- Instructions includes root-level files (AGENTS.md, CLAUDE.md, copilot-instructions.md) under "Workspace" +- Instructions includes `.instructions.md` files from `.github/instructions/` +- Agents shows friendly names (e.g., "Optimize" not "optimize.agent.md") +- Prompts excludes skill-type slash commands +- Hooks shows only workspace-local files (filter: `sources: [local]`) +- No "Extensions" or "Plugins" groups visible +- If user-level files exist in `~/.copilot/` or `~/.claude/`, a "User" group appears for applicable sections +- Debug report shows `Window: Sessions`, `Active root: /path/to/repository` +- Create button shows both "Workspace" and "User" options in dropdown #### Notes -- This is the core "happy path" for sessions — most customizations come from the workspace -- Count consistency between sidebar badges and editor item count is the key regression test +- The active root comes from the repository, not a worktree --- -### Scenario 3: User-level customizations from CLI paths +### Scenario 3: Create new workspace instruction in an active worktree session #### Preconditions -- Files exist in `~/.copilot/instructions/`, `~/.claude/rules/`, or `~/.claude/agents/` -- Active session with a repository open +- Active session with a worktree checked out (task started and running) +- Use the same repository as Scenario 2 (`microsoft/vscode`) #### Actions -1. Open management editor, select "Instructions" -2. Verify user-level instruction files appear under the "User" group -3. Select "Agents" section and check for `~/.claude/agents/` user agents -4. Run Developer: Customizations Debug and check the `includedUserFileRoots` filter -5. Verify that VS Code profile user files (e.g., `$PROFILE/instructions/`) are NOT shown +1. Observe sidebar customization counts reflect the worktree's customizations and are the same as Scenario 2 (since new worktree inherits from repo root, counts should be the same) +2. Open the management editor by clicking on "Instructions" +3. Observe items listed — should match files in the worktree (not the bare repo) +4. Verify there is a primary button "New Instructions (Workspace)" and another option in the dropdown for "New Instructions (User)" +5. Click the "+ New Instructions (Workspace)" button (primary action) +6. Select a name `` when the quickpick appears and confirm +7. Verify the file opens in the embedded editor +8. Verify the file path shown in the editor header is `/.github/instructions/.instructions.md` +9. Update the instruction file with some content, then press the back button +10. Confirm the instruction file was auto-committed and shows up in the worktree changes list +11. Reopen the customization management editor and click on "Instructions" again +12. Observe the new instruction appears in the "Workspace" group +13. Observe the sidebar badge count has incremented by 1 #### Expected Results -- User files from `~/.copilot/` and `~/.claude/` appear in the "User" group -- User files from the VS Code profile path do NOT appear (filtered by `includedUserFileRoots: [~/.copilot, ~/.claude, ~/.agents]`) -- Prompts section shows ALL user roots (filter has `includedUserFileRoots: undefined`) — including VS Code profile prompts -- Debug report Stage 2 shows "Removed" entries for any filtered-out user files -- Sidebar counts reflect the filtered user file counts +- Active root is the worktree path, not the repository path +- File is created under the worktree's `.github/instructions/` folder (not the bare repo) +- File auto-saves and auto-commits to the worktree +- Item count updates in both the sidebar badge and editor list after creation +- The new file appears in the list with a friendly name derived from the filename #### Notes -- This validates the `IStorageSourceFilter.includedUserFileRoots` allowlist -- Prompts are intentionally an exception — they show from all user roots since CLI now supports user prompts +- This is the primary creation flow — workspace instructions are the most common customization type +- Key difference from Scenario 2: active root is the worktree, creation targets the worktree --- -### Scenario 4: Creating new customization files +### Scenario 4: Create new user instruction in an active worktree session #### Preconditions -- Active session with a worktree +- Active session with a worktree checked out (continuing from Scenario 3) #### Actions -1. Open management editor, select "Instructions" -2. Click the "Add" button → "New Instructions (Workspace)" -3. Verify the file is created in `.github/instructions/` under the worktree -4. Click "Add" button dropdown → "New Instructions (User)" -5. Verify the file is created in `~/.copilot/instructions/` (not VS Code profile) -6. Select "Hooks" section -7. Click "Add" → verify a `hooks.json` is created in `.github/hooks/` -8. Verify the hooks.json has `"version": 1`, uses `"bash"` field, and contains all events from `COPILOT_CLI_HOOK_TYPE_MAP` +1. Open the management editor by clicking on "Instructions" +2. Click the "Add" dropdown arrow → click "New Instruction (User)" +3. Select a name `` when the quickpick appears and confirm +4. Verify the file opens in the embedded editor +5. Verify the file path shown in the editor header is `~/.copilot/instructions/.instructions.md` +6. Confirm the path is NOT the VS Code profile folder (e.g., NOT `~/.vscode-oss-sessions-dev/User/...`) +7. Press the back button to return to the list +8. Observe the new instruction appears in the "User" group +9. Observe the sidebar badge count reflects the new user instruction +10. Run Developer: Customizations Debug +11. Check the "Source Folders (creation targets)" section — verify `[user]` points to `~/.copilot/instructions` #### Expected Results -- Workspace files created under the active worktree's `.github/` folder -- User files created under `~/.copilot/{type}/` (from `AgenticPromptsService.getSourceFolders()` override) -- Hooks.json skeleton has correct Copilot CLI format: `version: 1`, `bash` (not `command`), all hook events derived from schema -- After creation, item count updates automatically -- Created files are editable in the embedded editor +- User file is created under `~/.copilot/instructions/` (not the VS Code profile folder) +- The file appears in the "User" group in the list +- Sidebar badge count includes the new user file +- Debug report confirms the user creation target is `~/.copilot/instructions` #### Notes -- This tests that `AgenticPromptsService.getSourceFolders()` correctly redirects user creation to `~/.copilot/` -- Hooks creation derives events from `COPILOT_CLI_HOOK_TYPE_MAP` — adding new events to the schema auto-includes them +- This validates that `AgenticPromptsService.getSourceFolders()` correctly redirects user creation to `~/.copilot/` +- The VS Code profile folder should never be used for user creation in sessions --- -### Scenario 5: Switching sessions updates customizations +### Scenario 5: Create a new hook in an active worktree session #### Preconditions -- Two sessions active: one with a repo that has many customizations, another with none -- Or: one active session, then start a new session with a different repo +- Active session with a worktree checked out (continuing from Scenario 3) +- No existing `hooks.json` in the worktree's `.github/hooks/` folder #### Actions -1. Note the sidebar customization counts for the first session -2. Switch to the second session (click it in the session list) -3. Observe sidebar counts update -4. Open the management editor and verify items reflect the new session's workspace -5. Switch back to the first session -6. Verify counts and items revert to the first session's state +1. Open the management editor by clicking on "Hooks" +2. Observe the current hook items (if any) +3. Click the "Add" button → observe a `hooks.json` is created +4. Verify the hooks.json opens in the embedded editor +5. Verify the file path is `/.github/hooks/hooks.json` +6. Read the generated JSON and check: + - `"version": 1` is present at the top level + - Hook entries use `"bash"` as the shell field (not `"command"`) + - All hook event types are present: `sessionStart`, `userPromptSubmitted`, `preToolUse`, `postToolUse` + - Each event has a `[{ "type": "command", "bash": "" }]` skeleton +7. Edit one of the hook entries (e.g., add a bash command to `sessionStart`) +8. Press the back button to return to the list +9. Observe the hooks.json appears in the "Workspace" group +10. Observe the sidebar badge count for Hooks has updated +11. Run Developer: Customizations Debug on the Hooks section +12. Verify `Active root` points to the worktree path +13. Compare Stage 1 counts with Stage 3 counts — they should be consistent #### Expected Results -- Sidebar counts update reactively when `activeSession` observable changes -- Management editor items refresh automatically (list widget subscribes to `activeProjectRoot`) -- Active root in the debug report changes to the new session's worktree -- No stale counts from the previous session persist -- If the new session has no worktree, counts show only user-level items (workspace = 0) -- "Add (Workspace)" button becomes disabled when no active root +- Hooks.json is created in the worktree's `.github/hooks/` folder +- JSON skeleton has correct Copilot CLI format: `"version": 1`, `"bash"` field +- All hook events from `COPILOT_CLI_HOOK_TYPE_MAP` are present in the skeleton +- Hooks section shows only workspace-local hook files (no user-level hooks visible) +- Item count updates after creation +- Debug report Stage 1 → Stage 3 pipeline shows no unexpected filtering #### Notes -- This tests the reactive wiring: `autorun` on `activeSession` triggers `_updateCounts()` in toolbar and `refresh()` in list widget -- Stale count bugs typically manifest when switching sessions — the count remains from the prior session +- Hook events are derived from `COPILOT_CLI_HOOK_TYPE_MAP` — adding new events to the schema auto-includes them in the skeleton +- Only `"bash"` is used (not `"command"`) to match the Copilot CLI schema +- The `"version": 1` field is required by the CLI for format detection diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts index a0c135aacc637..f3f644dffa886 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts @@ -39,7 +39,14 @@ import { Action, Separator } from '../../../../../base/common/actions.js'; import { IClipboardService } from '../../../../../platform/clipboard/common/clipboardService.js'; import { ISCMService } from '../../../scm/common/scm.js'; import { IHoverService } from '../../../../../platform/hover/browser/hover.js'; +import { IFileService } from '../../../../../platform/files/common/files.js'; +import { IPathService } from '../../../../services/path/common/pathService.js'; import { generateCustomizationDebugReport } from './aiCustomizationDebugPanel.js'; +import { parseHooksFromFile } from '../../common/promptSyntax/hookCompatibility.js'; +import { HOOK_TYPES, formatHookCommandLabel } from '../../common/promptSyntax/hookSchema.js'; +import { parse as parseJSONC } from '../../../../../base/common/json.js'; +import { Schemas } from '../../../../../base/common/network.js'; +import { OS } from '../../../../../base/common/platform.js'; const $ = DOM.$; @@ -367,6 +374,8 @@ export class AICustomizationListWidget extends Disposable { @IClipboardService private readonly clipboardService: IClipboardService, @ISCMService private readonly scmService: ISCMService, @IHoverService private readonly hoverService: IHoverService, + @IFileService private readonly fileService: IFileService, + @IPathService private readonly pathService: IPathService, ) { super(); this.element = $('.ai-customization-list-widget'); @@ -814,18 +823,54 @@ export class AICustomizationListWidget extends Disposable { }); } } else if (promptType === PromptsType.hook) { - // Show hook files (not individual hooks) so users can open and edit them + // Try to parse individual hooks from each file; fall back to showing the file itself const hookFiles = await this.promptsService.listPromptFiles(PromptsType.hook, CancellationToken.None); + const activeRoot = this.workspaceService.getActiveProjectRoot(); + const userHomeUri = await this.pathService.userHome(); + const userHome = userHomeUri.scheme === Schemas.file ? userHomeUri.fsPath : userHomeUri.path; + for (const hookFile of hookFiles) { - const filename = basename(hookFile.uri); - items.push({ - id: hookFile.uri.toString(), - uri: hookFile.uri, - name: this.getFriendlyName(filename), - filename, - storage: hookFile.storage, - promptType, - }); + let parsedHooks = false; + try { + const content = await this.fileService.readFile(hookFile.uri); + const json = parseJSONC(content.value.toString()); + const { hooks } = parseHooksFromFile(hookFile.uri, json, activeRoot, userHome); + + if (hooks.size > 0) { + parsedHooks = true; + for (const [hookType, entry] of hooks) { + const hookMeta = HOOK_TYPES.find(h => h.id === hookType); + for (let i = 0; i < entry.hooks.length; i++) { + const hook = entry.hooks[i]; + const cmdLabel = formatHookCommandLabel(hook, OS); + const truncatedCmd = cmdLabel.length > 60 ? cmdLabel.substring(0, 57) + '...' : cmdLabel; + items.push({ + id: `${hookFile.uri.toString()}#${entry.originalId}[${i}]`, + uri: hookFile.uri, + name: hookMeta?.label ?? entry.originalId, + filename: basename(hookFile.uri), + description: truncatedCmd || localize('hookUnset', "(unset)"), + storage: hookFile.storage, + promptType, + }); + } + } + } + } catch { + // Parse failed — fall through to show raw file + } + + if (!parsedHooks) { + const filename = basename(hookFile.uri); + items.push({ + id: hookFile.uri.toString(), + uri: hookFile.uri, + name: this.getFriendlyName(filename), + filename, + storage: hookFile.storage, + promptType, + }); + } } } else { // For instructions, fetch prompt files and group by storage diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts index 8177097058f42..a18e242ba6972 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts @@ -166,6 +166,7 @@ export class AICustomizationManagementEditor extends EditorPane { private selectedSection: AICustomizationManagementSection = AICustomizationManagementSection.Agents; private readonly editorDisposables = this._register(new DisposableStore()); + private _editorContentChanged = false; private readonly inEditorContextKey: IContextKey; private readonly sectionContextKey: IContextKey; @@ -635,6 +636,22 @@ export class AICustomizationManagementEditor extends EditorPane { public selectSectionById(sectionId: AICustomizationManagementSection): void { const index = this.sections.findIndex(s => s.id === sectionId); if (index >= 0) { + // Directly update state and UI, bypassing the early-return guard in selectSection + // to handle the case where the editor just opened with a persisted section that + // matches the requested one (content might not be loaded yet). + if (this.viewMode === 'editor') { + this.goBackToList(); + } + if (this.viewMode === 'mcpDetail') { + this.goBackFromMcpDetail(); + } + this.selectedSection = sectionId; + this.sectionContextKey.set(sectionId); + this.storageService.store(AI_CUSTOMIZATION_MANAGEMENT_SELECTED_SECTION_KEY, sectionId, StorageScope.PROFILE, StorageTarget.USER); + this.updateContentVisibility(); + if (this.isPromptsSection(sectionId)) { + void this.listWidget.setSection(sectionId); + } this.sectionsList.setFocus([index]); this.sectionsList.setSelection([index]); } @@ -723,8 +740,10 @@ export class AICustomizationManagementEditor extends EditorPane { this.embeddedEditor!.focus(); this.editorModelChangeDisposables.clear(); + this._editorContentChanged = false; const saveDelayer = this.editorModelChangeDisposables.add(new Delayer(500)); this.editorModelChangeDisposables.add(ref.object.textEditorModel.onDidChangeContent(() => { + this._editorContentChanged = true; this.editorSaveIndicator.className = 'editor-save-indicator visible'; this.editorSaveIndicator.classList.add(...ThemeIcon.asClassNameArray(Codicon.loading), 'codicon-modifier-spin'); this.editorSaveIndicator.title = localize('saving', "Saving..."); @@ -753,10 +772,10 @@ export class AICustomizationManagementEditor extends EditorPane { } private goBackToList(): void { - // Auto-commit workspace files when leaving the embedded editor + // Auto-commit workspace files when leaving the embedded editor (only if modified) const fileUri = this.currentEditingUri; const projectRoot = this.currentEditingProjectRoot; - if (fileUri && projectRoot) { + if (fileUri && projectRoot && this._editorContentChanged) { this.workspaceService.commitFiles(projectRoot, [fileUri]); } @@ -771,6 +790,9 @@ export class AICustomizationManagementEditor extends EditorPane { this.viewMode = 'list'; this.updateContentVisibility(); + // Refresh the list to pick up newly created/edited files + void this.listWidget?.refresh(); + if (this.dimension) { this.layout(this.dimension); } diff --git a/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts b/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts index 15899bc63071e..7cc5960a26f4d 100644 --- a/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts +++ b/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts @@ -6,31 +6,23 @@ import { asCSSUrl } from '../../../../../base/browser/cssValue.js'; import * as dom from '../../../../../base/browser/dom.js'; import { createCSSRule } from '../../../../../base/browser/domStylesheets.js'; -import { StandardKeyboardEvent } from '../../../../../base/browser/keyboardEvent.js'; import { IRenderedMarkdown } from '../../../../../base/browser/markdownRenderer.js'; import { Button } from '../../../../../base/browser/ui/button/button.js'; import { renderIcon } from '../../../../../base/browser/ui/iconLabel/iconLabels.js'; -import { Action, IAction } from '../../../../../base/common/actions.js'; -import { Codicon } from '../../../../../base/common/codicons.js'; import { Event } from '../../../../../base/common/event.js'; import { StringSHA1 } from '../../../../../base/common/hash.js'; import { IMarkdownString } from '../../../../../base/common/htmlContent.js'; -import { KeyCode } from '../../../../../base/common/keyCodes.js'; import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js'; import { IObservable, ISettableObservable, observableValue } from '../../../../../base/common/observable.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { URI } from '../../../../../base/common/uri.js'; -import { localize } from '../../../../../nls.js'; import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; -import { IContextMenuService } from '../../../../../platform/contextview/browser/contextView.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { ILogService } from '../../../../../platform/log/common/log.js'; import { IMarkdownRendererService } from '../../../../../platform/markdown/browser/markdownRenderer.js'; import { IOpenerService } from '../../../../../platform/opener/common/opener.js'; -import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; import { defaultButtonStyles } from '../../../../../platform/theme/browser/defaultStyles.js'; import { ChatAgentLocation } from '../../common/constants.js'; -import { IChatWidgetService } from '../chat.js'; import { chatViewsWelcomeRegistry, IChatViewsWelcomeDescriptor } from './chatViewsWelcome.js'; const $ = dom.$; @@ -133,18 +125,9 @@ export interface IChatViewWelcomeContent { readonly additionalMessage?: string | IMarkdownString; tips?: IMarkdownString; readonly inputPart?: HTMLElement; - readonly suggestedPrompts?: readonly IChatSuggestedPrompts[]; readonly useLargeIcon?: boolean; } -export interface IChatSuggestedPrompts { - readonly icon?: ThemeIcon; - readonly label: string; - readonly description?: string; - readonly prompt: string; - readonly uri?: URI; -} - export interface IChatViewWelcomeRenderOptions { readonly firstLinkToButton?: boolean; readonly location: ChatAgentLocation; @@ -159,10 +142,7 @@ export class ChatViewWelcomePart extends Disposable { options: IChatViewWelcomeRenderOptions | undefined, @IOpenerService private openerService: IOpenerService, @ILogService private logService: ILogService, - @IChatWidgetService private chatWidgetService: IChatWidgetService, - @ITelemetryService private telemetryService: ITelemetryService, @IMarkdownRendererService private readonly markdownRendererService: IMarkdownRendererService, - @IContextMenuService private readonly contextMenuService: IContextMenuService, ) { super(); @@ -213,88 +193,6 @@ export class ChatViewWelcomePart extends Disposable { } } - // Render suggested prompts for both new user and regular modes - if (content.suggestedPrompts && content.suggestedPrompts.length) { - const suggestedPromptsContainer = dom.append(this.element, $('.chat-welcome-view-suggested-prompts')); - const titleElement = dom.append(suggestedPromptsContainer, $('.chat-welcome-view-suggested-prompts-title')); - titleElement.textContent = localize('chatWidget.suggestedActions', 'Suggested Actions'); - - for (const prompt of content.suggestedPrompts) { - const promptElement = dom.append(suggestedPromptsContainer, $('.chat-welcome-view-suggested-prompt')); - // Make the prompt element keyboard accessible - promptElement.setAttribute('role', 'button'); - promptElement.setAttribute('tabindex', '0'); - const promptAriaLabel = prompt.description - ? localize('suggestedPromptAriaLabelWithDescription', 'Suggested prompt: {0}, {1}', prompt.label, prompt.description) - : localize('suggestedPromptAriaLabel', 'Suggested prompt: {0}', prompt.label); - promptElement.setAttribute('aria-label', promptAriaLabel); - const titleElement = dom.append(promptElement, $('.chat-welcome-view-suggested-prompt-title')); - titleElement.textContent = prompt.label; - const tooltip = localize('runPromptTitle', "Suggested prompt: {0}", prompt.prompt); - promptElement.title = tooltip; - titleElement.title = tooltip; - if (prompt.description) { - const descriptionElement = dom.append(promptElement, $('.chat-welcome-view-suggested-prompt-description')); - descriptionElement.textContent = prompt.description; - descriptionElement.title = prompt.description; - } - const executePrompt = () => { - type SuggestedPromptClickEvent = { suggestedPrompt: string }; - - type SuggestedPromptClickData = { - owner: 'bhavyaus'; - comment: 'Event used to gain insights into when suggested prompts are clicked.'; - suggestedPrompt: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The suggested prompt clicked.' }; - }; - - this.telemetryService.publicLog2('chat.clickedSuggestedPrompt', { - suggestedPrompt: prompt.prompt, - }); - - if (!this.chatWidgetService.lastFocusedWidget) { - const widgets = this.chatWidgetService.getWidgetsByLocations(ChatAgentLocation.Chat); - if (widgets.length) { - widgets[0].setInput(prompt.prompt); - } - } else { - this.chatWidgetService.lastFocusedWidget.setInput(prompt.prompt); - } - }; - // Add context menu handler - this._register(dom.addDisposableListener(promptElement, dom.EventType.CONTEXT_MENU, (e: MouseEvent) => { - e.preventDefault(); - e.stopImmediatePropagation(); - - const actions = this.getPromptContextMenuActions(prompt); - - this.contextMenuService.showContextMenu({ - getAnchor: () => ({ x: e.clientX, y: e.clientY }), - getActions: () => actions, - }); - })); - // Add click handler - this._register(dom.addDisposableListener(promptElement, dom.EventType.CLICK, executePrompt)); - // Add keyboard handler - this._register(dom.addDisposableListener(promptElement, dom.EventType.KEY_DOWN, (e) => { - const event = new StandardKeyboardEvent(e); - if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) { - e.preventDefault(); - e.stopPropagation(); - executePrompt(); - } - else if (event.equals(KeyCode.F10) && event.shiftKey) { - e.preventDefault(); - e.stopPropagation(); - const actions = this.getPromptContextMenuActions(prompt); - this.contextMenuService.showContextMenu({ - getAnchor: () => promptElement, - getActions: () => actions, - }); - } - })); - } - } - // Tips if (content.tips) { const tips = dom.append(this.element, $('.chat-welcome-view-tips')); @@ -306,39 +204,13 @@ export class ChatViewWelcomePart extends Disposable { } } - private getPromptContextMenuActions(prompt: IChatSuggestedPrompts): IAction[] { - const actions: IAction[] = []; - if (prompt.uri) { - const uri = prompt.uri; - actions.push(new Action( - 'chat.editPromptFile', - localize('editPromptFile', "Edit Prompt File"), - ThemeIcon.asClassName(Codicon.goToFile), - true, - async () => { - try { - await this.openerService.open(uri); - } catch (error) { - this.logService.error('Failed to open prompt file:', error); - } - } - )); - } - return actions; - } - public needsRerender(content: IChatViewWelcomeContent): boolean { // Heuristic based on content that changes between states return !!( this.content.title !== content.title || this.content.message.value !== content.message.value || this.content.additionalMessage !== content.additionalMessage || - this.content.tips?.value !== content.tips?.value || - this.content.suggestedPrompts?.length !== content.suggestedPrompts?.length || - this.content.suggestedPrompts?.some((prompt, index) => { - const incoming = content.suggestedPrompts?.[index]; - return incoming?.label !== prompt.label || incoming?.description !== prompt.description; - })); + this.content.tips?.value !== content.tips?.value); } private renderMarkdownMessageContent(content: IMarkdownString, options: IChatViewWelcomeRenderOptions | undefined): IRenderedMarkdown { diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts index 48e7be78cd212..20bc0e902eb2d 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts @@ -34,7 +34,7 @@ import { Range } from '../../../../../editor/common/core/range.js'; import { localize } from '../../../../../nls.js'; import { MenuId } from '../../../../../platform/actions/common/actions.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; -import { ContextKeyExpr, IContextKey, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; +import { IContextKey, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js'; import { ITextResourceEditorInput } from '../../../../../platform/editor/common/editor.js'; @@ -45,9 +45,6 @@ import { bindContextKey } from '../../../../../platform/observable/common/platfo import product from '../../../../../platform/product/common/product.js'; import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; import { IThemeService } from '../../../../../platform/theme/common/themeService.js'; -import { IWorkspaceContextService, WorkbenchState } from '../../../../../platform/workspace/common/workspace.js'; -import { EditorResourceAccessor } from '../../../../common/editor.js'; -import { IEditorService } from '../../../../services/editor/common/editorService.js'; import { IChatEntitlementService } from '../../../../services/chat/common/chatEntitlementService.js'; import { ILifecycleService } from '../../../../services/lifecycle/common/lifecycle.js'; import { checkModeOption } from '../../common/chat.js'; @@ -69,7 +66,6 @@ import { CodeBlockModelCollection } from '../../common/widget/codeBlockModelColl import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../common/constants.js'; import { ILanguageModelToolsService, isToolSet } from '../../common/tools/languageModelToolsService.js'; import { ComputeAutomaticInstructions } from '../../common/promptSyntax/computeAutomaticInstructions.js'; -import { PromptsConfig } from '../../common/promptSyntax/config/config.js'; import { IHandOff, PromptHeader } from '../../common/promptSyntax/promptFileParser.js'; import { IPromptsService, PromptsStorage } from '../../common/promptSyntax/service/promptsService.js'; import { GENERATE_AGENT_INSTRUCTIONS_COMMAND_ID, handleModeSwitch } from '../actions/chatActions.js'; @@ -82,7 +78,7 @@ import { ChatInputPart, IChatInputPartOptions, IChatInputStyles } from './input/ import { IChatListItemTemplate } from './chatListRenderer.js'; import { ChatListWidget } from './chatListWidget.js'; import { ChatEditorOptions } from './chatOptions.js'; -import { ChatViewWelcomePart, IChatSuggestedPrompts, IChatViewWelcomeContent } from '../viewsWelcome/chatViewWelcomeController.js'; +import { ChatViewWelcomePart, IChatViewWelcomeContent } from '../viewsWelcome/chatViewWelcomeController.js'; import { IChatTipService } from '../chatTipService.js'; import { ChatTipContentPart } from './chatContentParts/chatTipContentPart.js'; import { ChatContentMarkdownRenderer } from './chatContentMarkdownRenderer.js'; @@ -294,11 +290,6 @@ export class ChatWidget extends Disposable implements IChatWidget { private readonly _sessionHasDebugDataContextKey: IContextKey; private _attachmentCapabilities: IChatAgentAttachmentCapabilities = supportsAllAttachments; - // Cache for prompt file descriptions to avoid async calls during rendering - private readonly promptDescriptionsCache = new Map(); - private readonly promptUriCache = new Map(); - private _isLoadingPromptDescriptions = false; - private readonly viewModelDisposables = this._register(new DisposableStore()); private _viewModel: ChatViewModel | undefined; @@ -378,7 +369,6 @@ export class ChatWidget extends Disposable implements IChatWidget { private readonly viewOptions: IChatWidgetViewOptions, private readonly styles: IChatWidgetStyles, @ICodeEditorService private readonly codeEditorService: ICodeEditorService, - @IEditorService private readonly editorService: IEditorService, @IConfigurationService private readonly configurationService: IConfigurationService, @IDialogService private readonly dialogService: IDialogService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @@ -400,7 +390,6 @@ export class ChatWidget extends Disposable implements IChatWidget { @IChatSessionsService private readonly chatSessionsService: IChatSessionsService, @IAgentSessionsService private readonly agentSessionsService: IAgentSessionsService, @IChatTodoListService private readonly chatTodoListService: IChatTodoListService, - @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @ILifecycleService private readonly lifecycleService: ILifecycleService, @IChatAttachmentResolveService private readonly chatAttachmentResolveService: IChatAttachmentResolveService, @IChatTipService private readonly chatTipService: IChatTipService, @@ -533,7 +522,7 @@ export class ChatWidget extends Disposable implements IChatWidget { this.renderChatEditingSessionState(); })); - this._register(codeEditorService.registerCodeEditorOpenHandler(async (input: ITextResourceEditorInput, _source: ICodeEditor | null, _sideBySide?: boolean): Promise => { + this._register(this.codeEditorService.registerCodeEditorOpenHandler(async (input: ITextResourceEditorInput, _source: ICodeEditor | null, _sideBySide?: boolean): Promise => { const resource = input.resource; if (resource.scheme !== Schemas.vscodeChatCodeBlock) { return null; @@ -1141,183 +1130,9 @@ export class ChatWidget extends Disposable implements IChatWidget { message: new MarkdownString(DISCLAIMER), icon: Codicon.chatSparkle, additionalMessage, - suggestedPrompts: this.getPromptFileSuggestions(), }; } - private getPromptFileSuggestions(): IChatSuggestedPrompts[] { - - // Use predefined suggestions for new users - if (!this.chatEntitlementService.sentiment.installed) { - const isEmpty = this.contextService.getWorkbenchState() === WorkbenchState.EMPTY; - if (isEmpty) { - return [ - { - icon: Codicon.vscode, - label: localize('chatWidget.suggestedPrompts.gettingStarted', "Ask @vscode"), - prompt: localize('chatWidget.suggestedPrompts.gettingStartedPrompt', "@vscode How do I change the theme to light mode?"), - }, - { - icon: Codicon.newFolder, - label: localize('chatWidget.suggestedPrompts.newProject', "Create Project"), - prompt: localize('chatWidget.suggestedPrompts.newProjectPrompt', "Create a #new Hello World project in TypeScript"), - } - ]; - } else { - return [ - { - icon: Codicon.debugAlt, - label: localize('chatWidget.suggestedPrompts.buildWorkspace', "Build Workspace"), - prompt: localize('chatWidget.suggestedPrompts.buildWorkspacePrompt', "How do I build this workspace?"), - }, - { - icon: Codicon.gear, - label: localize('chatWidget.suggestedPrompts.findConfig', "Show Config"), - prompt: localize('chatWidget.suggestedPrompts.findConfigPrompt', "Where is the configuration for this project defined?"), - } - ]; - } - } - - // Get the current workspace folder context if available - const activeEditor = this.editorService.activeEditor; - const resource = activeEditor ? EditorResourceAccessor.getOriginalUri(activeEditor) : undefined; - - // Get the prompt file suggestions configuration - const suggestions = PromptsConfig.getPromptFilesRecommendationsValue(this.configurationService, resource); - if (!suggestions) { - return []; - } - - const result: IChatSuggestedPrompts[] = []; - const promptsToLoad: string[] = []; - - // First, collect all prompts that need loading (regardless of shouldInclude) - for (const [promptName] of Object.entries(suggestions)) { - const description = this.promptDescriptionsCache.get(promptName); - if (description === undefined) { - promptsToLoad.push(promptName); - } - } - - // If we have prompts to load, load them asynchronously and don't return anything yet - // But only if we're not already loading to prevent infinite loop - if (promptsToLoad.length > 0 && !this._isLoadingPromptDescriptions) { - this.loadPromptDescriptions(promptsToLoad); - return []; - } - - // Now process the suggestions with loaded descriptions - const promptsWithScores: { promptName: string; condition: boolean | string; score: number }[] = []; - - for (const [promptName, condition] of Object.entries(suggestions)) { - let score = 0; - - // Handle boolean conditions - if (typeof condition === 'boolean') { - score = condition ? 1 : 0; - } - // Handle when clause conditions - else if (typeof condition === 'string') { - try { - const whenClause = ContextKeyExpr.deserialize(condition); - if (whenClause) { - // Test against all open code editors - const allEditors = this.codeEditorService.listCodeEditors(); - - if (allEditors.length > 0) { - // Count how many editors match the when clause - score = allEditors.reduce((count, editor) => { - try { - const editorContext = this.contextKeyService.getContext(editor.getDomNode()); - return count + (whenClause.evaluate(editorContext) ? 1 : 0); - } catch (error) { - // Log error for this specific editor but continue with others - this.logService.warn('Failed to evaluate when clause for editor:', error); - return count; - } - }, 0); - } else { - // Fallback to global context if no editors are open - score = this.contextKeyService.contextMatchesRules(whenClause) ? 1 : 0; - } - } else { - score = 0; - } - } catch (error) { - // Log the error but don't fail completely - this.logService.warn('Failed to parse when clause for prompt file suggestion:', condition, error); - score = 0; - } - } - - if (score > 0) { - promptsWithScores.push({ promptName, condition, score }); - } - } - - // Sort by score (descending) and take top 5 - promptsWithScores.sort((a, b) => b.score - a.score); - const topPrompts = promptsWithScores.slice(0, 5); - - // Build the final result array - for (const { promptName } of topPrompts) { - const description = this.promptDescriptionsCache.get(promptName); - const commandLabel = localize('chatWidget.promptFile.commandLabel', "{0}", promptName); - const uri = this.promptUriCache.get(promptName); - const descriptionText = description?.trim() ? description : undefined; - result.push({ - icon: Codicon.run, - label: commandLabel, - description: descriptionText, - prompt: `/${promptName} `, - uri: uri - }); - } - - return result; - } - - private async loadPromptDescriptions(promptNames: string[]): Promise { - // Don't start loading if the widget is being disposed - if (this._store.isDisposed) { - return; - } - - // Set loading guard to prevent infinite loop - this._isLoadingPromptDescriptions = true; - try { - // Get all available prompt files with their metadata - const promptCommands = await this.promptsService.getPromptSlashCommands(CancellationToken.None); - - let cacheUpdated = false; - // Load descriptions only for the specified prompts - for (const promptCommand of promptCommands) { - if (promptNames.includes(promptCommand.name)) { - const description = promptCommand.description; - if (description) { - this.promptDescriptionsCache.set(promptCommand.name, description); - cacheUpdated = true; - } else { - // Set empty string to indicate we've checked this prompt - this.promptDescriptionsCache.set(promptCommand.name, ''); - cacheUpdated = true; - } - } - } - - // Fire event to trigger a re-render of the welcome view only if cache was updated - if (cacheUpdated) { - this.renderWelcomeViewContentIfNeeded(); - } - } catch (error) { - this.logService.warn('Failed to load specific prompt descriptions:', error); - } finally { - // Always clear the loading guard, even on error - this._isLoadingPromptDescriptions = false; - } - } - private async renderChatEditingSessionState() { if (!this.input) { return; diff --git a/src/vs/workbench/contrib/chat/browser/widget/media/chatViewWelcome.css b/src/vs/workbench/contrib/chat/browser/widget/media/chatViewWelcome.css index 4c9933dc63793..9a5cff033fa59 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/media/chatViewWelcome.css +++ b/src/vs/workbench/contrib/chat/browser/widget/media/chatViewWelcome.css @@ -16,7 +16,7 @@ .interactive-session.chat-view-getting-started-disabled { - /* hide most welcome pieces (except suggested actions) when we show recent sessions to make some space */ + /* hide most welcome pieces when we show recent sessions to make some space */ .chat-welcome-view .chat-welcome-view-icon, .chat-welcome-view .chat-welcome-view-title, .chat-welcome-view .chat-welcome-view-message, @@ -225,16 +225,6 @@ div.chat-welcome-view { color: var(--vscode-editorWidget-foreground); white-space: nowrap; } - - & > .chat-welcome-view-suggested-prompt-description { - font-size: 11px; - color: var(--vscode-descriptionForeground); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - flex: 0 1 auto; - min-width: 0; - } } > .chat-welcome-view-suggested-prompt:hover { diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/hookSchema.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/hookSchema.ts index fcbbdfea8207e..62fee88c20e54 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/hookSchema.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/hookSchema.ts @@ -523,13 +523,6 @@ export function formatHookCommandLabel(hook: IHookCommand, os: OperatingSystem): if (!command) { return ''; } - - // Add platform badge if using platform-specific override - if (isUsingPlatformOverride(hook, os)) { - const platformLabel = getPlatformLabel(os); - return `[${platformLabel}] ${command}`; - } - return command; } diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/hookSchema.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/hookSchema.test.ts index 63bcf59c0049f..56cf17fafc88f 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/hookSchema.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/hookSchema.test.ts @@ -461,7 +461,7 @@ suite('HookSchema', () => { assert.strictEqual(formatHookCommandLabel(hook, OperatingSystem.Windows), ''); }); - test('applies platform override for display with platform badge', () => { + test('applies platform override for display', () => { const hook: IHookCommand = { type: 'command', command: 'default-command', @@ -469,10 +469,10 @@ suite('HookSchema', () => { linux: 'linux-command', osx: 'osx-command' }; - // Should include platform badge when using platform-specific override - assert.strictEqual(formatHookCommandLabel(hook, OperatingSystem.Windows), '[Windows] win-command'); - assert.strictEqual(formatHookCommandLabel(hook, OperatingSystem.Macintosh), '[macOS] osx-command'); - assert.strictEqual(formatHookCommandLabel(hook, OperatingSystem.Linux), '[Linux] linux-command'); + // Should resolve to platform-specific command + assert.strictEqual(formatHookCommandLabel(hook, OperatingSystem.Windows), 'win-command'); + assert.strictEqual(formatHookCommandLabel(hook, OperatingSystem.Macintosh), 'osx-command'); + assert.strictEqual(formatHookCommandLabel(hook, OperatingSystem.Linux), 'linux-command'); }); test('no platform badge when falling back to default command', () => {