fix(claude-do): merge fix(ui): fehlgeschlagenes Einfügen/Droppen im interaktiven T
ClaudeDo-Task: 81931eef-4d05-49d0-a476-7346cba7fbe5
This commit is contained in:
@@ -384,6 +384,7 @@
|
|||||||
"conptyStarting": "Sitzung wird gestartet…",
|
"conptyStarting": "Sitzung wird gestartet…",
|
||||||
"conptyExited": "Sitzung beendet (Code {0}).",
|
"conptyExited": "Sitzung beendet (Code {0}).",
|
||||||
"usageWarningNotDelivered": "Die Listen-Handler-Sitzung konnte nicht vor dem Usage-Limit gewarnt werden.",
|
"usageWarningNotDelivered": "Die Listen-Handler-Sitzung konnte nicht vor dem Usage-Limit gewarnt werden.",
|
||||||
|
"terminalSendFailed": "Konnte nicht an die Terminal-Sitzung gesendet werden.",
|
||||||
"mergeHelperTitle": "Merge-Helfer",
|
"mergeHelperTitle": "Merge-Helfer",
|
||||||
"mergeHelperHandoffTitleSuffix": " (Übergabe)",
|
"mergeHelperHandoffTitleSuffix": " (Übergabe)",
|
||||||
"mergeHelperTaskTitle": "Listen-Handler: {0}",
|
"mergeHelperTaskTitle": "Listen-Handler: {0}",
|
||||||
|
|||||||
@@ -384,6 +384,7 @@
|
|||||||
"conptyStarting": "Starting session…",
|
"conptyStarting": "Starting session…",
|
||||||
"conptyExited": "Session ended (code {0}).",
|
"conptyExited": "Session ended (code {0}).",
|
||||||
"usageWarningNotDelivered": "Couldn't warn the list-handler session about the usage limit.",
|
"usageWarningNotDelivered": "Couldn't warn the list-handler session about the usage limit.",
|
||||||
|
"terminalSendFailed": "Couldn't send that to the terminal session.",
|
||||||
"mergeHelperTitle": "Merge Helper",
|
"mergeHelperTitle": "Merge Helper",
|
||||||
"mergeHelperHandoffTitleSuffix": " (Handoff)",
|
"mergeHelperHandoffTitleSuffix": " (Handoff)",
|
||||||
"mergeHelperTaskTitle": "List handler: {0}",
|
"mergeHelperTaskTitle": "List handler: {0}",
|
||||||
|
|||||||
@@ -162,15 +162,19 @@ public sealed class PtyTerminalSession : IDisposable
|
|||||||
/// writing the clipboard text raw, because that is what honours bracketed-paste mode — without
|
/// 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.
|
/// it a multi-line paste is read line by line and submits on the first newline.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task PasteAsync()
|
public async Task<bool> PasteAsync()
|
||||||
{
|
{
|
||||||
if (_control is not { } control || !IsRunning) return;
|
if (_control is not { } control || !IsRunning) return false;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (s_terminalViewField?.GetValue(control) is TerminalView view)
|
if (s_terminalViewField?.GetValue(control) is not TerminalView view) return false;
|
||||||
await view.PasteAsync();
|
await view.PasteAsync();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
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.
|
// Long enough that the two writes can't be read as one chunk by the child's stdin reader.
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using ClaudeDo.Ui.Localization;
|
||||||
using ClaudeDo.Ui.Services;
|
using ClaudeDo.Ui.Services;
|
||||||
using Iciclecreek.Terminal;
|
using Iciclecreek.Terminal;
|
||||||
|
|
||||||
@@ -111,6 +112,15 @@ public sealed partial class InteractiveTerminalViewModel : ViewModelBase, IDispo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Raised when a user-initiated paste or drop couldn't reach the session (write seam
|
||||||
|
/// gone, no running process) — mirrors the island ViewModels' ErrorReported, forwarded by
|
||||||
|
/// <see cref="ClaudeDo.Ui.ViewModels.MissionControl.ConPtyPaneViewModel"/> into the footer error strip.</summary>
|
||||||
|
public event Action<string>? ErrorReported;
|
||||||
|
|
||||||
|
/// <summary>Called by the view after a failed <see cref="PasteAsync"/>/<see cref="SendTextAsync"/>
|
||||||
|
/// so the failure surfaces through <see cref="ErrorReported"/> instead of vanishing silently.</summary>
|
||||||
|
public void ReportSendFailed() => ErrorReported?.Invoke(Loc.T("missionControl.terminalSendFailed"));
|
||||||
|
|
||||||
/// <summary>Raised when the process died non-zero *after* clearing <see cref="StartupGraceWindow"/> —
|
/// <summary>Raised when the process died non-zero *after* clearing <see cref="StartupGraceWindow"/> —
|
||||||
/// i.e. a session that ran, then broke. That used to be a silent dead tile with no signal at all;
|
/// i.e. a session that ran, then broke. That used to be a silent dead tile with no signal at all;
|
||||||
/// the worker turns it into a usage-limit auto-restart for list-handler sessions
|
/// the worker turns it into a usage-limit auto-restart for list-handler sessions
|
||||||
@@ -156,8 +166,9 @@ public sealed partial class InteractiveTerminalViewModel : ViewModelBase, IDispo
|
|||||||
public Task<bool> SendTextAsync(string text) => _session.SendTextAsync(text);
|
public Task<bool> SendTextAsync(string text) => _session.SendTextAsync(text);
|
||||||
|
|
||||||
/// <summary>Pastes the clipboard into the session (Ctrl+V) — see
|
/// <summary>Pastes the clipboard into the session (Ctrl+V) — see
|
||||||
/// <see cref="PtyTerminalSession.PasteAsync"/>.</summary>
|
/// <see cref="PtyTerminalSession.PasteAsync"/>. False when there is no running process to
|
||||||
public Task PasteAsync() => _session.PasteAsync();
|
/// paste into (or the write seam is gone).</summary>
|
||||||
|
public Task<bool> PasteAsync() => _session.PasteAsync();
|
||||||
|
|
||||||
public void Kill() => _session.Kill();
|
public void Kill() => _session.Kill();
|
||||||
|
|
||||||
|
|||||||
@@ -89,14 +89,18 @@ public sealed partial class ConPtyPaneViewModel : ViewModelBase, IDisposable
|
|||||||
{
|
{
|
||||||
terminal.PropertyChanged += OnTerminalPropertyChanged;
|
terminal.PropertyChanged += OnTerminalPropertyChanged;
|
||||||
terminal.ExitedAfterStartup += OnTerminalExitedAfterStartup;
|
terminal.ExitedAfterStartup += OnTerminalExitedAfterStartup;
|
||||||
|
terminal.ErrorReported += OnTerminalErrorReported;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Unsubscribe(InteractiveTerminalViewModel terminal)
|
private void Unsubscribe(InteractiveTerminalViewModel terminal)
|
||||||
{
|
{
|
||||||
terminal.PropertyChanged -= OnTerminalPropertyChanged;
|
terminal.PropertyChanged -= OnTerminalPropertyChanged;
|
||||||
terminal.ExitedAfterStartup -= OnTerminalExitedAfterStartup;
|
terminal.ExitedAfterStartup -= OnTerminalExitedAfterStartup;
|
||||||
|
terminal.ErrorReported -= OnTerminalErrorReported;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnTerminalErrorReported(string message) => ErrorReported?.Invoke(message);
|
||||||
|
|
||||||
private void OnTerminalExitedAfterStartup(int exitCode)
|
private void OnTerminalExitedAfterStartup(int exitCode)
|
||||||
{
|
{
|
||||||
if (TaskId is { } id) SessionDiedAfterStartup?.Invoke(id, exitCode);
|
if (TaskId is { } id) SessionDiedAfterStartup?.Invoke(id, exitCode);
|
||||||
|
|||||||
@@ -43,7 +43,13 @@ public partial class InteractiveTerminalView : UserControl
|
|||||||
if (DataContext is not InteractiveTerminalViewModel vm) return;
|
if (DataContext is not InteractiveTerminalViewModel vm) return;
|
||||||
|
|
||||||
e.Handled = true;
|
e.Handled = true;
|
||||||
_ = vm.PasteAsync();
|
_ = PasteAndReportAsync(vm);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async System.Threading.Tasks.Task PasteAndReportAsync(InteractiveTerminalViewModel vm)
|
||||||
|
{
|
||||||
|
if (!await vm.PasteAsync())
|
||||||
|
vm.ReportSendFailed();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsFilesDrop(DragEventArgs e) => e.DataTransfer?.Contains(DataFormat.File) == true;
|
private static bool IsFilesDrop(DragEventArgs e) => e.DataTransfer?.Contains(DataFormat.File) == true;
|
||||||
@@ -62,7 +68,13 @@ public partial class InteractiveTerminalView : UserControl
|
|||||||
e.Handled = true;
|
e.Handled = true;
|
||||||
|
|
||||||
var text = FormatDroppedPaths(e.DataTransfer!.TryGetFiles()?.Select(i => i.TryGetLocalPath()));
|
var text = FormatDroppedPaths(e.DataTransfer!.TryGetFiles()?.Select(i => i.TryGetLocalPath()));
|
||||||
if (text.Length > 0) _ = vm.SendTextAsync(text);
|
if (text.Length > 0) _ = SendTextAndReportAsync(vm, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async System.Threading.Tasks.Task SendTextAndReportAsync(InteractiveTerminalViewModel vm, string text)
|
||||||
|
{
|
||||||
|
if (!await vm.SendTextAsync(text))
|
||||||
|
vm.ReportSendFailed();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Joins dropped file paths into one insertable chunk: quoted when they contain a
|
/// <summary>Joins dropped file paths into one insertable chunk: quoted when they contain a
|
||||||
|
|||||||
@@ -35,4 +35,14 @@ public class PtyTerminalSessionTests
|
|||||||
|
|
||||||
Assert.Equal(0, gate.CurrentCount); // acquired
|
Assert.Equal(0, gate.CurrentCount); // acquired
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fix: PasteAsync used to be void and swallow every failure. Not-started is the cheap case to
|
||||||
|
// cover without a real TerminalControl/ConPTY spawn.
|
||||||
|
[Fact]
|
||||||
|
public async Task PasteAsync_ReturnsFalse_WhenNotStarted()
|
||||||
|
{
|
||||||
|
using var session = new PtyTerminalSession();
|
||||||
|
|
||||||
|
Assert.False(await session.PasteAsync());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -219,6 +219,35 @@ public class ConPtyPaneViewModelTests
|
|||||||
Assert.True(raised);
|
Assert.True(raised);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Terminal.ErrorReported forwarding (fix: a failed paste/drop had no path to the footer
|
||||||
|
// error strip) ────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TerminalErrorReported_Forwards_ToPaneErrorReported()
|
||||||
|
{
|
||||||
|
using var pane = NewTaskPane(NeverCompletes);
|
||||||
|
string? error = null;
|
||||||
|
pane.ErrorReported += msg => error = msg;
|
||||||
|
|
||||||
|
pane.Terminal.ReportSendFailed();
|
||||||
|
|
||||||
|
Assert.NotNull(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TerminalErrorReported_StillWiredAfterRetry()
|
||||||
|
{
|
||||||
|
using var pane = NewTaskPane(() => Failing());
|
||||||
|
pane.Start();
|
||||||
|
pane.RetryCommand.Execute(null);
|
||||||
|
string? error = null;
|
||||||
|
pane.ErrorReported += msg => error = msg;
|
||||||
|
|
||||||
|
pane.Terminal.ReportSendFailed();
|
||||||
|
|
||||||
|
Assert.NotNull(error);
|
||||||
|
}
|
||||||
|
|
||||||
private static void SetRunning(ConPtyPaneViewModel pane) => pane.Terminal.IsRunning = true;
|
private static void SetRunning(ConPtyPaneViewModel pane) => pane.Terminal.IsRunning = true;
|
||||||
|
|
||||||
// Drives the terminal past its startup grace window, then kills it: IsRunning stamps the launch
|
// Drives the terminal past its startup grace window, then kills it: IsRunning stamps the launch
|
||||||
|
|||||||
+31
@@ -124,4 +124,35 @@ public class InteractiveTerminalViewModelTests
|
|||||||
terminal.IsRunning = true; // records LaunchedAt via the first clock call
|
terminal.IsRunning = true; // records LaunchedAt via the first clock call
|
||||||
return terminal;
|
return terminal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Paste/drop failure reporting (fix: PasteAsync used to swallow every exception and return
|
||||||
|
// void, so a failed Ctrl+V or file drop gave the user no signal at all) ────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task PasteAsync_ReturnsFalse_WhenSessionNotRunning()
|
||||||
|
{
|
||||||
|
using var terminal = new InteractiveTerminalViewModel();
|
||||||
|
|
||||||
|
Assert.False(await terminal.PasteAsync());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SendTextAsync_ReturnsFalse_WhenSessionNotRunning()
|
||||||
|
{
|
||||||
|
using var terminal = new InteractiveTerminalViewModel();
|
||||||
|
|
||||||
|
Assert.False(await terminal.SendTextAsync("dropped/path"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ReportSendFailed_RaisesErrorReported()
|
||||||
|
{
|
||||||
|
using var terminal = new InteractiveTerminalViewModel();
|
||||||
|
string? error = null;
|
||||||
|
terminal.ErrorReported += msg => error = msg;
|
||||||
|
|
||||||
|
terminal.ReportSendFailed();
|
||||||
|
|
||||||
|
Assert.NotNull(error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user