diff --git a/server/claude-sdk.js b/server/claude-sdk.js
index ea47c37..150fa9e 100644
--- a/server/claude-sdk.js
+++ b/server/claude-sdk.js
@@ -250,7 +250,13 @@ function getAllSessions() {
* @returns {Object} Transformed message ready for WebSocket
*/
function transformMessage(sdkMessage) {
- // Pass-through; SDK messages match frontend format.
+ // Extract parent_tool_use_id for subagent tool grouping
+ if (sdkMessage.parent_tool_use_id) {
+ return {
+ ...sdkMessage,
+ parentToolUseId: sdkMessage.parent_tool_use_id
+ };
+ }
return sdkMessage;
}
diff --git a/server/projects.js b/server/projects.js
index 50a22c5..b736bbb 100755
--- a/server/projects.js
+++ b/server/projects.js
@@ -889,22 +889,81 @@ async function parseJsonlSessions(filePath) {
}
}
+// Parse an agent JSONL file and extract tool uses
+async function parseAgentTools(filePath) {
+ const tools = [];
+
+ try {
+ const fileStream = fsSync.createReadStream(filePath);
+ const rl = readline.createInterface({
+ input: fileStream,
+ crlfDelay: Infinity
+ });
+
+ for await (const line of rl) {
+ if (line.trim()) {
+ try {
+ const entry = JSON.parse(line);
+ // Look for assistant messages with tool_use
+ if (entry.message?.role === 'assistant' && Array.isArray(entry.message?.content)) {
+ for (const part of entry.message.content) {
+ if (part.type === 'tool_use') {
+ tools.push({
+ toolId: part.id,
+ toolName: part.name,
+ toolInput: part.input,
+ timestamp: entry.timestamp
+ });
+ }
+ }
+ }
+ // Look for tool results
+ if (entry.message?.role === 'user' && Array.isArray(entry.message?.content)) {
+ for (const part of entry.message.content) {
+ if (part.type === 'tool_result') {
+ // Find the matching tool and add result
+ const tool = tools.find(t => t.toolId === part.tool_use_id);
+ if (tool) {
+ tool.toolResult = {
+ content: typeof part.content === 'string' ? part.content :
+ Array.isArray(part.content) ? part.content.map(c => c.text || '').join('\n') :
+ JSON.stringify(part.content),
+ isError: Boolean(part.is_error)
+ };
+ }
+ }
+ }
+ }
+ } catch (parseError) {
+ // Skip malformed lines
+ }
+ }
+ }
+ } catch (error) {
+ console.warn(`Error parsing agent file ${filePath}:`, error.message);
+ }
+
+ return tools;
+}
+
// Get messages for a specific session with pagination support
async function getSessionMessages(projectName, sessionId, limit = null, offset = 0) {
const projectDir = path.join(os.homedir(), '.claude', 'projects', projectName);
try {
const files = await fs.readdir(projectDir);
- // agent-*.jsonl files contain session start data at this point. This needs to be revisited
- // periodically to make sure only accurate data is there and no new functionality is added there
+ // agent-*.jsonl files contain subagent tool history - we'll process them separately
const jsonlFiles = files.filter(file => file.endsWith('.jsonl') && !file.startsWith('agent-'));
-
+ const agentFiles = files.filter(file => file.endsWith('.jsonl') && file.startsWith('agent-'));
+
if (jsonlFiles.length === 0) {
return { messages: [], total: 0, hasMore: false };
}
-
+
const messages = [];
-
+ // Map of agentId -> tools for subagent tool grouping
+ const agentToolsCache = new Map();
+
// Process all JSONL files to find messages for this session
for (const file of jsonlFiles) {
const jsonlFile = path.join(projectDir, file);
@@ -913,7 +972,7 @@ async function getSessionMessages(projectName, sessionId, limit = null, offset =
input: fileStream,
crlfDelay: Infinity
});
-
+
for await (const line of rl) {
if (line.trim()) {
try {
@@ -927,26 +986,55 @@ async function getSessionMessages(projectName, sessionId, limit = null, offset =
}
}
}
-
+
+ // Collect agentIds from Task tool results
+ const agentIds = new Set();
+ for (const message of messages) {
+ if (message.toolUseResult?.agentId) {
+ agentIds.add(message.toolUseResult.agentId);
+ }
+ }
+
+ // Load agent tools for each agentId found
+ for (const agentId of agentIds) {
+ const agentFileName = `agent-${agentId}.jsonl`;
+ if (agentFiles.includes(agentFileName)) {
+ const agentFilePath = path.join(projectDir, agentFileName);
+ const tools = await parseAgentTools(agentFilePath);
+ agentToolsCache.set(agentId, tools);
+ }
+ }
+
+ // Attach agent tools to their parent Task messages
+ for (const message of messages) {
+ if (message.toolUseResult?.agentId) {
+ const agentId = message.toolUseResult.agentId;
+ const agentTools = agentToolsCache.get(agentId);
+ if (agentTools && agentTools.length > 0) {
+ message.subagentTools = agentTools;
+ }
+ }
+ }
+
// Sort messages by timestamp
- const sortedMessages = messages.sort((a, b) =>
+ const sortedMessages = messages.sort((a, b) =>
new Date(a.timestamp || 0) - new Date(b.timestamp || 0)
);
-
+
const total = sortedMessages.length;
-
+
// If no limit is specified, return all messages (backward compatibility)
if (limit === null) {
return sortedMessages;
}
-
+
// Apply pagination - for recent messages, we need to slice from the end
// offset 0 should give us the most recent messages
const startIndex = Math.max(0, total - offset - limit);
const endIndex = total - offset;
const paginatedMessages = sortedMessages.slice(startIndex, endIndex);
const hasMore = startIndex > 0;
-
+
return {
messages: paginatedMessages,
total,
diff --git a/src/components/app/AppContent.tsx b/src/components/app/AppContent.tsx
index 100129d..ab436aa 100644
--- a/src/components/app/AppContent.tsx
+++ b/src/components/app/AppContent.tsx
@@ -106,7 +106,7 @@ export default function AppContent() {
)}
-
+
{
+ setIsInputFocused(focused);
onInputFocusChange?.(focused);
},
[onInputFocusChange],
@@ -953,5 +956,6 @@ export function useChatComposerState({
handlePermissionDecision,
handleGrantToolPermission,
handleInputFocusChange,
+ isInputFocused,
};
}
diff --git a/src/components/chat/hooks/useChatRealtimeHandlers.ts b/src/components/chat/hooks/useChatRealtimeHandlers.ts
index 9d2071b..e84f726 100644
--- a/src/components/chat/hooks/useChatRealtimeHandlers.ts
+++ b/src/components/chat/hooks/useChatRealtimeHandlers.ts
@@ -336,9 +336,43 @@ export function useChatRealtimeHandlers({
}
if (structuredMessageData && Array.isArray(structuredMessageData.content)) {
+ const parentToolUseId = rawStructuredData?.parentToolUseId;
+
structuredMessageData.content.forEach((part: any) => {
if (part.type === 'tool_use') {
const toolInput = part.input ? JSON.stringify(part.input, null, 2) : '';
+
+ // Check if this is a child tool from a subagent
+ if (parentToolUseId) {
+ setChatMessages((previous) =>
+ previous.map((message) => {
+ if (message.toolId === parentToolUseId && message.isSubagentContainer) {
+ const childTool = {
+ toolId: part.id,
+ toolName: part.name,
+ toolInput: part.input,
+ toolResult: null,
+ timestamp: new Date(),
+ };
+ const existingChildren = message.subagentState?.childTools || [];
+ return {
+ ...message,
+ subagentState: {
+ childTools: [...existingChildren, childTool],
+ currentToolIndex: existingChildren.length,
+ isComplete: false,
+ },
+ };
+ }
+ return message;
+ }),
+ );
+ return;
+ }
+
+ // Check if this is a Task tool (subagent container)
+ const isSubagentContainer = part.name === 'Task';
+
setChatMessages((previous) => [
...previous,
{
@@ -350,6 +384,10 @@ export function useChatRealtimeHandlers({
toolInput,
toolId: part.id,
toolResult: null,
+ isSubagentContainer,
+ subagentState: isSubagentContainer
+ ? { childTools: [], currentToolIndex: -1, isComplete: false }
+ : undefined,
},
]);
return;
@@ -382,6 +420,8 @@ export function useChatRealtimeHandlers({
}
if (structuredMessageData?.role === 'user' && Array.isArray(structuredMessageData.content)) {
+ const parentToolUseId = rawStructuredData?.parentToolUseId;
+
structuredMessageData.content.forEach((part: any) => {
if (part.type !== 'tool_result') {
return;
@@ -389,8 +429,32 @@ export function useChatRealtimeHandlers({
setChatMessages((previous) =>
previous.map((message) => {
- if (message.isToolUse && message.toolId === part.tool_use_id) {
+ // Handle child tool results (route to parent's subagentState)
+ if (parentToolUseId && message.toolId === parentToolUseId && message.isSubagentContainer) {
return {
+ ...message,
+ subagentState: {
+ ...message.subagentState!,
+ childTools: message.subagentState!.childTools.map((child) => {
+ if (child.toolId === part.tool_use_id) {
+ return {
+ ...child,
+ toolResult: {
+ content: part.content,
+ isError: part.is_error,
+ timestamp: new Date(),
+ },
+ };
+ }
+ return child;
+ }),
+ },
+ };
+ }
+
+ // Handle normal tool results (including parent Task tool completion)
+ if (message.isToolUse && message.toolId === part.tool_use_id) {
+ const result = {
...message,
toolResult: {
content: part.content,
@@ -398,6 +462,14 @@ export function useChatRealtimeHandlers({
timestamp: new Date(),
},
};
+ // Mark subagent as complete when parent Task receives its result
+ if (message.isSubagentContainer && message.subagentState) {
+ result.subagentState = {
+ ...message.subagentState,
+ isComplete: true,
+ };
+ }
+ return result;
}
return message;
}),
diff --git a/src/components/chat/tools/ToolRenderer.tsx b/src/components/chat/tools/ToolRenderer.tsx
index 0d4fa07..e3c88b0 100644
--- a/src/components/chat/tools/ToolRenderer.tsx
+++ b/src/components/chat/tools/ToolRenderer.tsx
@@ -1,7 +1,8 @@
import React, { memo, useMemo, useCallback } from 'react';
import { getToolConfig } from './configs/toolConfigs';
-import { OneLineDisplay, CollapsibleDisplay, DiffViewer, MarkdownContent, FileListContent, TodoListContent, TaskListContent, TextContent, QuestionAnswerContent } from './components';
+import { OneLineDisplay, CollapsibleDisplay, DiffViewer, MarkdownContent, FileListContent, TodoListContent, TaskListContent, TextContent, QuestionAnswerContent, SubagentContainer } from './components';
import type { Project } from '../../../types/app';
+import type { SubagentChildTool } from '../types/types';
type DiffLine = {
type: string;
@@ -21,6 +22,12 @@ interface ToolRendererProps {
autoExpandTools?: boolean;
showRawParameters?: boolean;
rawToolInput?: string;
+ isSubagentContainer?: boolean;
+ subagentState?: {
+ childTools: SubagentChildTool[];
+ currentToolIndex: number;
+ isComplete: boolean;
+ };
}
function getToolCategory(toolName: string): string {
@@ -50,8 +57,24 @@ export const ToolRenderer: React.FC = memo(({
selectedProject,
autoExpandTools = false,
showRawParameters = false,
- rawToolInput
+ rawToolInput,
+ isSubagentContainer,
+ subagentState
}) => {
+ // Route subagent containers to dedicated component
+ if (isSubagentContainer && subagentState) {
+ if (mode === 'result') {
+ return null;
+ }
+ return (
+
+ );
+ }
+
const config = getToolConfig(toolName);
const displayConfig: any = mode === 'input' ? config.input : config.result;
diff --git a/src/components/chat/tools/components/CollapsibleSection.tsx b/src/components/chat/tools/components/CollapsibleSection.tsx
index f83135c..0d6615d 100644
--- a/src/components/chat/tools/components/CollapsibleSection.tsx
+++ b/src/components/chat/tools/components/CollapsibleSection.tsx
@@ -24,7 +24,7 @@ export const CollapsibleSection: React.FC = ({
}) => {
return (
-
+