refactor(interactive): remove streaming interactive stack (superseded by ConPTY)
The embedded ConPTY terminal replaced the in-app streaming interactive session, so delete the dead stack: StreamingClaudeSession, InteractiveSessionService, ProcessClaudeStreamTransport, IClaudeStreamTransport, ILiveSession, LiveSessionRegistry, IdleSessionReaper (+ WorkerConfig.InteractiveIdleTimeoutMinutes), the WorkerHub interactive methods + HubBroadcaster events, IWorkerClient interactive members, the TaskMonitorViewModel composer + SessionTerminalView composer markup, and the old 'Run interactively' entry. AskUser/PendingQuestionRegistry, the autonomous path, planning, ResumeTaskInTerminal, and all ConPTY code are kept. Localization pruned.
This commit is contained in:
@@ -35,7 +35,7 @@ Design/ — Tokens.axaml (design tokens; merged before styles) + IslandStyle
|
||||
|
||||
- **IslandsShellViewModel** — root coordinator; owns the three island VMs and the `WorkerClient`, wires cross-island events (selection, notes/prep mode, conflict resolution), owns connection state, the update banner, the inline worker-log strip (clickable → Log Visualizer overlay via `OpenLogVisualizerCommand`; `FlashFooterError` surfaces UI-action failures + the worker's Serilog Warn/Error there), responsive-layout flags (`ShowLists`/`ShowDetails` by window width), `PrimeStatus` flash, and the modal openers (About, RepoImport, WeeklyReport, WorktreesOverview, WorkerConnection help, LogVisualizer) plus `RestartWorkerAsync`/`CheckForUpdatesAsync`. Hosts `UpdateCheckService`.
|
||||
- **ListsIslandViewModel** — smart lists (My Day, Important, Planned, virtual queued/running/review), user lists, selection, list CRUD, drag-reorder, badge counts, opens list settings / repo import / worktrees overview, `OpenInExplorer`/`OpenInTerminal`.
|
||||
- **TasksIslandViewModel** — open/overdue/completed groups for the selected list with hierarchy-aware regrouping; task CRUD, drag-reorder, toggle done/star, schedule, enqueue/dequeue, cancel; review actions (approve, reject-rerun, reject-park, cancel); planning session lifecycle (open/resume/discard/finalize, `QueuePlanningSubtasksAsync`); `RunInteractivelyAsync`, `RefineTask`; MyDay extras (`IsMyDayList`, `ClearDayCommand`, `ShowPrepLogCommand`) and the pinned Notes pseudo-row (`ShowNotesRow`, `OpenNotesCommand`). Raises `NotesRequested`/`PrepRequested` events consumed by the shell.
|
||||
- **TasksIslandViewModel** — open/overdue/completed groups for the selected list with hierarchy-aware regrouping; task CRUD, drag-reorder, toggle done/star, schedule, enqueue/dequeue, cancel; review actions (approve, reject-rerun, reject-park, cancel); planning session lifecycle (open/resume/discard/finalize, `QueuePlanningSubtasksAsync`); `RefineTask`, `OpenConPtySessionRequested` (embedded ConPTY terminal), `PickUpInTerminalAsync`; MyDay extras (`IsMyDayList`, `ClearDayCommand`, `ShowPrepLogCommand`) and the pinned Notes pseudo-row (`ShowNotesRow`, `OpenNotesCommand`). Raises `NotesRequested`/`PrepRequested` events consumed by the shell.
|
||||
- **DetailsIslandViewModel** — the detail pane for a bound `TaskRowViewModel`. Owns live-log streaming (`Log` via `StreamLineFormatter`), debounced title/description editing, subtasks, session-outcome/roadblock split (splits `Result` at the roadblock marker into two cards), the three-tab work console (`output`/`git`/`session`), child surfacing (`ChildOutcomes` rows plus `ChildrenNeedingAttention`/`HasChildrenNeedingAttention` — children that failed, were cancelled, await review, or reported roadblocks — drive an attention band on the Session tab, which is only visible when `HasChildOutcomes`), and the modes: `IsNotesMode` (hosts `NotesEditorViewModel`), `IsPrepMode`, computed `IsTaskDetailVisible = !IsNotesMode && !IsPrepMode`. Three concerns are extracted into section VMs exposed as properties: **AgentConfigEditorViewModel** (scope=Task; per-task Model/MaxTurns/AgentPath overrides with `InheritedBadge` + `InheritanceResolver`, additive SystemPrompt, debounced auto-save; exposed as `AgentSettings`), **MergeSectionViewModel** (merge-target selection, mergeability indicator via `MergePreviewPresenter` over `PreviewMergeAsync`, `OpenDiffAsync` and `ReviewCombinedDiffCommand` — both build a `DiffViewerViewModel` and call `ShowDiffViewer`), **PrepPanelViewModel** (daily-prep panel: `PrepLog`, `PlanDayCommand` → `RunDailyPrepNowAsync`, persisted last run via `GetLastPrepLogAsync`). Attachments: `Attachments` (`ObservableCollection<AttachmentRowViewModel>`), `IsDragOver`, `DropStatus`, `CanAcceptDrop`, `AddFilesAsync`, `RemoveAttachmentCommand`; loads on task change; `ComposedPreview` includes attachment paths. Writes directly via `new AttachmentStore()` + `new TaskAttachmentRepository(ctx)`. Helper rows (`ChildOutcomeRowViewModel`, `SubtaskRowViewModel`, `LogLineViewModel`, `AttachmentRowViewModel`) live in the same file.
|
||||
- **TaskRowViewModel** / **ListNavItemViewModel** — lightweight display VMs (task row: status, planning phase, parent/blocked links, roadblock count, computed `IsDraft`/`IsPlanned`/`IsChild`/`IsPlanningParent`/`CanRefine`; list row: kind Smart/Virtual/User, count, icon/dot keys, drop hints).
|
||||
- **NotesEditorViewModel** — day navigator + bullet CRUD for daily notes via `INotesApi`.
|
||||
@@ -45,7 +45,7 @@ Design/ — Tokens.axaml (design tokens; merged before styles) + IslandStyle
|
||||
|
||||
## Services
|
||||
|
||||
- **WorkerClient** / **IWorkerClient** — SignalR client connecting to `http://127.0.0.1:47821/hub`, auto-reconnect with exponential backoff. The surface tracks `WorkerHub` (see `src/ClaudeDo.Worker/CLAUDE.md` for the canonical method/event list); groups: task execution (RunNow/Cancel/Continue/Reset/SetTaskStatus), review (`ApproveReviewAsync(taskId, targetBranch) -> MergeResultDto`, reject-to-queue/idle, cancel review, `PreviewMergeAsync -> MergePreviewDto`), planning sessions (start/resume/discard/finalize, queue subtasks, pending draft count, interactive terminal, refine), planning aggregate/integration-branch diffs, unit-merge continue/abort, single-task conflict resolving (start/get-conflict-documents/write-resolution/continue/abort), worktrees (overview, set state, force remove, cleanup, reset all), agents, app settings, lists/config, weekly report, daily notes, daily prep (`RunDailyPrepNowAsync`, `ClearMyDayAsync`, `GetLastPrepLogAsync`), prime schedules, recent worker logs (`GetRecentLogsAsync`). Events mirror `HubBroadcaster` (task/worktree/list/run updates, prep events, planning-merge events, refine events, worker log). Lifecycle (`StartAsync`/`StopAsync`) and a few admin methods live only on the concrete `WorkerClient`.
|
||||
- **WorkerClient** / **IWorkerClient** — SignalR client connecting to `http://127.0.0.1:47821/hub`, auto-reconnect with exponential backoff. The surface tracks `WorkerHub` (see `src/ClaudeDo.Worker/CLAUDE.md` for the canonical method/event list); groups: task execution (RunNow/Cancel/Continue/Reset/SetTaskStatus), review (`ApproveReviewAsync(taskId, targetBranch) -> MergeResultDto`, reject-to-queue/idle, cancel review, `PreviewMergeAsync -> MergePreviewDto`), planning sessions (start/resume/discard/finalize, queue subtasks, pending draft count, refine), pick-up-in-terminal + embedded ConPTY launch specs (`GetInteractiveLaunchSpecAsync`/`GetAdHocLaunchSpecAsync`), planning aggregate/integration-branch diffs, unit-merge continue/abort, single-task conflict resolving (start/get-conflict-documents/write-resolution/continue/abort), worktrees (overview, set state, force remove, cleanup, reset all), agents, app settings, lists/config, weekly report, daily notes, daily prep (`RunDailyPrepNowAsync`, `ClearMyDayAsync`, `GetLastPrepLogAsync`), prime schedules, recent worker logs (`GetRecentLogsAsync`). Events mirror `HubBroadcaster` (task/worktree/list/run updates, prep events, planning-merge events, refine events, worker log). Lifecycle (`StartAsync`/`StopAsync`) and a few admin methods live only on the concrete `WorkerClient`.
|
||||
- **INotesApi** / **WorkerNotesApi** — daily-note CRUD (`ListAsync(day)`, `AddAsync`, `UpdateAsync`, `DeleteAsync`); UI DTO `DailyNoteDto(Id, Date, Text, SortOrder)`.
|
||||
- **IPrimeScheduleApi** — prime-schedule CRUD (`ListAsync`, `UpsertAsync`, `DeleteAsync`).
|
||||
- **UpdateCheckService** — polls releases, exposes `LastCheckStatus`/`LatestVersion`/`CheckNowAsync` (feeds the shell's update banner).
|
||||
|
||||
@@ -25,11 +25,6 @@ public interface IWorkerClient : INotifyPropertyChanged
|
||||
/// <summary>A pending question was answered, timed out, or the run ended: (taskId, questionId).</summary>
|
||||
event Action<string, string>? TaskQuestionResolvedEvent;
|
||||
|
||||
event Action<string>? InteractiveSessionStartedEvent;
|
||||
event Action<string>? InteractiveSessionEndedEvent;
|
||||
event Action<string, IReadOnlyList<string>>? InteractiveQueueChangedEvent;
|
||||
event Action<string, string>? InteractiveMessageSentEvent;
|
||||
|
||||
event Action? PrepStartedEvent;
|
||||
event Action<string>? PrepLineEvent;
|
||||
event Action<bool>? PrepFinishedEvent;
|
||||
@@ -51,10 +46,6 @@ public interface IWorkerClient : INotifyPropertyChanged
|
||||
Task ContinueTaskAsync(string taskId, string followUpPrompt);
|
||||
/// <summary>Answer a question a running task raised via AskUser.</summary>
|
||||
Task AnswerTaskQuestionAsync(string taskId, string questionId, string answer);
|
||||
Task SendInteractiveMessageAsync(string taskId, string text);
|
||||
Task RemoveQueuedInteractiveMessageAsync(string taskId, string text);
|
||||
Task StopInteractiveSessionAsync(string taskId);
|
||||
Task InterruptInteractiveSessionAsync(string taskId);
|
||||
/// <summary>The question a running task is currently blocked on, if any (for re-attach).</summary>
|
||||
Task<PendingQuestionDto?> GetPendingQuestionAsync(string taskId);
|
||||
Task ResetTaskAsync(string taskId);
|
||||
@@ -83,9 +74,7 @@ public interface IWorkerClient : INotifyPropertyChanged
|
||||
Task<MergeResultDto> ContinueConflictMergeAsync(string taskId);
|
||||
Task AbortConflictMergeAsync(string taskId);
|
||||
Task StartPlanningSessionAsync(string taskId, CancellationToken ct = default);
|
||||
Task OpenInteractiveTerminalAsync(string taskId, CancellationToken ct = default);
|
||||
// Picks up a task's Claude session in a real terminal window (--resume) — distinct
|
||||
// from OpenInteractiveTerminalAsync (the in-app streaming session).
|
||||
// Picks up a task's Claude session in a real terminal window (--resume).
|
||||
Task ResumeTaskInTerminalAsync(string taskId, CancellationToken ct = default);
|
||||
/// <summary>Launch spec for an embedded ConPTY terminal to open an interactive session
|
||||
/// in a task's worktree (same worktree prep as an autonomous run).</summary>
|
||||
|
||||
@@ -49,10 +49,6 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
||||
public event Action<string>? TaskUpdatedEvent;
|
||||
public event Action<string, string, string>? TaskQuestionAskedEvent;
|
||||
public event Action<string, string>? TaskQuestionResolvedEvent;
|
||||
public event Action<string>? InteractiveSessionStartedEvent;
|
||||
public event Action<string>? InteractiveSessionEndedEvent;
|
||||
public event Action<string, IReadOnlyList<string>>? InteractiveQueueChangedEvent;
|
||||
public event Action<string, string>? InteractiveMessageSentEvent;
|
||||
public event Action? ConnectionRestoredEvent;
|
||||
public event Action<string>? WorktreeUpdatedEvent;
|
||||
public event Action<string>? ListUpdatedEvent;
|
||||
@@ -152,26 +148,6 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
||||
Dispatcher.UIThread.Post(() => TaskQuestionResolvedEvent?.Invoke(taskId, questionId));
|
||||
});
|
||||
|
||||
_hub.On<string>("InteractiveSessionStarted", taskId =>
|
||||
{
|
||||
Dispatcher.UIThread.Post(() => InteractiveSessionStartedEvent?.Invoke(taskId));
|
||||
});
|
||||
|
||||
_hub.On<string>("InteractiveSessionEnded", taskId =>
|
||||
{
|
||||
Dispatcher.UIThread.Post(() => InteractiveSessionEndedEvent?.Invoke(taskId));
|
||||
});
|
||||
|
||||
_hub.On<string, IReadOnlyList<string>>("InteractiveQueueChanged", (taskId, pending) =>
|
||||
{
|
||||
Dispatcher.UIThread.Post(() => InteractiveQueueChangedEvent?.Invoke(taskId, pending));
|
||||
});
|
||||
|
||||
_hub.On<string, string>("InteractiveMessageSent", (taskId, text) =>
|
||||
{
|
||||
Dispatcher.UIThread.Post(() => InteractiveMessageSentEvent?.Invoke(taskId, text));
|
||||
});
|
||||
|
||||
_hub.On<string>("WorktreeUpdated", taskId =>
|
||||
{
|
||||
Dispatcher.UIThread.Post(() => WorktreeUpdatedEvent?.Invoke(taskId));
|
||||
@@ -303,30 +279,6 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
||||
catch { /* offline or already resolved — the UI clears optimistically */ }
|
||||
}
|
||||
|
||||
public async Task SendInteractiveMessageAsync(string taskId, string text)
|
||||
{
|
||||
try { await _hub.InvokeAsync("SendInteractiveMessage", taskId, text); }
|
||||
catch { /* offline or session already ended */ }
|
||||
}
|
||||
|
||||
public async Task RemoveQueuedInteractiveMessageAsync(string taskId, string text)
|
||||
{
|
||||
try { await _hub.InvokeAsync("RemoveQueuedInteractiveMessage", taskId, text); }
|
||||
catch { /* offline or session already ended */ }
|
||||
}
|
||||
|
||||
public async Task StopInteractiveSessionAsync(string taskId)
|
||||
{
|
||||
try { await _hub.InvokeAsync("StopInteractiveSession", taskId); }
|
||||
catch { /* offline */ }
|
||||
}
|
||||
|
||||
public async Task InterruptInteractiveSessionAsync(string taskId)
|
||||
{
|
||||
try { await _hub.InvokeAsync("InterruptInteractiveSession", taskId); }
|
||||
catch { /* offline */ }
|
||||
}
|
||||
|
||||
public Task<PendingQuestionDto?> GetPendingQuestionAsync(string taskId)
|
||||
=> TryInvokeAsync<PendingQuestionDto>("GetPendingQuestion", taskId);
|
||||
|
||||
@@ -558,9 +510,6 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
||||
public async Task<PlanningSessionResumeInfo> ResumePlanningSessionAsync(string taskId, CancellationToken ct = default)
|
||||
=> await _hub.InvokeAsync<PlanningSessionResumeInfo>("ResumePlanningSessionAsync", taskId, ct);
|
||||
|
||||
public async Task OpenInteractiveTerminalAsync(string taskId, CancellationToken ct = default)
|
||||
=> await _hub.InvokeAsync("OpenInteractiveTerminalAsync", taskId, ct);
|
||||
|
||||
public async Task ResumeTaskInTerminalAsync(string taskId, CancellationToken ct = default)
|
||||
=> await _hub.InvokeAsync("ResumeTaskInTerminal", taskId, ct);
|
||||
|
||||
|
||||
@@ -71,9 +71,6 @@ public sealed partial class TaskMonitorViewModel : ViewModelBase, IMissionContro
|
||||
|
||||
private const string RoadblockMarker = "Roadblocks reported during the run:";
|
||||
|
||||
public ObservableCollection<QueuedMessageViewModel> QueuedMessages { get; } = new();
|
||||
public bool HasQueuedMessages => QueuedMessages.Count > 0;
|
||||
|
||||
// Captured handler delegates for disposal
|
||||
private readonly Action<string, string> _onTaskMessage;
|
||||
private readonly Action<string, string, DateTime> _onTaskStarted;
|
||||
@@ -81,19 +78,6 @@ public sealed partial class TaskMonitorViewModel : ViewModelBase, IMissionContro
|
||||
private readonly Action<string> _onTaskUpdated;
|
||||
private readonly Action<string, string, string> _onTaskQuestionAsked;
|
||||
private readonly Action<string, string> _onTaskQuestionResolved;
|
||||
private readonly Action<string> _onInteractiveStarted;
|
||||
private readonly Action<string> _onInteractiveEnded;
|
||||
private readonly Action<string, IReadOnlyList<string>> _onInteractiveQueueChanged;
|
||||
private readonly Action<string, string> _onInteractiveMessageSent;
|
||||
|
||||
// Interactive composer — active while the worker is in an interactive session.
|
||||
[ObservableProperty]
|
||||
[NotifyCanExecuteChangedFor(nameof(SubmitComposerCommand))]
|
||||
private bool _isInteractiveLive;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyCanExecuteChangedFor(nameof(SubmitComposerCommand))]
|
||||
private string _composerDraft = string.Empty;
|
||||
|
||||
// A question the running task raised via AskUser and is blocking on, plus the answer
|
||||
// the user is typing. Ephemeral (in-memory + live events) — the task is still Running.
|
||||
@@ -161,46 +145,6 @@ public sealed partial class TaskMonitorViewModel : ViewModelBase, IMissionContro
|
||||
ClearPendingQuestion();
|
||||
};
|
||||
_worker.TaskQuestionResolvedEvent += _onTaskQuestionResolved;
|
||||
|
||||
_onInteractiveStarted = taskId =>
|
||||
{
|
||||
if (taskId == _subscribedTaskId) { IsInteractiveLive = true; AgentState = "running"; }
|
||||
};
|
||||
_worker.InteractiveSessionStartedEvent += _onInteractiveStarted;
|
||||
|
||||
_onInteractiveEnded = taskId =>
|
||||
{
|
||||
if (taskId != _subscribedTaskId) return;
|
||||
IsInteractiveLive = false;
|
||||
AgentState = "done";
|
||||
QueuedMessages.Clear();
|
||||
OnPropertyChanged(nameof(HasQueuedMessages));
|
||||
};
|
||||
_worker.InteractiveSessionEndedEvent += _onInteractiveEnded;
|
||||
|
||||
_onInteractiveQueueChanged = (taskId, pending) =>
|
||||
{
|
||||
if (taskId != _subscribedTaskId) return;
|
||||
QueuedMessages.Clear();
|
||||
foreach (var m in pending)
|
||||
{
|
||||
var text = m;
|
||||
QueuedMessages.Add(new QueuedMessageViewModel
|
||||
{
|
||||
Text = text,
|
||||
RemoveCommand = new CommunityToolkit.Mvvm.Input.RelayCommand(() => _ = RemoveQueuedAsync(text)),
|
||||
});
|
||||
}
|
||||
OnPropertyChanged(nameof(HasQueuedMessages));
|
||||
};
|
||||
_worker.InteractiveQueueChangedEvent += _onInteractiveQueueChanged;
|
||||
|
||||
_onInteractiveMessageSent = (taskId, text) =>
|
||||
{
|
||||
if (taskId == _subscribedTaskId)
|
||||
Log.Add(new LogLineViewModel { Kind = LogKind.User, Text = text });
|
||||
};
|
||||
_worker.InteractiveMessageSentEvent += _onInteractiveMessageSent;
|
||||
}
|
||||
|
||||
// Surface a pending question (used by live event + re-attach hydration).
|
||||
@@ -210,45 +154,6 @@ public sealed partial class TaskMonitorViewModel : ViewModelBase, IMissionContro
|
||||
PendingQuestion = question;
|
||||
}
|
||||
|
||||
// Used by Mission Control when it creates the monitor after the started event already fired.
|
||||
public void SetInteractiveLive(bool live)
|
||||
{
|
||||
IsInteractiveLive = live;
|
||||
if (live) AgentState = "running";
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(CanSubmitComposer))]
|
||||
private async System.Threading.Tasks.Task SubmitComposer()
|
||||
{
|
||||
if (string.IsNullOrEmpty(_subscribedTaskId)) return;
|
||||
var text = ComposerDraft;
|
||||
if (string.IsNullOrWhiteSpace(text)) return;
|
||||
ComposerDraft = string.Empty;
|
||||
await _worker.SendInteractiveMessageAsync(_subscribedTaskId, text);
|
||||
}
|
||||
|
||||
private bool CanSubmitComposer() => IsInteractiveLive && !string.IsNullOrWhiteSpace(ComposerDraft);
|
||||
|
||||
[RelayCommand]
|
||||
private async System.Threading.Tasks.Task StopInteractive()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_subscribedTaskId) && IsInteractiveLive)
|
||||
await _worker.StopInteractiveSessionAsync(_subscribedTaskId);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async System.Threading.Tasks.Task InterruptInteractive()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_subscribedTaskId) && IsInteractiveLive)
|
||||
await _worker.InterruptInteractiveSessionAsync(_subscribedTaskId);
|
||||
}
|
||||
|
||||
private async System.Threading.Tasks.Task RemoveQueuedAsync(string text)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_subscribedTaskId))
|
||||
await _worker.RemoveQueuedInteractiveMessageAsync(_subscribedTaskId, text);
|
||||
}
|
||||
|
||||
private void ClearPendingQuestion()
|
||||
{
|
||||
PendingQuestionId = null;
|
||||
@@ -297,10 +202,6 @@ public sealed partial class TaskMonitorViewModel : ViewModelBase, IMissionContro
|
||||
SessionOutcome = null;
|
||||
Roadblocks = null;
|
||||
ClearPendingQuestion();
|
||||
IsInteractiveLive = false;
|
||||
ComposerDraft = string.Empty;
|
||||
QueuedMessages.Clear();
|
||||
OnPropertyChanged(nameof(HasQueuedMessages));
|
||||
}
|
||||
|
||||
[ObservableProperty]
|
||||
@@ -524,15 +425,5 @@ public sealed partial class TaskMonitorViewModel : ViewModelBase, IMissionContro
|
||||
_worker.TaskUpdatedEvent -= _onTaskUpdated;
|
||||
_worker.TaskQuestionAskedEvent -= _onTaskQuestionAsked;
|
||||
_worker.TaskQuestionResolvedEvent -= _onTaskQuestionResolved;
|
||||
_worker.InteractiveSessionStartedEvent -= _onInteractiveStarted;
|
||||
_worker.InteractiveSessionEndedEvent -= _onInteractiveEnded;
|
||||
_worker.InteractiveQueueChangedEvent -= _onInteractiveQueueChanged;
|
||||
_worker.InteractiveMessageSentEvent -= _onInteractiveMessageSent;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class QueuedMessageViewModel
|
||||
{
|
||||
public required string Text { get; init; }
|
||||
public required System.Windows.Input.ICommand RemoveCommand { get; init; }
|
||||
}
|
||||
|
||||
@@ -810,18 +810,8 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
|
||||
catch (Exception ex) { ErrorReported?.Invoke(Loc.T("vm.tasksIsland.planningOpenFailed", ex.Message)); }
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task RunInteractivelyAsync(TaskRowViewModel? row)
|
||||
{
|
||||
if (row is null || _worker is null) return;
|
||||
ForegroundHelper.AllowAny();
|
||||
try { await _worker.OpenInteractiveTerminalAsync(row.Id); }
|
||||
catch (Exception ex) { ErrorReported?.Invoke(Loc.T("vm.tasksIsland.runInteractiveFailed", ex.Message)); }
|
||||
}
|
||||
|
||||
// Distinct from RunInteractivelyAsync (the old in-app streaming session) — this opens the
|
||||
// task in an embedded ConPTY terminal pane in the Command Center. The shell owns the
|
||||
// Mission Control view model, so this just raises an event for it to act on.
|
||||
// Opens the task in an embedded ConPTY terminal pane in the Command Center. The shell owns
|
||||
// the Mission Control view model, so this just raises an event for it to act on.
|
||||
public event Action<string>? OpenConPtySessionRequested;
|
||||
|
||||
[RelayCommand]
|
||||
|
||||
@@ -22,7 +22,6 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
|
||||
private readonly Action<string, string, string, DateTime> _onTaskFinished;
|
||||
private readonly Action<string> _onTaskUpdated;
|
||||
private readonly Action _onConnectionRestored;
|
||||
private readonly Action<string> _onInteractiveStarted;
|
||||
|
||||
public ObservableCollection<TaskMonitorViewModel> Monitors { get; } = new();
|
||||
|
||||
@@ -94,14 +93,6 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
|
||||
_onConnectionRestored = () => { SeedActive(); _ = RefreshQueueAsync(); };
|
||||
_worker.ConnectionRestoredEvent += _onConnectionRestored;
|
||||
|
||||
_onInteractiveStarted = taskId =>
|
||||
{
|
||||
EnsureMonitor(taskId);
|
||||
var m = Monitors.FirstOrDefault(x => x.SubscribedTaskId == taskId);
|
||||
m?.SetInteractiveLive(true);
|
||||
};
|
||||
_worker.InteractiveSessionStartedEvent += _onInteractiveStarted;
|
||||
|
||||
SeedActive();
|
||||
_ = RefreshQueueAsync();
|
||||
}
|
||||
@@ -231,8 +222,7 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
|
||||
}
|
||||
|
||||
// Fetches the launch spec for a task's worktree and hosts an embedded ConPTY session as a
|
||||
// Command Center pane (task-based only). A distinct entry point from RunInteractivelyAsync's
|
||||
// streaming session — the two coexist until the streaming stack is removed.
|
||||
// Command Center pane (task-based only).
|
||||
public async System.Threading.Tasks.Task OpenConPtySessionAsync(string taskId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(taskId)) return;
|
||||
@@ -362,7 +352,6 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
|
||||
_worker.TaskFinishedEvent -= _onTaskFinished;
|
||||
_worker.TaskUpdatedEvent -= _onTaskUpdated;
|
||||
_worker.ConnectionRestoredEvent -= _onConnectionRestored;
|
||||
_worker.InteractiveSessionStartedEvent -= _onInteractiveStarted;
|
||||
Monitors.CollectionChanged -= OnMonitorsChanged;
|
||||
ConPtySessions.CollectionChanged -= OnConPtySessionsChanged;
|
||||
Panes.CollectionChanged -= OnPanesChanged;
|
||||
|
||||
@@ -263,83 +263,6 @@
|
||||
Command="{Binding RejectReviewCommand}" />
|
||||
</Grid>
|
||||
|
||||
<!-- Interactive composer + queued strip — chat with a live in-app session -->
|
||||
<StackPanel DockPanel.Dock="Bottom" Orientation="Vertical">
|
||||
<!-- Queued messages strip -->
|
||||
<Border IsVisible="{Binding Monitor.HasQueuedMessages}"
|
||||
BorderBrush="{DynamicResource LineBrush}"
|
||||
BorderThickness="0,1,0,0"
|
||||
Padding="12,4">
|
||||
<StackPanel Spacing="2">
|
||||
<TextBlock Classes="meta"
|
||||
Text="{loc:Tr session.composer.queued}"
|
||||
Foreground="{DynamicResource TextMuteBrush}" />
|
||||
<ItemsControl ItemsSource="{Binding Monitor.QueuedMessages}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:QueuedMessageViewModel">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto" Margin="0,1">
|
||||
<TextBlock Grid.Column="0"
|
||||
Text="⧗"
|
||||
Foreground="{DynamicResource TextMuteBrush}"
|
||||
FontFamily="{StaticResource MonoFont}"
|
||||
FontSize="{StaticResource FontSizeMono}"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,6,0" />
|
||||
<TextBlock Grid.Column="1"
|
||||
Text="{Binding Text}"
|
||||
Foreground="{DynamicResource TextDimBrush}"
|
||||
FontFamily="{StaticResource MonoFont}"
|
||||
FontSize="{StaticResource FontSizeMono}"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
VerticalAlignment="Center" />
|
||||
<Button Grid.Column="2"
|
||||
Classes="title-ctrl"
|
||||
Command="{Binding RemoveCommand}"
|
||||
ToolTip.Tip="{loc:Tr session.composer.unqueue}"
|
||||
Margin="4,0,0,0">
|
||||
<PathIcon Data="{StaticResource Icon.WinClose}" Width="8" Height="8"/>
|
||||
</Button>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<!-- Composer input row -->
|
||||
<Grid IsVisible="{Binding Monitor.IsInteractiveLive}"
|
||||
ColumnDefinitions="Auto,*,Auto,Auto"
|
||||
Margin="12,2,12,8">
|
||||
<TextBlock Grid.Column="0" Text="❯"
|
||||
FontFamily="{StaticResource MonoFont}"
|
||||
FontSize="{StaticResource FontSizeMono}"
|
||||
Foreground="{DynamicResource AccentBrush}"
|
||||
VerticalAlignment="Center" Margin="0,0,8,0" />
|
||||
<TextBox Grid.Column="1"
|
||||
Classes="review-prompt"
|
||||
Text="{Binding Monitor.ComposerDraft, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
AcceptsReturn="False"
|
||||
TextWrapping="Wrap"
|
||||
MaxHeight="160"
|
||||
PlaceholderText="{loc:Tr session.composer.placeholder}"
|
||||
VerticalContentAlignment="Center"
|
||||
FontFamily="{StaticResource MonoFont}"
|
||||
FontSize="{StaticResource FontSizeMono}">
|
||||
<TextBox.KeyBindings>
|
||||
<KeyBinding Gesture="Enter" Command="{Binding Monitor.SubmitComposerCommand}" />
|
||||
</TextBox.KeyBindings>
|
||||
</TextBox>
|
||||
<Button Grid.Column="2" Classes="prompt-action"
|
||||
VerticalAlignment="Center" Margin="12,0,0,0"
|
||||
Command="{Binding Monitor.InterruptInteractiveCommand}"
|
||||
ToolTip.Tip="{loc:Tr session.composer.interrupt}">
|
||||
<PathIcon Data="{StaticResource Icon.Stop}" Width="10" Height="10"/>
|
||||
</Button>
|
||||
<Button Grid.Column="3" Classes="prompt-action accent" Content="[Send]"
|
||||
VerticalAlignment="Center" Margin="4,0,0,0"
|
||||
Command="{Binding Monitor.SubmitComposerCommand}" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
|
||||
<ScrollViewer Name="LogScroll"
|
||||
VerticalScrollBarVisibility="Visible"
|
||||
AllowAutoHide="False"
|
||||
|
||||
@@ -50,79 +50,6 @@
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<!-- ── Queued strip + Composer bar ── -->
|
||||
<StackPanel DockPanel.Dock="Bottom" Orientation="Vertical">
|
||||
<!-- Queued messages strip -->
|
||||
<Border IsVisible="{Binding #Root.HasQueuedMessages}"
|
||||
Background="{DynamicResource Surface2Brush}"
|
||||
BorderBrush="{DynamicResource LineBrush}"
|
||||
BorderThickness="0,0,0,1"
|
||||
Padding="8,4">
|
||||
<StackPanel Spacing="2">
|
||||
<TextBlock Classes="meta"
|
||||
Text="{loc:Tr session.composer.queued}"
|
||||
Foreground="{DynamicResource TextMuteBrush}" />
|
||||
<ItemsControl ItemsSource="{Binding #Root.QueuedMessages}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:QueuedMessageViewModel">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto" Margin="0,1">
|
||||
<TextBlock Grid.Column="0"
|
||||
Text="⧗"
|
||||
Foreground="{DynamicResource TextMuteBrush}"
|
||||
FontFamily="{StaticResource MonoFont}"
|
||||
FontSize="{StaticResource FontSizeMono}"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,6,0" />
|
||||
<TextBlock Grid.Column="1"
|
||||
Text="{Binding Text}"
|
||||
Foreground="{DynamicResource TextDimBrush}"
|
||||
FontFamily="{StaticResource MonoFont}"
|
||||
FontSize="{StaticResource FontSizeMono}"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
VerticalAlignment="Center" />
|
||||
<Button Grid.Column="2"
|
||||
Classes="title-ctrl"
|
||||
Command="{Binding RemoveCommand}"
|
||||
ToolTip.Tip="{loc:Tr session.composer.unqueue}"
|
||||
Margin="4,0,0,0">
|
||||
<PathIcon Data="{StaticResource Icon.WinClose}" Width="8" Height="8"/>
|
||||
</Button>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<!-- Composer input row -->
|
||||
<Border IsVisible="{Binding #Root.IsComposerVisible}"
|
||||
Background="{DynamicResource Surface2Brush}"
|
||||
BorderBrush="{DynamicResource LineBrush}"
|
||||
BorderThickness="0,1,0,0"
|
||||
Padding="6,5">
|
||||
<Grid ColumnDefinitions="*,Auto,Auto">
|
||||
<TextBox Grid.Column="0"
|
||||
Text="{Binding #Root.ComposerText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
PlaceholderText="{Binding #Root.ComposerPlaceholder}"
|
||||
AcceptsReturn="False">
|
||||
<TextBox.KeyBindings>
|
||||
<KeyBinding Gesture="Enter" Command="{Binding #Root.SubmitCommand}"/>
|
||||
</TextBox.KeyBindings>
|
||||
</TextBox>
|
||||
<Button Grid.Column="1"
|
||||
Margin="6,0,0,0"
|
||||
Classes="title-ctrl"
|
||||
Command="{Binding #Root.InterruptCommand}"
|
||||
ToolTip.Tip="{loc:Tr session.composer.interrupt}">
|
||||
<PathIcon Data="{StaticResource Icon.Stop}" Width="10" Height="10"/>
|
||||
</Button>
|
||||
<Button Grid.Column="2"
|
||||
Margin="6,0,0,0"
|
||||
Content="{loc:Tr session.composer.send}"
|
||||
Command="{Binding #Root.SubmitCommand}"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<!-- ── Log output ── -->
|
||||
<ScrollViewer Name="LogScroll"
|
||||
VerticalScrollBarVisibility="Visible"
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Specialized;
|
||||
using System.Windows.Input;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Data;
|
||||
|
||||
namespace ClaudeDo.Ui.Views.Islands;
|
||||
|
||||
@@ -19,33 +17,12 @@ public partial class SessionTerminalView : UserControl
|
||||
AvaloniaProperty.Register<SessionTerminalView, bool>(nameof(IsDone));
|
||||
public static readonly StyledProperty<bool> IsFailedProperty =
|
||||
AvaloniaProperty.Register<SessionTerminalView, bool>(nameof(IsFailed));
|
||||
public static readonly StyledProperty<bool> IsComposerVisibleProperty =
|
||||
AvaloniaProperty.Register<SessionTerminalView, bool>(nameof(IsComposerVisible), defaultValue: false);
|
||||
public static readonly StyledProperty<string?> ComposerTextProperty =
|
||||
AvaloniaProperty.Register<SessionTerminalView, string?>(nameof(ComposerText), defaultBindingMode: BindingMode.TwoWay);
|
||||
public static readonly StyledProperty<ICommand?> SubmitCommandProperty =
|
||||
AvaloniaProperty.Register<SessionTerminalView, ICommand?>(nameof(SubmitCommand));
|
||||
public static readonly StyledProperty<ICommand?> InterruptCommandProperty =
|
||||
AvaloniaProperty.Register<SessionTerminalView, ICommand?>(nameof(InterruptCommand));
|
||||
public static readonly StyledProperty<string?> ComposerPlaceholderProperty =
|
||||
AvaloniaProperty.Register<SessionTerminalView, string?>(nameof(ComposerPlaceholder));
|
||||
public static readonly StyledProperty<System.Collections.IEnumerable?> QueuedMessagesProperty =
|
||||
AvaloniaProperty.Register<SessionTerminalView, System.Collections.IEnumerable?>(nameof(QueuedMessages));
|
||||
public static readonly StyledProperty<bool> HasQueuedMessagesProperty =
|
||||
AvaloniaProperty.Register<SessionTerminalView, bool>(nameof(HasQueuedMessages), defaultValue: false);
|
||||
|
||||
public IEnumerable? Entries { get => GetValue(EntriesProperty); set => SetValue(EntriesProperty, value); }
|
||||
public string? Label { get => GetValue(LabelProperty); set => SetValue(LabelProperty, value); }
|
||||
public bool IsRunning { get => GetValue(IsRunningProperty); set => SetValue(IsRunningProperty, value); }
|
||||
public bool IsDone { get => GetValue(IsDoneProperty); set => SetValue(IsDoneProperty, value); }
|
||||
public bool IsFailed { get => GetValue(IsFailedProperty); set => SetValue(IsFailedProperty, value); }
|
||||
public bool IsComposerVisible { get => GetValue(IsComposerVisibleProperty); set => SetValue(IsComposerVisibleProperty, value); }
|
||||
public string? ComposerText { get => GetValue(ComposerTextProperty); set => SetValue(ComposerTextProperty, value); }
|
||||
public ICommand? SubmitCommand { get => GetValue(SubmitCommandProperty); set => SetValue(SubmitCommandProperty, value); }
|
||||
public ICommand? InterruptCommand { get => GetValue(InterruptCommandProperty); set => SetValue(InterruptCommandProperty, value); }
|
||||
public string? ComposerPlaceholder { get => GetValue(ComposerPlaceholderProperty); set => SetValue(ComposerPlaceholderProperty, value); }
|
||||
public System.Collections.IEnumerable? QueuedMessages { get => GetValue(QueuedMessagesProperty); set => SetValue(QueuedMessagesProperty, value); }
|
||||
public bool HasQueuedMessages { get => GetValue(HasQueuedMessagesProperty); set => SetValue(HasQueuedMessagesProperty, value); }
|
||||
|
||||
private INotifyCollectionChanged? _subscribedCollection;
|
||||
|
||||
|
||||
@@ -49,8 +49,6 @@
|
||||
<MenuItem Header="{loc:Tr tasks.ctxMarkCancelled}" Tag="Cancelled" Click="OnSetStatusClick"/>
|
||||
</MenuItem>
|
||||
<Separator/>
|
||||
<MenuItem Header="{loc:Tr tasks.ctxRunInteractively}"
|
||||
Click="OnRunInteractivelyClick"/>
|
||||
<MenuItem Header="{loc:Tr tasks.ctxOpenConPtySession}"
|
||||
Click="OnOpenConPtySessionClick"/>
|
||||
<MenuItem Header="{loc:Tr tasks.ctxPickUpInTerminal}"
|
||||
|
||||
@@ -61,12 +61,6 @@ public partial class TaskRowView : UserControl
|
||||
await vm.OpenPlanningSessionCommand.ExecuteAsync(row);
|
||||
}
|
||||
|
||||
private async void OnRunInteractivelyClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is TaskRowViewModel row && FindTasksVm() is { } vm)
|
||||
await vm.RunInteractivelyCommand.ExecuteAsync(row);
|
||||
}
|
||||
|
||||
private void OnOpenConPtySessionClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is TaskRowViewModel row && FindTasksVm() is { } vm)
|
||||
|
||||
@@ -107,14 +107,7 @@
|
||||
Label="{Binding DisplayTitle}"
|
||||
IsRunning="{Binding IsRunning}"
|
||||
IsDone="{Binding IsDone}"
|
||||
IsFailed="{Binding IsFailed}"
|
||||
IsComposerVisible="{Binding IsInteractiveLive}"
|
||||
ComposerText="{Binding ComposerDraft, Mode=TwoWay}"
|
||||
SubmitCommand="{Binding SubmitComposerCommand}"
|
||||
InterruptCommand="{Binding InterruptInteractiveCommand}"
|
||||
ComposerPlaceholder="{loc:Tr session.composer.placeholder}"
|
||||
QueuedMessages="{Binding QueuedMessages}"
|
||||
HasQueuedMessages="{Binding HasQueuedMessages}" />
|
||||
IsFailed="{Binding IsFailed}" />
|
||||
|
||||
</DockPanel>
|
||||
</Border>
|
||||
|
||||
Reference in New Issue
Block a user