Correct the spec's binding approach (library LaunchProcess, not the abandoned
custom-pty bypass), add the monospace-font sizing gotcha, on-demand worktree +
prompt seeding, and the deferred Avalonia 12.1 upgrade. Add a ConPTY manual-
verification entry to open.md.
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.
A fresh (non-resume) task-based ConPTY session now opens claude on the task's
prompt (title + description) as the positional argument, so the session starts on
the task instead of an empty prompt. Resume sessions and ad-hoc sessions are
unchanged.
The control derives Cols/Rows from arranged-size / character-cell-size; without an
explicit monospace font (as the working spike set) the cell metrics are off and the
terminal miscomputes its size, so the child TUI renders into the wrong area. Match
the spike's font/BufferSize and stretch to fill the pane.
The custom Porta.Pty bypass (own read loop, key tunneling, resize sync) rendered
wrong, lagged, and dropped input. The spike proved TerminalControl.LaunchProcess()
renders correctly and stays responsive, so hand pty/input/render/resize/focus back
to the library. PtyTerminalSession shrinks to a thin wrapper: apply descriptor.Env
process-wide (Porta.Pty inherits the process env; no per-launch env seam), set
Process/Args/StartingDirectory, LaunchProcess(). Process="" still suppresses the
control's auto-launch so exactly one process starts.
Sizing: the pty was spawned at the stale 80x24 default because terminal.Cols/Rows
were read before any layout pass; and a resize during the spawn await was missed.
Force UpdateLayout() before reading the size, subscribe Resized before spawn,
resync once after, and add a LayoutUpdated-driven resync (deduped) as a safety net.
Lag: the read loop awaited a UI-thread dispatch per chunk, serializing pipe reads
behind rendering; switch to Dispatcher.Post (FIFO preserved, no backpressure).
TerminalView's OnKeyDown/OnTextInput early-return when its private pty connection
is null -- which it always is, since we bypass LaunchProcess() to inject a custom
env -- so keystrokes were silently dropped, and Terminal.DataReceived only carries
terminal auto-replies, never user input. Tunnel KeyDown/TextInput on the control,
translate via the terminal's public GenerateKeyInput/GenerateCharInput, and write
to our own IPtyConnection. Focus the control on start (LaunchProcess would have).
Adds a 'New session' header button in Mission Control that opens a folder picker
and starts a task-less embedded ConPTY session in the chosen directory. ConPtyPaneViewModel
TaskId is now nullable (ad-hoc panes have no task and are never deduped) with a
CreateAdHoc factory; the view does the picking, the VM stays picker-agnostic.
BuildForTaskAsync now creates a worktree on demand (via WorktreeManager.CreateAsync,
the same path TaskRunner uses) when a task has a configured working dir but no
Active/Kept worktree, returning a fresh-start spec -- so never-run tasks can be
opened interactively. Adds BuildForDirectoryAsync + GetAdHocLaunchSpec hub/client
for ad-hoc sessions in an arbitrary directory (no task, no worktree, no skill seeding).
Adds an 'Open ConPTY session' entry that fetches a task's launch spec and hosts
an embedded ConPTY terminal as a Mission Control pane, coexisting with the
streamed-log monitor panes (streaming stack untouched). Introduces IMissionControlPane
+ ConPtyPaneViewModel, a non-destructive Panes mirror (Monitors prefix + ConPtySessions
suffix) so unrelated monitor churn never tears down a live terminal, and a grid<->tabs
layout toggle. Launch failures surface via the footer error strip.
Adds a self-contained terminal host: PtyTerminalSession drives a Porta.Pty child
directly (custom env) and pumps it through Iciclecreek's XTerm.NET renderer via
TerminalControl.Terminal, bypassing LaunchProcess() so a fully-populated
environment can be passed. InteractiveTerminalView/ViewModel host it with an
order-independent attach/start. Adds Iciclecreek.Avalonia.Terminal 2.0.3.
Sets Process="" to suppress the control's auto-launch of a stray shell.
Adds InteractiveLaunchSpecService + GetInteractiveLaunchSpec hub method that
prepares a task worktree (session-skills seeding, run env) and returns a
LaunchSpec {cwd,exe,args,env} for a UI-hosted ConPTY terminal. Reuses
ISessionSkillSeeder, TaskRunner.UnionSkillNames, and WindowsTerminalLauncher
resume-arg/resolve logic. Guards mirror ResumeTaskInTerminal; a never-run task
yields a fresh-start spec instead of an error.
Interactive/streaming sessions are persistent claude.exe processes that
wait on stdin and never exit on their own. The only teardown was an
explicit StopInteractiveSession from the UI — there is no client-disconnect
or shutdown sweep — so an abandoned chat (UI closed, navigated away,
crashed) kept its claude.exe (+ conhost) alive for the worker's whole
lifetime. Under a long-running autostart worker these accumulate to dozens
of orphaned child processes.
LiveSessionRegistry now tracks per-session activity (Touch on every output
line and user action) and exposes ReapIdleAsync, which stops sessions idle
past a timeout while skipping any with a turn in flight. IdleSessionReaper
(BackgroundService) sweeps every 5 min; idle timeout defaults to 30 min,
configurable via interactive_idle_timeout_minutes (0 disables).
Spawned claude processes were only torn down on graceful cancellation
(Process.Kill(entireProcessTree)). A hard worker death — Task Manager
End Task, crash, OS restart, installer update — ran no cleanup, orphaning
the claude->node->conhost tree, which lingered and piled up across
restarts.
Assign every spawned claude process to a Windows Job Object with
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE. The worker holds the only handle, so
when it terminates for any reason the OS tears down the whole job. No-op
on non-Windows; best-effort with a one-time warning if the Win32 calls
fail.
Queued rows are now QueuedMessageViewModel (Text + RemoveCommand); each shows a
✕ (Icon.WinClose) that calls RemoveQueuedInteractiveMessageAsync(taskId, text).
The worker re-broadcasts the queue, rebuilding the strip without the removed
message. Adds session.composer.unqueue (en/de).
StreamingClaudeSession.RemoveQueuedAsync drops the first occurrence of a queued
message from _pending and re-broadcasts the updated queue. Wired through
InteractiveSessionService + WorkerHub.RemoveQueuedInteractiveMessage +
IWorkerClient.RemoveQueuedInteractiveMessageAsync. Removal by text (first match)
is robust to a turn flushing mid-click. Fakes + ILiveSession impls updated.
A queued message now appears in a pending strip above the input box (driven by
InteractiveQueueChanged), not optimistically in the transcript. The transcript
user line is added on delivery via InteractiveMessageSent. SessionTerminalView
gains QueuedMessages/HasQueuedMessages styled props (Mission Control); WorkConsole
binds Monitor.* (task detail). Adds session.composer.queued (en/de).
StreamingClaudeSession raises onQueueChanged (pending snapshot) and onUserMessageSent
(on delivery, incl. the seeded first prompt); InteractiveSessionService forwards these
as InteractiveQueueChanged/InteractiveMessageSent broadcasts. Lets the UI show queued
messages above the input and move a message into the transcript only when actually
delivered to Claude. Client events + fakes updated.
LogKindForegroundConverter drives the log message foreground via a local
binding (beats the dim local value), so user messages render in the accent
color instead of vanishing into the transcript. Adds a small stop (Icon.Stop)
button next to Send in both composers (SessionTerminalView + WorkConsole) wired
to InterruptInteractiveCommand → InterruptInteractiveSessionAsync. Adds
session.composer.interrupt (en/de).
StreamingClaudeSession now buffers a mid-turn user message in a FIFO queue and
flushes one when the turn's result arrives (no implicit interrupt). InterruptAsync
only writes the control_request (no-op when idle); the resulting turn-end then
flushes any queued message. New InteractiveSessionService.InterruptAsync +
WorkerHub.InterruptInteractiveSession + IWorkerClient.InterruptInteractiveSessionAsync.
SessionTerminalView gains an opt-in composer (IsComposerVisible / ComposerText /
SubmitCommand / ComposerPlaceholder styled props); Mission Control binds it to the
monitor VM. Task detail's WorkConsole output tab gets a matching shell-prompt
composer bound through Monitor.*, shown only while an interactive session is live.
log-user lines render in the accent color. Adds session.composer.* (en/de).
TaskMonitorViewModel gains IsInteractiveLive + ComposerDraft + SubmitComposer
(optimistic LogKind.User echo, then SendInteractiveMessageAsync) + StopInteractive,
driven by the InteractiveSessionStarted/Ended events. Since DetailsIslandViewModel
embeds this monitor, both task detail and Mission Control get the composer. Mission
Control auto-creates a monitor on InteractiveSessionStarted. Adds LogKind.User.
Adds SendInteractiveMessageAsync/StopInteractiveSessionAsync and the
InteractiveSessionStarted/Ended events to IWorkerClient + WorkerClient
(UI-thread dispatch mirroring TaskQuestionAsked). Updates the IWorkerClient
fakes in both test projects.
InteractiveSessionService resolves a task's list working dir + seeded prompt,
spawns a StreamingClaudeSession (claude stream-json in the list dir, model+auto
as before), registers it in LiveSessionRegistry, streams output over TaskMessage,
and broadcasts InteractiveSessionStarted/Ended (an exit watcher fires Ended). The
hub's OpenInteractiveTerminalAsync now starts this in-app session; SendInteractiveMessage
and StopInteractiveSession route to it. The external Windows-Terminal interactive
launch (LaunchInteractiveAsync / InteractiveLaunchContext / OpenInteractiveAsync) is
removed; planning sessions keep their terminal launch.
StreamingClaudeSession drives claude --input-format stream-json over a kept-
open stdin: sends user messages, interrupts the in-flight turn via the verified
control_request protocol, and tracks turn state from result events (treating an
interrupt-aborted error_during_execution result as turn-ended). IClaudeStreamTransport
abstracts the process I/O so it is unit-tested with a fake (no real claude).
LiveSessionRegistry maps taskId -> live session for the hub to route into.
Backs the upcoming in-app interactive sessions; autonomous task execution untouched.
Replace the external wt.exe 'Run interactively' launch with an in-app
streaming chat (persistent claude --input-format stream-json), rendered in
the shared SessionTerminalView in task detail and Mission Control. Autonomous
task execution is untouched. Mid-turn interrupt+redirect verified against CLI
2.1.191 via spike.
TaskMonitorViewModel surfaces a pending AskUser question (TaskQuestionAsked /
TaskQuestionResolved events) with an AnswerDraft + SubmitAnswerCommand that calls
the new IWorkerClient.AnswerTaskQuestionAsync; MonitorPaneView shows an accent
question banner with an input box above the terminal. Pending question is cleared
on answer/resolve/finish and re-hydrated on attach via GetPendingQuestionAsync.
en/de localization for missionControl.question.*; test fakes updated.
A running task can call mcp__claudedo_run__AskUser(question) to block (up to 3
min) on a human answer. PendingQuestionRegistry holds the pending question +
TaskCompletionSource; the tool broadcasts TaskQuestionAsked, awaits the answer
(WorkerHub.AnswerTaskQuestion resolves it), and returns it as the tool result —
or a 'proceed on your judgment' fallback on timeout. The run stays Running
throughout (no status/schema change). ClaudeProcess raises MCP_TOOL_TIMEOUT so
the 60s HTTP-MCP cap doesn't kill the wait; the run MCP is now wired for every
task, not just standalone ones. System prompt updated to reconcile 'unattended'.