-
- {outputSummary}
-
-
-
- {t('agentStatus.viewResult')}
-
-
+
+ {t('agentStatus.noActivity')}
) : (
@@ -552,6 +545,19 @@ function getAgentOutputSummary(content: string): string {
return text.length > 220 ? `${text.slice(0, 220)}...` : text
}
+function extractAgentDisplayText(content: unknown): string {
+ return stripAgentResultMetadata(extractTextContent(content))
+}
+
+function stripAgentResultMetadata(text: string): string {
+ return text
+ .replace(/^\s*agentId:.*(?:\r?\n)?/gm, '')
+ .replace(/[\s\S]*?<\/usage>/g, '')
+ .replace(/^\s*(?:total_tokens|tool_uses|duration_ms):\s*\d+\s*$/gm, '')
+ .replace(/\n{3,}/g, '\n\n')
+ .trim()
+}
+
function isAgentLaunchResult(content: unknown): boolean {
const text = extractTextContent(content).trim()
if (!text) return false
diff --git a/src/server/__tests__/sessions.test.ts b/src/server/__tests__/sessions.test.ts
index 721df398..7738fcb1 100644
--- a/src/server/__tests__/sessions.test.ts
+++ b/src/server/__tests__/sessions.test.ts
@@ -46,6 +46,21 @@ async function writeSessionFile(
return filePath
}
+async function writeSubagentTranscriptFile(
+ projectDir: string,
+ sessionId: string,
+ agentId: string,
+ entries: Record[],
+): Promise {
+ const dir = path.join(tmpDir, 'projects', projectDir, sessionId, 'subagents')
+ await fs.mkdir(dir, { recursive: true })
+ const normalizedAgentId = agentId.startsWith('agent-') ? agentId : `agent-${agentId}`
+ const filePath = path.join(dir, `${normalizedAgentId}.jsonl`)
+ const content = entries.map((e) => JSON.stringify(e)).join('\n') + '\n'
+ await fs.writeFile(filePath, content, 'utf-8')
+ return filePath
+}
+
async function writeSkill(
rootDir: string,
skillName: string,
@@ -289,6 +304,110 @@ describe('SessionService', () => {
expect(messages).toHaveLength(2)
})
+ it('should append subagent tool calls under their parent agent tool result', async () => {
+ const sessionId = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'
+ const projectDir = '-tmp-project'
+ const agentId = 'abc123'
+
+ await writeSessionFile(projectDir, sessionId, [
+ makeSnapshotEntry(),
+ makeUserEntry('Dispatch an agent'),
+ {
+ type: 'assistant',
+ message: {
+ role: 'assistant',
+ content: [
+ {
+ type: 'tool_use',
+ id: 'Agent:0',
+ name: 'Agent',
+ input: { description: 'Inspect alpha' },
+ },
+ ],
+ },
+ uuid: crypto.randomUUID(),
+ timestamp: '2026-01-01T00:00:02.000Z',
+ },
+ {
+ type: 'user',
+ message: {
+ role: 'user',
+ content: [
+ {
+ type: 'tool_result',
+ tool_use_id: 'Agent:0',
+ content: [
+ {
+ type: 'text',
+ text: `alpha summary\nagentId: ${agentId} (use SendMessage with to: '${agentId}' to continue this agent)\ntotal_tokens: 10\ntool_uses: 2\nduration_ms: 30`,
+ },
+ ],
+ },
+ ],
+ },
+ uuid: crypto.randomUUID(),
+ timestamp: '2026-01-01T00:00:03.000Z',
+ },
+ ])
+ await writeSubagentTranscriptFile(projectDir, sessionId, agentId, [
+ {
+ type: 'assistant',
+ message: {
+ role: 'assistant',
+ content: [
+ {
+ type: 'tool_use',
+ id: 'Read:0',
+ name: 'Read',
+ input: { file_path: '/tmp/alpha.txt' },
+ },
+ ],
+ },
+ uuid: crypto.randomUUID(),
+ timestamp: '2026-01-01T00:00:04.000Z',
+ },
+ {
+ type: 'user',
+ message: {
+ role: 'user',
+ content: [
+ {
+ type: 'tool_result',
+ tool_use_id: 'Read:0',
+ content: 'alpha body',
+ },
+ ],
+ },
+ uuid: crypto.randomUUID(),
+ timestamp: '2026-01-01T00:00:05.000Z',
+ },
+ ])
+
+ const messages = await service.getSessionMessages(sessionId)
+ const childToolUse = messages.find(
+ (message) => message.type === 'tool_use' && message.parentToolUseId === 'Agent:0',
+ )
+ const childToolResult = messages.find(
+ (message) => message.type === 'tool_result' && message.parentToolUseId === 'Agent:0',
+ )
+
+ expect(childToolUse?.content).toEqual([
+ {
+ type: 'tool_use',
+ id: 'Agent:0/abc123/Read:0',
+ name: 'Read',
+ input: { file_path: '/tmp/alpha.txt' },
+ },
+ ])
+ expect(childToolResult?.content).toEqual([
+ {
+ type: 'tool_result',
+ tool_use_id: 'Agent:0/abc123/Read:0',
+ content: 'alpha body',
+ },
+ ])
+ })
+
it('should hide synthetic interruption, no-response, and command breadcrumb transcript entries', async () => {
const sessionId = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'
await writeSessionFile('-tmp-project', sessionId, [
diff --git a/src/server/services/sessionService.ts b/src/server/services/sessionService.ts
index 651daa97..1220a0af 100644
--- a/src/server/services/sessionService.ts
+++ b/src/server/services/sessionService.ts
@@ -132,6 +132,8 @@ type RawEntry = {
[key: string]: unknown
}
+type ContentBlock = Record
+
const USER_INTERRUPTION_TEXTS = new Set([
'[Request interrupted by user]',
'[Request interrupted by user for tool use]',
@@ -350,6 +352,145 @@ export class SessionService {
return undefined
}
+ private extractAgentToolUseIdsFromMessage(message: MessageEntry): string[] {
+ if (message.type !== 'tool_use' || !Array.isArray(message.content)) {
+ return []
+ }
+
+ return (message.content as ContentBlock[])
+ .filter((block) => block.type === 'tool_use' && block.name === 'Agent')
+ .flatMap((block) => (typeof block.id === 'string' ? [block.id] : []))
+ }
+
+ private extractTextFromContent(content: unknown): string {
+ if (typeof content === 'string') return content
+ if (!Array.isArray(content)) return ''
+
+ return (content as ContentBlock[])
+ .flatMap((block) => (typeof block.text === 'string' ? [block.text] : []))
+ .join('\n')
+ }
+
+ private extractAgentIdFromResultText(text: string): string | undefined {
+ const match = text.match(/(?:^|\n)\s*agentId:\s*([A-Za-z0-9_-]+)/)
+ return match?.[1]
+ }
+
+ private extractAgentResultLinks(messages: MessageEntry[]): Map {
+ const agentToolUseIds = new Set(
+ messages.flatMap((message) => this.extractAgentToolUseIdsFromMessage(message)),
+ )
+ const resultLinks = new Map()
+
+ for (const message of messages) {
+ if (message.type !== 'tool_result' || !Array.isArray(message.content)) {
+ continue
+ }
+
+ for (const block of message.content as ContentBlock[]) {
+ if (block.type !== 'tool_result' || typeof block.tool_use_id !== 'string') {
+ continue
+ }
+ if (!agentToolUseIds.has(block.tool_use_id)) {
+ continue
+ }
+
+ const agentId = this.extractAgentIdFromResultText(
+ this.extractTextFromContent(block.content),
+ )
+ if (agentId) {
+ resultLinks.set(block.tool_use_id, agentId)
+ }
+ }
+ }
+
+ return resultLinks
+ }
+
+ private namespaceSubagentContentIds(content: unknown, namespace: string): unknown {
+ if (!Array.isArray(content)) return content
+
+ return (content as ContentBlock[]).map((block) => {
+ if (!block || typeof block !== 'object') return block
+ if (block.type === 'tool_use' && typeof block.id === 'string') {
+ return { ...block, id: `${namespace}/${block.id}` }
+ }
+ if (block.type === 'tool_result' && typeof block.tool_use_id === 'string') {
+ return { ...block, tool_use_id: `${namespace}/${block.tool_use_id}` }
+ }
+ return block
+ })
+ }
+
+ private subagentTranscriptPath(
+ projectDir: string,
+ sessionId: string,
+ agentId: string,
+ ): string {
+ const normalizedAgentId = agentId.startsWith('agent-') ? agentId : `agent-${agentId}`
+ return path.join(
+ this.getProjectsDir(),
+ projectDir,
+ sessionId,
+ 'subagents',
+ `${normalizedAgentId}.jsonl`,
+ )
+ }
+
+ private async loadSubagentToolMessages(
+ projectDir: string,
+ sessionId: string,
+ parentToolUseId: string,
+ agentId: string,
+ ): Promise {
+ const filePath = this.subagentTranscriptPath(projectDir, sessionId, agentId)
+ const entries = await this.readJsonlFile(filePath)
+ const namespace = `${parentToolUseId}/${agentId}`
+ const messages: MessageEntry[] = []
+
+ for (const entry of entries) {
+ if (!entry.message?.role || entry.isMeta) continue
+ if (this.shouldHideTranscriptEntry(entry)) continue
+ if (entry.type !== 'user' && entry.type !== 'assistant' && entry.type !== 'system') {
+ continue
+ }
+
+ const message = this.entryToMessage(
+ {
+ ...entry,
+ message: {
+ ...entry.message,
+ content: this.namespaceSubagentContentIds(entry.message.content, namespace),
+ },
+ },
+ parentToolUseId,
+ )
+ if (message && (message.type === 'tool_use' || message.type === 'tool_result')) {
+ messages.push(message)
+ }
+ }
+
+ return messages
+ }
+
+ private async appendSubagentToolMessages(
+ projectDir: string,
+ sessionId: string,
+ messages: MessageEntry[],
+ ): Promise {
+ const resultLinks = this.extractAgentResultLinks(messages)
+ if (resultLinks.size === 0) {
+ return messages
+ }
+
+ const childMessages = await Promise.all(
+ [...resultLinks.entries()].map(([parentToolUseId, agentId]) =>
+ this.loadSubagentToolMessages(projectDir, sessionId, parentToolUseId, agentId),
+ ),
+ )
+ return [...messages, ...childMessages.flat()]
+ }
+
private resolveParentToolUseId(
entry: RawEntry,
entriesByUuid: Map,
@@ -783,7 +924,11 @@ export class SessionService {
const stat = await fs.stat(filePath)
const entries = await this.readJsonlFile(filePath)
- const messages = this.entriesToMessages(entries)
+ const messages = await this.appendSubagentToolMessages(
+ projectDir,
+ sessionId,
+ this.entriesToMessages(entries),
+ )
const title = this.extractTitle(entries)
const workDir = this.resolveWorkDirFromEntries(entries, projectDir)
const workDirExists = await this.pathExists(workDir)
@@ -819,7 +964,11 @@ export class SessionService {
}
const entries = await this.readJsonlFile(found.filePath)
- return this.entriesToMessages(entries)
+ return await this.appendSubagentToolMessages(
+ found.projectDir,
+ sessionId,
+ this.entriesToMessages(entries),
+ )
}
/**