diff --git a/src/ClaudeDo.Ui/CLAUDE.md b/src/ClaudeDo.Ui/CLAUDE.md index 3c296c9a..9b652da7 100644 --- a/src/ClaudeDo.Ui/CLAUDE.md +++ b/src/ClaudeDo.Ui/CLAUDE.md @@ -66,6 +66,7 @@ Modals use `TaskCompletionSource` results behind the reusable `ModalShell` contr - "Run Now" CanExecute re-evaluates when worker connection state changes - Icon gotcha: `PathIcon` fills geometry. Line-art/stroke icons must be defined as filled geometry or rendered as a stroked `Path` (e.g. `Icon.PlanDay` via the `Path.plan-icon` style); a pure stroke path used with `PathIcon` is invisible. - Window key bindings live on `MainWindow`: `Ctrl+K` focuses search, `Ctrl+N` the add-task box. Do **not** bind bare punctuation gestures — `OemQuestion` used to hold search focus and silently swallowed `#` app-wide on a German layout. +- `FocusClearing` also clears focus from a TextBox on Escape, mirroring its click-outside behavior — but the KeyDown handler is scoped to `MainWindow` specifically (`AddClassHandler`, not ``). Modal windows (`AboutModalView` etc.) each bind their own `Window.KeyBindings` Escape → close; since modals are separate `Window` instances, this handler never runs there, so Escape still closes them unchanged. Mission Control's ConPTY tiles (`InteractiveTerminalView`) live in `MissionControlWindow`, also unaffected — Escape always reaches the PTY there. - `Ellipse.spinner` (IslandStyles) is the shared indeterminate spinner: used for a starting ConPTY pane (`InteractiveTerminalViewModel.IsStarting`) and in place of the refine button while `TaskRowViewModel.IsRefining`. - `ConPtyPaneViewModel` resolves its own launch spec (ctor takes a descriptor **factory**; the host wires handlers and then calls `Start()`), so the Mission Control tile appears immediately with its spinner while the worker is still preparing the worktree. A failed launch keeps the tile with its inline error banner instead of never appearing. - `SessionTerminalView` is the reusable log terminal (StyledProperties `Entries`, `Label`, `IsRunning`, `IsDone`, `IsFailed`) used for both the task `Log` and the prep `PrepLog`. diff --git a/src/ClaudeDo.Ui/Services/FocusClearing.cs b/src/ClaudeDo.Ui/Services/FocusClearing.cs index 3edef8fb..a169df1e 100644 --- a/src/ClaudeDo.Ui/Services/FocusClearing.cs +++ b/src/ClaudeDo.Ui/Services/FocusClearing.cs @@ -3,12 +3,16 @@ using Avalonia.Controls; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.VisualTree; +using ClaudeDo.Ui.Views; namespace ClaudeDo.Ui.Services; /// /// Clears focus from a TextBox when the user clicks outside of any text box, so input /// fields behave like the user expects. Registered once for every window in the app. +/// Escape does the same, but only in the main window: modal windows bind Escape to closing +/// themselves, and Mission Control's ConPTY tiles need Escape to reach the terminal, so this +/// intentionally does not use a generic TopLevel handler. /// public static class FocusClearing { @@ -16,6 +20,9 @@ public static class FocusClearing { InputElement.PointerPressedEvent.AddClassHandler( OnPointerPressed, RoutingStrategies.Tunnel, handledEventsToo: true); + + InputElement.KeyDownEvent.AddClassHandler( + OnKeyDown, RoutingStrategies.Tunnel); } private static void OnPointerPressed(TopLevel topLevel, PointerPressedEventArgs e) @@ -31,4 +38,19 @@ public static class FocusClearing focusManager.Focus(null); } + + private static void OnKeyDown(MainWindow window, KeyEventArgs e) + { + if (e.Key != Key.Escape) + return; + + if (window.FocusManager is not { } focusManager) + return; + + if (focusManager.GetFocusedElement() is not TextBox) + return; + + focusManager.Focus(null); + e.Handled = true; + } }