diff --git a/apps/mobile/src/features/threads/ThreadDetailScreen.tsx b/apps/mobile/src/features/threads/ThreadDetailScreen.tsx index defcc19f15a6..78febaf0fc73 100644 --- a/apps/mobile/src/features/threads/ThreadDetailScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadDetailScreen.tsx @@ -363,6 +363,8 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread }); const agentsSegment = resolveSubagentPillSegment(turnSubagents); const composerEditorRef = useRef(null); + // A provider-native subagent shows status instead of a composer. + const isProviderSubagent = isProviderNativeSubagentThread(props.selectedThread.source); // Entering edit mode from the queue sheet should land in a ready composer, // not require a second tap on a composer already holding the message. const editingRunId = props.queuedRunEdit?.runId ?? null; @@ -1042,7 +1044,9 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread setupWorkingStartedAt={props.setupWorkingStartedAt} queuedMessages={props.queuedMessages} dispatchingMessageId={props.dispatchingMessageId} - onEditPendingMessage={handleEditPendingMessage} + // A native subagent has no composer to edit a pending message in; + // Cancel on the edit banner would discard it. + onEditPendingMessage={isProviderSubagent ? null : handleEditPendingMessage} contentPresentation={props.contentPresentation} agentLabel={agentLabel} threadTitle={props.selectedThread.title} @@ -1230,7 +1234,7 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread : undefined } > - {isProviderNativeSubagentThread(props.selectedThread.source) ? ( + {isProviderSubagent ? ( ; readonly dispatchingMessageId: MessageId | null; - readonly onEditPendingMessage: (message: QueuedThreadMessage) => void; + /** Null where a pending message cannot be edited (no composer to edit it in). */ + readonly onEditPendingMessage: ((message: QueuedThreadMessage) => void) | null; readonly environmentId: EnvironmentId; readonly threadId: ThreadId; readonly threadTitle: string; @@ -1771,7 +1772,8 @@ function renderFeedEntry( {entry.pendingMessage && !entry.acknowledged ? "Pending" : timestampLabel} - {entry.pendingMessage && + {props.onEditPendingMessage !== null && + entry.pendingMessage && !entry.acknowledged && !entry.pendingMessage.creation && entry.pendingMessage.messageId !== props.dispatchingMessageId ? ( @@ -1781,7 +1783,9 @@ function renderFeedEntry( hitSlop={8} className="size-7 items-center justify-center" onPress={() => { - if (entry.pendingMessage) props.onEditPendingMessage(entry.pendingMessage); + if (entry.pendingMessage && props.onEditPendingMessage) { + props.onEditPendingMessage(entry.pendingMessage); + } }} > diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index d346a8bf9706..1b72043838f7 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -4040,13 +4040,10 @@ export default function ChatView(props: ChatViewProps) { // can measure whether its relocated controls fit. The visible chrome remains // content-driven: Git/environment context or controls that actually fit. // A provider-native subagent cannot take messages: a status bar replaces the - // composer and its strips. While the subagent asks for an approval or an - // answer, the composer that renders those stays below the bar, without the - // thread settings that belong to the provider. + // composer and its strips. Its approvals and questions are asked on the + // top-level parent thread. const showProviderSubagentBar = isProviderSubagent; - const providerSubagentNeedsResponse = - isProviderSubagent && (pendingApprovals.length > 0 || pendingUserInputs.length > 0); - const composerMounted = !showProviderSubagentBar || providerSubagentNeedsResponse; + const composerMounted = !showProviderSubagentBar; const providerSubagentModels = selectedProviderEntry?.models ?? EMPTY_PROVIDER_MODELS; const providerSubagentCatalogModel = providerSubagentModels.find( (model) => model.slug === activeThread?.modelSelection.model, @@ -10683,7 +10680,6 @@ export default function ChatView(props: ChatViewProps) { ) : null} {!composerMounted ? null : ( { @@ -5318,7 +5304,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) const hiddenRestingBlockIds = restingBlockDefs .slice(restingBlockDefs.length - restingHiddenBlockCount) .map((def) => def.id); - const composerControls = hideThreadSettings ? null : showProviderUnavailable ? ( + const composerControls = showProviderUnavailable ? ( { }); }); -describe("composerModelPickerCanStayOpen", () => { - it("closes the picker while a native subagent hides the thread settings", () => { - expect( - composerModelPickerCanStayOpen({ controlsHidden: false, threadSettingsHidden: true }), - ).toBe(false); - expect( - composerModelPickerCanStayOpen({ controlsHidden: true, threadSettingsHidden: false }), - ).toBe(false); - expect( - composerModelPickerCanStayOpen({ controlsHidden: false, threadSettingsHidden: false }), - ).toBe(true); - }); -}); - describe("overlayComposerIsResting", () => { it("drops a resting reservation once a status bar replaces the composer", () => { // The composer rested on a scroll, then the thread swapped it for the diff --git a/apps/web/src/components/composerFooterLayout.ts b/apps/web/src/components/composerFooterLayout.ts index 2eb7a9550a38..48297caf59af 100644 --- a/apps/web/src/components/composerFooterLayout.ts +++ b/apps/web/src/components/composerFooterLayout.ts @@ -106,19 +106,6 @@ export function overlayComposerIsResting(input: { return input.composerMounted && input.composerReportedResting; } -/** - * The model picker lives in the composer controls. When they are hidden (a - * collapsed composer, or a native subagent whose settings belong to its - * provider) the picker cannot stay open, or a shortcut that opened it would - * pop it up later on another thread. - */ -export function composerModelPickerCanStayOpen(input: { - controlsHidden: boolean; - threadSettingsHidden: boolean; -}): boolean { - return !input.controlsHidden && !input.threadSettingsHidden; -} - export function shouldAnimateComposerRestingTransition(input: { hasCompletedInitialLayout: boolean; stateChanged: boolean; diff --git a/docs/user/thread-sidebar.md b/docs/user/thread-sidebar.md index 369e85493d73..46524d9878f0 100644 --- a/docs/user/thread-sidebar.md +++ b/docs/user/thread-sidebar.md @@ -167,7 +167,8 @@ retry and the normal snooze choices. On web and desktop, use **Agents** to follow work delegated to subagents. Subagent threads started by the agent can't take messages; message the parent -thread instead. +thread instead. When such a subagent needs an approval or an answer, the parent +thread asks for it. Expand a tool call in the conversation to see its full command and output. Summaries shorten shell wrappers and can still describe the latest call after it