diff --git a/.github/hooks/hooks.json b/.github/hooks/hooks.json new file mode 100644 index 00000000000..59c170e420e --- /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 7e335ef7326..bf959abffad 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 c5c913e1797..382c1b38051 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 d53188c77bd..eb76bcf42f2 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/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts index a0c135aacc6..f3f644dffa8 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 8177097058f..a18e242ba69 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/common/promptSyntax/hookSchema.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/hookSchema.ts index fcbbdfea820..62fee88c20e 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 63bcf59c004..56cf17fafc8 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', () => {