diff --git a/docs/explore-notes/conpty-sessions.md b/docs/explore-notes/conpty-sessions.md index 92725c7f..bbeab8fa 100644 --- a/docs/explore-notes/conpty-sessions.md +++ b/docs/explore-notes/conpty-sessions.md @@ -327,6 +327,8 @@ bails out immediately instead of racing past the collection-based dedupe. (scoped to `MainWindow` via `AddClassHandler`) never runs there — **Escape always reaches the PTY**. See the note in `src/ClaudeDo.Ui/CLAUDE.md`. +**AltGr bypass:** Windows reports AltGr as Ctrl+Alt, and Iciclecreek's `TerminalView.OnKeyDown` turns any Ctrl/Alt combo into a control sequence (NUL/0x1C/ESC) and marks it handled, so `@ \ { } € ~ |` never arrive as text. `InteractiveTerminalView.OnPreviewKeyDown` (tunnel) detects it via `IsAltGrText` and sends `KeySymbol` through `SendTextAsync` first. + `TaskRowViewModel.HasInteractiveSession` shows an accent "Interactive" chip instead of "Parked"; tapping it jumps to that Mission Control pane. `TasksIslandViewModel.SyncInteractiveSessions` mirrors Mission Control's open panes onto the rows. diff --git a/src/ClaudeDo.Ui/Views/InteractiveTerminalView.axaml.cs b/src/ClaudeDo.Ui/Views/InteractiveTerminalView.axaml.cs index 7d7bf9db..b0c2f2a7 100644 --- a/src/ClaudeDo.Ui/Views/InteractiveTerminalView.axaml.cs +++ b/src/ClaudeDo.Ui/Views/InteractiveTerminalView.axaml.cs @@ -40,6 +40,15 @@ public partial class InteractiveTerminalView : UserControl private void OnPreviewKeyDown(object? sender, KeyEventArgs e) { + // AltGr arrives as Ctrl+Alt; the library would turn it into a control sequence (and mark it handled). + if (IsAltGrText(e.KeyModifiers, e.KeySymbol)) + { + if (DataContext is not InteractiveTerminalViewModel altGrVm) return; + e.Handled = true; + _ = SendTextAndReportAsync(altGrVm, e.KeySymbol!); + return; + } + if (e.Key != Key.V || !e.KeyModifiers.HasFlag(KeyModifiers.Control) || e.KeyModifiers.HasFlag(KeyModifiers.Alt)) return; if (DataContext is not InteractiveTerminalViewModel vm) return; @@ -120,6 +129,12 @@ public partial class InteractiveTerminalView : UserControl vm.ReportSendFailed(); } + /// True for an AltGr keystroke (Windows reports it as Ctrl+Alt) whose + /// is already a single printable character, e.g. "@" or "\". + internal static bool IsAltGrText(KeyModifiers modifiers, string? keySymbol) => + modifiers.HasFlag(KeyModifiers.Control) && modifiers.HasFlag(KeyModifiers.Alt) + && keySymbol is { Length: 1 } && !char.IsControl(keySymbol[0]); + /// Joins dropped file paths into one insertable chunk: quoted when they contain a /// space, space-separated, with a trailing space so the user can keep typing. internal static string FormatDroppedPaths(IEnumerable? paths) diff --git a/tests/ClaudeDo.Ui.Tests/Views/InteractiveTerminalDropPathsTests.cs b/tests/ClaudeDo.Ui.Tests/Views/InteractiveTerminalDropPathsTests.cs index 48d34130..2bfb7b81 100644 --- a/tests/ClaudeDo.Ui.Tests/Views/InteractiveTerminalDropPathsTests.cs +++ b/tests/ClaudeDo.Ui.Tests/Views/InteractiveTerminalDropPathsTests.cs @@ -1,3 +1,4 @@ +using Avalonia.Input; using ClaudeDo.Ui.Views; using Xunit; @@ -23,4 +24,18 @@ public class InteractiveTerminalDropPathsTests Assert.Equal(string.Empty, InteractiveTerminalView.FormatDroppedPaths(new string?[] { null })); Assert.Equal(string.Empty, InteractiveTerminalView.FormatDroppedPaths(null)); } + + [Theory] + [InlineData(KeyModifiers.Control | KeyModifiers.Alt, "@", true)] + [InlineData(KeyModifiers.Control | KeyModifiers.Alt, "\\", true)] + [InlineData(KeyModifiers.Control, "c", false)] + [InlineData(KeyModifiers.Alt, "@", false)] + [InlineData(KeyModifiers.None, "@", false)] + [InlineData(KeyModifiers.Control | KeyModifiers.Alt, "\u0003", false)] + [InlineData(KeyModifiers.Control | KeyModifiers.Alt, "", false)] + [InlineData(KeyModifiers.Control | KeyModifiers.Alt, null, false)] + public void IsAltGrText_only_for_ctrl_alt_with_one_printable_char(KeyModifiers modifiers, string? symbol, bool expected) + { + Assert.Equal(expected, InteractiveTerminalView.IsAltGrText(modifiers, symbol)); + } }