QuickClaude 1.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
weishaupt
2026-10-09 11:32:32 -04:00
co-authored by Claude Opus 5.5
commit 8431323848
10 changed files with 1329 additions and 0 deletions
+222
View File
@@ -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;
}
}
}