Skip to content

Commit 28a550e

Browse files
authored
sessions: tweaks to customizations (#298155)
* add hook for sessions selfhosting * Fix section selection, list refresh, commit guard, hooks display, count races - selectSectionById: inline all state updates to avoid race with onDidChangeSelection - goBackToList: refresh list to show newly created files - goBackToList: only auto-commit if editor content was actually modified - Hooks: parse individual hooks from files, show hook type labels and commands - Hooks: show (unset) for empty hook commands - Toolbar: use MODAL_GROUP when opening editor in sessions - Overview: use MODAL_GROUP when opening editor in sessions - Toolbar: add _updateCountsRequestId guard to prevent stale count renders - SessionsViewPane: subscribe to activeProjectRoot for total count updates - List widget: add IFileService + IPathService for hook file parsing * Fix hook parsing: use JSONC parser and async userHome - Replace JSON.parse with parseJSONC to handle comments/trailing commas - Use await pathService.userHome() instead of preferLocal for remote compat - Match the pattern used in promptsServiceImpl.ts getHooks()
1 parent 11e0160 commit 28a550e

File tree

8 files changed

+131
-28
lines changed

8 files changed

+131
-28
lines changed

.github/hooks/hooks.json

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"version": 1,
3+
"hooks": {
4+
"sessionStart": [
5+
{
6+
"type": "command",
7+
"bash": "if [ -f ~/.vscode-worktree-setup ]; then nohup npm ci > /tmp/npm-ci-$(date +%Y-%m-%d_%H-%M-%S).log 2>&1 & fi"
8+
}
9+
],
10+
"userPromptSubmitted": [
11+
{
12+
"type": "command",
13+
"bash": ""
14+
}
15+
],
16+
"preToolUse": [
17+
{
18+
"type": "command",
19+
"bash": ""
20+
}
21+
],
22+
"postToolUse": [
23+
{
24+
"type": "command",
25+
"bash": ""
26+
}
27+
]
28+
}
29+
}

src/vs/sessions/contrib/aiCustomizationTreeView/browser/aiCustomizationOverviewView.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import { AICustomizationManagementEditor } from '../../../../workbench/contrib/c
2727
import { agentIcon, instructionsIcon, promptIcon, skillIcon } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationIcons.js';
2828
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
2929
import { IAICustomizationWorkspaceService } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js';
30-
import { IEditorService } from '../../../../workbench/services/editor/common/editorService.js';
30+
import { IEditorService, MODAL_GROUP } from '../../../../workbench/services/editor/common/editorService.js';
3131

3232
const $ = DOM.$;
3333

