QuickClaude 1.0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,222 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Threading;
|
||||
|
||||
namespace QuickClaude
|
||||
{
|
||||
// QuickClaude.exe --confirm: Claude Code's PreToolUse hook for shell commands.
|
||||
// Reads the hook JSON from stdin, asks the user, prints an allow/deny decision.
|
||||
public static class ConfirmHook
|
||||
{
|
||||
public static int Run()
|
||||
{
|
||||
string toolName = "Bash", command = null, cwd = null;
|
||||
try
|
||||
{
|
||||
using var stdin = new StreamReader(Console.OpenStandardInput(), new UTF8Encoding(false));
|
||||
using var doc = JsonDocument.Parse(stdin.ReadToEnd());
|
||||
var root = doc.RootElement;
|
||||
toolName = Str(root, "tool_name") ?? toolName;
|
||||
cwd = Str(root, "cwd");
|
||||
if (root.TryGetProperty("tool_input", out var input)) command = Str(input, "command");
|
||||
}
|
||||
catch { }
|
||||
|
||||
// Nothing to show: say nothing, so Claude Code's normal permission rules decide
|
||||
if (string.IsNullOrEmpty(command)) return 0;
|
||||
|
||||
new Application { ShutdownMode = ShutdownMode.OnExplicitShutdown };
|
||||
var win = new ConfirmWindow(toolName, command, cwd);
|
||||
win.ShowDialog();
|
||||
|
||||
string decision = win.Allowed ? "allow" : "deny";
|
||||
string reason = win.Allowed ? "The user allowed this command."
|
||||
: win.TimedOut ? "The user didn't answer within 2 minutes, so the command was not run. Carry on without it."
|
||||
: "The user declined to run this command. Carry on without it.";
|
||||
|
||||
var json = JsonSerializer.Serialize(new
|
||||
{
|
||||
hookSpecificOutput = new
|
||||
{
|
||||
hookEventName = "PreToolUse",
|
||||
permissionDecision = decision,
|
||||
permissionDecisionReason = reason
|
||||
}
|
||||
});
|
||||
using var stdout = new StreamWriter(Console.OpenStandardOutput(), new UTF8Encoding(false));
|
||||
stdout.Write(json);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static string Str(JsonElement e, string name) =>
|
||||
e.ValueKind == JsonValueKind.Object && e.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.String ? v.GetString() : null;
|
||||
}
|
||||
|
||||
public class ConfirmWindow : Window
|
||||
{
|
||||
public bool Allowed { get; private set; }
|
||||
public bool TimedOut { get; private set; }
|
||||
|
||||
static readonly TimeSpan Limit = TimeSpan.FromMinutes(2);
|
||||
internal static readonly Brush Text = Brushes.White;
|
||||
internal static readonly Brush Faint = new SolidColorBrush(Color.FromArgb(140, 255, 255, 255));
|
||||
internal static readonly Brush Panel = new SolidColorBrush(Color.FromArgb(225, 20, 20, 20));
|
||||
internal static readonly Brush Edge = new SolidColorBrush(Color.FromArgb(70, 255, 255, 255));
|
||||
internal static readonly Brush EdgeBright = new SolidColorBrush(Color.FromArgb(200, 255, 255, 255));
|
||||
internal static readonly Brush Hover = new SolidColorBrush(Color.FromArgb(40, 255, 255, 255));
|
||||
internal static readonly Brush Press = new SolidColorBrush(Color.FromArgb(70, 255, 255, 255));
|
||||
|
||||
readonly DispatcherTimer timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
|
||||
readonly DateTime deadline = DateTime.UtcNow + Limit;
|
||||
readonly TextBlock countdown = new TextBlock();
|
||||
|
||||
public ConfirmWindow(string toolName, string command, string cwd)
|
||||
{
|
||||
Title = "QuickClaude - run command?";
|
||||
WindowStyle = WindowStyle.None;
|
||||
AllowsTransparency = true;
|
||||
Background = Brushes.Transparent;
|
||||
ResizeMode = ResizeMode.NoResize;
|
||||
Topmost = true;
|
||||
ShowInTaskbar = true;
|
||||
Width = 520;
|
||||
SizeToContent = SizeToContent.Height;
|
||||
WindowStartupLocation = WindowStartupLocation.CenterScreen;
|
||||
FontFamily = MainWindow.Mono;
|
||||
FontSize = MainWindow.TextSize;
|
||||
Foreground = Text;
|
||||
|
||||
var header = new TextBlock
|
||||
{
|
||||
Text = "clod wants to run " + toolName + (string.IsNullOrEmpty(cwd) ? "" : " in " + cwd),
|
||||
Foreground = Faint,
|
||||
TextWrapping = TextWrapping.Wrap
|
||||
};
|
||||
|
||||
// The exact command, selectable, scrolls if it's very long
|
||||
var cmd = new TextBox
|
||||
{
|
||||
Text = command,
|
||||
IsReadOnly = true,
|
||||
Background = Brushes.Transparent,
|
||||
Foreground = Text,
|
||||
CaretBrush = Text,
|
||||
BorderThickness = new Thickness(0),
|
||||
Padding = new Thickness(0),
|
||||
FocusVisualStyle = null,
|
||||
FontFamily = MainWindow.Mono,
|
||||
FontSize = MainWindow.TextSize,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
MaxHeight = 320,
|
||||
Margin = new Thickness(0, 8, 0, 8),
|
||||
Template = MainWindow.BareTextBoxTemplate,
|
||||
IsUndoEnabled = false
|
||||
};
|
||||
|
||||
countdown.Foreground = Faint;
|
||||
countdown.VerticalAlignment = VerticalAlignment.Center;
|
||||
UpdateCountdown();
|
||||
|
||||
var deny = MakeButton("Deny");
|
||||
var allow = MakeButton("Allow");
|
||||
deny.Click += (s, e) => Finish(false);
|
||||
allow.Click += (s, e) => Finish(true);
|
||||
allow.Margin = new Thickness(8, 0, 0, 0);
|
||||
|
||||
var buttons = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Right };
|
||||
buttons.Children.Add(deny);
|
||||
buttons.Children.Add(allow);
|
||||
|
||||
var bottom = new Grid();
|
||||
bottom.Children.Add(countdown);
|
||||
bottom.Children.Add(buttons);
|
||||
|
||||
var stack = new StackPanel();
|
||||
stack.Children.Add(header);
|
||||
stack.Children.Add(cmd);
|
||||
stack.Children.Add(bottom);
|
||||
|
||||
var frame = new Border
|
||||
{
|
||||
Background = Panel,
|
||||
BorderBrush = Edge,
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(0),
|
||||
Padding = new Thickness(12),
|
||||
Child = stack
|
||||
};
|
||||
frame.MouseLeftButtonDown += (s, e) => { try { DragMove(); } catch { } };
|
||||
Content = frame;
|
||||
|
||||
PreviewKeyDown += (s, e) => { if (e.Key == Key.Escape) { e.Handled = true; Finish(false); } };
|
||||
timer.Tick += (s, e) =>
|
||||
{
|
||||
if (DateTime.UtcNow >= deadline) { TimedOut = true; Finish(false); }
|
||||
else UpdateCountdown();
|
||||
};
|
||||
Loaded += (s, e) => { timer.Start(); Activate(); deny.Focus(); };
|
||||
Closed += (s, e) => timer.Stop();
|
||||
}
|
||||
|
||||
void UpdateCountdown()
|
||||
{
|
||||
var left = deadline - DateTime.UtcNow;
|
||||
if (left < TimeSpan.Zero) left = TimeSpan.Zero;
|
||||
countdown.Text = $"denies in {(int)left.TotalMinutes}:{left.Seconds:00}";
|
||||
}
|
||||
|
||||
void Finish(bool allowed)
|
||||
{
|
||||
Allowed = allowed;
|
||||
timer.Stop();
|
||||
Close();
|
||||
}
|
||||
|
||||
static Button MakeButton(string text) => new Button
|
||||
{
|
||||
Content = text,
|
||||
Foreground = Text,
|
||||
Background = Brushes.Transparent,
|
||||
BorderBrush = Edge,
|
||||
Padding = new Thickness(14, 4, 14, 4),
|
||||
FocusVisualStyle = null,
|
||||
Cursor = Cursors.Hand,
|
||||
Template = ButtonTemplate
|
||||
};
|
||||
|
||||
// Square, flat button: brighter edge when focused, light fill on hover. No theme chrome.
|
||||
static readonly ControlTemplate ButtonTemplate = MakeButtonTemplate();
|
||||
|
||||
static ControlTemplate MakeButtonTemplate()
|
||||
{
|
||||
var bd = new FrameworkElementFactory(typeof(Border), "bd");
|
||||
bd.SetValue(Border.BorderThicknessProperty, new Thickness(1));
|
||||
bd.SetValue(Border.BorderBrushProperty, new TemplateBindingExtension(Control.BorderBrushProperty));
|
||||
bd.SetValue(Border.BackgroundProperty, new TemplateBindingExtension(Control.BackgroundProperty));
|
||||
bd.SetValue(Border.PaddingProperty, new TemplateBindingExtension(Control.PaddingProperty));
|
||||
var cp = new FrameworkElementFactory(typeof(ContentPresenter));
|
||||
cp.SetValue(ContentPresenter.HorizontalAlignmentProperty, HorizontalAlignment.Center);
|
||||
cp.SetValue(ContentPresenter.VerticalAlignmentProperty, VerticalAlignment.Center);
|
||||
bd.AppendChild(cp);
|
||||
|
||||
var t = new ControlTemplate(typeof(Button)) { VisualTree = bd };
|
||||
var hover = new Trigger { Property = UIElement.IsMouseOverProperty, Value = true };
|
||||
hover.Setters.Add(new Setter(Border.BackgroundProperty, Hover, "bd"));
|
||||
var pressed = new Trigger { Property = System.Windows.Controls.Primitives.ButtonBase.IsPressedProperty, Value = true };
|
||||
pressed.Setters.Add(new Setter(Border.BackgroundProperty, Press, "bd"));
|
||||
var focused = new Trigger { Property = UIElement.IsKeyboardFocusedProperty, Value = true };
|
||||
focused.Setters.Add(new Setter(Border.BorderBrushProperty, EdgeBright, "bd"));
|
||||
t.Triggers.Add(hover);
|
||||
t.Triggers.Add(pressed);
|
||||
t.Triggers.Add(focused);
|
||||
t.Seal();
|
||||
return t;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user