statusbar-quick-actions 0.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1754 @@
1
+ /**
2
+ * StatusBar Quick Actions Extension
3
+ * A comprehensive extension for customizable statusbar buttons
4
+ */
5
+
6
+ import * as vscode from "vscode";
7
+ import * as fs from "fs";
8
+ import {
9
+ StatusBarButtonConfig,
10
+ ExtensionConfig,
11
+ ButtonState,
12
+ ExecutionResult,
13
+ ExecutionOptions,
14
+ } from "./types";
15
+ import { ConfigManager } from "./configuration";
16
+ import { CommandExecutor } from "./executor";
17
+ import { ThemeManager } from "./theme";
18
+ import { VisibilityManager } from "./visibility";
19
+ import { MaterialIconManager } from "./material-icons";
20
+ import { OutputPanelManager } from "./output-panel";
21
+ import { PresetManager } from "./preset-manager";
22
+ import { DynamicLabelManager } from "./dynamic-label";
23
+
24
+ /**
25
+ * Main extension class
26
+ */
27
+ export class StatusBarQuickActionsExtension {
28
+ private context: vscode.ExtensionContext;
29
+ private configManager: ConfigManager;
30
+ private commandExecutor: CommandExecutor;
31
+ private themeManager: ThemeManager;
32
+ private visibilityManager!: VisibilityManager;
33
+ private materialIconManager!: MaterialIconManager;
34
+ private outputPanelManager!: OutputPanelManager;
35
+ private presetManager!: PresetManager;
36
+ private dynamicLabelManager!: DynamicLabelManager;
37
+ private buttonStates: Map<string, ButtonState> = new Map<
38
+ string,
39
+ ButtonState
40
+ >();
41
+ private disposables: vscode.Disposable[] = [];
42
+ private editorChangeListener: vscode.Disposable | null = null;
43
+ private isActivated = false;
44
+ private debugMode = false;
45
+
46
+ constructor(context: vscode.ExtensionContext) {
47
+ this.context = context;
48
+ this.configManager = new ConfigManager();
49
+ this.commandExecutor = new CommandExecutor();
50
+ this.themeManager = new ThemeManager();
51
+ this.debugMode = vscode.workspace
52
+ .getConfiguration("statusbarQuickActions.settings")
53
+ .get<boolean>("debug", false);
54
+ }
55
+
56
+ /**
57
+ * Log debug messages only when debug mode is enabled
58
+ */
59
+ private debugLog(...args: unknown[]): void {
60
+ if (this.debugMode) {
61
+ console.log("[StatusBar Quick Actions]", ...args);
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Activate the extension
67
+ */
68
+ public async activate(): Promise<void> {
69
+ if (this.isActivated) {
70
+ return;
71
+ }
72
+
73
+ try {
74
+ // Initialize critical managers in parallel
75
+ await this.initializeManagers();
76
+
77
+ // Register commands (synchronous, fast)
78
+ this.registerCommands();
79
+
80
+ // Set up configuration watching (synchronous, fast)
81
+ this.setupConfigurationWatching();
82
+
83
+ // Load initial configuration and create buttons
84
+ await this.loadConfiguration();
85
+
86
+ this.isActivated = true;
87
+ this.debugLog("Extension activated successfully");
88
+
89
+ // Defer non-critical operations to avoid blocking activation
90
+ setImmediate(() => {
91
+ this.showWelcomeMessageIfNeeded().catch((error) => {
92
+ this.debugLog("Failed to show welcome message:", error);
93
+ });
94
+ });
95
+ } catch (error) {
96
+ console.error(
97
+ "Failed to activate StatusBar Quick Actions extension:",
98
+ error,
99
+ );
100
+ vscode.window.showErrorMessage(
101
+ `Failed to activate StatusBar Quick Actions: ${error}`,
102
+ );
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Show welcome message on first activation (deferred)
108
+ */
109
+ private async showWelcomeMessageIfNeeded(): Promise<void> {
110
+ if (!this.context.globalState.get("hasBeenActivated")) {
111
+ await this.showWelcomeMessage();
112
+ await this.context.globalState.update("hasBeenActivated", true);
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Deactivate the extension
118
+ */
119
+ public deactivate(): void {
120
+ if (!this.isActivated) {
121
+ return;
122
+ }
123
+
124
+ // Dispose of all resources
125
+ this.disposables.forEach((disposable) => disposable.dispose());
126
+ this.buttonStates.clear();
127
+
128
+ // Dispose new managers
129
+ if (this.outputPanelManager) {
130
+ this.outputPanelManager.dispose();
131
+ }
132
+ if (this.visibilityManager) {
133
+ this.visibilityManager.dispose();
134
+ }
135
+ if (this.presetManager) {
136
+ this.presetManager.dispose();
137
+ }
138
+ if (this.dynamicLabelManager) {
139
+ this.dynamicLabelManager.dispose();
140
+ }
141
+
142
+ this.isActivated = false;
143
+ this.debugLog("Extension deactivated");
144
+ }
145
+
146
+ /**
147
+ * Initialize all managers - optimized for parallel execution
148
+ */
149
+ private async initializeManagers(): Promise<void> {
150
+ try {
151
+ // Initialize configuration manager first (synchronous, required by others)
152
+ this.configManager.initialize(this.context);
153
+ this.debugLog("ConfigManager initialized");
154
+
155
+ // Get configs once to avoid multiple reads
156
+ const outputConfig = this.configManager.getConfigValue(
157
+ "settings.output",
158
+ this.getDefaultOutputConfig(),
159
+ );
160
+ const performanceConfig = this.configManager.getConfigValue(
161
+ "settings.performance",
162
+ this.getDefaultPerformanceConfig(),
163
+ );
164
+
165
+ // Initialize managers in parallel where possible
166
+ await Promise.all([
167
+ // Theme manager (async)
168
+ this.themeManager.initialize(this.context).then(() => {
169
+ this.debugLog("ThemeManager initialized");
170
+ }),
171
+
172
+ // Dynamic Label Manager (async)
173
+ (async () => {
174
+ this.dynamicLabelManager = new DynamicLabelManager();
175
+ await this.dynamicLabelManager.initialize();
176
+ this.dynamicLabelManager.onLabelRefresh = (buttonId) => {
177
+ this.refreshButtonLabel(buttonId);
178
+ };
179
+ this.debugLog("DynamicLabelManager initialized");
180
+ })(),
181
+ ]);
182
+
183
+ // Initialize synchronous managers (fast, no await needed)
184
+ this.materialIconManager = new MaterialIconManager();
185
+ this.outputPanelManager = new OutputPanelManager(outputConfig);
186
+ this.visibilityManager = new VisibilityManager(
187
+ performanceConfig.visibilityDebounceMs,
188
+ );
189
+ this.presetManager = new PresetManager();
190
+ this.presetManager.initialize(this.context);
191
+
192
+ this.debugLog("All synchronous managers initialized");
193
+
194
+ // Setup editor change listener (lightweight)
195
+ this.setupEditorChangeListener();
196
+ this.debugLog("Managers initialization complete");
197
+ } catch (error) {
198
+ const errorMessage =
199
+ error instanceof Error ? error.message : String(error);
200
+ console.error("Failed to initialize managers:", errorMessage);
201
+ vscode.window.showErrorMessage(
202
+ `StatusBar Quick Actions: Failed to initialize - ${errorMessage}`,
203
+ );
204
+ throw error; // Re-throw to prevent activation from completing
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Register extension commands
210
+ */
211
+ private registerCommands(): void {
212
+ // Edit button command
213
+ this.disposables.push(
214
+ vscode.commands.registerCommand(
215
+ "statusbarQuickActions.editButton",
216
+ this.editButton.bind(this),
217
+ ),
218
+ );
219
+
220
+ // View history command
221
+ this.disposables.push(
222
+ vscode.commands.registerCommand(
223
+ "statusbarQuickActions.viewHistory",
224
+ this.viewHistory.bind(this),
225
+ ),
226
+ );
227
+
228
+ // Clear history command
229
+ this.disposables.push(
230
+ vscode.commands.registerCommand(
231
+ "statusbarQuickActions.clearHistory",
232
+ this.clearHistory.bind(this),
233
+ ),
234
+ );
235
+
236
+ // Preset management commands
237
+ this.disposables.push(
238
+ vscode.commands.registerCommand(
239
+ "statusbarQuickActions.managePresets",
240
+ this.managePresets.bind(this),
241
+ ),
242
+ );
243
+
244
+ this.disposables.push(
245
+ vscode.commands.registerCommand(
246
+ "statusbarQuickActions.applyPreset",
247
+ this.applyPresetCommand.bind(this),
248
+ ),
249
+ );
250
+
251
+ this.disposables.push(
252
+ vscode.commands.registerCommand(
253
+ "statusbarQuickActions.saveAsPreset",
254
+ this.saveAsPreset.bind(this),
255
+ ),
256
+ );
257
+
258
+ // Register individual button commands
259
+ this.registerButtonCommands();
260
+ }
261
+
262
+ /**
263
+ * Register commands for each button
264
+ */
265
+ private registerButtonCommands(): void {
266
+ const config = this.configManager.getConfig();
267
+ config.buttons.forEach((button) => {
268
+ const commandId = `statusbarQuickActions.execute_${button.id}`;
269
+ this.disposables.push(
270
+ vscode.commands.registerCommand(commandId, () =>
271
+ this.executeButton(button.id),
272
+ ),
273
+ );
274
+ });
275
+ }
276
+
277
+ /**
278
+ * Set up configuration change watching
279
+ */
280
+ private setupConfigurationWatching(): void {
281
+ this.disposables.push(
282
+ this.configManager.onConfigurationChanged(async (newConfig) => {
283
+ // Update debug mode when configuration changes
284
+ this.debugMode = vscode.workspace
285
+ .getConfiguration("statusbarQuickActions.settings")
286
+ .get<boolean>("debug", false);
287
+ this.debugLog("Configuration changed, updating buttons");
288
+ await this.updateConfiguration(newConfig);
289
+ }),
290
+ );
291
+ }
292
+
293
+ /**
294
+ * Load configuration and create statusbar items
295
+ */
296
+ private async loadConfiguration(): Promise<void> {
297
+ const config = this.configManager.getConfig();
298
+ await this.updateConfiguration(config);
299
+ }
300
+
301
+ /**
302
+ * Update configuration and recreate statusbar items
303
+ */
304
+ private async updateConfiguration(config: ExtensionConfig): Promise<void> {
305
+ this.debugLog(
306
+ "Updating configuration with buttons:",
307
+ config.buttons.length,
308
+ );
309
+
310
+ // Debug: Log each button configuration
311
+ if (this.debugMode) {
312
+ config.buttons.forEach((button, index) => {
313
+ this.debugLog(
314
+ `Button ${index}: ${button.id} - ${button.text || "no text"}`,
315
+ button,
316
+ );
317
+ });
318
+ }
319
+
320
+ // Validate configuration first
321
+ const validation = this.configManager.validateConfig(config);
322
+ if (!validation.isValid) {
323
+ const errorMessage = `Invalid button configuration:\n${validation.errors.join("\n")}`;
324
+ console.error(errorMessage);
325
+ vscode.window.showErrorMessage(
326
+ `StatusBar Quick Actions: Configuration validation failed. Check console for details.`,
327
+ );
328
+ // Show detailed error in output channel
329
+ const outputChannel = vscode.window.createOutputChannel(
330
+ "StatusBar Quick Actions - Errors",
331
+ );
332
+ outputChannel.appendLine("Configuration Validation Errors:");
333
+ validation.errors.forEach((error) =>
334
+ outputChannel.appendLine(` - ${error}`),
335
+ );
336
+ outputChannel.show(true);
337
+ }
338
+
339
+ // Remove existing statusbar items
340
+ this.buttonStates.forEach((state) => {
341
+ state.item.dispose();
342
+ });
343
+ this.buttonStates.clear();
344
+
345
+ // Create new statusbar items (even if validation failed, try to create valid ones)
346
+ let createdCount = 0;
347
+ let failedCount = 0;
348
+ let disabledCount = 0;
349
+
350
+ // Create buttons in parallel for better performance
351
+ const buttonCreationPromises = config.buttons.map(async (buttonConfig) => {
352
+ if (buttonConfig.enabled === false) {
353
+ this.debugLog(`Button ${buttonConfig.id} is disabled, skipping`);
354
+ disabledCount++;
355
+ return { created: false, disabled: true };
356
+ }
357
+
358
+ const created = await this.createStatusBarItem(buttonConfig);
359
+ return { created, disabled: false };
360
+ });
361
+
362
+ const results = await Promise.all(buttonCreationPromises);
363
+ results.forEach((result) => {
364
+ if (result.created) {
365
+ createdCount++;
366
+ } else if (!result.disabled) {
367
+ failedCount++;
368
+ }
369
+ });
370
+
371
+ // Log summary
372
+ this.debugLog(
373
+ `Created ${createdCount} buttons, ${failedCount} failed, ${disabledCount} disabled`,
374
+ );
375
+
376
+ // Show notification if no buttons were created
377
+ if (createdCount === 0 && config.buttons.length > 0) {
378
+ vscode.window.showWarningMessage(
379
+ `StatusBar Quick Actions: No buttons could be created. Check the output panel for errors.`,
380
+ );
381
+ }
382
+ }
383
+
384
+ /**
385
+ * Create a statusbar item for a button configuration
386
+ * @returns true if button was created successfully, false otherwise
387
+ */
388
+ private async createStatusBarItem(
389
+ buttonConfig: StatusBarButtonConfig,
390
+ ): Promise<boolean> {
391
+ try {
392
+ this.debugLog(`Creating status bar item for button: ${buttonConfig.id}`);
393
+
394
+ // Validate button configuration
395
+ if (!buttonConfig.id) {
396
+ const error = `Button ${buttonConfig.id || "unknown"} missing ID`;
397
+ this.debugLog(error);
398
+ throw new Error("Button ID is required");
399
+ }
400
+ if (!buttonConfig.text && !buttonConfig.icon) {
401
+ const error = `Button ${buttonConfig.id} missing both text and icon`;
402
+ this.debugLog(error);
403
+ throw new Error("Either button text or icon is required");
404
+ }
405
+ if (!buttonConfig.command) {
406
+ const error = `Button ${buttonConfig.id} missing command`;
407
+ this.debugLog(error);
408
+ throw new Error("Button command is required");
409
+ }
410
+
411
+ // Create statusbar item
412
+ const alignment =
413
+ buttonConfig.alignment === "left"
414
+ ? vscode.StatusBarAlignment.Left
415
+ : vscode.StatusBarAlignment.Right;
416
+ const priority = buttonConfig.priority ?? 100;
417
+
418
+ const statusBarItem = vscode.window.createStatusBarItem(
419
+ alignment,
420
+ priority,
421
+ );
422
+ this.debugLog(
423
+ `Created status bar item for ${buttonConfig.id} (alignment: ${alignment}, priority: ${priority})`,
424
+ );
425
+
426
+ // Set button properties
427
+ const displayText = this.getButtonDisplayText(buttonConfig);
428
+ this.debugLog(`Button ${buttonConfig.id} display text: "${displayText}"`);
429
+ if (!displayText || displayText.trim() === "") {
430
+ const error = `Button ${buttonConfig.id} has empty display text`;
431
+ this.debugLog(error);
432
+ throw new Error("Button display text cannot be empty");
433
+ }
434
+ statusBarItem.text = displayText;
435
+ statusBarItem.tooltip =
436
+ buttonConfig.tooltip || buttonConfig.text || "Quick Action";
437
+ statusBarItem.command = `statusbarQuickActions.execute_${buttonConfig.id}`;
438
+ this.debugLog(
439
+ `Button ${buttonConfig.id} command: ${statusBarItem.command}`,
440
+ );
441
+
442
+ // Apply theme colors
443
+ this.themeManager.applyThemeToStatusBarItem(statusBarItem);
444
+
445
+ // Set accessibility properties
446
+ statusBarItem.accessibilityInformation = {
447
+ label:
448
+ buttonConfig.tooltip ||
449
+ buttonConfig.text ||
450
+ `Button ${buttonConfig.id}`,
451
+ role: "button",
452
+ };
453
+
454
+ // Create button state with empty history (will be loaded async)
455
+ const buttonState: ButtonState = {
456
+ item: statusBarItem,
457
+ config: buttonConfig,
458
+ isExecuting: false,
459
+ history: [], // Start with empty history, load async
460
+ accessibility: {
461
+ role: "button",
462
+ ariaLabel:
463
+ buttonConfig.tooltip ||
464
+ buttonConfig.text ||
465
+ `Button ${buttonConfig.id}`,
466
+ focusOrder: priority,
467
+ },
468
+ };
469
+
470
+ this.buttonStates.set(buttonConfig.id, buttonState);
471
+ this.disposables.push(statusBarItem);
472
+
473
+ // Show button immediately (don't block on history or dynamic labels)
474
+ statusBarItem.show();
475
+
476
+ // Load history and dynamic labels asynchronously (non-blocking)
477
+ setImmediate(() => {
478
+ this.loadHistoryAsync(buttonConfig.id).catch((error) => {
479
+ this.debugLog(
480
+ `Failed to load history for ${buttonConfig.id}:`,
481
+ error,
482
+ );
483
+ });
484
+
485
+ if (buttonConfig.dynamicLabel) {
486
+ this.refreshButtonLabel(buttonConfig.id).catch((error) => {
487
+ this.debugLog(
488
+ `Failed to refresh label for ${buttonConfig.id}:`,
489
+ error,
490
+ );
491
+ });
492
+ }
493
+ });
494
+
495
+ this.debugLog(`Button ${buttonConfig.id} created and shown`);
496
+ return true;
497
+ } catch (error) {
498
+ const errorMessage =
499
+ error instanceof Error ? error.message : String(error);
500
+ console.error(
501
+ `Failed to create statusbar item for button ${buttonConfig.id}:`,
502
+ errorMessage,
503
+ );
504
+ vscode.window.showErrorMessage(
505
+ `Failed to create button "${buttonConfig.text || buttonConfig.id}": ${errorMessage}`,
506
+ );
507
+ return false;
508
+ }
509
+ }
510
+
511
+ /**
512
+ * Get display text for button (text or icon)
513
+ */
514
+ private getButtonDisplayText(buttonConfig: StatusBarButtonConfig): string {
515
+ if (buttonConfig.icon) {
516
+ // Resolve Material icons to Codicons if needed
517
+ const resolvedIconId = this.materialIconManager.resolveIcon(
518
+ buttonConfig.icon,
519
+ );
520
+
521
+ const iconPrefix =
522
+ buttonConfig.icon.animation === "spin"
523
+ ? `$(${resolvedIconId}~spin)`
524
+ : buttonConfig.icon.animation === "pulse"
525
+ ? `$(${resolvedIconId}~pulse)`
526
+ : `$(${resolvedIconId})`;
527
+ return iconPrefix;
528
+ }
529
+ return buttonConfig.text;
530
+ }
531
+
532
+ /**
533
+ * Execute a button command
534
+ */
535
+ private async executeButton(buttonId: string): Promise<void> {
536
+ const buttonState = this.buttonStates.get(buttonId);
537
+ if (!buttonState || buttonState.isExecuting) {
538
+ return;
539
+ }
540
+
541
+ const config = buttonState.config;
542
+
543
+ try {
544
+ // Set executing state
545
+ buttonState.isExecuting = true;
546
+ this.updateButtonState(buttonId, buttonState);
547
+
548
+ // Show progress if enabled
549
+ if (config.execution?.showProgress) {
550
+ this.showProgress(buttonId);
551
+ }
552
+
553
+ // Prepare execution options
554
+ const executionOptions: ExecutionOptions = {
555
+ workingDirectory: config.workingDirectory,
556
+ environment: config.environment,
557
+ };
558
+
559
+ if (config.execution?.timeout) {
560
+ executionOptions.timeout = config.execution.timeout;
561
+ }
562
+
563
+ // Add streaming support if output panel is enabled
564
+ const outputConfig = this.outputPanelManager.getConfig();
565
+ if (outputConfig.enabled) {
566
+ // Ensure panel exists
567
+ this.outputPanelManager.getOrCreatePanel(buttonId, config.text);
568
+
569
+ if (outputConfig.clearOnRun) {
570
+ this.outputPanelManager.clearPanel(buttonId);
571
+ }
572
+
573
+ executionOptions.streaming = {
574
+ enabled: true,
575
+ onStdout: (data) => {
576
+ this.outputPanelManager.appendOutput(buttonId, data, "stdout");
577
+ },
578
+ onStderr: (data) => {
579
+ this.outputPanelManager.appendOutput(buttonId, data, "stderr");
580
+ },
581
+ };
582
+
583
+ this.outputPanelManager.showPanel(buttonId, true);
584
+ }
585
+
586
+ // Execute the command
587
+ const result = await this.commandExecutor.execute(
588
+ config.command,
589
+ executionOptions,
590
+ );
591
+
592
+ // Update button state
593
+ buttonState.isExecuting = false;
594
+ buttonState.lastResult = result;
595
+
596
+ // Add to history if enabled
597
+ if (config.history?.enabled !== false) {
598
+ await this.addToHistory(buttonId, result);
599
+ }
600
+
601
+ this.updateButtonState(buttonId, buttonState);
602
+
603
+ // Show result
604
+ await this.showExecutionResult(buttonId, result);
605
+ } catch (error) {
606
+ buttonState.isExecuting = false;
607
+ this.updateButtonState(buttonId, buttonState);
608
+
609
+ const errorResult: ExecutionResult = {
610
+ code: -1,
611
+ stdout: "",
612
+ stderr: error instanceof Error ? error.message : String(error),
613
+ duration: 0,
614
+ timestamp: new Date(),
615
+ command: "Unknown",
616
+ };
617
+
618
+ buttonState.lastResult = errorResult;
619
+ this.updateButtonState(buttonId, buttonState);
620
+
621
+ await this.showExecutionError(buttonId, error);
622
+ }
623
+ }
624
+
625
+ /**
626
+ * Update button state in UI
627
+ */
628
+ private updateButtonState(buttonId: string, buttonState: ButtonState): void {
629
+ const config = buttonState.config;
630
+
631
+ if (buttonState.isExecuting) {
632
+ // Show executing state
633
+ if (config.icon?.animation) {
634
+ buttonState.item.text =
635
+ config.icon.animation === "spin"
636
+ ? "$(sync~spin)"
637
+ : config.icon.animation === "pulse"
638
+ ? "$(sync~pulse)"
639
+ : this.getButtonDisplayText(config);
640
+ } else {
641
+ buttonState.item.text = `$(sync~spin) ${config.text}`;
642
+ }
643
+ } else {
644
+ // Show normal state
645
+ buttonState.item.text = this.getButtonDisplayText(config);
646
+ }
647
+
648
+ // Update tooltip with last result if available
649
+ if (buttonState.lastResult) {
650
+ const result = buttonState.lastResult;
651
+ const status = result.code === 0 ? "✅" : "❌";
652
+ const duration = result.duration ? ` (${result.duration}ms)` : "";
653
+ buttonState.item.tooltip = `${config.tooltip || config.text}\n${status} Last run: ${result.timestamp.toLocaleTimeString()}${duration}`;
654
+ } else {
655
+ buttonState.item.tooltip = config.tooltip || config.text;
656
+ }
657
+
658
+ buttonState.item.show();
659
+ }
660
+
661
+ /**
662
+ * Show progress indicator
663
+ */
664
+ private showProgress(buttonId: string): void {
665
+ const buttonState = this.buttonStates.get(buttonId);
666
+ if (!buttonState) {
667
+ return;
668
+ }
669
+
670
+ // Use VS Code's built-in progress API for better integration
671
+ vscode.window.withProgress(
672
+ {
673
+ location: vscode.ProgressLocation.Notification,
674
+ title: `Executing: ${buttonState.config.text}`,
675
+ cancellable: false,
676
+ },
677
+ async (progress) => {
678
+ // Simulate progress (in a real implementation, this would be updated by the command executor)
679
+ for (let i = 0; i <= 100; i += 10) {
680
+ if (!buttonState.isExecuting) {
681
+ break;
682
+ }
683
+ progress.report({ increment: 10, message: `${i}%` });
684
+ await new Promise((resolve) => setTimeout(resolve, 200));
685
+ }
686
+ },
687
+ );
688
+ }
689
+
690
+ /**
691
+ * Show execution result
692
+ */
693
+ private async showExecutionResult(
694
+ buttonId: string,
695
+ result: ExecutionResult,
696
+ ): Promise<void> {
697
+ const buttonState = this.buttonStates.get(buttonId);
698
+ if (!buttonState) {
699
+ return;
700
+ }
701
+
702
+ const config = buttonState.config;
703
+
704
+ // Show success notification
705
+ if (result.code === 0) {
706
+ const message = this.getResultMessage(result);
707
+ vscode.window
708
+ .showInformationMessage(`✅ ${config.text}: ${message}`, "View Output")
709
+ .then((selection) => {
710
+ if (selection === "View Output") {
711
+ this.showOutput(result);
712
+ }
713
+ });
714
+ }
715
+ }
716
+
717
+ /**
718
+ * Show execution error
719
+ */
720
+ private async showExecutionError(
721
+ buttonId: string,
722
+ error: unknown,
723
+ ): Promise<void> {
724
+ const buttonState = this.buttonStates.get(buttonId);
725
+ if (!buttonState) {
726
+ return;
727
+ }
728
+
729
+ const config = buttonState.config;
730
+ const errorMessage = error instanceof Error ? error.message : String(error);
731
+ vscode.window
732
+ .showErrorMessage(`❌ ${config.text}: ${errorMessage}`, "View Details")
733
+ .then((selection) => {
734
+ if (selection === "View Details") {
735
+ vscode.window.showErrorMessage(errorMessage, { modal: true });
736
+ }
737
+ });
738
+ }
739
+
740
+ /**
741
+ * Get result message for display
742
+ */
743
+ private getResultMessage(result: ExecutionResult): string {
744
+ const showTime = true;
745
+ const timeStr =
746
+ showTime && result.duration ? ` in ${result.duration}ms` : "";
747
+
748
+ if (result.stdout && result.stdout.trim()) {
749
+ const output = result.stdout.trim().split("\n")[0];
750
+ return output.length > 100
751
+ ? `${output.substring(0, 100)}...${timeStr}`
752
+ : `${output}${timeStr}`;
753
+ }
754
+
755
+ return `Completed successfully${timeStr}`;
756
+ }
757
+
758
+ /**
759
+ * Show command output
760
+ */
761
+ private showOutput(result: ExecutionResult): void {
762
+ const output = `Command Output:\n${result.stdout}\n\nErrors:\n${result.stderr}`;
763
+ vscode.window.showInformationMessage(output, { modal: true });
764
+ }
765
+
766
+ /**
767
+ * Edit button configuration - Main settings menu
768
+ */
769
+ private async editButton(): Promise<void> {
770
+ const mainMenuItems: vscode.QuickPickItem[] = [
771
+ {
772
+ label: "$(add) Add New Button",
773
+ description: "Create a new status bar button",
774
+ },
775
+ {
776
+ label: "$(edit) Edit Existing Button",
777
+ description: "Modify an existing button configuration",
778
+ },
779
+ {
780
+ label: "$(trash) Delete Button",
781
+ description: "Remove a button from the status bar",
782
+ },
783
+ {
784
+ label: "$(copy) Duplicate Button",
785
+ description: "Create a copy of an existing button",
786
+ },
787
+ {
788
+ label: "$(sync) Toggle Button",
789
+ description: "Enable or disable a button",
790
+ },
791
+ {
792
+ label: "$(archive) Manage Presets",
793
+ description: "Save, load, or manage configuration presets",
794
+ },
795
+ {
796
+ label: "$(settings-gear) Open Full Settings",
797
+ description: "Open VS Code settings page",
798
+ },
799
+ {
800
+ label: "$(export) Export Configuration",
801
+ description: "Export all button configurations to a file",
802
+ },
803
+ {
804
+ label: "$(import) Import Configuration",
805
+ description: "Import button configurations from a file",
806
+ },
807
+ ];
808
+
809
+ const selected = await vscode.window.showQuickPick(mainMenuItems, {
810
+ placeHolder: "StatusBar Quick Actions - Settings",
811
+ matchOnDescription: true,
812
+ });
813
+
814
+ if (!selected) {
815
+ return;
816
+ }
817
+
818
+ switch (selected.label) {
819
+ case "$(add) Add New Button":
820
+ await this.addNewButton();
821
+ break;
822
+ case "$(edit) Edit Existing Button":
823
+ await this.selectAndEditButton();
824
+ break;
825
+ case "$(trash) Delete Button":
826
+ await this.deleteButton();
827
+ break;
828
+ case "$(copy) Duplicate Button":
829
+ await this.duplicateButton();
830
+ break;
831
+ case "$(sync) Toggle Button":
832
+ await this.toggleButton();
833
+ break;
834
+ case "$(archive) Manage Presets":
835
+ await this.managePresets();
836
+ break;
837
+ case "$(settings-gear) Open Full Settings":
838
+ vscode.commands.executeCommand(
839
+ "workbench.action.openSettings",
840
+ "@ext:involvex.statusbar-quick-actions",
841
+ );
842
+ break;
843
+ case "$(export) Export Configuration":
844
+ await this.exportConfiguration();
845
+ break;
846
+ case "$(import) Import Configuration":
847
+ await this.importConfiguration();
848
+ break;
849
+ }
850
+ }
851
+
852
+ /**
853
+ * Add a new button interactively
854
+ */
855
+ private async addNewButton(): Promise<void> {
856
+ // Get button text
857
+ const text = await vscode.window.showInputBox({
858
+ prompt: "Enter button text (supports emojis)",
859
+ placeHolder: "e.g., Build 🔨",
860
+ validateInput: (value) => (value ? null : "Button text is required"),
861
+ });
862
+
863
+ if (!text) {
864
+ return;
865
+ }
866
+
867
+ // Get command type
868
+ const commandTypes: vscode.QuickPickItem[] = [
869
+ { label: "npm", description: "Run npm script" },
870
+ { label: "yarn", description: "Run yarn script" },
871
+ { label: "pnpm", description: "Run pnpm script" },
872
+ { label: "bun", description: "Run bun script" },
873
+ { label: "shell", description: "Run shell command" },
874
+ { label: "vscode", description: "Run VS Code command" },
875
+ { label: "task", description: "Run VS Code task" },
876
+ { label: "github", description: "Run GitHub CLI command" },
877
+ { label: "npx", description: "Run npx command" },
878
+ { label: "pnpx", description: "Run pnpx command" },
879
+ { label: "bunx", description: "Run bunx command" },
880
+ { label: "detect", description: "Auto-detect package manager" },
881
+ ];
882
+
883
+ const commandType = await vscode.window.showQuickPick(commandTypes, {
884
+ placeHolder: "Select command type",
885
+ });
886
+
887
+ if (!commandType) {
888
+ return;
889
+ }
890
+
891
+ // Get command/script
892
+ const command = await vscode.window.showInputBox({
893
+ prompt:
894
+ commandType.label === "npm" ||
895
+ commandType.label === "yarn" ||
896
+ commandType.label === "pnpm" ||
897
+ commandType.label === "bun" ||
898
+ commandType.label === "npx" ||
899
+ commandType.label === "pnpx" ||
900
+ commandType.label === "bunx" ||
901
+ commandType.label === "detect"
902
+ ? "Enter script name"
903
+ : "Enter command",
904
+ placeHolder:
905
+ commandType.label === "npm" ? "e.g., build" : 'e.g., echo "Hello"',
906
+ validateInput: (value) => (value ? null : "Command is required"),
907
+ });
908
+
909
+ if (!command) {
910
+ return;
911
+ }
912
+
913
+ // Generate unique ID
914
+ const id = `button_${Date.now()}`;
915
+
916
+ // Create new button configuration
917
+ const newButton: StatusBarButtonConfig = {
918
+ id,
919
+ text,
920
+ tooltip: text,
921
+ command: {
922
+ type: commandType.label as
923
+ | "npm"
924
+ | "yarn"
925
+ | "pnpm"
926
+ | "bun"
927
+ | "npx"
928
+ | "pnpx"
929
+ | "bunx"
930
+ | "shell"
931
+ | "github"
932
+ | "vscode"
933
+ | "task"
934
+ | "detect",
935
+ script: [
936
+ "npm",
937
+ "yarn",
938
+ "pnpm",
939
+ "bun",
940
+ "bunx",
941
+ "npx",
942
+ "pnpx",
943
+ "detect",
944
+ ].includes(commandType.label)
945
+ ? command
946
+ : undefined,
947
+ command: ![
948
+ "npm",
949
+ "yarn",
950
+ "pnpm",
951
+ "bun",
952
+ "bunx",
953
+ "npx",
954
+ "pnpx",
955
+ "detect",
956
+ ].includes(commandType.label)
957
+ ? command
958
+ : undefined,
959
+ },
960
+ enabled: true,
961
+ alignment: "left",
962
+ priority: 100,
963
+ };
964
+
965
+ // Add to configuration
966
+ const config = this.configManager.getConfig();
967
+ config.buttons.push(newButton);
968
+ await this.configManager.setConfig("buttons", config.buttons);
969
+
970
+ vscode.window.showInformationMessage(
971
+ `✅ Button "${text}" added successfully!`,
972
+ );
973
+ }
974
+
975
+ /**
976
+ * Select and edit an existing button
977
+ */
978
+ private async selectAndEditButton(): Promise<void> {
979
+ const config = this.configManager.getConfig();
980
+ if (config.buttons.length === 0) {
981
+ vscode.window.showInformationMessage("No buttons configured yet.");
982
+ return;
983
+ }
984
+
985
+ const items = config.buttons.map((button) => ({
986
+ label: button.text,
987
+ description: button.command.type,
988
+ detail: button.tooltip,
989
+ button: button,
990
+ }));
991
+
992
+ const selected = await vscode.window.showQuickPick(items, {
993
+ placeHolder: "Select a button to edit",
994
+ });
995
+
996
+ if (selected) {
997
+ vscode.commands.executeCommand(
998
+ "workbench.action.openSettings",
999
+ `@ext:involvex.statusbar-quick-actions buttons`,
1000
+ );
1001
+ }
1002
+ }
1003
+
1004
+ /**
1005
+ * Delete a button
1006
+ */
1007
+ private async deleteButton(): Promise<void> {
1008
+ const config = this.configManager.getConfig();
1009
+ if (config.buttons.length === 0) {
1010
+ vscode.window.showInformationMessage("No buttons configured yet.");
1011
+ return;
1012
+ }
1013
+
1014
+ const items = config.buttons.map((button) => ({
1015
+ label: button.text,
1016
+ description: button.command.type,
1017
+ detail: button.id,
1018
+ }));
1019
+
1020
+ const selected = await vscode.window.showQuickPick(items, {
1021
+ placeHolder: "Select a button to delete",
1022
+ });
1023
+
1024
+ if (!selected) {
1025
+ return;
1026
+ }
1027
+
1028
+ const confirm = await vscode.window.showWarningMessage(
1029
+ `Delete button "${selected.label}"?`,
1030
+ { modal: true },
1031
+ "Yes, Delete",
1032
+ "No",
1033
+ );
1034
+
1035
+ if (confirm === "Yes, Delete") {
1036
+ const updatedButtons = config.buttons.filter(
1037
+ (b) => b.id !== selected.detail,
1038
+ );
1039
+ await this.configManager.setConfig("buttons", updatedButtons);
1040
+ vscode.window.showInformationMessage(
1041
+ `✅ Button "${selected.label}" deleted`,
1042
+ );
1043
+ }
1044
+ }
1045
+
1046
+ /**
1047
+ * Duplicate a button
1048
+ */
1049
+ private async duplicateButton(): Promise<void> {
1050
+ const config = this.configManager.getConfig();
1051
+ if (config.buttons.length === 0) {
1052
+ vscode.window.showInformationMessage("No buttons configured yet.");
1053
+ return;
1054
+ }
1055
+
1056
+ const items = config.buttons.map((button) => ({
1057
+ label: button.text,
1058
+ description: button.command.type,
1059
+ button: button,
1060
+ }));
1061
+
1062
+ const selected = await vscode.window.showQuickPick(items, {
1063
+ placeHolder: "Select a button to duplicate",
1064
+ });
1065
+
1066
+ if (!selected) {
1067
+ return;
1068
+ }
1069
+
1070
+ const newButton = {
1071
+ ...selected.button,
1072
+ id: `button_${Date.now()}`,
1073
+ text: `${selected.button.text} (Copy)`,
1074
+ };
1075
+
1076
+ config.buttons.push(newButton);
1077
+ await this.configManager.setConfig("buttons", config.buttons);
1078
+ vscode.window.showInformationMessage(`✅ Button duplicated successfully!`);
1079
+ }
1080
+
1081
+ /**
1082
+ * Toggle button enabled state
1083
+ */
1084
+ private async toggleButton(): Promise<void> {
1085
+ const config = this.configManager.getConfig();
1086
+ if (config.buttons.length === 0) {
1087
+ vscode.window.showInformationMessage("No buttons configured yet.");
1088
+ return;
1089
+ }
1090
+
1091
+ const items = config.buttons.map((button) => ({
1092
+ label: button.text,
1093
+ description: button.enabled ? "$(check) Enabled" : "$(x) Disabled",
1094
+ button: button,
1095
+ }));
1096
+
1097
+ const selected = await vscode.window.showQuickPick(items, {
1098
+ placeHolder: "Select a button to toggle",
1099
+ });
1100
+
1101
+ if (!selected) {
1102
+ return;
1103
+ }
1104
+
1105
+ const buttonIndex = config.buttons.findIndex(
1106
+ (b) => b.id === selected.button.id,
1107
+ );
1108
+ config.buttons[buttonIndex].enabled = !config.buttons[buttonIndex].enabled;
1109
+
1110
+ await this.configManager.setConfig("buttons", config.buttons);
1111
+ const status = config.buttons[buttonIndex].enabled ? "enabled" : "disabled";
1112
+ vscode.window.showInformationMessage(
1113
+ `✅ Button "${selected.label}" ${status}`,
1114
+ );
1115
+ }
1116
+
1117
+ /**
1118
+ * Export configuration to file
1119
+ */
1120
+ private async exportConfiguration(): Promise<void> {
1121
+ const config = this.configManager.getConfig();
1122
+
1123
+ const uri = await vscode.window.showSaveDialog({
1124
+ filters: { JSON: ["json"] },
1125
+ defaultUri: vscode.Uri.file("statusbar-quick-actions-config.json"),
1126
+ });
1127
+
1128
+ if (uri) {
1129
+ fs.writeFileSync(uri.fsPath, JSON.stringify(config, null, 2));
1130
+ vscode.window.showInformationMessage(
1131
+ `✅ Configuration exported to ${uri.fsPath}`,
1132
+ );
1133
+ }
1134
+ }
1135
+
1136
+ /**
1137
+ * Import configuration from file
1138
+ */
1139
+ private async importConfiguration(): Promise<void> {
1140
+ const uri = await vscode.window.showOpenDialog({
1141
+ filters: { JSON: ["json"] },
1142
+ canSelectMany: false,
1143
+ });
1144
+
1145
+ if (uri && uri[0]) {
1146
+ try {
1147
+ const content = fs.readFileSync(uri[0].fsPath, "utf8");
1148
+ const importedConfig = JSON.parse(content);
1149
+
1150
+ const merge = await vscode.window.showQuickPick(
1151
+ ["Replace All", "Merge with Existing"],
1152
+ { placeHolder: "Import mode" },
1153
+ );
1154
+
1155
+ if (!merge) {
1156
+ return;
1157
+ }
1158
+
1159
+ if (merge === "Replace All") {
1160
+ await this.configManager.setConfig(
1161
+ "buttons",
1162
+ importedConfig.buttons || [],
1163
+ );
1164
+ } else {
1165
+ const config = this.configManager.getConfig();
1166
+ const mergedButtons = [
1167
+ ...config.buttons,
1168
+ ...(importedConfig.buttons || []),
1169
+ ];
1170
+ await this.configManager.setConfig("buttons", mergedButtons);
1171
+ }
1172
+
1173
+ vscode.window.showInformationMessage(
1174
+ "✅ Configuration imported successfully!",
1175
+ );
1176
+ } catch (error) {
1177
+ vscode.window.showErrorMessage(
1178
+ `Failed to import configuration: ${error}`,
1179
+ );
1180
+ }
1181
+ }
1182
+ }
1183
+
1184
+ /**
1185
+ * Add execution result to history
1186
+ */
1187
+ private async addToHistory(
1188
+ buttonId: string,
1189
+ result: ExecutionResult,
1190
+ ): Promise<void> {
1191
+ try {
1192
+ const historyKey = `history_${buttonId}`;
1193
+ const history: ExecutionResult[] = this.context.globalState.get(
1194
+ historyKey,
1195
+ [],
1196
+ );
1197
+
1198
+ // Add new result
1199
+ history.unshift(result);
1200
+
1201
+ // Limit history size (default 20, configurable per button)
1202
+ const buttonState = this.buttonStates.get(buttonId);
1203
+ const maxEntries = buttonState?.config.history?.maxEntries || 20;
1204
+ while (history.length > maxEntries) {
1205
+ history.pop();
1206
+ }
1207
+
1208
+ // Save to global state
1209
+ await this.context.globalState.update(historyKey, history);
1210
+
1211
+ // Also update button state for quick access
1212
+ if (buttonState) {
1213
+ buttonState.history = history;
1214
+ }
1215
+ } catch (error) {
1216
+ console.error(
1217
+ `Failed to add execution to history for button ${buttonId}:`,
1218
+ error,
1219
+ );
1220
+ }
1221
+ }
1222
+
1223
+ /**
1224
+ * Load history from global state
1225
+ */
1226
+ private async loadHistory(buttonId: string): Promise<ExecutionResult[]> {
1227
+ try {
1228
+ const historyKey = `history_${buttonId}`;
1229
+ return this.context.globalState.get(historyKey, []);
1230
+ } catch (error) {
1231
+ console.error(`Failed to load history for button ${buttonId}:`, error);
1232
+ return [];
1233
+ }
1234
+ }
1235
+
1236
+ /**
1237
+ * Load history asynchronously and update button state
1238
+ */
1239
+ private async loadHistoryAsync(buttonId: string): Promise<void> {
1240
+ const buttonState = this.buttonStates.get(buttonId);
1241
+ if (!buttonState) {
1242
+ return;
1243
+ }
1244
+
1245
+ const history = await this.loadHistory(buttonId);
1246
+ buttonState.history = history;
1247
+ this.debugLog(`History loaded for ${buttonId}: ${history.length} entries`);
1248
+ }
1249
+
1250
+ /**
1251
+ * Get all history entries across all buttons
1252
+ */
1253
+ private async getAllHistory(): Promise<Map<string, ExecutionResult[]>> {
1254
+ const allHistory = new Map<string, ExecutionResult[]>();
1255
+
1256
+ for (const [buttonId] of this.buttonStates) {
1257
+ const history = await this.loadHistory(buttonId);
1258
+ if (history.length > 0) {
1259
+ allHistory.set(buttonId, history);
1260
+ }
1261
+ }
1262
+
1263
+ return allHistory;
1264
+ }
1265
+
1266
+ /**
1267
+ * View command history
1268
+ */
1269
+ private async viewHistory(): Promise<void> {
1270
+ const allHistory = await this.getAllHistory();
1271
+
1272
+ if (allHistory.size === 0) {
1273
+ vscode.window.showInformationMessage("No command history available yet.");
1274
+ return;
1275
+ }
1276
+
1277
+ // Create quick pick items from history
1278
+ const items: vscode.QuickPickItem[] = [];
1279
+
1280
+ for (const [buttonId, history] of allHistory) {
1281
+ const buttonState = this.buttonStates.get(buttonId);
1282
+ const buttonName = buttonState?.config.text || buttonId;
1283
+
1284
+ items.push({
1285
+ label: `$(inbox) ${buttonName}`,
1286
+ kind: vscode.QuickPickItemKind.Separator,
1287
+ });
1288
+
1289
+ history.forEach((entry) => {
1290
+ const status = entry.code === 0 ? "$(check)" : "$(error)";
1291
+ const time = entry.timestamp.toLocaleString();
1292
+ const duration = entry.duration ? ` (${entry.duration}ms)` : "";
1293
+
1294
+ items.push({
1295
+ label: `${status} ${entry.command}`,
1296
+ description: `${time}${duration}`,
1297
+ detail: entry.stderr || entry.stdout?.substring(0, 100),
1298
+ buttons: [
1299
+ {
1300
+ iconPath: new vscode.ThemeIcon("output"),
1301
+ tooltip: "View Full Output",
1302
+ },
1303
+ ],
1304
+ });
1305
+ });
1306
+ }
1307
+
1308
+ // Show quick pick
1309
+ const selected = await vscode.window.showQuickPick(items, {
1310
+ placeHolder: "Command Execution History",
1311
+ matchOnDescription: true,
1312
+ matchOnDetail: true,
1313
+ });
1314
+
1315
+ if (selected && selected.detail) {
1316
+ // Show detailed output in a new text document
1317
+ const doc = await vscode.workspace.openTextDocument({
1318
+ content: `Command: ${selected.label}\nTime: ${selected.description}\n\nOutput:\n${selected.detail}`,
1319
+ language: "text",
1320
+ });
1321
+ await vscode.window.showTextDocument(doc);
1322
+ }
1323
+ }
1324
+
1325
+ /**
1326
+ * Clear command history
1327
+ */
1328
+ private async clearHistory(): Promise<void> {
1329
+ const confirm = await vscode.window.showWarningMessage(
1330
+ "Are you sure you want to clear all command history?",
1331
+ { modal: true },
1332
+ "Yes, Clear History",
1333
+ "No",
1334
+ );
1335
+
1336
+ if (confirm === "Yes, Clear History") {
1337
+ try {
1338
+ // Clear history for all buttons
1339
+ for (const [buttonId, buttonState] of this.buttonStates) {
1340
+ const historyKey = `history_${buttonId}`;
1341
+ await this.context.globalState.update(historyKey, []);
1342
+ buttonState.history = [];
1343
+ }
1344
+
1345
+ vscode.window.showInformationMessage(
1346
+ "✅ Command history cleared successfully",
1347
+ );
1348
+ } catch (error) {
1349
+ vscode.window.showErrorMessage(`Failed to clear history: ${error}`);
1350
+ }
1351
+ }
1352
+ }
1353
+
1354
+ /**
1355
+ * Show welcome message on first activation
1356
+ */
1357
+ private async showWelcomeMessage(): Promise<void> {
1358
+ const config = this.configManager.getConfig();
1359
+
1360
+ if (config.buttons.length === 0) {
1361
+ vscode.window
1362
+ .showInformationMessage(
1363
+ "👋 Welcome to StatusBar Quick Actions! Configure your first button in Settings.",
1364
+ "Open Settings",
1365
+ )
1366
+ .then((selection) => {
1367
+ if (selection === "Open Settings") {
1368
+ vscode.commands.executeCommand(
1369
+ "workbench.action.openSettings",
1370
+ "@ext:statusbar-quick-actions",
1371
+ );
1372
+ }
1373
+ });
1374
+ }
1375
+ }
1376
+
1377
+ /**
1378
+ * Get default output panel configuration
1379
+ */
1380
+ private getDefaultOutputConfig() {
1381
+ return {
1382
+ enabled: true,
1383
+ mode: "per-button" as const,
1384
+ format: "formatted" as const,
1385
+ clearOnRun: false,
1386
+ showTimestamps: true,
1387
+ preserveHistory: true,
1388
+ maxLines: 1000,
1389
+ };
1390
+ }
1391
+
1392
+ /**
1393
+ * Get default performance configuration
1394
+ */
1395
+ private getDefaultPerformanceConfig() {
1396
+ return {
1397
+ visibilityDebounceMs: 300,
1398
+ enableVirtualization: false,
1399
+ cacheResults: true,
1400
+ };
1401
+ }
1402
+
1403
+ /**
1404
+ * Setup editor change listener for debounced visibility checks
1405
+ */
1406
+ private setupEditorChangeListener(): void {
1407
+ this.editorChangeListener = vscode.window.onDidChangeActiveTextEditor(
1408
+ () => {
1409
+ // Debounced visibility check for all buttons
1410
+ this.buttonStates.forEach((buttonState, buttonId) => {
1411
+ if (buttonState.config.visibility) {
1412
+ const customDebounce = buttonState.config.visibility.debounceMs;
1413
+
1414
+ this.visibilityManager.checkVisibilityDebounced(
1415
+ buttonId,
1416
+ buttonState.config.visibility,
1417
+ customDebounce,
1418
+ (isVisible) => {
1419
+ // Update button visibility
1420
+ if (isVisible) {
1421
+ buttonState.item.show();
1422
+ } else {
1423
+ buttonState.item.hide();
1424
+ }
1425
+ },
1426
+ );
1427
+ }
1428
+ });
1429
+ },
1430
+ );
1431
+
1432
+ this.disposables.push(this.editorChangeListener);
1433
+ }
1434
+
1435
+ /**
1436
+ * Manage presets UI
1437
+ */
1438
+ private async managePresets(): Promise<void> {
1439
+ const items: vscode.QuickPickItem[] = [
1440
+ {
1441
+ label: "$(add) Create New Preset",
1442
+ description: "Save current configuration as a preset",
1443
+ },
1444
+ {
1445
+ label: "$(archive) Apply Preset",
1446
+ description: "Load a saved preset",
1447
+ },
1448
+ {
1449
+ label: "$(list-unordered) View All Presets",
1450
+ description: "Browse and manage saved presets",
1451
+ },
1452
+ {
1453
+ label: "$(export) Export Preset",
1454
+ description: "Export a preset to a file",
1455
+ },
1456
+ {
1457
+ label: "$(import) Import Preset",
1458
+ description: "Import a preset from a file",
1459
+ },
1460
+ ];
1461
+
1462
+ const selected = await vscode.window.showQuickPick(items, {
1463
+ placeHolder: "Preset Management",
1464
+ matchOnDescription: true,
1465
+ });
1466
+
1467
+ if (!selected) {
1468
+ return;
1469
+ }
1470
+
1471
+ switch (selected.label) {
1472
+ case "$(add) Create New Preset":
1473
+ await this.saveAsPreset();
1474
+ break;
1475
+ case "$(archive) Apply Preset":
1476
+ await this.applyPresetCommand();
1477
+ break;
1478
+ case "$(list-unordered) View All Presets":
1479
+ await this.viewAllPresets();
1480
+ break;
1481
+ case "$(export) Export Preset":
1482
+ await this.exportPresetCommand();
1483
+ break;
1484
+ case "$(import) Import Preset":
1485
+ await this.importPresetCommand();
1486
+ break;
1487
+ }
1488
+ }
1489
+
1490
+ /**
1491
+ * Save current configuration as a preset
1492
+ */
1493
+ private async saveAsPreset(): Promise<void> {
1494
+ const name = await vscode.window.showInputBox({
1495
+ prompt: "Enter preset name",
1496
+ placeHolder: "e.g., My Development Setup",
1497
+ validateInput: (value) => (value ? null : "Name is required"),
1498
+ });
1499
+
1500
+ if (!name) {
1501
+ return;
1502
+ }
1503
+
1504
+ const description = await vscode.window.showInputBox({
1505
+ prompt: "Enter preset description (optional)",
1506
+ placeHolder: "e.g., Standard buttons for Node.js development",
1507
+ });
1508
+
1509
+ try {
1510
+ const currentConfig = this.configManager.getConfig();
1511
+ await this.presetManager.createPresetFromConfig(
1512
+ name,
1513
+ description || "",
1514
+ currentConfig,
1515
+ );
1516
+
1517
+ vscode.window.showInformationMessage(
1518
+ `✅ Preset "${name}" created successfully!`,
1519
+ );
1520
+ } catch (error) {
1521
+ vscode.window.showErrorMessage(`Failed to create preset: ${error}`);
1522
+ }
1523
+ }
1524
+
1525
+ /**
1526
+ * Apply a preset command
1527
+ */
1528
+ private async applyPresetCommand(): Promise<void> {
1529
+ const presets = this.presetManager.getAllPresets();
1530
+
1531
+ if (presets.length === 0) {
1532
+ vscode.window.showInformationMessage("No presets available yet.");
1533
+ return;
1534
+ }
1535
+
1536
+ const items = presets.map((preset) => ({
1537
+ label: preset.name,
1538
+ description: preset.description,
1539
+ detail: `${preset.buttons.length} buttons · Created ${preset.metadata?.created.toLocaleDateString()}`,
1540
+ preset,
1541
+ }));
1542
+
1543
+ const selected = await vscode.window.showQuickPick(items, {
1544
+ placeHolder: "Select a preset to apply",
1545
+ matchOnDescription: true,
1546
+ matchOnDetail: true,
1547
+ });
1548
+
1549
+ if (!selected) {
1550
+ return;
1551
+ }
1552
+
1553
+ // Ask for application mode
1554
+ const modeItems: vscode.QuickPickItem[] = [
1555
+ {
1556
+ label: "Replace",
1557
+ description: "Replace all current buttons with preset buttons",
1558
+ },
1559
+ {
1560
+ label: "Merge",
1561
+ description:
1562
+ "Merge preset buttons with current buttons (overwrite duplicates)",
1563
+ },
1564
+ {
1565
+ label: "Append",
1566
+ description: "Add preset buttons to current buttons",
1567
+ },
1568
+ ];
1569
+
1570
+ const modeSelected = await vscode.window.showQuickPick(modeItems, {
1571
+ placeHolder: "How should the preset be applied?",
1572
+ });
1573
+
1574
+ if (!modeSelected) {
1575
+ return;
1576
+ }
1577
+
1578
+ const mode = modeSelected.label.toLowerCase() as
1579
+ | "replace"
1580
+ | "merge"
1581
+ | "append";
1582
+
1583
+ // Show impact preview
1584
+ const impact = this.configManager.getPresetImpact(selected.preset, mode);
1585
+ const confirm = await vscode.window.showWarningMessage(
1586
+ `Apply preset "${selected.preset.name}"?\n\nImpact:\n• ${impact.added} buttons added\n• ${impact.modified} buttons modified\n• ${impact.removed} buttons removed`,
1587
+ { modal: true },
1588
+ "Yes, Apply",
1589
+ "No",
1590
+ );
1591
+
1592
+ if (confirm !== "Yes, Apply") {
1593
+ return;
1594
+ }
1595
+
1596
+ try {
1597
+ await this.configManager.applyPreset(selected.preset, mode);
1598
+ vscode.window.showInformationMessage(
1599
+ `✅ Preset "${selected.preset.name}" applied successfully!`,
1600
+ );
1601
+ } catch (error) {
1602
+ vscode.window.showErrorMessage(`Failed to apply preset: ${error}`);
1603
+ }
1604
+ }
1605
+
1606
+ /**
1607
+ * View all presets
1608
+ */
1609
+ private async viewAllPresets(): Promise<void> {
1610
+ const presets = this.presetManager.getAllPresets();
1611
+
1612
+ if (presets.length === 0) {
1613
+ vscode.window.showInformationMessage("No presets available yet.");
1614
+ return;
1615
+ }
1616
+
1617
+ const items = presets.map((preset) => ({
1618
+ label: preset.name,
1619
+ description: `${preset.buttons.length} buttons`,
1620
+ detail: preset.description,
1621
+ buttons: [
1622
+ {
1623
+ iconPath: new vscode.ThemeIcon("edit"),
1624
+ tooltip: "Rename Preset",
1625
+ },
1626
+ {
1627
+ iconPath: new vscode.ThemeIcon("copy"),
1628
+ tooltip: "Duplicate Preset",
1629
+ },
1630
+ {
1631
+ iconPath: new vscode.ThemeIcon("trash"),
1632
+ tooltip: "Delete Preset",
1633
+ },
1634
+ ],
1635
+ preset,
1636
+ }));
1637
+
1638
+ const selected = await vscode.window.showQuickPick(items, {
1639
+ placeHolder: "Manage Presets",
1640
+ matchOnDescription: true,
1641
+ matchOnDetail: true,
1642
+ });
1643
+
1644
+ if (selected) {
1645
+ // For now, just apply the preset if selected
1646
+ await this.applyPresetCommand();
1647
+ }
1648
+ }
1649
+
1650
+ /**
1651
+ * Export preset command
1652
+ */
1653
+ private async exportPresetCommand(): Promise<void> {
1654
+ const presets = this.presetManager.getAllPresets();
1655
+
1656
+ if (presets.length === 0) {
1657
+ vscode.window.showInformationMessage("No presets available to export.");
1658
+ return;
1659
+ }
1660
+
1661
+ const items = presets.map((preset) => ({
1662
+ label: preset.name,
1663
+ description: `${preset.buttons.length} buttons`,
1664
+ preset,
1665
+ }));
1666
+
1667
+ const selected = await vscode.window.showQuickPick(items, {
1668
+ placeHolder: "Select a preset to export",
1669
+ });
1670
+
1671
+ if (!selected) {
1672
+ return;
1673
+ }
1674
+
1675
+ try {
1676
+ await this.presetManager.exportPreset(selected.preset);
1677
+ } catch (error) {
1678
+ vscode.window.showErrorMessage(`Failed to export preset: ${error}`);
1679
+ }
1680
+ }
1681
+
1682
+ /**
1683
+ * Import preset command
1684
+ */
1685
+ private async importPresetCommand(): Promise<void> {
1686
+ try {
1687
+ const preset = await this.presetManager.importPreset();
1688
+ if (preset) {
1689
+ // Optionally ask if they want to apply it immediately
1690
+ const apply = await vscode.window.showInformationMessage(
1691
+ `Preset "${preset.name}" imported. Apply it now?`,
1692
+ "Yes",
1693
+ "No",
1694
+ );
1695
+
1696
+ if (apply === "Yes") {
1697
+ await this.configManager.applyPreset(preset, "replace");
1698
+ }
1699
+ }
1700
+ } catch (error) {
1701
+ vscode.window.showErrorMessage(`Failed to import preset: ${error}`);
1702
+ }
1703
+ }
1704
+
1705
+ /**
1706
+ * Refresh a button's dynamic label
1707
+ */
1708
+ private async refreshButtonLabel(buttonId: string): Promise<void> {
1709
+ const buttonState = this.buttonStates.get(buttonId);
1710
+ if (!buttonState || !buttonState.config.dynamicLabel) {
1711
+ return;
1712
+ }
1713
+
1714
+ try {
1715
+ const newLabel = await this.dynamicLabelManager.evaluateLabel(
1716
+ buttonId,
1717
+ buttonState.config.dynamicLabel,
1718
+ );
1719
+
1720
+ // Update button text with dynamic label
1721
+ if (buttonState.config.icon) {
1722
+ // If icon exists, append label after icon
1723
+ const iconText = this.getButtonDisplayText(buttonState.config);
1724
+ buttonState.item.text = `${iconText} ${newLabel}`;
1725
+ } else {
1726
+ // Replace text entirely
1727
+ buttonState.item.text = newLabel;
1728
+ }
1729
+ } catch (error) {
1730
+ console.error(`Failed to refresh label for ${buttonId}:`, error);
1731
+ }
1732
+ }
1733
+ }
1734
+
1735
+ /**
1736
+ * Extension activation function
1737
+ */
1738
+ export function activate(context: vscode.ExtensionContext): void {
1739
+ console.log("Activating StatusBar Quick Actions extension...");
1740
+
1741
+ const extension = new StatusBarQuickActionsExtension(context);
1742
+ context.subscriptions.push({
1743
+ dispose: () => extension.deactivate(),
1744
+ });
1745
+
1746
+ extension.activate();
1747
+ }
1748
+
1749
+ /**
1750
+ * Extension deactivation function
1751
+ */
1752
+ export function deactivate(): void {
1753
+ console.log("Deactivating StatusBar Quick Actions extension...");
1754
+ }