feat(opencode): add subagent timeline support (#658)

Map OpenCode `task` tool events into the existing sub_agent timeline shape,
enabling live activity tracking for OpenCode subagent sessions.

- Add `childSessionId` (optional) to sub_agent schema
- Make `actions` optional with default [] for backward compatibility
- Parse task tool input/output into sub_agent detail (tool-call-detail-parser)
- Track child session linkage and buffer/flush child tool parts
- Render sub_agent actions and session IDs in the app component
- Add glob tool detail mapping
- Include backward-compat schema test

Co-authored-by: Ryan Swift <ryan@mlh.io>
This commit is contained in:
Mohamed Boudra
2026-05-02 13:19:42 +08:00
committed by GitHub
co-authored by Ryan Swift
parent a64be66353
commit fdc510d3a0
8 changed files with 718 additions and 16 deletions
@@ -244,15 +244,95 @@ function resolveSubAgentFallbackHeader(
interface SubAgentDetailProps {
log: string;
actions: Extract<ToolCallDetail, { type: "sub_agent" }>["actions"];
childSessionId: string | null | undefined;
subAgentType: string | null | undefined;
description: string | null | undefined;
ds: DetailStyles;
}
function SubAgentDetailSection({ log, subAgentType, description, ds }: SubAgentDetailProps) {
const activityLog = log.replace(/^\n+/, "");
const hasLog = activityLog.length > 0;
function buildSubAgentActionLog(actions: SubAgentDetailProps["actions"]): string {
return actions
.map((action) =>
action.summary ? `[${action.toolName}] ${action.summary}` : `[${action.toolName}]`,
)
.join("\n");
}
function stripSubAgentActionLog(log: string, actions: SubAgentDetailProps["actions"]): string {
const actionLog = buildSubAgentActionLog(actions);
const trimmedLog = log.replace(/^\n+/, "");
if (!actionLog || !trimmedLog.startsWith(actionLog)) {
return trimmedLog;
}
return trimmedLog.slice(actionLog.length).replace(/^\n+/, "");
}
function SubAgentActionRow({ action }: { action: SubAgentDetailProps["actions"][number] }) {
return (
<View style={styles.subAgentActionRow}>
<Text selectable style={styles.subAgentActionTool}>
{formatSubAgentToolName(action.toolName)}
</Text>
{action.summary ? (
<Text selectable style={styles.subAgentActionSummary}>
{action.summary}
</Text>
) : null}
</View>
);
}
function formatSubAgentToolName(toolName: string): string {
const trimmed = toolName.trim();
if (!trimmed) {
return toolName;
}
return trimmed
.replace(/[._-]+/g, " ")
.split(" ")
.filter((segment) => segment.length > 0)
.map((segment) => `${segment[0]?.toUpperCase() ?? ""}${segment.slice(1)}`)
.join(" ");
}
function SubAgentLogText({
activityLog,
fallbackHeader,
hasActions,
}: {
activityLog: string;
fallbackHeader: string;
hasActions: boolean;
}) {
if (activityLog.length > 0) {
return (
<Text selectable style={styles.scrollText}>
{activityLog}
</Text>
);
}
if (!hasActions) {
return (
<Text selectable style={styles.scrollText}>
{fallbackHeader}
</Text>
);
}
return null;
}
function SubAgentDetailSection({
log,
actions,
childSessionId,
subAgentType,
description,
ds,
}: SubAgentDetailProps) {
const activityLog = stripSubAgentActionLog(log, actions);
const fallbackHeader = resolveSubAgentFallbackHeader(subAgentType, description);
const hasActions = actions.length > 0;
return (
<View style={ds.sectionFillStyle}>
<View style={ds.codeBlockFillStyle}>
@@ -270,9 +350,23 @@ function SubAgentDetailSection({ log, subAgentType, description, ds }: SubAgentD
contentContainerStyle={styles.codeHorizontalContent}
>
<View style={styles.codeLine}>
<Text selectable style={styles.scrollText}>
{hasLog ? activityLog : fallbackHeader}
</Text>
{childSessionId ? (
<Text selectable style={styles.subAgentSessionText}>
session {childSessionId}
</Text>
) : null}
{hasActions ? (
<View style={styles.subAgentActions}>
{actions.map((action) => (
<SubAgentActionRow key={action.index} action={action} />
))}
</View>
) : null}
<SubAgentLogText
activityLog={activityLog}
fallbackHeader={fallbackHeader}
hasActions={hasActions}
/>
</View>
</ScrollView>
</ScrollView>
@@ -530,6 +624,8 @@ function buildDetailSections(
<SubAgentDetailSection
key="sub-agent"
log={detail.log}
actions={detail.actions}
childSessionId={detail.childSessionId}
subAgentType={detail.subAgentType}
description={detail.description}
ds={ds}
@@ -731,6 +827,35 @@ const styles = StyleSheet.create((theme) => {
shellPrompt: {
color: theme.colors.foregroundMuted,
},
subAgentSessionText: {
fontFamily: Fonts.mono,
fontSize: theme.fontSize.xs,
color: theme.colors.foregroundMuted,
lineHeight: 18,
marginBottom: theme.spacing[2],
},
subAgentActions: {
gap: theme.spacing[1],
marginBottom: theme.spacing[2],
},
subAgentActionRow: {
flexDirection: "row",
alignItems: "center",
gap: theme.spacing[2],
},
subAgentActionTool: {
minWidth: 56,
fontFamily: Fonts.mono,
fontSize: theme.fontSize.xs,
color: theme.colors.foregroundMuted,
lineHeight: 18,
},
subAgentActionSummary: {
fontFamily: Fonts.mono,
fontSize: theme.fontSize.xs,
color: theme.colors.foreground,
lineHeight: 18,
},
jsonScroll: {
borderWidth: theme.borderWidth[1],
borderColor: theme.colors.border,