import type { Event, Message, Part, PermissionRequest, QuestionRequest, ToolPart } from "@opencode-ai/sdk/v2" import * as Locale from "@/util/locale" import { bootstrapSessionData, createSessionData, formatError, reduceSessionData, type SessionData, } from "./session-data" import type { FooterSubagentState, FooterSubagentTab, StreamCommit } from "./types" export const SUBAGENT_BOOTSTRAP_LIMIT = 200 export const SUBAGENT_CALL_BOOTSTRAP_LIMIT = 80 const SUBAGENT_COMMIT_LIMIT = 80 const SUBAGENT_CALL_LIMIT = 32 const SUBAGENT_ROLE_LIMIT = 32 const SUBAGENT_ERROR_LIMIT = 16 const SUBAGENT_ECHO_LIMIT = 8 type SessionMessage = { parts: Part[] } type BootstrapChildMessage = SessionMessage & { info: Message } type Frame = { key: string commit: StreamCommit } type DetailState = { sessionID: string data: SessionData frames: Frame[] } export type SubagentData = { tabs: Map details: Map } export type BootstrapSubagentInput = { data: SubagentData messages: SessionMessage[] children: Array<{ id: string; title?: string }> permissions: PermissionRequest[] questions: QuestionRequest[] } function createDetail(sessionID: string): DetailState { return { sessionID, data: createSessionData({ includeUserText: true, }), frames: [], } } function ensureDetail(data: SubagentData, sessionID: string) { const current = data.details.get(sessionID) if (current) { return current } const next = createDetail(sessionID) data.details.set(sessionID, next) return next } export function sameSubagentTab(a: FooterSubagentTab | undefined, b: FooterSubagentTab | undefined) { if (!a || !b) { return false } return ( a.sessionID === b.sessionID && a.partID === b.partID && a.callID === b.callID && a.label === b.label && a.description === b.description && a.status === b.status && a.title === b.title && a.toolCalls === b.toolCalls && a.lastUpdatedAt === b.lastUpdatedAt ) } function sameQueue(left: T[], right: T[]) { return ( left.length === right.length && left.every((item, index) => item.id === right[index]?.id && item === right[index]) ) } function queueSnapshot(data: SessionData) { return { permissions: data.permissions.slice(), questions: data.questions.slice(), } } function queueChanged(data: SessionData, before: ReturnType) { return !sameQueue(before.permissions, data.permissions) || !sameQueue(before.questions, data.questions) } function sameCommit(left: StreamCommit, right: StreamCommit) { return ( left.kind === right.kind && left.text === right.text && left.phase === right.phase && left.source === right.source && left.messageID === right.messageID && left.partID === right.partID && left.tool === right.tool && left.interrupted === right.interrupted && left.toolState === right.toolState && left.toolError === right.toolError ) } function text(value: unknown): string | undefined { if (typeof value !== "string") { return undefined } const next = value.trim() return next || undefined } function num(value: unknown): number | undefined { if (typeof value === "number" && Number.isFinite(value)) { return value } return undefined } function inputLabel(input: Record): string | undefined { const description = text(input.description) if (description) { return description } const command = text(input.command) if (command) { return command } const filePath = text(input.filePath) ?? text(input.filepath) if (filePath) { return filePath } const pattern = text(input.pattern) if (pattern) { return pattern } const query = text(input.query) if (query) { return query } const url = text(input.url) if (url) { return url } const path = text(input.path) if (path) { return path } const prompt = text(input.prompt) if (prompt) { return prompt } return undefined } function stateTitle(part: ToolPart) { return text("title" in part.state ? part.state.title : undefined) } function callKey(messageID: string | undefined, callID: string | undefined): string | undefined { if (!messageID || !callID) { return undefined } return `${messageID}:${callID}` } function compactToolState(part: ToolPart): ToolPart["state"] { if (part.state.status === "pending") { return { status: "pending", input: part.state.input, raw: part.state.raw, } } if (part.state.status === "running") { return { status: "running", input: part.state.input, time: part.state.time, ...(part.state.metadata ? { metadata: part.state.metadata } : {}), ...(part.state.title ? { title: part.state.title } : {}), } } if (part.state.status === "completed") { return { status: "completed", input: part.state.input, output: part.state.output, title: part.state.title, metadata: part.state.metadata, time: part.state.time, } } return { status: "error", input: part.state.input, error: part.state.error, time: part.state.time, ...(part.state.metadata ? { metadata: part.state.metadata } : {}), } } function recent(input: Iterable, limit: number) { const list = [...input] return list.slice(Math.max(0, list.length - limit)) } function copyMap(source: Map, keep: Set) { const out = new Map() for (const [key, value] of source) { if (!keep.has(key)) { continue } out.set(key, value) } return out } function compactToolPart(part: ToolPart): ToolPart { return { id: part.id, type: "tool", sessionID: part.sessionID, messageID: part.messageID, callID: part.callID, tool: part.tool, state: compactToolState(part), ...(part.metadata ? { metadata: part.metadata } : {}), } } function compactCommit(commit: StreamCommit): StreamCommit { if (!commit.part) { return commit } return { ...commit, part: compactToolPart(commit.part), } } function stateUpdatedAt(part: ToolPart) { if (!("time" in part.state)) { return Date.now() } const time = part.state.time if (!("end" in time)) { return time.start ?? Date.now() } return time.end ?? time.start ?? Date.now() } function metadata(part: ToolPart, key: string) { return ("metadata" in part.state ? part.state.metadata?.[key] : undefined) ?? part.metadata?.[key] } function taskTab(part: ToolPart, sessionID: string): FooterSubagentTab { const label = Locale.titlecase(text(part.state.input.subagent_type) ?? "general") const description = text(part.state.input.description) ?? stateTitle(part) ?? inputLabel(part.state.input) ?? "" const status = part.state.status === "error" ? "error" : part.state.status === "completed" ? "completed" : "running" return { sessionID, partID: part.id, callID: part.callID, label, description, status, title: stateTitle(part), toolCalls: num(metadata(part, "toolcalls")) ?? num(metadata(part, "toolCalls")) ?? num(metadata(part, "calls")), lastUpdatedAt: stateUpdatedAt(part), } } function taskSessionID(part: ToolPart) { return text(metadata(part, "sessionId")) ?? text(metadata(part, "sessionID")) } function syncTaskTab(data: SubagentData, part: ToolPart, children?: Set) { if (part.tool !== "task") { return false } const sessionID = taskSessionID(part) if (!sessionID) { return false } if (children && children.size > 0 && !children.has(sessionID)) { return false } const next = taskTab(part, sessionID) if (sameSubagentTab(data.tabs.get(sessionID), next)) { ensureDetail(data, sessionID) return false } data.tabs.set(sessionID, next) ensureDetail(data, sessionID) return true } function frameKey(commit: StreamCommit) { if (commit.partID) { return `${commit.kind}:${commit.partID}:${commit.phase}` } if (commit.messageID) { return `${commit.kind}:${commit.messageID}:${commit.phase}` } return `${commit.kind}:${commit.phase}:${commit.text}` } function limitFrames(detail: DetailState) { if (detail.frames.length <= SUBAGENT_COMMIT_LIMIT) { return } detail.frames.splice(0, detail.frames.length - SUBAGENT_COMMIT_LIMIT) } function mergeLiveCommit(current: StreamCommit, next: StreamCommit) { if (current.phase !== "progress" || next.phase !== "progress") { if (sameCommit(current, next)) { return current } return next } const merged = { ...current, ...next, text: current.text + next.text, } if (sameCommit(current, merged)) { return current } return merged } function appendCommits(detail: DetailState, commits: StreamCommit[]) { let changed = false for (const commit of commits.map(compactCommit)) { const key = frameKey(commit) const index = detail.frames.findIndex((item) => item.key === key) if (index === -1) { detail.frames.push({ key, commit, }) changed = true continue } const next = mergeLiveCommit(detail.frames[index].commit, commit) if (sameCommit(detail.frames[index].commit, next)) { continue } detail.frames[index] = { key, commit: next, } changed = true } if (changed) { limitFrames(detail) } return changed } function ensureBlockerTab( data: SubagentData, sessionID: string, title: string | undefined, kind: "permission" | "question", ) { const current = data.tabs.get(sessionID) if (current) { ensureDetail(data, sessionID) if (current.status !== "running") { return false } const next = { ...current, description: kind === "permission" ? "Pending permission" : "Pending question", status: "running" as const, title: current.title ?? title, lastUpdatedAt: Date.now(), } if (sameSubagentTab(current, next)) { return false } data.tabs.set(sessionID, next) return true } data.tabs.set(sessionID, { sessionID, partID: `bootstrap:${sessionID}`, callID: `bootstrap:${sessionID}`, label: text(title) ?? Locale.titlecase(kind), description: kind === "permission" ? "Pending permission" : "Pending question", status: "running", lastUpdatedAt: Date.now(), }) ensureDetail(data, sessionID) return true } function compactCallMap(detail: DetailState) { const keep = new Set(recent(detail.data.call.keys(), SUBAGENT_CALL_LIMIT)) for (const request of detail.data.permissions) { const key = callKey(request.tool?.messageID, request.tool?.callID) if (key) { keep.add(key) } } for (const item of detail.frames) { const key = callKey(item.commit.part?.messageID, item.commit.part?.callID) if (key) { keep.add(key) } } return copyMap(detail.data.call, keep) } function compactEchoMap(data: SessionData, messageIDs: Set) { const keys = new Set([...messageIDs, ...recent(data.echo.keys(), SUBAGENT_ECHO_LIMIT)]) return copyMap(data.echo, keys) } function compactIDs(detail: DetailState) { return new Set(recent(detail.data.ids, SUBAGENT_COMMIT_LIMIT + SUBAGENT_ERROR_LIMIT)) } function compactDetail(detail: DetailState) { const next = createSessionData({ includeUserText: true, }) const activePartIDs = new Set(detail.data.part.keys()) const framePartIDs = new Set(detail.frames.flatMap((item) => (item.commit.partID ? [item.commit.partID] : []))) const partIDs = new Set([...activePartIDs, ...framePartIDs, ...detail.data.tools]) const messageIDs = new Set([ ...[...activePartIDs] .map((partID) => detail.data.msg.get(partID)) .filter((item): item is string => typeof item === "string"), ...recent(detail.data.role.keys(), SUBAGENT_ROLE_LIMIT), ]) next.announced = detail.data.announced next.permissions = detail.data.permissions next.questions = detail.data.questions next.ids = compactIDs(detail) next.tools = new Set([...detail.data.tools].filter((item) => partIDs.has(item))) next.call = compactCallMap(detail) next.role = copyMap(detail.data.role, messageIDs) next.msg = copyMap(detail.data.msg, activePartIDs) next.part = copyMap(detail.data.part, activePartIDs) next.text = copyMap(detail.data.text, activePartIDs) next.sent = copyMap(detail.data.sent, activePartIDs) next.end = new Set([...detail.data.end].filter((item) => activePartIDs.has(item))) next.echo = compactEchoMap(detail.data, messageIDs) detail.data = next } function applyChildEvent(input: { detail: DetailState event: Event thinking: boolean limits: Record }) { const before = queueSnapshot(input.detail.data) const out = reduceSessionData({ data: input.detail.data, event: input.event, sessionID: input.detail.sessionID, thinking: input.thinking, limits: input.limits, }) const changed = appendCommits(input.detail, out.commits) compactDetail(input.detail) return changed || queueChanged(input.detail.data, before) } function bootstrapChildEvent(input: { detail: DetailState event: Event thinking: boolean limits: Record }) { const out = reduceSessionData({ data: input.detail.data, event: input.event, sessionID: input.detail.sessionID, thinking: input.thinking, limits: input.limits, }) return appendCommits(input.detail, out.commits) } function bootstrapChildMessages(input: { detail: DetailState messages: BootstrapChildMessage[] thinking: boolean limits: Record }) { let changed = false for (const message of input.messages) { changed = bootstrapChildEvent({ detail: input.detail, event: { id: `bootstrap:message:${message.info.id}`, type: "message.updated", properties: { sessionID: input.detail.sessionID, info: message.info, }, }, thinking: input.thinking, limits: input.limits, }) || changed for (const part of message.parts) { changed = bootstrapChildEvent({ detail: input.detail, event: { id: `bootstrap:part:${part.id}`, type: "message.part.updated", properties: { sessionID: input.detail.sessionID, part, time: 0, }, }, thinking: input.thinking, limits: input.limits, }) || changed } } compactDetail(input.detail) return changed } function knownSession(data: SubagentData, sessionID: string) { return data.tabs.has(sessionID) } export function listSubagentPermissions(data: SubagentData) { return [...data.details.values()].flatMap((detail) => detail.data.permissions) } export function listSubagentQuestions(data: SubagentData) { return [...data.details.values()].flatMap((detail) => detail.data.questions) } export function createSubagentData(): SubagentData { return { tabs: new Map(), details: new Map(), } } function snapshotDetail(detail: DetailState) { return { sessionID: detail.sessionID, commits: detail.frames.map((item) => item.commit), } } export function listSubagentTabs(data: SubagentData) { return [...data.tabs.values()].sort((a, b) => { const active = Number(b.status === "running") - Number(a.status === "running") if (active !== 0) { return active } return b.lastUpdatedAt - a.lastUpdatedAt }) } function snapshotQueues(data: SubagentData) { return { permissions: listSubagentPermissions(data).sort((a, b) => a.id.localeCompare(b.id)), questions: listSubagentQuestions(data).sort((a, b) => a.id.localeCompare(b.id)), } } function snapshotState(data: SubagentData, details: FooterSubagentState["details"]): FooterSubagentState { return { tabs: listSubagentTabs(data), details, ...snapshotQueues(data), } } export function snapshotSubagentData(data: SubagentData): FooterSubagentState { return snapshotState( data, Object.fromEntries([...data.details.entries()].map(([sessionID, detail]) => [sessionID, snapshotDetail(detail)])), ) } export function snapshotSelectedSubagentData( data: SubagentData, selectedSessionID: string | undefined, ): FooterSubagentState { const detail = selectedSessionID ? data.details.get(selectedSessionID) : undefined return snapshotState(data, detail ? { [detail.sessionID]: snapshotDetail(detail) } : {}) } export function bootstrapSubagentData(input: BootstrapSubagentInput) { const child = new Map(input.children.map((item) => [item.id, item])) const children = new Set(child.keys()) let changed = false for (const message of input.messages) { for (const part of message.parts) { if (part.type !== "tool") { continue } changed = syncTaskTab(input.data, part, children) || changed } } for (const item of input.permissions) { if (!children.has(item.sessionID)) { continue } changed = ensureBlockerTab(input.data, item.sessionID, child.get(item.sessionID)?.title, "permission") || changed } for (const item of input.questions) { if (!children.has(item.sessionID)) { continue } changed = ensureBlockerTab(input.data, item.sessionID, child.get(item.sessionID)?.title, "question") || changed } for (const sessionID of input.data.tabs.keys()) { const detail = ensureDetail(input.data, sessionID) const before = queueSnapshot(detail.data) bootstrapSessionData({ data: detail.data, messages: [], permissions: input.permissions .filter((item) => item.sessionID === sessionID) .sort((a, b) => a.id.localeCompare(b.id)), questions: input.questions .filter((item) => item.sessionID === sessionID) .sort((a, b) => a.id.localeCompare(b.id)), }) compactDetail(detail) changed = queueChanged(detail.data, before) || changed } return changed } export function bootstrapSubagentCalls(input: { data: SubagentData sessionID: string messages: BootstrapChildMessage[] thinking: boolean limits: Record }) { if (!knownSession(input.data, input.sessionID) || input.messages.length === 0) { return false } const detail = ensureDetail(input.data, input.sessionID) const before = queueSnapshot(detail.data) const beforeCallCount = detail.data.call.size bootstrapSessionData({ data: detail.data, messages: input.messages, permissions: detail.data.permissions, questions: detail.data.questions, }) const changed = bootstrapChildMessages({ detail, messages: input.messages, thinking: input.thinking, limits: input.limits, }) return changed || beforeCallCount !== detail.data.call.size || queueChanged(detail.data, before) } export function clearFinishedSubagents(data: SubagentData) { let changed = false for (const [sessionID, tab] of data.tabs.entries()) { if (tab.status === "running") { continue } data.tabs.delete(sessionID) data.details.delete(sessionID) changed = true } return changed } export function reduceSubagentData(input: { data: SubagentData event: Event sessionID: string thinking: boolean limits: Record }) { const event = input.event if (event.type === "message.part.updated") { const part = event.properties.part if (part.sessionID === input.sessionID) { if (part.type !== "tool") { return false } return syncTaskTab(input.data, part) } } const sessionID = event.type === "message.updated" || event.type === "message.part.delta" || event.type === "permission.asked" || event.type === "permission.replied" || event.type === "question.asked" || event.type === "question.replied" || event.type === "question.rejected" || event.type === "session.error" || event.type === "session.status" ? event.properties.sessionID : event.type === "message.part.updated" ? event.properties.part.sessionID : undefined if (!sessionID || !knownSession(input.data, sessionID)) { return false } const detail = ensureDetail(input.data, sessionID) if (event.type === "session.status") { if (event.properties.status.type !== "retry") { return false } return appendCommits(detail, [ { kind: "error", text: event.properties.status.message, phase: "start", source: "system", messageID: `retry:${event.properties.status.attempt}`, }, ]) } if (event.type === "session.error" && event.properties.error) { return appendCommits(detail, [ { kind: "error", text: formatError(event.properties.error), phase: "start", source: "system", messageID: `session.error:${event.properties.sessionID}:${formatError(event.properties.error)}`, }, ]) } return applyChildEvent({ detail, event, thinking: input.thinking, limits: input.limits, }) }