@@ -187,7 +187,7 @@ export class AICustomizationOverviewView extends ViewPane {
187187

188188
private async openSection(sectionId: AICustomizationManagementSection): Promise<void> {
189189
const input = AICustomizationManagementEditorInput.getOrCreate();
190-
const editor = await this.editorService.openEditor(input, { pinned: true });
190+
const editor = await this.editorService.openEditor(input, { pinned: true }, MODAL_GROUP);
191191

192192
// Deep-link to the section
193193
if (editor instanceof AICustomizationManagementEditor) {

src/vs/sessions/contrib/sessions/browser/customizationsToolbar.contribution.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import { ISessionsManagementService } from './sessionsManagementService.js';
3030
import { Button } from '../../../../base/browser/ui/button/button.js';
3131
import { defaultButtonStyles } from '../../../../platform/theme/browser/defaultStyles.js';
3232
import { getSourceCounts, getSourceCountsTotal, ISourceCounts } from './customizationCounts.js';
33-
import { IEditorService } from '../../../../workbench/services/editor/common/editorService.js';
33+
import { IEditorService, MODAL_GROUP } from '../../../../workbench/services/editor/common/editorService.js';
3434
import { IAICustomizationWorkspaceService } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js';
3535

3636
interface ICustomizationItemConfig {
@@ -154,18 +154,28 @@ class CustomizationLinkViewItem extends ActionViewItem {
154154
this._updateCounts();
155155
}
156156

157+
private _updateCountsRequestId = 0;
158+
157159
private async _updateCounts(): Promise<void> {
158160
if (!this._countContainer) {
159161
return;
160162
}
161163

164+
const requestId = ++this._updateCountsRequestId;
165+
162166
if (this._config.promptType) {
163167
const type = this._config.promptType;
164168
const filter = this._workspaceService.getStorageSourceFilter(type);
165169
const counts = await getSourceCounts(this._promptsService, type, filter, this._workspaceContextService, this._workspaceService);
170+
if (requestId !== this._updateCountsRequestId) {
171+
return;
172+
}
166173
this._renderSourceCounts(this._countContainer, counts);
167174
} else if (this._config.getCount) {
168175
const count = await this._config.getCount(this._languageModelsService, this._mcpService);
176+
if (requestId !== this._updateCountsRequestId) {
177+
return;
178+
}
169179
this._renderSimpleCount(this._countContainer, count);
170180
}
171181
}
@@ -244,7 +254,7 @@ class CustomizationsToolbarContribution extends Disposable implements IWorkbench
244254
async run(accessor: ServicesAccessor): Promise<void> {
245255
const editorService = accessor.get(IEditorService);
246256
const input = AICustomizationManagementEditorInput.getOrCreate();
247-
const editor = await editorService.openEditor(input, { pinned: true });
257+
const editor = await editorService.openEditor(input, { pinned: true }, MODAL_GROUP);
248258
if (editor instanceof AICustomizationManagementEditor) {
249259
editor.selectSectionById(config.section);
250260
}

src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,10 @@ export class AgenticSessionsViewPane extends ViewPane {
249249
this.mcpService.servers.read(reader);
250250
updateHeaderTotalCount();
251251
}));
252+
this._register(autorun(reader => {
253+
this.workspaceService.activeProjectRoot.read(reader);
254+
updateHeaderTotalCount();
255+
}));
252256
updateHeaderTotalCount();
253257

254258
// Toggle collapse on header click

src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts

Lines changed: 55 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,14 @@ import { Action, Separator } from '../../../../../base/common/actions.js';
3939
import { IClipboardService } from '../../../../../platform/clipboard/common/clipboardService.js';
4040
import { ISCMService } from '../../../scm/common/scm.js';
4141
import { IHoverService } from '../../../../../platform/hover/browser/hover.js';
42+
import { IFileService } from '../../../../../platform/files/common/files.js';
43+
import { IPathService } from '../../../../services/path/common/pathService.js';
4244
import { generateCustomizationDebugReport } from './aiCustomizationDebugPanel.js';
45+
import { parseHooksFromFile } from '../../common/promptSyntax/hookCompatibility.js';
46+
import { HOOK_TYPES, formatHookCommandLabel } from '../../common/promptSyntax/hookSchema.js';
47+
import { parse as parseJSONC } from '../../../../../base/common/json.js';
48+
import { Schemas } from '../../../../../base/common/network.js';
49+
import { OS } from '../../../../../base/common/platform.js';
4350

4451
const $ = DOM.$;
4552

@@ -367,6 +374,8 @@ export class AICustomizationListWidget extends Disposable {
367374
@IClipboardService private readonly clipboardService: IClipboardService,
368375
@ISCMService private readonly scmService: ISCMService,
369376
@IHoverService private readonly hoverService: IHoverService,
377+
@IFileService private readonly fileService: IFileService,
378+
@IPathService private readonly pathService: IPathService,
370379
) {
371380
super();
372381
this.element = $('.ai-customization-list-widget');
@@ -814,18 +823,54 @@ export class AICustomizationListWidget extends Disposable {
814823
});
815824
}
816825
} else if (promptType === PromptsType.hook) {
817-
// Show hook files (not individual hooks) so users can open and edit them
826+
// Try to parse individual hooks from each file; fall back to showing the file itself
818827
const hookFiles = await this.promptsService.listPromptFiles(PromptsType.hook, CancellationToken.None);
828+
const activeRoot = this.workspaceService.getActiveProjectRoot();
829+
const userHomeUri = await this.pathService.userHome();
830+
const userHome = userHomeUri.scheme === Schemas.file ? userHomeUri.fsPath : userHomeUri.path;
831+
819832
for (const hookFile of hookFiles) {
820-
const filename = basename(hookFile.uri);
821-
items.push({
822-
id: hookFile.uri.toString(),
823-
uri: hookFile.uri,
824-
name: this.getFriendlyName(filename),
825-
filename,
826-
storage: hookFile.storage,
827-
promptType,
828-
});
833+
let parsedHooks = false;
834+
try {
835+
const content = await this.fileService.readFile(hookFile.uri);
836+
const json = parseJSONC(content.value.toString());
837+
const { hooks } = parseHooksFromFile(hookFile.uri, json, activeRoot, userHome);
838+
839+
if (hooks.size > 0) {
840+
parsedHooks = true;
841+
for (const [hookType, entry] of hooks) {
842+
const hookMeta = HOOK_TYPES.find(h => h.id === hookType);
843+
for (let i = 0; i < entry.hooks.length; i++) {
844+
const hook = entry.hooks[i];
845+
const cmdLabel = formatHookCommandLabel(hook, OS);
846+
const truncatedCmd = cmdLabel.length > 60 ? cmdLabel.substring(0, 57) + '...' : cmdLabel;
847+
items.push({
848+
id: `${hookFile.uri.toString()}#${entry.originalId}[${i}]`,
849+
uri: hookFile.uri,
850+
name: hookMeta?.label ?? entry.originalId,
851+
filename: basename(hookFile.uri),
852+
description: truncatedCmd || localize('hookUnset', "(unset)"),
853+
storage: hookFile.storage,
854+
promptType,
855+
});
856+
}
857+
}
858+
}
859+
} catch {
860+
// Parse failed — fall through to show raw file
861+
}
862+
863+
if (!parsedHooks) {
864+
const filename = basename(hookFile.uri);
865+
items.push({
866+
id: hookFile.uri.toString(),
867+
uri: hookFile.uri,
868+
name: this.getFriendlyName(filename),
869+
filename,
870+
storage: hookFile.storage,
871+
promptType,
872+
});
873+
}
829874
}
830875
} else {
831876
// For instructions, fetch prompt files and group by storage

src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ export class AICustomizationManagementEditor extends EditorPane {
166166
private selectedSection: AICustomizationManagementSection = AICustomizationManagementSection.Agents;
167167

168168
private readonly editorDisposables = this._register(new DisposableStore());
169+
private _editorContentChanged = false;
169170

170171
private readonly inEditorContextKey: IContextKey<boolean>;
171172
private readonly sectionContextKey: IContextKey<string>;
@@ -635,6 +636,22 @@ export class AICustomizationManagementEditor extends EditorPane {
635636
public selectSectionById(sectionId: AICustomizationManagementSection): void {
636637
const index = this.sections.findIndex(s => s.id === sectionId);
637638
if (index >= 0) {
639+
// Directly update state and UI, bypassing the early-return guard in selectSection
640+
// to handle the case where the editor just opened with a persisted section that
641+
// matches the requested one (content might not be loaded yet).
642+
if (this.viewMode === 'editor') {
643+
this.goBackToList();
644+
}
645+
if (this.viewMode === 'mcpDetail') {
646+
this.goBackFromMcpDetail();
647+
}
648+
this.selectedSection = sectionId;
649+
this.sectionContextKey.set(sectionId);
650+
this.storageService.store(AI_CUSTOMIZATION_MANAGEMENT_SELECTED_SECTION_KEY, sectionId, StorageScope.PROFILE, StorageTarget.USER);
651+
this.updateContentVisibility();
652+
if (this.isPromptsSection(sectionId)) {
653+
void this.listWidget.setSection(sectionId);
654+
}
638655
this.sectionsList.setFocus([index]);
639656
this.sectionsList.setSelection([index]);
640657
}
@@ -723,8 +740,10 @@ export class AICustomizationManagementEditor extends EditorPane {
723740
this.embeddedEditor!.focus();
724741

725742
this.editorModelChangeDisposables.clear();
743+
this._editorContentChanged = false;
726744
const saveDelayer = this.editorModelChangeDisposables.add(new Delayer<void>(500));
727745
this.editorModelChangeDisposables.add(ref.object.textEditorModel.onDidChangeContent(() => {
746+
this._editorContentChanged = true;
728747
this.editorSaveIndicator.className = 'editor-save-indicator visible';
729748
this.editorSaveIndicator.classList.add(...ThemeIcon.asClassNameArray(Codicon.loading), 'codicon-modifier-spin');
730749
this.editorSaveIndicator.title = localize('saving', "Saving...");
@@ -753,10 +772,10 @@ export class AICustomizationManagementEditor extends EditorPane {
753772
}
754773

755774
private goBackToList(): void {
756-
// Auto-commit workspace files when leaving the embedded editor
775+
// Auto-commit workspace files when leaving the embedded editor (only if modified)
757776
const fileUri = this.currentEditingUri;
758777
const projectRoot = this.currentEditingProjectRoot;
759-
if (fileUri && projectRoot) {
778+
if (fileUri && projectRoot && this._editorContentChanged) {
760779
this.workspaceService.commitFiles(projectRoot, [fileUri]);
761780
}
762781

@@ -771,6 +790,9 @@ export class AICustomizationManagementEditor extends EditorPane {
771790
this.viewMode = 'list';
772791
this.updateContentVisibility();
773792

793+
// Refresh the list to pick up newly created/edited files
794+
void this.listWidget?.refresh();
795+
774796
if (this.dimension) {
775797
this.layout(this.dimension);
776798
}

src/vs/workbench/contrib/chat/common/promptSyntax/hookSchema.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -523,13 +523,6 @@ export function formatHookCommandLabel(hook: IHookCommand, os: OperatingSystem):
523523
if (!command) {
524524
return '';
525525
}
526-
527-
// Add platform badge if using platform-specific override
528-
if (isUsingPlatformOverride(hook, os)) {
529-
const platformLabel = getPlatformLabel(os);
530-
return `[${platformLabel}] ${command}`;
531-
}
532-
533526
return command;
534527
}
535528

src/vs/workbench/contrib/chat/test/common/promptSyntax/hookSchema.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -461,18 +461,18 @@ suite('HookSchema', () => {
461461
assert.strictEqual(formatHookCommandLabel(hook, OperatingSystem.Windows), '');
462462
});
463463

464-
test('applies platform override for display with platform badge', () => {
464+
test('applies platform override for display', () => {
465465
const hook: IHookCommand = {
466466
type: 'command',
467467
command: 'default-command',
468468
windows: 'win-command',
469469
linux: 'linux-command',
470470
osx: 'osx-command'
471471
};
472-
// Should include platform badge when using platform-specific override
473-
assert.strictEqual(formatHookCommandLabel(hook, OperatingSystem.Windows), '[Windows] win-command');
474-
assert.strictEqual(formatHookCommandLabel(hook, OperatingSystem.Macintosh), '[macOS] osx-command');
475-
assert.strictEqual(formatHookCommandLabel(hook, OperatingSystem.Linux), '[Linux] linux-command');
472+
// Should resolve to platform-specific command
473+
assert.strictEqual(formatHookCommandLabel(hook, OperatingSystem.Windows), 'win-command');
474+
assert.strictEqual(formatHookCommandLabel(hook, OperatingSystem.Macintosh), 'osx-command');
475+
assert.strictEqual(formatHookCommandLabel(hook, OperatingSystem.Linux), 'linux-command');
476476
});
477477

478478
test('no platform badge when falling back to default command', () => {

0 commit comments

Comments
 (0)