Make parent-owned sub-agent threads read-only in the TUI (#33841)

## Why

Multi-agent V2 spawned threads are controlled by their parent and reject direct
app-server input. The TUI should expose that ownership instead of presenting an
input flow that the server will reject.

## What changed

- Add the experimental `Thread.canAcceptDirectInput` capability for loaded
  threads and populate it from the same policy used to validate `turn/start`
  and `turn/steer`. Stored, unloaded threads report `null`.
- Carry the capability through TUI thread discovery, selection, startup, and
  resume flows, with a compatibility fallback for older app servers.
- Present parent-owned threads as view-only, preserve drafts and queued input,
  block agent-directed commands and settings shortcuts, and keep local
  navigation and inspection commands available.

## Testing

- Cover V1 writable and V2 view-only spawned threads through selection and
  resume.
- Cover composer submission paths, queued and initial input preservation,
  allowed navigation commands, settings shortcuts, and the view-only UI.

GitOrigin-RevId: 495ad051249e5577fb0b0c1cc8c40a0998d04918
This commit is contained in:
Felipe Coury authored and copyberry committed 2026-07-17 15:14:47 +00:00
1 parent 315195492c
commit 3f0669dbd6
37 files changed
+781 -99

No files matched your search

+12
View File
@@ -202,6 +202,8 @@ const CONNECTORS_SELECTION_VIEW_ID: &str = "connectors-selection";
const PET_SELECTION_LOADING_VIEW_ID: &str = "pet-selection-loading";
const AMBIENT_PET_WRAP_GAP_COLUMNS: u16 = 2;
const TUI_STUB_MESSAGE: &str = "Not available in TUI yet.";
const PARENT_OWNED_INPUT_MESSAGE: &str =
"This sub-agent is controlled by its parent. Direct input is disabled.";
/// Choose the keybinding used to edit the most-recently queued message.
///
@@ -650,6 +652,7 @@ pub(crate) struct ChatWidget {
thread_name: Option<String>,
thread_rename_block_message: Option<String>,
active_side_conversation: bool,
blocks_direct_input: bool,
normal_placeholder_text: String,
side_placeholder_text: String,
forked_from: Option<ThreadId>,
@@ -1732,6 +1735,15 @@ impl ChatWidget {
T: Into<AppCommand>,
{
let op: AppCommand = op.into();
if self.blocks_direct_input
&& matches!(
&op,
AppCommand::UserTurn { .. } | AppCommand::Review { .. } | AppCommand::Compact
)
{
self.add_error_message(PARENT_OWNED_INPUT_MESSAGE.to_string());
return false;
}
self.prepare_local_op_submission(&op);
if op.is_review() && !self.bottom_pane.is_task_running() {
self.bottom_pane.set_task_running(/*running*/ true);