Part of #1483
Summary
Complete remote session opening UI, modal stacks, running-row interrupt/terminate/Continue in background controls, immutable queue UX, and notification kinds.
Scope
Implement only commit 9 of the approved design. Preserve the contracts below exactly; local and remote behavior must remain compatible. Imported surfaces consumed here must preserve the exact property-based Usage and AgentInformation names, fields, types, and semantics from detail 2; protocol DTO/property records must preserve the approved serializers and fixed discriminators; any imported interrupt contract remains InterruptAsync(Guid commandId, CancellationToken ct = default) with no InterruptAgentSessionRequest; APIs owned by #1471-#1477 stay unchanged unless this design explicitly changes their presentation; and the audited design total remains exactly 28 request/options types (22 public and 6 internal).
Files
Files: AgentViewModel, AgentSessionWorkspaceTabViewModel, editor/modal controls,
Phantom.Workspaces/ViewModels/RunningAgentBrainViewModel.cs,
Phantom.Workspaces/ViewModels/RunningAgentRowViewModel.cs,
Phantom.Workspaces/Controls/RunningAgentBrainControl.axaml(.cs), Notification,
NotificationEntry, NotificationTargetRequest, INotificationService, NotificationService, and
NotificationsViewModel.
Tests: UI request/options required-member/default/named-initializer tests, UI public API tests
above, multiple modal ownership, descendant aggregation, independent
notification clearing, remote interrupt versus terminate, and checkbox binding, command,
enabled/pending state, and accessibility.
Detailed design
UI integration types - Existing, Modified
-
RunningAgentBrainViewModel, RunningAgentRowViewModel, and RunningAgentBrainControl:
Phantom.Workspaces.ViewModels / Phantom.Workspaces.Controls;
Phantom.Workspaces/ViewModels/RunningAgentBrainViewModel.cs,
Phantom.Workspaces/ViewModels/RunningAgentRowViewModel.cs, and
Phantom.Workspaces/Controls/RunningAgentBrainControl.axaml(.cs). These are the existing
brain-icon popup, Rows collection, and row type; this design adds no parallel “sessions flyout.”
RunningAgentBrainViewModel.Refresh() continues to derive top-level rows from
IRunningAgentChatTable.RunningSessions and continues filtering IsSubAgent.
RunningAgentRowViewModel adds the following public row state:
public bool IsRemote { get; }
public bool ContinueInBackground { get; }
public int ViewerCount { get; }
public bool IsBackgroundOptionEnabled { get; }
public bool IsInterruptEnabled { get; }
public ICommand InterruptCommand { get; }
public ICommand TerminateCommand { get; }
public ICommand SetContinueInBackgroundCommand { get; }
Its two existing public constructors remain source-compatible. An internal constructor/factory
used by RunningAgentBrainViewModel supplies runtime metadata and commands; legacy callers receive
disabled no-op runtime commands and false/zero metadata.
InterruptCommand acquires the existing session lease and invokes IAgentChat.Interrupt; it is
enabled only while that row has an active interruptible turn.
TerminateCommand calls IRunningAgentChatTable.TerminateAsync and removes the row only through
the resulting RunningSessions change. Both are disabled while terminal/disconnected, and
terminate disables every row command while pending. The background command is created by
RunningAgentBrainViewModel and calls
IRunningAgentChatTable.SetContinueInBackgroundAsync(SessionId, requestedValue). It is disabled
while an update is pending, after terminal/disconnect, or when the row lacks a persisted top-level
session/owning transport; it is enabled for an attached remote row and for a locally owned runtime
capable of accepting remote attachments. ContinueInBackground changes only from the
authoritative running-session metadata after persistence/event application. Failure leaves the
old checked state and reports the existing safe operation error.
RunningAgentBrainControl places one checkbox in each running row beside the existing activate
button. Its visible label and AutomationProperties.Name are exactly “Continue in background”;
its tooltip is “Keep this agent running when the last viewer disconnects.” IsChecked is
one-way bound to ContinueInBackground, IsEnabled to IsBackgroundOptionEnabled, and its
command/boolean parameter to SetContinueInBackgroundCommand. Keyboard focus and Space invoke the
checkbox without activating the row; the row's activate button remains separately focusable. A
red, right-aligned X button binds InterruptCommand, IsEnabled to IsInterruptEnabled,
tooltip and AutomationProperties.Name to “Interrupt agent”, and is distinct from the
terminate action. A separate destructive Terminate button follows it, binds
TerminateCommand, is disabled while termination is pending or the row is disconnected/terminal,
and uses tooltip and AutomationProperties.Name “Terminate agent session”. It sends the
explicit terminate verb; it never merely closes the tab or releases a viewer lease.
-
AgentViewModel:
Phantom.Workspaces.Agent.Gui.ViewModels;
Phantom.Workspaces.Agent.Gui/ViewModels/AgentViewModel.cs; existing public sealed class.
Its constructor becomes AgentViewModel(AgentViewModelOptions options). AgentViewModelOptions
has required-init IAgentChat AgentChat, string DisplayName, string Description,
ObservableLoggerFactory LoggerFactory, and TaskScheduler ForegroundScheduler, plus
AgentViewModel? ParentAgentViewModel { get; init; } = null. Existing foreground validation
remains. It uses the common collections/events, awaits async remote tool toggles, and owns a
ReadOnlyObservableCollection<AgentSessionModalViewModel> Modals. Modal response calls
Task RespondToModalAsync(string modalId, JsonElement response, CancellationToken ct = default),
which delegates to IAgentChat, preserves the modal until a dismiss event, and maps cancellation
or safe remote errors without optimistic removal.
Input is gated only while this editor has an unresolved modal; descendant modals affect only root
notification aggregation. Its existing public AgentChat getter changes to
public IAgentChat AgentChat { get; }. Disposal unsubscribes before disposing its chat lease/proxy.
-
InputQueueViewModel:
Phantom.Workspaces.Agent.Gui.ViewModels; existing public sealed class. Its constructor and
fields consume IAgentChat.InputQueues, IAgentInputQueue, and immutable snapshots rather than
concrete AgentChat, AgentChatQueue, or AgentInputQueueManager. Submit, create, edit, remove,
move, hold/release, and target selection await the aggregate commands. Controls stay unchanged
until the acknowledged owner delta/result is applied; conflict replaces the projection from the
returned snapshot and prompts/retries only after reconciling user intent. Changed is marshalled
to the existing foreground scheduler, and disposal unsubscribes without disposing the chat-owned
aggregate.
-
AgentSessionWorkspaceTabViewModel:
Phantom.Workspaces.ViewModels;
Phantom.Workspaces/ViewModels/AgentSessionWorkspaceTabViewModel.cs; existing public sealed class.
Adds read-only bool IsRemote, string? RemoteProfileDisplayName, and
bool HasModalsNeedingInput; SetReady derives these from the chat/view model. The
modal-pending notification clears only when aggregate modal count reaches zero; chat-idle
retains activation-clearing behavior.
-
OpenAgentSessionShortcutHandler:
same namespace; existing public sealed class. Handle,
TryCreateAgentSessionTabForRestoreAsync, TryCreateTabForRestoreAsync, and
CreateAgentSessionTabAsync all call one new internal
OpenPersistedSessionAsync(JsonElement, AgentSessionOpenIntent, CancellationToken).
TryCreateAgentSessionTabForRestoreAsync takes
CreateAgentSessionTabForRestoreRequest plus CancellationToken; CreateAgentSessionTabAsync
takes CreateAgentSessionTabRequest plus CancellationToken; and
ComposeSessionAgentViewModel takes ComposeSessionAgentViewModelOptions. The inherited
Handle and TryCreateTabForRestoreAsync overrides retain their framework signatures.
ConnectOnOwner builds a remote acquisition request; ResumeLocally performs takeover before a
local acquisition. It never parses executor bindings or compiles trust policy.
-
Notification, NotificationEntry, INotificationService, NotificationService, and
NotificationsViewModel: existing notification types in
Phantom.Workspaces/Services/Notifications and Phantom.Workspaces/ViewModels. Notification
adds nonblank string Kind with the "legacy" default; NotificationEntry exposes it.
INotificationService and NotificationService add
void Remove(NotificationTargetRequest request) and
void MarkRead(NotificationTargetRequest request) while retaining the existing tab-wide overloads for
callers that intentionally affect every kind. Notify upserts only the matching (TabKey, Kind).
NotificationsViewModel.UnreadCount and HasUnread aggregate all entries and therefore require no
signature change.
public record Notification
{
public required TabDescriptor TabDescriptor { get; init; }
public required string Heading { get; init; }
public required string Description { get; init; }
public required DateTime When { get; init; }
public required RunningState RunningState { get; init; }
public required NotificationState NotificationState { get; init; }
public string Kind { get; init; } = "legacy";
}
public sealed record NotificationEntry
{
public required string TabKey { get; init; }
public required string Kind { get; init; }
// Existing required TabDescriptor, Heading, Description, When,
// IsRunning, IsInteresting, IsRead, and IsSnoozed properties remain.
}
public sealed record NotificationTargetRequest
{
public required string TabId { get; init; }
public required string Kind { get; init; }
}
public interface INotificationService
{
void Notify(Notification notification);
void Remove(string tabId);
void Remove(NotificationTargetRequest request);
void MarkRead(string tabId);
void MarkRead(NotificationTargetRequest request);
// Existing members remain.
}
NotificationService.Notify rejects null and blank Kind; "legacy" preserves existing caller
source compatibility while still giving every stored entry a nonblank key.
The added/changed public signatures are:
public sealed record AgentViewModelOptions
{
public required IAgentChat AgentChat { get; init; }
public required string DisplayName { get; init; }
public required string Description { get; init; }
public required ObservableLoggerFactory LoggerFactory { get; init; }
public required TaskScheduler ForegroundScheduler { get; init; }
public AgentViewModel? ParentAgentViewModel { get; init; } = null;
}
public AgentViewModel(AgentViewModelOptions options);
public IAgentChat AgentChat { get; }
public ReadOnlyObservableCollection<AgentSessionModalViewModel> Modals { get; }
public Task RespondToModalAsync(
string modalId, JsonElement response, CancellationToken ct = default);
public bool IsRemote { get; }
public string? RemoteProfileDisplayName { get; }
public bool HasModalsNeedingInput { get; }
public void SetReady(AgentViewModel agentViewModel, ObservableLoggerFactory factory);
public override Task<bool> Handle(
MainWindowViewModel mainWindowViewModel,
Shortcut shortcut,
SubscribedEntityViewModel entityViewModel);
public Task<AgentSessionWorkspaceTabViewModel?> TryCreateAgentSessionTabForRestoreAsync(
CreateAgentSessionTabForRestoreRequest request, CancellationToken ct = default);
public override Task<WorkspaceTabViewModel?> TryCreateTabForRestoreAsync(
MainWindowViewModel mainWindowViewModel,
SubscribedEntityViewModel entityViewModel,
string? tabId,
string? title,
string? dockRegion);
public Task<AgentSessionWorkspaceTabViewModel> CreateAgentSessionTabAsync(
CreateAgentSessionTabRequest request, CancellationToken ct = default);
public AgentViewModel ComposeSessionAgentViewModel(ComposeSessionAgentViewModelOptions options);
public sealed record CreateAgentSessionTabForRestoreRequest
{
public required MainWindowViewModel MainWindowViewModel { get; init; }
public required SubscribedEntityViewModel AgentSessionEntity { get; init; }
public string? TabId { get; init; } = null;
public string? Title { get; init; } = null;
public string? DockRegion { get; init; } = null;
}
public sealed record CreateAgentSessionTabRequest
{
public required MainWindowViewModel MainWindowViewModel { get; init; }
public required SubscribedEntityViewModel AgentSessionEntity { get; init; }
public required IAgentChat AgentChat { get; init; }
}
public sealed record ComposeSessionAgentViewModelOptions
{
public required MainWindowViewModel MainWindowViewModel { get; init; }
public required ObservableLoggerFactory LoggerFactory { get; init; }
public required IAgentChat AgentChat { get; init; }
public required SubscribedEntityViewModel AgentSessionEntity { get; init; }
public required AgentSessionWorkspaceTabViewModel Tab { get; init; }
public required TaskScheduler ForegroundScheduler { get; init; }
}
var viewModel = new AgentViewModel(
new AgentViewModelOptions
{
AgentChat = agentChat,
DisplayName = displayName,
Description = description,
LoggerFactory = loggerFactory,
ForegroundScheduler = foregroundScheduler,
});
All four UI getters are foreground-owned and nonblocking. SetReady remains one-shot, rejects null
arguments/failed or disposed tabs, replaces loading state atomically, then raises property and
notification changes. No UI operation crosses transport except through the IAgentChat command
methods.
AgentSessionModalViewModel is a New public sealed view model in
Phantom.Workspaces.Agent.Gui.ViewModels,
Phantom.Workspaces.Agent.Gui/ViewModels/AgentSessionModalViewModel.cs. It exposes immutable
string Id, string Title, string Body, and AgentChatModalContent Content, plus
Task RespondAsync(JsonElement response, CancellationToken ct = default). It delegates once to its
owning AgentViewModel; invalid freeform, choice, or approval responses fail before transport,
cancellation leaves the modal pending, and only a later ordered dismiss event removes it.
AgentSessionModalStackControl is a New public sealed Avalonia control in
Phantom.Workspaces.Agent.Gui.Controls,
Phantom.Workspaces.Agent.Gui/Controls/AgentSessionModalStackControl.axaml(.cs). It adds no public
method and binds only the current editor's modal collection.
SlashCommandContext is Existing, Modified in Phantom.Workspaces.Llm.SlashCommands,
Phantom.Workspaces.Llm.Core/SlashCommands/SlashCommandContext.cs: its
public required IAgentChat AgentChat { get; init; } replaces the concrete type. The init setter
rejects null. Handlers needing engine-only APIs are not registered by RemoteAgentChat.SlashCommands;
common handlers use only IAgentChat. This prevents a hidden concrete cast in the open path.
UI, sessions view, and notifications
OpenAgentSessionShortcutHandler is the sole GUI choice point for local, connect-on-owner, and
takeover paths, including restore.
RunningAgentBrainViewModel.Rows contains RunningAgentRowViewModel values with placement, owner
display, interrupt, a separate terminate action, viewer count, and authoritative background
preference. A lease close never sends the terminate verb, although final release may activate the
default lifecycle policy.
AgentViewModel owns only its modal stack and input gate. The root's
HasModalsNeedingInput is the OR of self and descendants.
AgentChatEditorControl renders AgentSessionModalStackControl above the input queue.
InputQueueViewModel binds only immutable common snapshots. It awaits commands and owner deltas;
it never receives owner collections or performs an optimistic mutation.
- Notifications are per-tab and keyed by kind. Activation clears
chat-idle; only an empty aggregate
modal set clears modal-pending. Notification, NotificationEntry, and NotificationService
add a nonblank Kind (Notification.Kind defaults to "legacy", while stored entries require an
explicit value); replacement/removal/read state uses (TabKey, Kind) rather than
only TabKey. Existing callers use a stable legacy kind. NotificationsViewModel.UnreadCount
counts all unread entries and HasUnread is UnreadCount > 0, so it is the app-wide logical OR
across chat-idle, modal-pending, and future kinds.
- New
Phantom.Workspaces.Data.Core/JsonEntities/entity-type-views/agent-session-entity-type-view.json
groups by the existing persisted host-profile-entity-id through group-by-parent; there is no
parallel Sessions UI.
Tests
UI public API tests
AgentViewModelTests:
Constructor_RemoteChat_UsesCommonSurfaceWithoutConcreteCast.
AgentViewModelOptions_NamedInitializer_PreservesRequiredValuesAndParentDefault.
Constructor_WrongForegroundContext_Throws.
RespondToModalAsync_CurrentModal_SendsResponseAndKeepsInputGatedUntilDismissed.
RespondToModalAsync_UnknownModal_ThrowsArgumentException.
RespondToModalAsync_Cancelled_DoesNotDismissModal.
ModalEvent_DescendantModal_UpdatesRootAggregateOnly.
DisposeAsync_RemoteChat_UnsubscribesBeforeDetaching.
InterruptCommand_RemoteChat_InvokesCommonInterrupt.
ConfigureSlashCommands_RemoteChat_RegistersOnlyCommonHandlers.
InputQueueViewModelTests:
CommandPending_RemoteQueue_DoesNotMutateProjectionOptimistically.
CommandConflict_StaleRevision_RefreshesFromAuthoritativeSnapshot.
CommandApplied_AuthoritativeDeltaAppliedBeforeTaskCompletes.
SlashCommandContextTests:
AgentChatSetter_LocalOrRemote_PreservesCommonChat.
AgentChatSetter_Null_RejectsInitialization.
AgentSessionModalViewModelTests:
RespondAsync_ValidOption_DelegatesOnceAndWaitsForDismissEvent.
RespondAsync_InvalidOption_RejectsBeforeTransport.
RespondAsync_Cancelled_KeepsModalPending.
AgentSessionWorkspaceTabViewModelTests:
SetReady_RemoteAgent_SetsRemoteMetadata.
SetReady_LocalAgent_ClearsRemoteMetadata.
SetReady_AgentWithModal_SetsModalPendingNotification.
ModalDismissed_LastRelevantModal_ClearsModalPendingNotification.
TabActivated_IdleAndModalNotifications_ClearsOnlyIdle.
OpenAgentSessionShortcutHandlerTests:
Handle_OwnerIsCurrentProfile_AcquiresLocalRuntime.
Handle_OwnerIsRemoteConnectChoice_AttachesOnOwner.
Handle_OwnerIsRemoteResumeChoice_CompletesTakeoverBeforeLocalAcquire.
TryCreateAgentSessionTabForRestoreAsync_RemoteOwner_UsesSameChoicePipeline.
TryCreateTabForRestoreAsync_RemoteOwner_UsesSameChoicePipeline.
Handle_RemoteOwnerPrompt_ShowsAuthorizedRunningStatus.
Handle_RemoteOwnerPrompt_ShowsAuthorizedNotRunningStatus.
Handle_RemoteOwnerPrompt_UnauthorizedOrMissingShowsUnavailable.
CreateAgentSessionTabAsync_PersistedSession_PassesEntityToAcquisition.
ComposeSessionAgentViewModel_RemoteChat_ConfiguresCommonSlashCommandSurface.
SessionUiRequestOptions_NamedInitializers_PreserveRequiredValuesAndOptionalDefaults.
DisposeAsync_InitializationInFlight_CancelsWithoutPublishingReadyTab.
RunningAgentBrainViewModelTests:
Refresh_RemoteTopLevelSession_CreatesRunningAgentRowWithRetentionState.
Refresh_Subagent_RemainsExcluded.
InterruptCommand_EnabledRow_InvokesCommonInterrupt.
InterruptCommand_NoInterruptibleRun_IsDisabled.
SetContinueInBackgroundCommand_EnabledRemoteRow_CallsRunningTableOnce.
SetContinueInBackgroundCommand_UpdatePending_DisablesUntilAuthoritativeRefresh.
SetContinueInBackgroundCommand_Rejected_KeepsAuthoritativeCheckedState.
TerminateCommand_RemoteRow_TerminatesAndRemovesRow.
RunningAgentBrainControlTests:
ContinueInBackgroundCheckbox_RemoteRow_BindsCheckedEnabledAndCommand.
ContinueInBackgroundCheckbox_Accessibility_UsesRequiredLabelAndTooltip.
ContinueInBackgroundCheckbox_KeyboardSpace_DoesNotActivateRow.
InterruptButton_ActiveRun_IsRedRightAlignedAndAccessible.
TerminateButton_ConnectedRow_IsDistinctAccessibleAndSendsExplicitTerminate.
NotificationServiceTests and NotificationsViewModelTests:
Notification_PropertyShape_RequiredMembersAndLegacyKindDefault_ArePreserved.
Notification_NamedInitializer_PreservesAllValues.
Notify_SameTabDifferentKinds_PreservesIndependentEntries.
Notify_BlankKind_ThrowsArgumentException.
NotificationTargetRequest_NamedInitializer_TargetsOneKind.
Remove_TabAndKind_RemovesOnlyMatchingKind.
MarkRead_TabAndKind_MarksOnlyMatchingKind.
HasUnread_AnyUnreadKind_ReturnsTrueUntilAllKindsRead.
TabActivated_ChatIdleAndModalPending_ClearsOnlyChatIdle.
ModalDismissed_LastRelevantModal_ClearsOnlyModalPending.
Dependencies
Part of #1483
Summary
Complete remote session opening UI, modal stacks, running-row interrupt/terminate/Continue in background controls, immutable queue UX, and notification kinds.
Scope
Implement only commit 9 of the approved design. Preserve the contracts below exactly; local and remote behavior must remain compatible. Imported surfaces consumed here must preserve the exact property-based
UsageandAgentInformationnames, fields, types, and semantics from detail 2; protocol DTO/property records must preserve the approved serializers and fixed discriminators; any imported interrupt contract remainsInterruptAsync(Guid commandId, CancellationToken ct = default)with noInterruptAgentSessionRequest; APIs owned by #1471-#1477 stay unchanged unless this design explicitly changes their presentation; and the audited design total remains exactly 28 request/options types (22 public and 6 internal).Files
Files:
AgentViewModel,AgentSessionWorkspaceTabViewModel, editor/modal controls,Phantom.Workspaces/ViewModels/RunningAgentBrainViewModel.cs,Phantom.Workspaces/ViewModels/RunningAgentRowViewModel.cs,Phantom.Workspaces/Controls/RunningAgentBrainControl.axaml(.cs),Notification,NotificationEntry,NotificationTargetRequest,INotificationService,NotificationService, andNotificationsViewModel.Tests: UI request/options required-member/default/named-initializer tests, UI public API tests
above, multiple modal ownership, descendant aggregation, independent
notification clearing, remote interrupt versus terminate, and checkbox binding, command,
enabled/pending state, and accessibility.
Detailed design
UI integration types - Existing, Modified
RunningAgentBrainViewModel,RunningAgentRowViewModel, andRunningAgentBrainControl:Phantom.Workspaces.ViewModels/Phantom.Workspaces.Controls;Phantom.Workspaces/ViewModels/RunningAgentBrainViewModel.cs,Phantom.Workspaces/ViewModels/RunningAgentRowViewModel.cs, andPhantom.Workspaces/Controls/RunningAgentBrainControl.axaml(.cs). These are the existingbrain-icon popup,
Rowscollection, and row type; this design adds no parallel “sessions flyout.”RunningAgentBrainViewModel.Refresh()continues to derive top-level rows fromIRunningAgentChatTable.RunningSessionsand continues filteringIsSubAgent.RunningAgentRowViewModeladds the following public row state:Its two existing public constructors remain source-compatible. An internal constructor/factory
used by
RunningAgentBrainViewModelsupplies runtime metadata and commands; legacy callers receivedisabled no-op runtime commands and
false/zero metadata.InterruptCommandacquires the existing session lease and invokesIAgentChat.Interrupt; it isenabled only while that row has an active interruptible turn.
TerminateCommandcallsIRunningAgentChatTable.TerminateAsyncand removes the row only throughthe resulting
RunningSessionschange. Both are disabled while terminal/disconnected, andterminate disables every row command while pending. The background command is created by
RunningAgentBrainViewModeland callsIRunningAgentChatTable.SetContinueInBackgroundAsync(SessionId, requestedValue). It is disabledwhile an update is pending, after terminal/disconnect, or when the row lacks a persisted top-level
session/owning transport; it is enabled for an attached remote row and for a locally owned runtime
capable of accepting remote attachments.
ContinueInBackgroundchanges only from theauthoritative running-session metadata after persistence/event application. Failure leaves the
old checked state and reports the existing safe operation error.
RunningAgentBrainControlplaces one checkbox in each running row beside the existing activatebutton. Its visible label and
AutomationProperties.Nameare exactly “Continue in background”;its tooltip is “Keep this agent running when the last viewer disconnects.”
IsCheckedisone-way bound to
ContinueInBackground,IsEnabledtoIsBackgroundOptionEnabled, and itscommand/boolean parameter to
SetContinueInBackgroundCommand. Keyboard focus and Space invoke thecheckbox without activating the row; the row's activate button remains separately focusable. A
red, right-aligned X button binds
InterruptCommand,IsEnabledtoIsInterruptEnabled,tooltip and
AutomationProperties.Nameto “Interrupt agent”, and is distinct from theterminate action. A separate destructive Terminate button follows it, binds
TerminateCommand, is disabled while termination is pending or the row is disconnected/terminal,and uses tooltip and
AutomationProperties.Name“Terminate agent session”. It sends theexplicit terminate verb; it never merely closes the tab or releases a viewer lease.
AgentViewModel:Phantom.Workspaces.Agent.Gui.ViewModels;Phantom.Workspaces.Agent.Gui/ViewModels/AgentViewModel.cs; existingpublic sealed class.Its constructor becomes
AgentViewModel(AgentViewModelOptions options).AgentViewModelOptionshas required-init
IAgentChat AgentChat,string DisplayName,string Description,ObservableLoggerFactory LoggerFactory, andTaskScheduler ForegroundScheduler, plusAgentViewModel? ParentAgentViewModel { get; init; } = null. Existing foreground validationremains. It uses the common collections/events, awaits async remote tool toggles, and owns a
ReadOnlyObservableCollection<AgentSessionModalViewModel> Modals. Modal response callsTask RespondToModalAsync(string modalId, JsonElement response, CancellationToken ct = default),which delegates to
IAgentChat, preserves the modal until a dismiss event, and maps cancellationor safe remote errors without optimistic removal.
Input is gated only while this editor has an unresolved modal; descendant modals affect only root
notification aggregation. Its existing public
AgentChatgetter changes topublic IAgentChat AgentChat { get; }. Disposal unsubscribes before disposing its chat lease/proxy.InputQueueViewModel:Phantom.Workspaces.Agent.Gui.ViewModels; existingpublic sealed class. Its constructor andfields consume
IAgentChat.InputQueues,IAgentInputQueue, and immutable snapshots rather thanconcrete
AgentChat,AgentChatQueue, orAgentInputQueueManager. Submit, create, edit, remove,move, hold/release, and target selection await the aggregate commands. Controls stay unchanged
until the acknowledged owner delta/result is applied; conflict replaces the projection from the
returned snapshot and prompts/retries only after reconciling user intent.
Changedis marshalledto the existing foreground scheduler, and disposal unsubscribes without disposing the chat-owned
aggregate.
AgentSessionWorkspaceTabViewModel:Phantom.Workspaces.ViewModels;Phantom.Workspaces/ViewModels/AgentSessionWorkspaceTabViewModel.cs; existing public sealed class.Adds read-only
bool IsRemote,string? RemoteProfileDisplayName, andbool HasModalsNeedingInput;SetReadyderives these from the chat/view model. Themodal-pendingnotification clears only when aggregate modal count reaches zero;chat-idleretains activation-clearing behavior.
OpenAgentSessionShortcutHandler:same namespace; existing public sealed class.
Handle,TryCreateAgentSessionTabForRestoreAsync,TryCreateTabForRestoreAsync, andCreateAgentSessionTabAsyncall call one new internalOpenPersistedSessionAsync(JsonElement, AgentSessionOpenIntent, CancellationToken).TryCreateAgentSessionTabForRestoreAsynctakesCreateAgentSessionTabForRestoreRequestplusCancellationToken;CreateAgentSessionTabAsynctakes
CreateAgentSessionTabRequestplusCancellationToken; andComposeSessionAgentViewModeltakesComposeSessionAgentViewModelOptions. The inheritedHandleandTryCreateTabForRestoreAsyncoverrides retain their framework signatures.ConnectOnOwnerbuilds a remote acquisition request;ResumeLocallyperforms takeover before alocal acquisition. It never parses executor bindings or compiles trust policy.
Notification,NotificationEntry,INotificationService,NotificationService, andNotificationsViewModel: existing notification types inPhantom.Workspaces/Services/NotificationsandPhantom.Workspaces/ViewModels.Notificationadds nonblank
string Kindwith the"legacy"default;NotificationEntryexposes it.INotificationServiceandNotificationServiceaddvoid Remove(NotificationTargetRequest request)andvoid MarkRead(NotificationTargetRequest request)while retaining the existing tab-wide overloads forcallers that intentionally affect every kind.
Notifyupserts only the matching(TabKey, Kind).NotificationsViewModel.UnreadCountandHasUnreadaggregate all entries and therefore require nosignature change.
NotificationService.Notifyrejects null and blankKind;"legacy"preserves existing callersource compatibility while still giving every stored entry a nonblank key.
The added/changed public signatures are:
All four UI getters are foreground-owned and nonblocking.
SetReadyremains one-shot, rejects nullarguments/failed or disposed tabs, replaces loading state atomically, then raises property and
notification changes. No UI operation crosses transport except through the
IAgentChatcommandmethods.
AgentSessionModalViewModelis a New public sealed view model inPhantom.Workspaces.Agent.Gui.ViewModels,Phantom.Workspaces.Agent.Gui/ViewModels/AgentSessionModalViewModel.cs. It exposes immutablestring Id,string Title,string Body, andAgentChatModalContent Content, plusTask RespondAsync(JsonElement response, CancellationToken ct = default). It delegates once to itsowning
AgentViewModel; invalid freeform, choice, or approval responses fail before transport,cancellation leaves the modal pending, and only a later ordered dismiss event removes it.
AgentSessionModalStackControlis a New public sealed Avalonia control inPhantom.Workspaces.Agent.Gui.Controls,Phantom.Workspaces.Agent.Gui/Controls/AgentSessionModalStackControl.axaml(.cs). It adds no publicmethod and binds only the current editor's modal collection.
SlashCommandContextis Existing, Modified inPhantom.Workspaces.Llm.SlashCommands,Phantom.Workspaces.Llm.Core/SlashCommands/SlashCommandContext.cs: itspublic required IAgentChat AgentChat { get; init; }replaces the concrete type. The init setterrejects null. Handlers needing engine-only APIs are not registered by
RemoteAgentChat.SlashCommands;common handlers use only
IAgentChat. This prevents a hidden concrete cast in the open path.UI, sessions view, and notifications
OpenAgentSessionShortcutHandleris the sole GUI choice point for local, connect-on-owner, andtakeover paths, including restore.
RunningAgentBrainViewModel.RowscontainsRunningAgentRowViewModelvalues with placement, ownerdisplay, interrupt, a separate terminate action, viewer count, and authoritative background
preference. A lease close never sends the terminate verb, although final release may activate the
default lifecycle policy.
AgentViewModelowns only its modal stack and input gate. The root'sHasModalsNeedingInputis the OR of self and descendants.AgentChatEditorControlrendersAgentSessionModalStackControlabove the input queue.InputQueueViewModelbinds only immutable common snapshots. It awaits commands and owner deltas;it never receives owner collections or performs an optimistic mutation.
chat-idle; only an empty aggregatemodal set clears
modal-pending.Notification,NotificationEntry, andNotificationServiceadd a nonblank
Kind(Notification.Kinddefaults to"legacy", while stored entries require anexplicit value); replacement/removal/read state uses
(TabKey, Kind)rather thanonly
TabKey. Existing callers use a stable legacy kind.NotificationsViewModel.UnreadCountcounts all unread entries and
HasUnreadisUnreadCount > 0, so it is the app-wide logical ORacross
chat-idle,modal-pending, and future kinds.Phantom.Workspaces.Data.Core/JsonEntities/entity-type-views/agent-session-entity-type-view.jsongroups by the existing persisted
host-profile-entity-idthroughgroup-by-parent; there is noparallel Sessions UI.
Tests
UI public API tests
AgentViewModelTests:Constructor_RemoteChat_UsesCommonSurfaceWithoutConcreteCast.AgentViewModelOptions_NamedInitializer_PreservesRequiredValuesAndParentDefault.Constructor_WrongForegroundContext_Throws.RespondToModalAsync_CurrentModal_SendsResponseAndKeepsInputGatedUntilDismissed.RespondToModalAsync_UnknownModal_ThrowsArgumentException.RespondToModalAsync_Cancelled_DoesNotDismissModal.ModalEvent_DescendantModal_UpdatesRootAggregateOnly.DisposeAsync_RemoteChat_UnsubscribesBeforeDetaching.InterruptCommand_RemoteChat_InvokesCommonInterrupt.ConfigureSlashCommands_RemoteChat_RegistersOnlyCommonHandlers.InputQueueViewModelTests:CommandPending_RemoteQueue_DoesNotMutateProjectionOptimistically.CommandConflict_StaleRevision_RefreshesFromAuthoritativeSnapshot.CommandApplied_AuthoritativeDeltaAppliedBeforeTaskCompletes.SlashCommandContextTests:AgentChatSetter_LocalOrRemote_PreservesCommonChat.AgentChatSetter_Null_RejectsInitialization.AgentSessionModalViewModelTests:RespondAsync_ValidOption_DelegatesOnceAndWaitsForDismissEvent.RespondAsync_InvalidOption_RejectsBeforeTransport.RespondAsync_Cancelled_KeepsModalPending.AgentSessionWorkspaceTabViewModelTests:SetReady_RemoteAgent_SetsRemoteMetadata.SetReady_LocalAgent_ClearsRemoteMetadata.SetReady_AgentWithModal_SetsModalPendingNotification.ModalDismissed_LastRelevantModal_ClearsModalPendingNotification.TabActivated_IdleAndModalNotifications_ClearsOnlyIdle.OpenAgentSessionShortcutHandlerTests:Handle_OwnerIsCurrentProfile_AcquiresLocalRuntime.Handle_OwnerIsRemoteConnectChoice_AttachesOnOwner.Handle_OwnerIsRemoteResumeChoice_CompletesTakeoverBeforeLocalAcquire.TryCreateAgentSessionTabForRestoreAsync_RemoteOwner_UsesSameChoicePipeline.TryCreateTabForRestoreAsync_RemoteOwner_UsesSameChoicePipeline.Handle_RemoteOwnerPrompt_ShowsAuthorizedRunningStatus.Handle_RemoteOwnerPrompt_ShowsAuthorizedNotRunningStatus.Handle_RemoteOwnerPrompt_UnauthorizedOrMissingShowsUnavailable.CreateAgentSessionTabAsync_PersistedSession_PassesEntityToAcquisition.ComposeSessionAgentViewModel_RemoteChat_ConfiguresCommonSlashCommandSurface.SessionUiRequestOptions_NamedInitializers_PreserveRequiredValuesAndOptionalDefaults.DisposeAsync_InitializationInFlight_CancelsWithoutPublishingReadyTab.RunningAgentBrainViewModelTests:Refresh_RemoteTopLevelSession_CreatesRunningAgentRowWithRetentionState.Refresh_Subagent_RemainsExcluded.InterruptCommand_EnabledRow_InvokesCommonInterrupt.InterruptCommand_NoInterruptibleRun_IsDisabled.SetContinueInBackgroundCommand_EnabledRemoteRow_CallsRunningTableOnce.SetContinueInBackgroundCommand_UpdatePending_DisablesUntilAuthoritativeRefresh.SetContinueInBackgroundCommand_Rejected_KeepsAuthoritativeCheckedState.TerminateCommand_RemoteRow_TerminatesAndRemovesRow.RunningAgentBrainControlTests:ContinueInBackgroundCheckbox_RemoteRow_BindsCheckedEnabledAndCommand.ContinueInBackgroundCheckbox_Accessibility_UsesRequiredLabelAndTooltip.ContinueInBackgroundCheckbox_KeyboardSpace_DoesNotActivateRow.InterruptButton_ActiveRun_IsRedRightAlignedAndAccessible.TerminateButton_ConnectedRow_IsDistinctAccessibleAndSendsExplicitTerminate.NotificationServiceTestsandNotificationsViewModelTests:Notification_PropertyShape_RequiredMembersAndLegacyKindDefault_ArePreserved.Notification_NamedInitializer_PreservesAllValues.Notify_SameTabDifferentKinds_PreservesIndependentEntries.Notify_BlankKind_ThrowsArgumentException.NotificationTargetRequest_NamedInitializer_TargetsOneKind.Remove_TabAndKind_RemovesOnlyMatchingKind.MarkRead_TabAndKind_MarksOnlyMatchingKind.HasUnread_AnyUnreadKind_ReturnsTrueUntilAllKindsRead.TabActivated_ChatIdleAndModalPending_ClearsOnlyChatIdle.ModalDismissed_LastRelevantModal_ClearsOnlyModalPending.Dependencies