Skip to content
Open
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
27 changes: 23 additions & 4 deletions packages/opencode/src/cli/cmd/run/stream.transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ function sid(event: Event): string | undefined {
}

if (
event.type === "session.created" ||
event.type === "session.updated" ||
event.type === "session.next.shell.started" ||
event.type === "session.next.shell.ended" ||
event.type === "permission.asked" ||
Expand Down Expand Up @@ -444,7 +446,19 @@ function createLayer(input: StreamInput) {
const replayedParts = new Set<string>()
const recovering = new Set<string>()
const tracked = (sessionID: string | undefined) =>
sessionID === input.sessionID || (!!sessionID && state.subagent.tabs.has(sessionID))
sessionID === input.sessionID ||
(!!sessionID && (state.subagent.tabs.has(sessionID) || state.subagent.children.has(sessionID)))
const trackedEvent = (event: Event) => {
if (tracked(sid(event))) {
return true
}

if (event.type === "session.created" || event.type === "session.updated") {
return event.properties.info.parentID === input.sessionID
}

return false
}
const currentSubagentState = () => {
if (state.selectedSubagent && !state.subagent.tabs.has(state.selectedSubagent)) {
state.selectedSubagent = undefined
Expand Down Expand Up @@ -627,7 +641,7 @@ function createLayer(input: StreamInput) {
})

const bootstrap = Effect.fn("RunStreamTransport.bootstrap")(function* () {
const [messagesList, children, permissions, questions] = yield* Effect.all(
const [messagesList, children, status, permissions, questions] = yield* Effect.all(
[
messages(
input.sessionID,
Expand All @@ -645,6 +659,10 @@ function createLayer(input: StreamInput) {
Effect.map((item) => item.data ?? []),
Effect.orElseSucceed(() => []),
),
Effect.promise(() => input.sdk.session.status()).pipe(
Effect.map((item) => item.data ?? {}),
Effect.orElseSucceed(() => ({})),
),
Effect.promise(() => input.sdk.permission.list()).pipe(
Effect.map((item) => item.data ?? []),
Effect.orElseSucceed(() => []),
Expand Down Expand Up @@ -709,6 +727,7 @@ function createLayer(input: StreamInput) {
data: state.subagent,
messages: messagesList,
children,
status,
permissions,
questions,
})
Expand Down Expand Up @@ -901,7 +920,7 @@ function createLayer(input: StreamInput) {
const next: Event[] = []
let changed = false
for (const event of pending) {
if (!tracked(sid(event))) {
if (!trackedEvent(event)) {
next.push(event)
continue
}
Expand Down Expand Up @@ -951,7 +970,7 @@ function createLayer(input: StreamInput) {
return
}

if (!tracked(sessionID)) {
if (!trackedEvent(event)) {
if (sessionID) {
input.trace?.write("recv.event", event)
buffered.push(event)
Expand Down
169 changes: 155 additions & 14 deletions packages/opencode/src/cli/cmd/run/subagent-data.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import type { Event, Message, Part, PermissionRequest, QuestionRequest, ToolPart } from "@opencode-ai/sdk/v2"
import type {
Event,
Message,
Part,
PermissionRequest,
QuestionRequest,
SessionStatus,
ToolPart,
} from "@opencode-ai/sdk/v2"
import * as Locale from "@/util/locale"
import {
bootstrapSessionData,
Expand Down Expand Up @@ -37,15 +45,28 @@ type DetailState = {
frames: Frame[]
}

type ChildSessionInfo = {
id: string
title?: string
parentID?: string
agent?: string
time?: {
created?: number
updated?: number
}
}

export type SubagentData = {
tabs: Map<string, FooterSubagentTab>
details: Map<string, DetailState>
children: Map<string, ChildSessionInfo>
}

export type BootstrapSubagentInput = {
data: SubagentData
messages: SessionMessage[]
children: Array<{ id: string; title?: string }>
children: ChildSessionInfo[]
status?: Record<string, SessionStatus>
permissions: PermissionRequest[]
questions: QuestionRequest[]
}
Expand Down Expand Up @@ -313,6 +334,88 @@ function taskSessionID(part: ToolPart) {
return text(metadata(part, "sessionId")) ?? text(metadata(part, "sessionID"))
}

function childSessionAgent(info: ChildSessionInfo) {
return text(info.agent) ?? text(info.title?.match(/\(@([^)]+) subagent\)/)?.[1])
}

function childSessionLabel(info: ChildSessionInfo) {
const agent = childSessionAgent(info)
if (agent) {
return Locale.titlecase(agent)
}

return "Child Session"
}

function childDescription(info: ChildSessionInfo) {
return text(info.title) ?? "Direct child session"
}

function childStatus(status: SessionStatus | undefined, current: FooterSubagentTab | undefined) {
if (!status) {
return current?.status ?? "completed"
}

if (status.type === "busy") {
return "running" as const
}

if (status.type === "retry") {
return "error" as const
}

return "completed" as const
}

function childUpdatedAt(info: ChildSessionInfo, current: FooterSubagentTab | undefined) {
return info.time?.updated ?? info.time?.created ?? current?.lastUpdatedAt ?? Date.now()
}

function rememberChild(data: SubagentData, info: ChildSessionInfo) {
const current = data.children.get(info.id)
const next = current
? {
...current,
...info,
time: {
...current.time,
...info.time,
},
}
: info

data.children.set(next.id, next)
return next
}

function ensureChildTab(data: SubagentData, info: ChildSessionInfo, status?: SessionStatus) {
const child = rememberChild(data, info)
const current = data.tabs.get(child.id)
if (current && !current.partID.startsWith("child:")) {
ensureDetail(data, child.id)
return false
}

const next = {
sessionID: child.id,
partID: `child:${child.id}`,
callID: `child:${child.id}`,
label: childSessionLabel(child),
description: childDescription(child),
status: childStatus(status, current),
title: child.title,
lastUpdatedAt: childUpdatedAt(child, current),
}
if (sameSubagentTab(current, next)) {
ensureDetail(data, child.id)
return false
}

data.tabs.set(child.id, next)
ensureDetail(data, child.id)
return true
}

function syncTaskTab(data: SubagentData, part: ToolPart, children?: Set<string>) {
if (part.tool !== "task") {
return false
Expand Down Expand Up @@ -599,7 +702,7 @@ function bootstrapChildMessages(input: {
}

function knownSession(data: SubagentData, sessionID: string) {
return data.tabs.has(sessionID)
return data.tabs.has(sessionID) || data.children.has(sessionID)
}

export function listSubagentPermissions(data: SubagentData) {
Expand All @@ -614,6 +717,7 @@ export function createSubagentData(): SubagentData {
return {
tabs: new Map(),
details: new Map(),
children: new Map(),
}
}

Expand Down Expand Up @@ -681,6 +785,22 @@ export function bootstrapSubagentData(input: BootstrapSubagentInput) {
}
}

for (const item of input.children) {
const hasBlocker =
input.permissions.some((request) => request.sessionID === item.id) ||
input.questions.some((request) => request.sessionID === item.id)
const status = input.status?.[item.id]
if (!status && !hasBlocker && !input.data.tabs.has(item.id)) {
continue
}

if (status?.type === "idle" && !hasBlocker && !input.data.tabs.has(item.id)) {
continue
}

changed = ensureChildTab(input.data, item, hasBlocker ? { type: "busy" } : status) || changed
}

for (const item of input.permissions) {
if (!children.has(item.sessionID)) {
continue
Expand Down Expand Up @@ -759,6 +879,7 @@ export function clearFinishedSubagents(data: SubagentData) {

data.tabs.delete(sessionID)
data.details.delete(sessionID)
data.children.delete(sessionID)
changed = true
}

Expand All @@ -774,6 +895,22 @@ export function reduceSubagentData(input: {
}) {
const event = input.event

if (event.type === "session.created") {
if (event.properties.info.parentID !== input.sessionID) {
return false
}

return ensureChildTab(input.data, event.properties.info, { type: "busy" })
}

if (event.type === "session.updated") {
if (event.properties.info.parentID !== input.sessionID && !knownSession(input.data, event.properties.info.id)) {
return false
}

return ensureChildTab(input.data, event.properties.info)
}

if (event.type === "message.part.updated") {
const part = event.properties.part
if (part.sessionID === input.sessionID) {
Expand Down Expand Up @@ -806,19 +943,23 @@ export function reduceSubagentData(input: {

const detail = ensureDetail(input.data, sessionID)
if (event.type === "session.status") {
if (event.properties.status.type !== "retry") {
return false
const info = input.data.children.get(sessionID)
const tabChanged = info ? ensureChildTab(input.data, info, event.properties.status) : false
if (event.properties.status.type === "retry") {
return (
appendCommits(detail, [
{
kind: "error",
text: event.properties.status.message,
phase: "start",
source: "system",
messageID: `retry:${event.properties.status.attempt}`,
},
]) || tabChanged
)
}

return appendCommits(detail, [
{
kind: "error",
text: event.properties.status.message,
phase: "start",
source: "system",
messageID: `retry:${event.properties.status.attempt}`,
},
])
return tabChanged
}

if (event.type === "session.error" && event.properties.error) {
Expand Down
Loading
Loading