fix(ui): keep ConPTY sessions alive across Mission Control layout rebuilds
TerminalView kills its child process on any logical-tree detach unless BeginReparent() suppressed it, and RebuildOverviewGrid() detaches every existing pane whenever one is added - so opening a second interactive session killed the first. Focus-mode tab switches had the same defect. PtyTerminalSession now puts the control in permanent reparent mode after launch (teardown stays explicit via ConPtyPaneViewModel.Dispose -> Kill), and the new ConPtyPaneHost reparents each pane's single long-lived view across grid rebuilds and tab switches instead of letting the DataTemplate instantiate a dead replacement.
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using Avalonia.Controls;
|
||||
using ClaudeDo.Ui.ViewModels.MissionControl;
|
||||
|
||||
namespace ClaudeDo.Ui.Views.MissionControl;
|
||||
|
||||
/// <summary>
|
||||
/// Hosts the single long-lived <see cref="ConPtyPaneView"/> of a <see cref="ConPtyPaneViewModel"/>.
|
||||
/// The Mission Control layouts recreate their DataTemplate content freely (the overview grid is
|
||||
/// rebuilt from scratch on every pane/column change, focus-mode tabs re-present on selection), but
|
||||
/// the running PTY session is bound to the original TerminalControl — a freshly instantiated view
|
||||
/// would render a dead, empty terminal. This host therefore reparents the pane's one real view
|
||||
/// instead of letting the template create a new one. Safe while the session runs because
|
||||
/// <see cref="Services.PtyTerminalSession"/> puts the control in permanent reparent mode
|
||||
/// (detach never kills the child process).
|
||||
/// </summary>
|
||||
public sealed class ConPtyPaneHost : Decorator
|
||||
{
|
||||
// Keyed weakly so a closed pane's view dies with its view model.
|
||||
private static readonly ConditionalWeakTable<ConPtyPaneViewModel, ConPtyPaneView> Views = new();
|
||||
|
||||
public ConPtyPaneHost()
|
||||
{
|
||||
DataContextChanged += (_, _) => Reclaim();
|
||||
AttachedToVisualTree += (_, _) => Reclaim();
|
||||
}
|
||||
|
||||
/// <summary>Takes ownership of the pane's cached view, stealing it from whichever host showed
|
||||
/// it last — but only while effectively visible, so the hidden layout (overview grid vs. focus
|
||||
/// tabs) never yanks the view away from the visible one.</summary>
|
||||
public void Reclaim()
|
||||
{
|
||||
var vm = DataContext as ConPtyPaneViewModel;
|
||||
|
||||
// A recycled host now bound to a different pane must not keep showing the old view.
|
||||
// The cached views carry their own local DataContext, so this check can't be fooled
|
||||
// by DataContext inheritance.
|
||||
if (Child is ConPtyPaneView stale && !ReferenceEquals(stale.DataContext, vm))
|
||||
Child = null;
|
||||
|
||||
if (vm is null || !IsEffectivelyVisible) return;
|
||||
|
||||
var view = Views.GetValue(vm, static key => new ConPtyPaneView { DataContext = key });
|
||||
if (ReferenceEquals(Child, view)) return;
|
||||
if (view.Parent is ConPtyPaneHost previous) previous.Child = null;
|
||||
Child = view;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user