Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions apps/mobile/src/features/threads/ThreadDetailScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,8 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread
});
const agentsSegment = resolveSubagentPillSegment(turnSubagents);
const composerEditorRef = useRef<ComposerEditorHandle>(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;
Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -1230,7 +1234,7 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread
: undefined
}
>
{isProviderNativeSubagentThread(props.selectedThread.source) ? (
{isProviderSubagent ? (
<View
className="self-center px-3 pt-1.5"
style={{
Expand Down
10 changes: 7 additions & 3 deletions apps/mobile/src/features/threads/ThreadFeed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,8 @@ export interface ThreadFeedProps {
readonly setupWorkingStartedAt?: string | null;
readonly queuedMessages: ReadonlyArray<QueuedThreadMessage>;
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;
Expand Down Expand Up @@ -1771,7 +1772,8 @@ function renderFeedEntry(
<Text className="font-t3-medium text-xs tabular-nums text-foreground-secondary">
{entry.pendingMessage && !entry.acknowledged ? "Pending" : timestampLabel}
</Text>
{entry.pendingMessage &&
{props.onEditPendingMessage !== null &&
entry.pendingMessage &&
!entry.acknowledged &&
!entry.pendingMessage.creation &&
entry.pendingMessage.messageId !== props.dispatchingMessageId ? (
Expand All @@ -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);
}
}}
>
<SymbolView name="pencil" size={14} tintColor={iconSubtleColor} />
Expand Down
10 changes: 3 additions & 7 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

馃煚 High components/ChatView.tsx:4046

Provider-native child threads with persisted pending approvals or user inputs become permanently unresponsive: composerMounted is false, so ChatComposer鈥攖he only renderer for those controls鈥攊s removed and the subagent remains blocked. This affects in-flight subagents across reconnects or deployment because changing future server routing does not migrate existing pendingApprovals/pendingUserInputs; keep the response controls mounted for existing requests or render them in the provider subagent bar.

馃殌 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/components/ChatView.tsx around line 4046:

Provider-native child threads with persisted pending approvals or user inputs become permanently unresponsive: `composerMounted` is `false`, so `ChatComposer`鈥攖he only renderer for those controls鈥攊s removed and the subagent remains blocked. This affects in-flight subagents across reconnects or deployment because changing future server routing does not migrate existing `pendingApprovals`/`pendingUserInputs`; keep the response controls mounted for existing requests or render them in the provider subagent bar.

const providerSubagentModels = selectedProviderEntry?.models ?? EMPTY_PROVIDER_MODELS;
const providerSubagentCatalogModel = providerSubagentModels.find(
(model) => model.slug === activeThread?.modelSelection.model,
Expand Down Expand Up @@ -10683,7 +10680,6 @@ export default function ChatView(props: ChatViewProps) {
) : null}
{!composerMounted ? null : (
<ChatComposer
hideThreadSettings={isProviderSubagent}
multipleModelSelections={multipleModelSelections}
supportsMultipleModels={
serverConfig?.environment.capabilities.requiredWorktreeBootstrap ===
Expand Down
18 changes: 2 additions & 16 deletions apps/web/src/components/chat/ChatComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,6 @@ import {
import { useComposerPathSearch } from "../../lib/composerPathSearchState";
import { replaceComposerContextReferences } from "@t3tools/shared/composerContextReferences";
import {
composerModelPickerCanStayOpen,
getRestingComposerImagePreviewCounts,
resolveRestingComposerControlsLayout,
shouldAnimateComposerRestingTransition,
Expand Down Expand Up @@ -1512,11 +1511,6 @@ export interface ChatComposerProps {
isLocalDraftThread: boolean;
forceExpandedOnMobile: boolean;
projectSelectionRequired: boolean;
/**
* The thread's model, effort, and access belong to its provider (a native
* subagent answering a question): hide those pickers and attachments.
*/
hideThreadSettings?: boolean;

// Session phase
phase: SessionPhase;
Expand Down Expand Up @@ -1691,7 +1685,6 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
isLocalDraftThread: _isLocalDraftThread,
forceExpandedOnMobile,
projectSelectionRequired,
hideThreadSettings = false,
phase,
isConnecting,
isSendBusy,
Expand Down Expand Up @@ -2761,7 +2754,6 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
const showCollapsedMobilePromptRow =
isComposerCollapsedMobile && !isComposerApprovalState && pendingUserInputs.length === 0;
const showComposerAttachAction =
!hideThreadSettings &&
fileStagingLimit !== null &&
(!activePendingProgress ||
(supportsQuestionAttachments &&
Expand Down Expand Up @@ -5066,13 +5058,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
const composerControlsVisibleInStrip =
composerControlsCollapsed && restingControlsHost !== null && restingControlsVisible;
const composerControlsHidden = composerControlsCollapsed && !restingControlsVisible;
if (
isComposerModelPickerOpen &&
!composerModelPickerCanStayOpen({
controlsHidden: composerControlsHidden,
threadSettingsHidden: hideThreadSettings,
})
) {
if (composerControlsHidden && isComposerModelPickerOpen) {
setIsComposerModelPickerOpen(false);
}
useLayoutEffect(() => {
Expand Down Expand Up @@ -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 ? (
<ComposerControl
type="button"
disabled={!providerSetupInstanceId}
Expand Down
15 changes: 0 additions & 15 deletions apps/web/src/components/composerFooterLayout.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
COMPOSER_FOOTER_WIDE_ACTIONS_COMPACT_BREAKPOINT_PX,
COMPOSER_RESTING_EXPANSION_MIN_PX,
getRestingComposerImagePreviewCounts,
composerModelPickerCanStayOpen,
overlayComposerIsResting,
resolveComposerTimelineInset,
resolveScrollToEndClearance,
Expand Down Expand Up @@ -79,20 +78,6 @@ describe("shouldUseCompactComposerPrimaryActions", () => {
});
});

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
Expand Down
13 changes: 0 additions & 13 deletions apps/web/src/components/composerFooterLayout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
3 changes: 2 additions & 1 deletion docs/user/thread-sidebar.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading