Skip to content

Commit 80674d1

Browse files
nullvariantclaude
andauthored
fix: set ignoreFocusOut on all QuickPick dialogs to prevent data loss (#253)
Switching to another application while a QuickPick was open would dismiss it and discard all entered data. This was most critical in the new profile creation form where multiple fields could be lost. Set ignoreFocusOut = true on all 5 QuickPick instances to match the existing InputBox behavior. Bump version to 0.16.9. 🖥️ IDE: [Cursor](https://cursor.sh) 🔌 Extension: [Claude Code](https://claude.ai/download) Model-Raw: claude-opus-4-5-20251101 Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 58838d4 commit 80674d1

9 files changed

Lines changed: 113 additions & 2 deletions

File tree

extensions/git-id-switcher/CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.16.9] - 2026-02-01
11+
12+
### Fixed
13+
14+
- **All QuickPick dialogs now persist when the editor loses focus**:
15+
- Previously, switching to another application (e.g., to copy text) would dismiss the QuickPick and discard all entered data
16+
- Set `ignoreFocusOut = true` on all 5 QuickPick instances: new profile form, edit form, profile selector, delete picker, and manage screen
17+
- Matches the existing InputBox behavior which already retained focus
18+
1019
## [0.16.8] - 2026-01-31
1120

1221
### Changed

extensions/git-id-switcher/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "git-id-switcher",
33
"displayName": "%extension.displayName%",
44
"description": "%extension.description%",
5-
"version": "0.16.8",
5+
"version": "0.16.9",
66
"publisher": "nullvariant",
77
"icon": "images/icon.png",
88
"engines": {

extensions/git-id-switcher/src/test/e2e/deleteIdentityPicker.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ function createMockVSCode(options: {
6565
}) {
6666
let capturedItems: CapturedQuickPickItem[] = [];
6767
let capturedPlaceholder = '';
68+
let capturedIgnoreFocusOut = false;
6869
let capturedTitle = '';
6970
const showWarningMessageCalls: string[] = [];
7071

@@ -102,6 +103,8 @@ function createMockVSCode(options: {
102103
set placeholder(value: string) {
103104
capturedPlaceholder = value;
104105
},
106+
get ignoreFocusOut() { return capturedIgnoreFocusOut; },
107+
set ignoreFocusOut(value: boolean) { capturedIgnoreFocusOut = value; },
105108
matchOnDescription: false,
106109
matchOnDetail: false,
107110
get selectedItems(): T[] {
@@ -145,6 +148,7 @@ function createMockVSCode(options: {
145148
_getCapturedItems: () => capturedItems,
146149
_getCapturedPlaceholder: () => capturedPlaceholder,
147150
_getCapturedTitle: () => capturedTitle,
151+
_getCapturedIgnoreFocusOut: () => capturedIgnoreFocusOut,
148152
_getShowWarningMessageCalls: () => showWarningMessageCalls,
149153
};
150154
}
@@ -180,6 +184,20 @@ describe('showDeleteIdentityQuickPick E2E Test Suite', function () {
180184
});
181185
});
182186

187+
describe('Focus Retention', () => {
188+
it('should set ignoreFocusOut to prevent dismissal on focus change', async () => {
189+
const mockVSCode = createMockVSCode({
190+
identities: [TEST_IDENTITIES.work],
191+
selectedIdentity: TEST_IDENTITIES.work,
192+
});
193+
_setMockVSCode(mockVSCode as never);
194+
195+
await showDeleteIdentityQuickPick();
196+
197+
assert.strictEqual(mockVSCode._getCapturedIgnoreFocusOut(), true, 'Delete identity QuickPick should have ignoreFocusOut=true');
198+
});
199+
});
200+
183201
describe('Item Generation', () => {
184202
it('should create items with correct labels', async () => {
185203
const mockVSCode = createMockVSCode({

extensions/git-id-switcher/src/test/e2e/handlers.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ function createMockVSCode(options: {
102102
title: '',
103103
placeholder: '',
104104
buttons: [],
105+
ignoreFocusOut: false,
105106
matchOnDescription: false,
106107
matchOnDetail: false,
107108
selectedItems: options.showQuickPickResult ? [{ identity: options.showQuickPickResult }] : [],
@@ -231,6 +232,7 @@ function createAddMockVSCode(options: {
231232
title: '',
232233
placeholder: '',
233234
buttons: [] as unknown[],
235+
ignoreFocusOut: false,
234236
onDidAccept: (callback: () => void) => {
235237
acceptCallback = callback;
236238
return { dispose: () => {} };

extensions/git-id-switcher/src/test/e2e/identityManager.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ function createMockVSCode(options: {
119119
buttons: unknown[];
120120
title: string;
121121
placeholder: string;
122+
ignoreFocusOut: boolean;
122123
}) => void;
123124
onInputBoxCreated?: (inputBox: {
124125
buttons: unknown[];
@@ -317,6 +318,7 @@ function createMockVSCode(options: {
317318
let _title = '';
318319
let _placeholder = '';
319320
let _buttons: unknown[] = [];
321+
let _ignoreFocusOut = false;
320322

321323
const quickPick = {
322324
get title() { return _title; },
@@ -325,6 +327,8 @@ function createMockVSCode(options: {
325327
set placeholder(value: string) { _placeholder = value; },
326328
get buttons() { return _buttons; },
327329
set buttons(value: unknown[]) { _buttons = value; },
330+
get ignoreFocusOut() { return _ignoreFocusOut; },
331+
set ignoreFocusOut(value: boolean) { _ignoreFocusOut = value; },
328332
get items() { return _items; },
329333
set items(value: T[]) { _items = value; },
330334
get selectedItems() { return _selectedItems; },
@@ -338,6 +342,7 @@ function createMockVSCode(options: {
338342
buttons: _buttons,
339343
title: _title,
340344
placeholder: _placeholder,
345+
ignoreFocusOut: _ignoreFocusOut,
341346
});
342347
}
343348
// Auto-trigger selection based on test configuration
@@ -911,6 +916,23 @@ describe('identityManager E2E Test Suite', function () {
911916
}
912917
});
913918

919+
it('should set ignoreFocusOut to prevent data loss on focus change', async () => {
920+
let capturedIgnoreFocusOut = false;
921+
922+
const mockVSCode = createMockVSCode({
923+
identities: [],
924+
quickPickSelections: [undefined],
925+
onQuickPickCreated: (quickPick) => {
926+
capturedIgnoreFocusOut = quickPick.ignoreFocusOut;
927+
},
928+
});
929+
_setMockVSCode(mockVSCode as never);
930+
931+
await showAddIdentityForm();
932+
933+
assert.strictEqual(capturedIgnoreFocusOut, true, 'Add form QuickPick should have ignoreFocusOut=true');
934+
});
935+
914936
it('should mark required fields (id, name, email) with asterisk', async () => {
915937
let capturedItems: unknown[] = [];
916938

@@ -1217,6 +1239,25 @@ describe('identityManager E2E Test Suite', function () {
12171239
// ===========================================================================
12181240

12191241
describe('Edit Identity Form', () => {
1242+
describe('Focus Retention', () => {
1243+
it('should set ignoreFocusOut to prevent data loss on focus change', async () => {
1244+
let capturedIgnoreFocusOut = false;
1245+
1246+
const mockVSCode = createMockVSCode({
1247+
identities: [TEST_IDENTITIES.work],
1248+
quickPickSelections: [undefined],
1249+
onQuickPickCreated: (quickPick) => {
1250+
capturedIgnoreFocusOut = quickPick.ignoreFocusOut;
1251+
},
1252+
});
1253+
_setMockVSCode(mockVSCode as never);
1254+
1255+
await showEditProfileFlow(TEST_IDENTITIES.work);
1256+
1257+
assert.strictEqual(capturedIgnoreFocusOut, true, 'Edit form QuickPick should have ignoreFocusOut=true');
1258+
});
1259+
});
1260+
12201261
describe('Back Button (Field Selection)', () => {
12211262
it('should have QuickInputButtons.Back in field selection', async () => {
12221263
let capturedButtons: unknown[] = [];

extensions/git-id-switcher/src/test/e2e/identityPicker.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ function createMockVSCode(options: {
8585
let capturedPlaceholder = '';
8686
let capturedTitle = '';
8787
let capturedButtons: unknown[] = [];
88+
let capturedIgnoreFocusOut = false;
8889
const showWarningMessageCalls: string[] = [];
8990

9091
return {
@@ -131,6 +132,8 @@ function createMockVSCode(options: {
131132
set activeItems(_value: T[]) {
132133
// Capture but don't need to track for tests
133134
},
135+
get ignoreFocusOut() { return capturedIgnoreFocusOut; },
136+
set ignoreFocusOut(value: boolean) { capturedIgnoreFocusOut = value; },
134137
matchOnDescription: false,
135138
matchOnDetail: false,
136139
get selectedItems(): T[] {
@@ -198,6 +201,7 @@ function createMockVSCode(options: {
198201
_getCapturedPlaceholder: () => capturedPlaceholder,
199202
_getCapturedTitle: () => capturedTitle,
200203
_getCapturedButtons: () => capturedButtons,
204+
_getCapturedIgnoreFocusOut: () => capturedIgnoreFocusOut,
201205
_getShowWarningMessageCalls: () => showWarningMessageCalls,
202206
};
203207
}
@@ -233,6 +237,20 @@ describe('showIdentityQuickPick E2E Test Suite', function () {
233237
});
234238
});
235239

240+
describe('Focus Retention', () => {
241+
it('should set ignoreFocusOut to prevent dismissal on focus change', async () => {
242+
const mockVSCode = createMockVSCode({
243+
identities: [TEST_IDENTITIES.work],
244+
selectedIdentity: TEST_IDENTITIES.work,
245+
});
246+
_setMockVSCode(mockVSCode as never);
247+
248+
await showIdentityQuickPick();
249+
250+
assert.strictEqual(mockVSCode._getCapturedIgnoreFocusOut(), true, 'Select profile QuickPick should have ignoreFocusOut=true');
251+
});
252+
});
253+
236254
describe('Item Generation', () => {
237255
it('should create items with correct labels', async () => {
238256
const mockVSCode = createMockVSCode({
@@ -461,6 +479,7 @@ function createManageMockVSCode(options: {
461479
let capturedTitle = '';
462480
let capturedButtons: unknown[] = [];
463481
let capturedActiveItems: CapturedManageQuickPickItem[] = [];
482+
let capturedIgnoreFocusOut = false;
464483

465484
return {
466485
workspace: {
@@ -503,6 +522,8 @@ function createManageMockVSCode(options: {
503522
get activeItems(): T[] {
504523
return capturedActiveItems as unknown as T[];
505524
},
525+
get ignoreFocusOut() { return capturedIgnoreFocusOut; },
526+
set ignoreFocusOut(value: boolean) { capturedIgnoreFocusOut = value; },
506527
matchOnDescription: false,
507528
matchOnDetail: false,
508529
get selectedItems(): T[] {
@@ -604,6 +625,7 @@ function createManageMockVSCode(options: {
604625
_getCapturedTitle: () => capturedTitle,
605626
_getCapturedButtons: () => capturedButtons,
606627
_getCapturedActiveItems: () => capturedActiveItems,
628+
_getCapturedIgnoreFocusOut: () => capturedIgnoreFocusOut,
607629
};
608630
}
609631

@@ -618,6 +640,20 @@ describe('showManageIdentitiesQuickPick E2E Test Suite', function () {
618640
_resetCache();
619641
});
620642

643+
describe('Focus Retention', () => {
644+
it('should set ignoreFocusOut to prevent dismissal on focus change', async () => {
645+
const mockVSCode = createManageMockVSCode({
646+
identities: [TEST_IDENTITIES.work],
647+
triggerAction: 'hide',
648+
});
649+
_setMockVSCode(mockVSCode as never);
650+
651+
await showManageIdentitiesQuickPick([TEST_IDENTITIES.work]);
652+
653+
assert.strictEqual(mockVSCode._getCapturedIgnoreFocusOut(), true, 'Manage profiles QuickPick should have ignoreFocusOut=true');
654+
});
655+
});
656+
621657
describe('Basic UI', () => {
622658
it('should display items correctly', async () => {
623659
const mockVSCode = createManageMockVSCode({

extensions/git-id-switcher/src/ui/documentationInternal.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export const DOCUMENT_HASHES: Record<string, string> = {
2525
'AGENTS.md': '54f16b767e57686b3eb46a2b4aa02b378554cc492c32c49ed96588f6d184b6b8',
2626
'CODE_OF_CONDUCT.md': 'a5eb286c902437bbe0f6d409894f20e51c172fa869fe2f151bfa388f9d911b54',
2727
'CONTRIBUTING.md': '4150f8810aec7b2e8eff9f3c69ee1bae1374843f50a812efa6778cba27a833cd',
28-
'extensions/git-id-switcher/CHANGELOG.md': '5e0833ce75bf64f50e010d3930eae07a0f7418f6854333b863f1c6a25ebf81be',
28+
'extensions/git-id-switcher/CHANGELOG.md': '3b18be1ee2c5d09c0b9c8c4ebdef1380bcad712d627e3d698fe14548080e0cad',
2929
'extensions/git-id-switcher/docs/ARCHITECTURE.md': 'a12dd717f83b28b648972a826701a29fcfd575e351c487f8c421402f80ac3d25',
3030
'extensions/git-id-switcher/docs/CONTRIBUTING.md': '7d6ad2bc4d8c838790754cb9df848cb65f9fdce7e1a13e5c965b83a3d5b6378c',
3131
'extensions/git-id-switcher/docs/DESIGN_PHILOSOPHY.md': 'f9718b61ac161cb466dbc76845688e7acacf4e5fdc4b8b9553269dba4a094f6b',

extensions/git-id-switcher/src/ui/identityManager.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -867,6 +867,7 @@ async function executeAddFormLoop(
867867
const quickPick = vs.window.createQuickPick<AddFormQuickPickItem>();
868868
quickPick.title = vs.l10n.t('New Profile');
869869
quickPick.placeholder = vs.l10n.t('Filter...');
870+
quickPick.ignoreFocusOut = true;
870871
quickPick.buttons = [vs.QuickInputButtons.Back];
871872

872873
try {
@@ -994,6 +995,7 @@ async function showFieldSelectionQuickPick(
994995
const quickPick = vs.window.createQuickPick<FieldQuickPickItem>();
995996
quickPick.title = vs.l10n.t('Edit Identity: {0}', identity.id);
996997
quickPick.placeholder = vs.l10n.t('Filter...');
998+
quickPick.ignoreFocusOut = true;
997999
quickPick.buttons = [vs.QuickInputButtons.Back];
9981000
quickPick.items = buildFieldItems(vs, identity, savedField, identityCount);
9991001

extensions/git-id-switcher/src/ui/identityPicker.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ export async function showIdentityQuickPick(
122122
quickPick.items = allItems;
123123
quickPick.title = vs.l10n.t('Select Profile');
124124
quickPick.placeholder = vs.l10n.t('Search profiles...');
125+
quickPick.ignoreFocusOut = true;
125126
quickPick.buttons = [manageButton];
126127
quickPick.matchOnDescription = true;
127128
quickPick.matchOnDetail = true;
@@ -243,6 +244,7 @@ export async function showDeleteIdentityQuickPick(
243244
quickPick.items = items;
244245
quickPick.title = vs.l10n.t('Delete Identity');
245246
quickPick.placeholder = vs.l10n.t('Select identity to delete');
247+
quickPick.ignoreFocusOut = true;
246248
quickPick.matchOnDescription = true;
247249
quickPick.matchOnDetail = true;
248250

@@ -349,6 +351,7 @@ export async function showManageIdentitiesQuickPick(
349351
const quickPick = vs.window.createQuickPick<ManageIdentityQuickPickItem>();
350352
quickPick.items = items;
351353
quickPick.title = vs.l10n.t('Manage Profiles');
354+
quickPick.ignoreFocusOut = true;
352355
quickPick.buttons = [vs.QuickInputButtons.Back, addButton];
353356
quickPick.matchOnDescription = true;
354357
quickPick.matchOnDetail = true;

0 commit comments

Comments
 (0)