Show provider subagents in the subagents track (#2013)

* Add provider-owned subagent stream contract

* OpenCode provider: detect child sessions, attach adopted children to the hosting server, synthesize external turns

- Emit internal provider_child_session_detected / provider_session_deleted stream events from the global event stream (before the no-active-turn gate), including BFS hydration of existing descendants on subscribe
- Track child sessions in a registry mapping child session id -> hosting server url; resumeSession attaches via acquireExisting() so adopted children share the parent's server generation and keep receiving live events
- Synthesize external turns for adopted (externally driven) sessions so timeline, tool calls, and permission requests stream without a Paseo-initiated prompt
- Honor launchContext.env in resumeSession (acquireDedicated parity with createSession)
- Stop folding child tool parts into the parent's sub_agent action log; the parent row stays as a pointer

* Render provider subagents in workspace timelines

* fix provider subagent timeline reliability

* fix provider subagent lifecycle edge cases

* fix provider subagent event fidelity

* Fix provider subagent history fidelity

* Fix restored provider subagent state

* Fix provider subagent visibility edges

* Restore provider subagent timelines

* Page provider subagent history

* Forward subagent questions reliably

* fix(subagents): preserve OpenCode child context

* fix(subagents): harden provider timeline routing

* fix(subagents): preserve provider page continuity

* fix(subagents): retain live child prompts

---------

Co-authored-by: Omer <639682+omercnet@users.noreply.github.com>
This commit is contained in:
Mohamed Boudra
2026-07-13 03:13:00 +08:00
committed by GitHub
co-authored by Omer
parent 41d882859c
commit 66445adc07
46 changed files with 5056 additions and 337 deletions
+68 -31
View File
@@ -228,13 +228,20 @@ export interface AgentStreamViewHandle {
export interface AgentStreamViewProps {
agentId: string;
serverId?: string;
agent: AgentScreenAgent;
context: AgentScreenAgent;
streamItems: StreamItem[];
streamHead?: StreamItem[];
pendingPermissions: Map<string, PendingPermission>;
routeBottomAnchorRequest?: BottomAnchorRouteRequest | null;
isAuthoritativeHistoryReady?: boolean;
toast?: ToastApi | null;
onOpenWorkspaceFile?: (request: WorkspaceFileOpenRequest) => void;
readOnly?: boolean;
historyPagination?: {
hasOlder: boolean;
isLoadingOlder: boolean;
onLoadOlder: () => void;
};
}
const AGENT_CAPABILITY_FLAG_KEYS: (keyof AgentCapabilityFlags)[] = [
@@ -306,13 +313,16 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
{
agentId,
serverId,
agent,
context,
streamItems,
streamHead: providedStreamHead,
pendingPermissions,
routeBottomAnchorRequest = null,
isAuthoritativeHistoryReady = true,
toast,
onOpenWorkspaceFile,
readOnly = false,
historyPagination,
},
ref,
) {
@@ -337,27 +347,37 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
const setExplorerTabForCheckout = usePanelStore((state) => state.setExplorerTabForCheckout);
// Get serverId (fallback to agent's serverId if not provided)
const resolvedServerId = serverId ?? agent.serverId ?? "";
const resolvedServerId = serverId ?? context.serverId ?? "";
const client = useSessionStore((state) => state.sessions[resolvedServerId]?.client ?? null);
const streamHead = useSessionStore((state) =>
const sessionStreamHead = useSessionStore((state) =>
state.sessions[resolvedServerId]?.agentStreamHead?.get(agentId),
);
const streamHead = providedStreamHead ?? sessionStreamHead;
const supportsAgentForkContext = useSessionStore(
(state) => state.sessions[resolvedServerId]?.serverInfo?.features?.agentForkContext === true,
(state) =>
!readOnly &&
state.sessions[resolvedServerId]?.serverInfo?.features?.agentForkContext === true,
);
const workspaceRoot = agent.cwd?.trim() || "";
const workspaceRoot = context.cwd?.trim() || "";
const { requestDirectoryListing } = useFileExplorerActions({
serverId: resolvedServerId,
workspaceId: agent.workspaceId,
workspaceId: context.workspaceId,
workspaceRoot,
});
const { isLoadingOlder, hasOlder, loadOlder } = useLoadOlderAgentHistory({
const agentHistoryPagination = useLoadOlderAgentHistory({
serverId: resolvedServerId,
agentId,
toast,
});
const { isLoadingOlder, hasOlder, loadOlder } = historyPagination
? {
isLoadingOlder: historyPagination.isLoadingOlder,
hasOlder: historyPagination.hasOlder,
loadOlder: historyPagination.onLoadOlder,
}
: agentHistoryPagination;
// Keep entry/exit animations off on Android due to RN dispatchDraw crashes
// tracked in react-native-reanimated#8422.
const shouldDisableEntryExitAnimations = Platform.OS === "android";
@@ -379,7 +399,7 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
return;
}
const normalized = normalizeInlinePathTarget(target.path, agent.cwd);
const normalized = normalizeInlinePathTarget(target.path, context.cwd);
if (!normalized) {
return;
}
@@ -402,10 +422,10 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
return;
}
if (agent.workspaceId) {
if (context.workspaceId) {
navigateToPreparedWorkspaceTab({
serverId: resolvedServerId,
workspaceId: agent.workspaceId,
workspaceId: context.workspaceId,
target: createWorkspaceFileTabTarget(location),
});
}
@@ -419,8 +439,8 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
const checkout = {
serverId: resolvedServerId,
cwd: agent.cwd,
isGit: agent.projectPlacement?.checkout?.isGit ?? true,
cwd: context.cwd,
isGit: context.projectPlacement?.checkout?.isGit ?? true,
};
setExplorerTabForCheckout({ ...checkout, tab: "files" });
openFileExplorerForCheckout({
@@ -444,7 +464,7 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
if (!client) {
throw new Error(t("workspace.terminal.hostDisconnected"));
}
const draftSetup = buildForkDraftSetup(agent);
const draftSetup = buildForkDraftSetup(context);
const prepareForkDraft = async () => {
const draftId = generateDraftId();
const payload = await client.buildAgentForkContext(
@@ -466,7 +486,7 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
};
if (target === "tab") {
const workspaceId = agent.workspaceId;
const workspaceId = context.workspaceId;
if (!workspaceId) {
throw new Error(t("message.actions.forkMissingWorkspace"));
}
@@ -481,7 +501,7 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
const draftId = await prepareForkDraft();
const sourceDirectory =
agent.projectPlacement?.checkout?.cwd?.trim() || agent.cwd.trim() || undefined;
context.projectPlacement?.checkout?.cwd?.trim() || context.cwd.trim() || undefined;
if (draftSetup) {
useWorkspaceDraftSubmissionStore.getState().setDraftSetup({
draftId,
@@ -493,8 +513,8 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
buildNewWorkspaceRoute({
serverId: resolvedServerId,
sourceDirectory,
displayName: agent.projectPlacement?.projectName,
projectId: agent.projectPlacement?.projectKey,
displayName: context.projectPlacement?.projectName,
projectId: context.projectPlacement?.projectKey,
draftId,
}),
);
@@ -520,24 +540,24 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
const baseRenderModel = useMemo(() => {
return buildAgentStreamRenderModel({
agentStatus: agent.status,
agentStatus: context.status,
tail: effectiveStreamItems,
head: effectiveStreamHead ?? EMPTY_STREAM_HEAD,
platform: isWeb ? "web" : "native",
isMobileBreakpoint: isMobile,
});
}, [agent.status, isMobile, effectiveStreamHead, effectiveStreamItems]);
}, [context.status, isMobile, effectiveStreamHead, effectiveStreamItems]);
const streamLayout = useMemo(
() =>
layoutStream({
strategy: streamRenderStrategy,
agentStatus: agent.status,
agentStatus: context.status,
history: baseRenderModel.history,
liveHead: baseRenderModel.segments.liveHead,
timingByAssistantId: baseRenderModel.turnTiming.byAssistantId,
}),
[
agent.status,
context.status,
baseRenderModel.history,
baseRenderModel.segments.liveHead,
baseRenderModel.turnTiming.byAssistantId,
@@ -590,14 +610,14 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
images={item.images}
attachments={item.attachments}
timestamp={item.timestamp.getTime()}
capabilities={agent.capabilities}
capabilities={context.capabilities}
client={client}
isFirstInGroup={layoutItem.isFirstInUserGroup}
isLastInGroup={layoutItem.isLastInUserGroup}
/>
);
},
[agent.capabilities, agentId, client, resolvedServerId],
[context.capabilities, agentId, client, resolvedServerId],
);
const renderAssistantMessageItem = useCallback(
@@ -668,7 +688,7 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
error={data.error}
status={data.status}
detail={data.detail}
cwd={agent.cwd}
cwd={context.cwd}
metadata={data.metadata}
isLastInSequence={layoutItem.isLastInToolSequence}
onOpenFilePath={handleToolCallOpenFile}
@@ -690,7 +710,7 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
/>
);
},
[agent.cwd, setInlineDetailsExpanded, handleToolCallOpenFile],
[context.cwd, setInlineDetailsExpanded, handleToolCallOpenFile],
);
const renderStreamItemContent = useCallback(
@@ -747,10 +767,10 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
content,
layoutItem,
strategy: streamRenderStrategy,
onForkAssistantTurn: handleForkAssistantTurn,
onForkAssistantTurn: readOnly ? undefined : handleForkAssistantTurn,
});
},
[handleForkAssistantTurn, renderStreamItemContent, streamRenderStrategy],
[handleForkAssistantTurn, readOnly, renderStreamItemContent, streamRenderStrategy],
);
const pendingPermissionItems = useMemo(
@@ -758,7 +778,7 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
[pendingPermissions, agentId],
);
const showRunningTurnFooter = agent.status === "running";
const showRunningTurnFooter = context.status === "running";
const pendingPermissionsNode = useMemo(
() =>
renderPendingPermissionsNode({
@@ -775,11 +795,12 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
inFlightTurnStartedAt={baseRenderModel.turnTiming.runningStartedAt}
host={bottomTurnFooterHost}
strategy={streamRenderStrategy}
onForkAssistantTurn={handleForkAssistantTurn}
onForkAssistantTurn={readOnly ? undefined : handleForkAssistantTurn}
/>
) : null,
[
handleForkAssistantTurn,
readOnly,
showRunningTurnFooter,
baseRenderModel.turnTiming.runningStartedAt,
bottomTurnFooterHost,
@@ -1004,6 +1025,17 @@ function bottomAnchorRouteRequestsEqual(
);
}
function historyPaginationPropsEqual(
left: AgentStreamViewProps["historyPagination"],
right: AgentStreamViewProps["historyPagination"],
): boolean {
return (
left?.hasOlder === right?.hasOlder &&
left?.isLoadingOlder === right?.isLoadingOlder &&
left?.onLoadOlder === right?.onLoadOlder
);
}
function agentStreamViewPropsEqual(
left: AgentStreamViewProps,
right: AgentStreamViewProps,
@@ -1011,8 +1043,9 @@ function agentStreamViewPropsEqual(
const reasons: string[] = [];
if (left.agentId !== right.agentId) reasons.push("agentId");
if (left.serverId !== right.serverId) reasons.push("serverId");
reasons.push(...collectAgentScreenAgentDiffs(left.agent, right.agent));
reasons.push(...collectAgentScreenAgentDiffs(left.context, right.context));
if (left.streamItems !== right.streamItems) reasons.push("streamItems");
if (left.streamHead !== right.streamHead) reasons.push("streamHead");
if (left.pendingPermissions !== right.pendingPermissions) reasons.push("pendingPermissions");
if (
!bottomAnchorRouteRequestsEqual(left.routeBottomAnchorRequest, right.routeBottomAnchorRequest)
@@ -1024,6 +1057,10 @@ function agentStreamViewPropsEqual(
}
if (left.toast !== right.toast) reasons.push("toast");
if (left.onOpenWorkspaceFile !== right.onOpenWorkspaceFile) reasons.push("onOpenWorkspaceFile");
if (left.readOnly !== right.readOnly) reasons.push("readOnly");
if (!historyPaginationPropsEqual(left.historyPagination, right.historyPagination)) {
reasons.push("historyPagination");
}
recordRenderProfileReasons(`AgentStreamView:${right.agentId}`, reasons);
return reasons.length === 0;
}