-
Notifications
You must be signed in to change notification settings - Fork 13
Expand file tree
/
Copy pathClaudeCodeControl.CustomCommands.cs
More file actions
546 lines (490 loc) · 20.3 KB
/
ClaudeCodeControl.CustomCommands.cs
File metadata and controls
546 lines (490 loc) · 20.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
/* *******************************************************************************************************************
* Application: ClaudeCodeExtension
*
* Autor: Daniel Carvalho Liedke
*
* Copyright © Daniel Carvalho Liedke 2026
* Usage and reproduction in any manner whatsoever without the written permission of Daniel Carvalho Liedke is strictly forbidden.
*
* Purpose: User-defined custom commands — configuration dialog, toolbar dropdown,
* and dispatch into the embedded terminal.
*
* *******************************************************************************************************************/
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using Microsoft.VisualStudio.Shell;
namespace ClaudeCodeVS
{
public partial class ClaudeCodeControl
{
#region Toolbar Button + Dropdown Population
/// <summary>
/// Refreshes the visibility and menu contents of the custom-commands toolbar button.
/// Hides the button entirely when no custom commands are configured.
/// </summary>
private void RefreshCustomCommandsButton()
{
try
{
if (CustomCommandsButton == null) return;
var commands = _settings?.CustomCommands;
bool hasAny = commands != null && commands.Count > 0;
CustomCommandsButton.Visibility = hasAny ? Visibility.Visible : Visibility.Collapsed;
var menu = CustomCommandsButton.ContextMenu;
if (menu == null) return;
menu.Items.Clear();
if (!hasAny) return;
foreach (var cmd in commands)
{
string label = string.IsNullOrWhiteSpace(cmd.Name) ? cmd.Command : cmd.Name;
var item = new MenuItem
{
Header = label,
ToolTip = cmd.Command,
Tag = cmd
};
item.Click += CustomCommandMenuItem_Click;
menu.Items.Add(item);
}
}
catch (Exception ex)
{
Debug.WriteLine($"Error refreshing custom commands button: {ex.Message}");
}
}
/// <summary>
/// Handles the toolbar custom-commands button click - opens the dropdown menu.
/// </summary>
private void CustomCommandsButton_Click(object sender, RoutedEventArgs e)
{
var button = sender as Button;
if (button?.ContextMenu != null)
{
button.ContextMenu.PlacementTarget = button;
button.ContextMenu.Placement = System.Windows.Controls.Primitives.PlacementMode.Bottom;
button.ContextMenu.IsOpen = true;
}
}
/// <summary>
/// Handles a click on a single custom-command entry in the toolbar dropdown.
/// Sends the configured command literal directly to the active code agent.
/// </summary>
#pragma warning disable VSTHRD100 // Avoid async void methods - WPF event handler
private async void CustomCommandMenuItem_Click(object sender, RoutedEventArgs e)
#pragma warning restore VSTHRD100
{
try
{
var item = sender as MenuItem;
var cmd = item?.Tag as CustomCommand;
if (cmd == null || string.IsNullOrEmpty(cmd.Command)) return;
await SendTextToTerminalAsync(cmd.Command);
}
catch (Exception ex)
{
Debug.WriteLine($"Error sending custom command: {ex.Message}");
MessageBox.Show($"Failed to send custom command: {ex.Message}",
"Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
#endregion
#region Configure Custom Commands Menu Handler
/// <summary>
/// Handles the "Configure Custom Commands..." menu item click.
/// Opens the management dialog and refreshes the toolbar button on close.
/// </summary>
private void ConfigureCustomCommandsMenuItem_Click(object sender, RoutedEventArgs e)
{
try
{
if (_settings == null) _settings = new ClaudeCodeSettings();
if (_settings.CustomCommands == null) _settings.CustomCommands = new List<CustomCommand>();
ShowCustomCommandsDialog();
SaveSettings();
RefreshCustomCommandsButton();
}
catch (Exception ex)
{
Debug.WriteLine($"Error configuring custom commands: {ex.Message}");
MessageBox.Show($"Error configuring custom commands: {ex.Message}",
"Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
#endregion
#region Configuration Dialog
/// <summary>
/// Resolves VS theme brushes for use inside programmatically-built dialogs.
/// Falls back to system colors when the resource lookup fails.
/// </summary>
private void GetThemeBrushes(out Brush background, out Brush foreground)
{
try
{
background = (SolidColorBrush)FindResource(VsBrushes.WindowKey);
foreground = (SolidColorBrush)FindResource(VsBrushes.WindowTextKey);
}
catch
{
background = SystemColors.WindowBrush;
foreground = SystemColors.WindowTextBrush;
}
}
/// <summary>
/// Resolves the shared <c>AdaptiveButtonStyle</c> from the user control's resources
/// so dialog buttons get VS-theme-aware hover and pressed states instead of the
/// default Aero light-blue (which washes out white text in dark theme).
/// </summary>
private Style GetDialogButtonStyle()
{
try
{
return TryFindResource("AdaptiveButtonStyle") as Style;
}
catch
{
return null;
}
}
/// <summary>
/// Shows the modal dialog for adding, editing, and removing custom commands.
/// Mutates _settings.CustomCommands in place; caller is responsible for saving.
/// </summary>
private void ShowCustomCommandsDialog()
{
GetThemeBrushes(out Brush themeBg, out Brush themeFg);
var dialog = new Window
{
Title = "Configure Custom Commands",
Width = 600,
Height = 420,
WindowStartupLocation = WindowStartupLocation.CenterOwner,
ResizeMode = ResizeMode.CanResize,
MinWidth = 480,
MinHeight = 320,
Background = themeBg,
Foreground = themeFg,
ShowInTaskbar = false
};
try { dialog.Owner = Application.Current?.MainWindow; } catch { }
var grid = new Grid { Margin = new Thickness(12) };
grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
var label = new TextBlock
{
Text = "Custom commands appear in the toolbar dropdown next to the agent menu. " +
"Clicking one sends the configured text directly to the active code agent — " +
"useful for slash commands (e.g. /codex-review) or canned prompts.",
TextWrapping = TextWrapping.Wrap,
Foreground = themeFg,
Margin = new Thickness(0, 0, 0, 10)
};
Grid.SetRow(label, 0);
grid.Children.Add(label);
// Center: list of commands + side buttons
var listGrid = new Grid();
listGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
listGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
Grid.SetRow(listGrid, 1);
grid.Children.Add(listGrid);
var listBox = new ListBox
{
Background = themeBg,
Foreground = themeFg,
BorderBrush = themeFg,
Margin = new Thickness(0, 0, 8, 0)
};
Grid.SetColumn(listBox, 0);
listGrid.Children.Add(listBox);
// Render each entry as "Name — Command"
Action refreshList = () =>
{
listBox.Items.Clear();
foreach (var cmd in _settings.CustomCommands)
{
string display = string.IsNullOrWhiteSpace(cmd.Name)
? cmd.Command
: $"{cmd.Name} — {cmd.Command}";
var lbi = new ListBoxItem
{
Content = display,
Tag = cmd,
Background = themeBg,
Foreground = themeFg
};
listBox.Items.Add(lbi);
}
};
refreshList();
var sideStack = new StackPanel { Orientation = Orientation.Vertical, Width = 90 };
Grid.SetColumn(sideStack, 1);
listGrid.Children.Add(sideStack);
Style buttonStyle = GetDialogButtonStyle();
Func<string, Button> makeSideButton = (text) =>
{
var b = new Button
{
Content = text,
Height = 28,
Margin = new Thickness(0, 0, 0, 6)
};
if (buttonStyle != null)
{
b.Style = buttonStyle;
}
else
{
b.Background = themeBg;
b.Foreground = themeFg;
b.BorderBrush = themeFg;
}
return b;
};
var addButton = makeSideButton("Add...");
var editButton = makeSideButton("Edit...");
var removeButton = makeSideButton("Remove");
var moveUpButton = makeSideButton("Move Up");
var moveDownButton = makeSideButton("Move Down");
sideStack.Children.Add(addButton);
sideStack.Children.Add(editButton);
sideStack.Children.Add(removeButton);
sideStack.Children.Add(moveUpButton);
sideStack.Children.Add(moveDownButton);
// Add - opens the Name + Command input dialog
addButton.Click += (s, args) =>
{
var newCmd = ShowCustomCommandEditorDialog(null, dialog);
if (newCmd != null)
{
_settings.CustomCommands.Add(newCmd);
refreshList();
listBox.SelectedIndex = _settings.CustomCommands.Count - 1;
}
};
// Edit - opens the editor pre-filled with the selected entry
editButton.Click += (s, args) =>
{
int idx = listBox.SelectedIndex;
if (idx < 0 || idx >= _settings.CustomCommands.Count) return;
var existing = _settings.CustomCommands[idx];
var edited = ShowCustomCommandEditorDialog(existing, dialog);
if (edited != null)
{
_settings.CustomCommands[idx] = edited;
refreshList();
listBox.SelectedIndex = idx;
}
};
// Remove - confirms then deletes
removeButton.Click += (s, args) =>
{
int idx = listBox.SelectedIndex;
if (idx < 0 || idx >= _settings.CustomCommands.Count) return;
var cmd = _settings.CustomCommands[idx];
string display = string.IsNullOrWhiteSpace(cmd.Name) ? cmd.Command : cmd.Name;
var result = MessageBox.Show($"Remove custom command \"{display}\"?",
"Confirm Remove", MessageBoxButton.YesNo, MessageBoxImage.Question);
if (result == MessageBoxResult.Yes)
{
_settings.CustomCommands.RemoveAt(idx);
refreshList();
if (_settings.CustomCommands.Count > 0)
{
listBox.SelectedIndex = Math.Min(idx, _settings.CustomCommands.Count - 1);
}
}
};
moveUpButton.Click += (s, args) =>
{
int idx = listBox.SelectedIndex;
if (idx <= 0) return;
var item = _settings.CustomCommands[idx];
_settings.CustomCommands.RemoveAt(idx);
_settings.CustomCommands.Insert(idx - 1, item);
refreshList();
listBox.SelectedIndex = idx - 1;
};
moveDownButton.Click += (s, args) =>
{
int idx = listBox.SelectedIndex;
if (idx < 0 || idx >= _settings.CustomCommands.Count - 1) return;
var item = _settings.CustomCommands[idx];
_settings.CustomCommands.RemoveAt(idx);
_settings.CustomCommands.Insert(idx + 1, item);
refreshList();
listBox.SelectedIndex = idx + 1;
};
// Double-click an entry to edit
listBox.MouseDoubleClick += (s, args) => editButton.RaiseEvent(new RoutedEventArgs(Button.ClickEvent));
// Bottom button row
var bottomPanel = new StackPanel
{
Orientation = Orientation.Horizontal,
HorizontalAlignment = HorizontalAlignment.Right,
Margin = new Thickness(0, 10, 0, 0)
};
Grid.SetRow(bottomPanel, 2);
var closeButton = new Button
{
Content = "Close",
Width = 80,
Height = 26,
IsDefault = true,
IsCancel = true
};
if (buttonStyle != null)
{
closeButton.Style = buttonStyle;
}
else
{
closeButton.Background = themeBg;
closeButton.Foreground = themeFg;
closeButton.BorderBrush = themeFg;
}
closeButton.Click += (s, args) => { dialog.DialogResult = true; };
bottomPanel.Children.Add(closeButton);
grid.Children.Add(bottomPanel);
dialog.Content = grid;
dialog.ShowDialog();
}
/// <summary>
/// Shows the small dialog for adding or editing a single custom command.
/// </summary>
/// <param name="existing">The command to edit, or null to add a new one.</param>
/// <param name="owner">The parent dialog window for centering/modality.</param>
/// <returns>A new or edited <see cref="CustomCommand"/>, or null if cancelled.</returns>
private CustomCommand ShowCustomCommandEditorDialog(CustomCommand existing, Window owner)
{
GetThemeBrushes(out Brush themeBg, out Brush themeFg);
var dialog = new Window
{
Title = existing == null ? "Add Custom Command" : "Edit Custom Command",
Width = 520,
Height = 240,
WindowStartupLocation = WindowStartupLocation.CenterOwner,
ResizeMode = ResizeMode.NoResize,
Background = themeBg,
Foreground = themeFg,
ShowInTaskbar = false,
Owner = owner
};
var grid = new Grid { Margin = new Thickness(12) };
grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
var nameLabel = new TextBlock
{
Text = "Name (shown in dropdown):",
Foreground = themeFg,
Margin = new Thickness(0, 0, 0, 4)
};
Grid.SetRow(nameLabel, 0);
grid.Children.Add(nameLabel);
var nameBox = new TextBox
{
Text = existing?.Name ?? "",
Background = themeBg,
Foreground = themeFg,
BorderBrush = themeFg,
Margin = new Thickness(0, 0, 0, 10)
};
Grid.SetRow(nameBox, 1);
grid.Children.Add(nameBox);
var cmdLabel = new TextBlock
{
Text = "Command (sent to agent verbatim — slash command or prompt):",
Foreground = themeFg,
Margin = new Thickness(0, 0, 0, 4)
};
Grid.SetRow(cmdLabel, 2);
grid.Children.Add(cmdLabel);
var cmdBox = new TextBox
{
Text = existing?.Command ?? "",
Background = themeBg,
Foreground = themeFg,
BorderBrush = themeFg,
AcceptsReturn = true,
TextWrapping = TextWrapping.Wrap,
VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
MinHeight = 60,
Margin = new Thickness(0, 0, 0, 10)
};
Grid.SetRow(cmdBox, 3);
Grid.SetRowSpan(cmdBox, 2);
grid.Children.Add(cmdBox);
var buttonPanel = new StackPanel
{
Orientation = Orientation.Horizontal,
HorizontalAlignment = HorizontalAlignment.Right
};
Grid.SetRow(buttonPanel, 5);
Style editorButtonStyle = GetDialogButtonStyle();
var okButton = new Button
{
Content = "OK",
Width = 75,
Height = 25,
Margin = new Thickness(0, 0, 8, 0),
IsDefault = true
};
var cancelButton = new Button
{
Content = "Cancel",
Width = 75,
Height = 25,
IsCancel = true
};
if (editorButtonStyle != null)
{
okButton.Style = editorButtonStyle;
cancelButton.Style = editorButtonStyle;
}
else
{
okButton.Background = themeBg;
okButton.Foreground = themeFg;
okButton.BorderBrush = themeFg;
cancelButton.Background = themeBg;
cancelButton.Foreground = themeFg;
cancelButton.BorderBrush = themeFg;
}
buttonPanel.Children.Add(okButton);
buttonPanel.Children.Add(cancelButton);
grid.Children.Add(buttonPanel);
CustomCommand result = null;
okButton.Click += (s, args) =>
{
string nm = nameBox.Text?.Trim() ?? "";
string cm = cmdBox.Text?.Trim() ?? "";
if (string.IsNullOrEmpty(cm))
{
MessageBox.Show("Command text cannot be empty.", "Validation",
MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
result = new CustomCommand
{
Name = string.IsNullOrEmpty(nm) ? cm : nm,
Command = cm
};
dialog.DialogResult = true;
};
dialog.Content = grid;
dialog.Loaded += (s, args) => nameBox.Focus();
dialog.ShowDialog();
return result;
}
#endregion
}
}