From 23491d3b497f2ba7c23c101e39853d8ee6a7fbf0 Mon Sep 17 00:00:00 2001 From: mika kuns Date: Mon, 7 Sep 2026 08:17:00 +0200 Subject: [PATCH] feat(ui): Einfuegen und Datei-Drop im interaktiven Terminal Ctrl+V wurde von TerminalControl als rohes Control-Byte an das Kind weitergegeben und tat nichts; jetzt wird es im Tunnel abgefangen und geht ueber TerminalView.PasteAsync, damit Bracketed Paste greift und ein mehrzeiliger Paste nicht auf der ersten Zeile abschickt. Gedroppte Dateien werden als Pfade in den Composer getippt, ohne Enter. --- .../Services/PtyTerminalSession.cs | 36 +++++++++++++ .../InteractiveTerminalViewModel.cs | 8 +++ .../Views/InteractiveTerminalView.axaml | 3 +- .../Views/InteractiveTerminalView.axaml.cs | 50 +++++++++++++++++++ .../InteractiveTerminalDropPathsTests.cs | 26 ++++++++++ 5 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 tests/ClaudeDo.Ui.Tests/Views/InteractiveTerminalDropPathsTests.cs diff --git a/src/ClaudeDo.Ui/Services/PtyTerminalSession.cs b/src/ClaudeDo.Ui/Services/PtyTerminalSession.cs index 82980292..597469df 100644 --- a/src/ClaudeDo.Ui/Services/PtyTerminalSession.cs +++ b/src/ClaudeDo.Ui/Services/PtyTerminalSession.cs @@ -137,6 +137,42 @@ public sealed class PtyTerminalSession : IDisposable } } + /// + /// Inserts at the cursor without submitting it — the drop-a-file-path + /// case. Same write seam as , just without the Enter. + /// + public async Task SendTextAsync(string text) + { + if (_control is not { } control || !IsRunning || string.IsNullOrEmpty(text)) return false; + try + { + await SendRawAsync(control, text); + return true; + } + catch (Exception) + { + return false; + } + } + + /// + /// Pastes the clipboard into the session. wires paste to the + /// right-click menu only, so Ctrl+V otherwise reaches the child as a raw control byte and does + /// nothing. Goes through the emulator's own rather than + /// writing the clipboard text raw, because that is what honours bracketed-paste mode — without + /// it a multi-line paste is read line by line and submits on the first newline. + /// + public async Task PasteAsync() + { + if (_control is not { } control || !IsRunning) return; + try + { + if (s_terminalViewField?.GetValue(control) is TerminalView view) + await view.PasteAsync(); + } + catch (Exception) { /* best-effort, same as SendLineAsync */ } + } + // Long enough that the two writes can't be read as one chunk by the child's stdin reader. private static readonly TimeSpan SubmitDelay = TimeSpan.FromMilliseconds(150); diff --git a/src/ClaudeDo.Ui/ViewModels/InteractiveTerminalViewModel.cs b/src/ClaudeDo.Ui/ViewModels/InteractiveTerminalViewModel.cs index ca72e00c..b0fed886 100644 --- a/src/ClaudeDo.Ui/ViewModels/InteractiveTerminalViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/InteractiveTerminalViewModel.cs @@ -136,6 +136,14 @@ public sealed partial class InteractiveTerminalViewModel : ViewModelBase, IDispo /// type into (or the write seam is gone). public Task SendLineAsync(string text) => _session.SendLineAsync(text); + /// Inserts text at the cursor without submitting — see + /// (dropped file paths). + public Task SendTextAsync(string text) => _session.SendTextAsync(text); + + /// Pastes the clipboard into the session (Ctrl+V) — see + /// . + public Task PasteAsync() => _session.PasteAsync(); + public void Kill() => _session.Kill(); public void Dispose() diff --git a/src/ClaudeDo.Ui/Views/InteractiveTerminalView.axaml b/src/ClaudeDo.Ui/Views/InteractiveTerminalView.axaml index 8277ddbb..520972aa 100644 --- a/src/ClaudeDo.Ui/Views/InteractiveTerminalView.axaml +++ b/src/ClaudeDo.Ui/Views/InteractiveTerminalView.axaml @@ -4,7 +4,8 @@ xmlns:term="using:Iciclecreek.Terminal" x:Class="ClaudeDo.Ui.Views.InteractiveTerminalView" x:DataType="vm:InteractiveTerminalViewModel" - x:Name="Root"> + x:Name="Root" + DragDrop.AllowDrop="True">