diff --git a/packages/app/src/components/agent-activity.tsx b/packages/app/src/components/agent-activity.tsx
new file mode 100644
index 000000000..b2a100085
--- /dev/null
+++ b/packages/app/src/components/agent-activity.tsx
@@ -0,0 +1,439 @@
+import { useState } from 'react';
+import { View, Text, Pressable } from 'react-native';
+import { StyleSheet } from 'react-native-unistyles';
+import type { AgentActivity, GroupedTextMessage, SessionUpdate } from '@/types/agent-activity';
+
+interface AgentActivityItemProps {
+ item: GroupedTextMessage | AgentActivity;
+}
+
+function formatTimestamp(date: Date): string {
+ return new Intl.DateTimeFormat('en-US', {
+ hour: 'numeric',
+ minute: '2-digit',
+ second: '2-digit',
+ hour12: true,
+ }).format(date);
+}
+
+function getToolIcon(toolKind?: string): string {
+ switch (toolKind) {
+ case 'read':
+ return '📖';
+ case 'edit':
+ return '✏️';
+ case 'delete':
+ return '🗑️';
+ case 'move':
+ return '📦';
+ case 'search':
+ return '🔍';
+ case 'execute':
+ return '▶️';
+ case 'think':
+ return '💭';
+ case 'fetch':
+ return '🌐';
+ case 'switch_mode':
+ return '🔄';
+ default:
+ return '🔧';
+ }
+}
+
+function getStatusColor(status?: string): string {
+ switch (status) {
+ case 'pending':
+ return '#9ca3af';
+ case 'in_progress':
+ return '#fbbf24';
+ case 'completed':
+ return '#22c55e';
+ case 'failed':
+ return '#ef4444';
+ default:
+ return '#6b7280';
+ }
+}
+
+function GroupedTextItem({ item }: { item: GroupedTextMessage }) {
+ const isThought = item.messageType === 'thought';
+
+ return (
+
+
+ {formatTimestamp(item.startTimestamp)}
+
+ {isThought && (
+ 💭 Thinking
+ )}
+
+ {item.text}
+
+
+ );
+}
+
+function ToolCallItem({ update, timestamp }: { update: SessionUpdate; timestamp: Date }) {
+ const [isExpanded, setIsExpanded] = useState(false);
+
+ if (update.kind !== 'tool_call' && update.kind !== 'tool_call_update') {
+ return null;
+ }
+
+ const isUpdate = update.kind === 'tool_call_update';
+ const title = update.title || 'Tool Call';
+ const status = update.status;
+ const toolKind = update.toolKind;
+
+ return (
+
+ setIsExpanded(!isExpanded)}
+ style={stylesheet.toolHeader}
+ >
+
+
+ {formatTimestamp(timestamp)}
+
+
+ {getToolIcon(toolKind)}
+ {title}
+ {status && (
+
+ {status}
+
+ )}
+
+
+ {isExpanded ? '▼' : '▶'}
+
+
+ {isExpanded && (
+
+ {update.rawInput && (
+
+ Input:
+
+ {JSON.stringify(update.rawInput, null, 2)}
+
+
+ )}
+ {update.rawOutput && (
+
+ Output:
+
+ {JSON.stringify(update.rawOutput, null, 2)}
+
+
+ )}
+ {!update.rawInput && !update.rawOutput && (
+
+ {isUpdate ? 'Tool call updated' : 'No details available'}
+
+ )}
+
+ )}
+
+ );
+}
+
+function PlanItem({ update, timestamp }: { update: SessionUpdate; timestamp: Date }) {
+ const [isExpanded, setIsExpanded] = useState(true);
+
+ if (update.kind !== 'plan') {
+ return null;
+ }
+
+ return (
+
+ setIsExpanded(!isExpanded)}
+ style={stylesheet.planHeader}
+ >
+
+ {formatTimestamp(timestamp)}
+
+ 📋 Plan ({update.entries.length} tasks)
+
+
+ {isExpanded ? '▼' : '▶'}
+
+
+ {isExpanded && (
+
+ {update.entries.map((entry, idx) => (
+
+
+ {entry.status === 'completed' ? '✓' : entry.status === 'in_progress' ? '⏳' : '○'}
+
+ {entry.content}
+
+ ))}
+
+ )}
+
+ );
+}
+
+function UnknownActivityItem({ update, timestamp }: { update: SessionUpdate; timestamp: Date }) {
+ const [showDrawer, setShowDrawer] = useState(false);
+
+ return (
+
+ setShowDrawer(!showDrawer)}
+ style={stylesheet.unknownHeader}
+ >
+ {formatTimestamp(timestamp)}
+
+ {update.kind}
+
+
+
+ {showDrawer && (
+
+
+ {JSON.stringify(update, null, 2)}
+
+
+ )}
+
+ );
+}
+
+export function AgentActivityItem({ item }: AgentActivityItemProps) {
+ // Grouped text message
+ if ('kind' in item && item.kind === 'grouped_text') {
+ return ;
+ }
+
+ // Individual activity
+ const activity = item as AgentActivity;
+ const update = activity.update;
+
+ // Tool calls
+ if (update.kind === 'tool_call' || update.kind === 'tool_call_update') {
+ return ;
+ }
+
+ // Plan
+ if (update.kind === 'plan') {
+ return ;
+ }
+
+ // Available commands update
+ if (update.kind === 'available_commands_update') {
+ return (
+
+ {formatTimestamp(activity.timestamp)}
+
+ Commands updated ({update.availableCommands.length} available)
+
+
+ );
+ }
+
+ // Current mode update
+ if (update.kind === 'current_mode_update') {
+ return (
+
+ {formatTimestamp(activity.timestamp)}
+
+ Mode changed to: {update.currentModeId}
+
+
+ );
+ }
+
+ // Unknown activity type
+ return ;
+}
+
+const stylesheet = StyleSheet.create((theme) => ({
+ card: {
+ backgroundColor: theme.colors.card,
+ borderRadius: theme.borderRadius.lg,
+ padding: theme.spacing[3],
+ marginBottom: theme.spacing[2],
+ },
+ thoughtCard: {
+ backgroundColor: theme.colors.muted,
+ borderLeftWidth: 3,
+ borderLeftColor: theme.colors.palette.purple[500],
+ },
+ timestamp: {
+ color: theme.colors.mutedForeground,
+ fontSize: theme.fontSize.xs,
+ marginBottom: theme.spacing[1],
+ },
+ thoughtLabel: {
+ color: theme.colors.palette.purple[400],
+ fontSize: theme.fontSize.xs,
+ fontWeight: theme.fontWeight.semibold,
+ marginBottom: theme.spacing[1],
+ },
+ text: {
+ color: theme.colors.foreground,
+ fontSize: theme.fontSize.sm,
+ lineHeight: 20,
+ },
+ thoughtText: {
+ color: theme.colors.mutedForeground,
+ fontStyle: 'italic',
+ },
+ toolCard: {
+ backgroundColor: theme.colors.card,
+ borderRadius: theme.borderRadius.lg,
+ marginBottom: theme.spacing[2],
+ overflow: 'hidden',
+ },
+ toolHeader: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ padding: theme.spacing[3],
+ },
+ toolHeaderLeft: {
+ flex: 1,
+ },
+ toolTitleRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: theme.spacing[2],
+ marginTop: theme.spacing[1],
+ },
+ toolIcon: {
+ fontSize: theme.fontSize.base,
+ },
+ toolTitle: {
+ color: theme.colors.palette.blue[400],
+ fontSize: theme.fontSize.sm,
+ fontWeight: theme.fontWeight.semibold,
+ flex: 1,
+ },
+ statusBadge: {
+ paddingHorizontal: theme.spacing[2],
+ paddingVertical: theme.spacing[1],
+ borderRadius: theme.borderRadius.md,
+ },
+ statusText: {
+ color: theme.colors.foreground,
+ fontSize: theme.fontSize.xs,
+ fontWeight: theme.fontWeight.medium,
+ },
+ expandIcon: {
+ color: theme.colors.mutedForeground,
+ fontSize: theme.fontSize.xs,
+ },
+ toolContent: {
+ borderTopWidth: theme.borderWidth[1],
+ borderTopColor: theme.colors.border,
+ padding: theme.spacing[3],
+ },
+ section: {
+ marginBottom: theme.spacing[3],
+ },
+ sectionTitle: {
+ color: theme.colors.foreground,
+ fontSize: theme.fontSize.xs,
+ fontWeight: theme.fontWeight.semibold,
+ marginBottom: theme.spacing[1],
+ },
+ code: {
+ color: theme.colors.mutedForeground,
+ fontSize: theme.fontSize.xs,
+ fontFamily: 'monospace',
+ backgroundColor: theme.colors.muted,
+ padding: theme.spacing[2],
+ borderRadius: theme.borderRadius.md,
+ },
+ emptyText: {
+ color: theme.colors.mutedForeground,
+ fontSize: theme.fontSize.sm,
+ fontStyle: 'italic',
+ },
+ planCard: {
+ backgroundColor: theme.colors.card,
+ borderRadius: theme.borderRadius.lg,
+ marginBottom: theme.spacing[2],
+ overflow: 'hidden',
+ },
+ planHeader: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ padding: theme.spacing[3],
+ },
+ planHeaderLeft: {
+ flex: 1,
+ },
+ planTitle: {
+ color: theme.colors.palette.green[400],
+ fontSize: theme.fontSize.sm,
+ fontWeight: theme.fontWeight.semibold,
+ marginTop: theme.spacing[1],
+ },
+ planContent: {
+ borderTopWidth: theme.borderWidth[1],
+ borderTopColor: theme.colors.border,
+ padding: theme.spacing[3],
+ },
+ planEntry: {
+ flexDirection: 'row',
+ alignItems: 'flex-start',
+ gap: theme.spacing[2],
+ marginBottom: theme.spacing[2],
+ },
+ planEntryStatus: {
+ fontSize: theme.fontSize.sm,
+ marginTop: 2,
+ },
+ planEntryText: {
+ color: theme.colors.foreground,
+ fontSize: theme.fontSize.sm,
+ flex: 1,
+ },
+ infoText: {
+ color: theme.colors.mutedForeground,
+ fontSize: theme.fontSize.sm,
+ },
+ unknownCard: {
+ backgroundColor: theme.colors.card,
+ borderRadius: theme.borderRadius.lg,
+ marginBottom: theme.spacing[2],
+ overflow: 'hidden',
+ },
+ unknownHeader: {
+ padding: theme.spacing[3],
+ },
+ unknownBadge: {
+ backgroundColor: theme.colors.palette.orange[600],
+ paddingHorizontal: theme.spacing[3],
+ paddingVertical: theme.spacing[2],
+ borderRadius: theme.borderRadius.md,
+ marginTop: theme.spacing[2],
+ alignSelf: 'flex-start',
+ },
+ unknownBadgeText: {
+ color: theme.colors.foreground,
+ fontSize: theme.fontSize.xs,
+ fontWeight: theme.fontWeight.semibold,
+ },
+ drawerContent: {
+ borderTopWidth: theme.borderWidth[1],
+ borderTopColor: theme.colors.border,
+ padding: theme.spacing[3],
+ backgroundColor: theme.colors.muted,
+ },
+}));
diff --git a/packages/app/src/components/agent-stream-view.tsx b/packages/app/src/components/agent-stream-view.tsx
index f8542f938..f818d80e7 100644
--- a/packages/app/src/components/agent-stream-view.tsx
+++ b/packages/app/src/components/agent-stream-view.tsx
@@ -1,9 +1,11 @@
-import { useState, useEffect, useRef } from 'react';
+import { useEffect, useRef, useMemo } from 'react';
import { View, Text, ScrollView, Pressable } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { StyleSheet } from 'react-native-unistyles';
import type { AgentStatus } from '@server/server/acp/types';
import type { SessionNotification } from '@agentclientprotocol/sdk';
+import { AgentActivityItem } from './agent-activity';
+import { parseSessionUpdate, groupTextChunks, type AgentActivity } from '@/types/agent-activity';
export interface AgentStreamViewProps {
agentId: string;
@@ -66,125 +68,6 @@ function formatTimestamp(date: Date): string {
}).format(date);
}
-function AgentUpdate({
- update,
-}: {
- update: { timestamp: Date; notification: SessionNotification };
-}) {
- const [isExpanded, setIsExpanded] = useState(true);
- const notification = update.notification as any;
-
- // Parse different notification types
- if (notification.type === 'sessionUpdate') {
- const sessionUpdate = notification.sessionUpdate || {};
-
- // Handle message chunks
- if (sessionUpdate.kind === 'agent_message_chunk') {
- return (
-
-
- {formatTimestamp(update.timestamp)}
-
- {sessionUpdate.chunk || ''}
-
- );
- }
-
- // Handle tool calls
- if (sessionUpdate.kind === 'tool_call') {
- return (
-
- setIsExpanded(!isExpanded)}
- style={stylesheet.collapsibleHeader}
- >
-
- {formatTimestamp(update.timestamp)}
-
-
- Tool Call: {sessionUpdate.toolName || 'unknown'}
-
-
- {isExpanded && sessionUpdate.arguments && (
-
-
- {JSON.stringify(sessionUpdate.arguments, null, 2)}
-
-
- )}
-
- );
- }
-
- // Handle tool call updates
- if (sessionUpdate.kind === 'tool_call_update') {
- return (
-
-
- {formatTimestamp(update.timestamp)}
-
-
- {sessionUpdate.status || 'updating'}
-
-
- );
- }
-
- // Handle available commands
- if (sessionUpdate.kind === 'available_commands_update') {
- const commands = sessionUpdate.commands || [];
- return (
-
- setIsExpanded(!isExpanded)}
- style={stylesheet.collapsibleHeader}
- >
-
- {formatTimestamp(update.timestamp)}
-
-
- Available Commands ({commands.length})
-
-
- {isExpanded && (
-
- {commands.map((cmd: any, idx: number) => (
-
- • {cmd.name || cmd}
-
- ))}
-
- )}
-
- );
- }
-
- // Generic session update
- return (
-
-
- {formatTimestamp(update.timestamp)}
-
-
- {JSON.stringify(sessionUpdate, null, 2)}
-
-
- );
- }
-
- // Fallback for unknown notification types
- return (
-
-
- {formatTimestamp(update.timestamp)}
-
-
- {JSON.stringify(notification, null, 2)}
-
-
- );
-}
-
export function AgentStreamView({
agentId,
agent,
@@ -196,10 +79,29 @@ export function AgentStreamView({
const scrollViewRef = useRef(null);
const insets = useSafeAreaInsets();
+ // Parse and group activities
+ const groupedActivities = useMemo(() => {
+ // Parse all notifications into typed activities
+ const activities: AgentActivity[] = updates
+ .map((update) => {
+ const parsedUpdate = parseSessionUpdate(update.notification);
+ if (!parsedUpdate) return null;
+
+ return {
+ timestamp: update.timestamp,
+ update: parsedUpdate,
+ };
+ })
+ .filter((activity): activity is AgentActivity => activity !== null);
+
+ // Group consecutive text chunks
+ return groupTextChunks(activities);
+ }, [updates]);
+
// Auto-scroll to bottom when new updates arrive
useEffect(() => {
scrollViewRef.current?.scrollToEnd({ animated: true });
- }, [updates]);
+ }, [groupedActivities]);
const canCancel = agent.status === 'processing';
const canKill = agent.status !== 'killed' && agent.status !== 'completed';
@@ -274,14 +176,16 @@ export function AgentStreamView({
style={stylesheet.scrollView}
contentContainerStyle={{ paddingTop: 16, paddingBottom: Math.max(insets.bottom, 32) }}
>
- {updates.length === 0 ? (
+ {groupedActivities.length === 0 ? (
No updates yet.{'\n'}Waiting for agent activity...
) : (
- updates.map((update, idx) => )
+ groupedActivities.map((item, idx) => (
+
+ ))
)}
@@ -394,60 +298,4 @@ const stylesheet = StyleSheet.create((theme) => ({
fontSize: theme.fontSize.sm,
textAlign: "center",
},
- updateCard: {
- backgroundColor: theme.colors.card,
- borderRadius: theme.borderRadius.lg,
- padding: theme.spacing[3],
- marginBottom: theme.spacing[2],
- },
- collapsibleCard: {
- backgroundColor: theme.colors.card,
- borderRadius: theme.borderRadius.lg,
- marginBottom: theme.spacing[2],
- overflow: "hidden",
- },
- collapsibleHeader: {
- paddingHorizontal: theme.spacing[3],
- paddingVertical: theme.spacing[2],
- },
- collapsibleContent: {
- paddingHorizontal: theme.spacing[3],
- paddingBottom: theme.spacing[3],
- borderTopWidth: theme.borderWidth[1],
- borderTopColor: theme.colors.border,
- },
- timestampText: {
- color: theme.colors.mutedForeground,
- fontSize: theme.fontSize.xs,
- marginBottom: theme.spacing[1],
- },
- messageText: {
- color: theme.colors.foreground,
- fontSize: theme.fontSize.sm,
- },
- toolCallText: {
- color: theme.colors.palette.blue[400],
- fontSize: theme.fontSize.sm,
- fontWeight: theme.fontWeight.semibold,
- },
- updateStatusText: {
- color: theme.colors.palette.yellow[400],
- fontSize: theme.fontSize.sm,
- },
- commandsText: {
- color: theme.colors.palette.green[400],
- fontSize: theme.fontSize.sm,
- fontWeight: theme.fontWeight.semibold,
- },
- commandItem: {
- color: theme.colors.mutedForeground,
- fontSize: theme.fontSize.xs,
- marginTop: theme.spacing[1],
- },
- codeText: {
- color: theme.colors.mutedForeground,
- fontSize: theme.fontSize.xs,
- fontFamily: "monospace",
- marginTop: theme.spacing[2],
- },
}));
diff --git a/packages/app/src/types/agent-activity.ts b/packages/app/src/types/agent-activity.ts
new file mode 100644
index 000000000..90c9be6e1
--- /dev/null
+++ b/packages/app/src/types/agent-activity.ts
@@ -0,0 +1,267 @@
+import type { SessionNotification } from '@agentclientprotocol/sdk';
+
+/**
+ * Discriminated union types for session updates
+ */
+export type SessionUpdate =
+ | UserMessageChunk
+ | AgentMessageChunk
+ | AgentThoughtChunk
+ | ToolCall
+ | ToolCallUpdate
+ | Plan
+ | AvailableCommandsUpdate
+ | CurrentModeUpdate;
+
+export interface UserMessageChunk {
+ kind: 'user_message_chunk';
+ content: {
+ type: 'text';
+ text: string;
+ };
+}
+
+export interface AgentMessageChunk {
+ kind: 'agent_message_chunk';
+ content: {
+ type: 'text';
+ text: string;
+ };
+}
+
+export interface AgentThoughtChunk {
+ kind: 'agent_thought_chunk';
+ content: {
+ type: 'text';
+ text: string;
+ };
+}
+
+export interface ToolCall {
+ kind: 'tool_call';
+ toolCallId: string;
+ title: string;
+ status?: 'pending' | 'in_progress' | 'completed' | 'failed';
+ toolKind?: 'read' | 'edit' | 'delete' | 'move' | 'search' | 'execute' | 'think' | 'fetch' | 'switch_mode' | 'other';
+ rawInput?: Record;
+ rawOutput?: Record;
+ content?: unknown[];
+ locations?: unknown[];
+}
+
+export interface ToolCallUpdate {
+ kind: 'tool_call_update';
+ toolCallId: string;
+ title?: string | null;
+ status?: 'pending' | 'in_progress' | 'completed' | 'failed' | null;
+ toolKind?: 'read' | 'edit' | 'delete' | 'move' | 'search' | 'execute' | 'think' | 'fetch' | 'switch_mode' | 'other' | null;
+ rawInput?: Record;
+ rawOutput?: Record;
+ content?: unknown[] | null;
+ locations?: unknown[] | null;
+}
+
+export interface Plan {
+ kind: 'plan';
+ entries: Array<{
+ content: string;
+ status: 'pending' | 'in_progress' | 'completed';
+ priority: 'high' | 'medium' | 'low';
+ }>;
+}
+
+export interface AvailableCommandsUpdate {
+ kind: 'available_commands_update';
+ availableCommands: Array<{
+ name: string;
+ description: string;
+ }>;
+}
+
+export interface CurrentModeUpdate {
+ kind: 'current_mode_update';
+ currentModeId: string;
+}
+
+/**
+ * Activity item with timestamp
+ */
+export interface AgentActivity {
+ timestamp: Date;
+ update: SessionUpdate;
+}
+
+/**
+ * Grouped text message (consecutive chunks combined)
+ */
+export interface GroupedTextMessage {
+ kind: 'grouped_text';
+ messageType: 'user' | 'agent' | 'thought';
+ text: string;
+ startTimestamp: Date;
+ endTimestamp: Date;
+}
+
+/**
+ * Parse SessionNotification into typed SessionUpdate
+ */
+export function parseSessionUpdate(notification: SessionNotification): SessionUpdate | null {
+ const update = (notification as any).update;
+
+ if (!update || !update.sessionUpdate) {
+ return null;
+ }
+
+ const kind = update.sessionUpdate;
+
+ switch (kind) {
+ case 'user_message_chunk':
+ return {
+ kind: 'user_message_chunk',
+ content: update.content,
+ };
+
+ case 'agent_message_chunk':
+ return {
+ kind: 'agent_message_chunk',
+ content: update.content,
+ };
+
+ case 'agent_thought_chunk':
+ return {
+ kind: 'agent_thought_chunk',
+ content: update.content,
+ };
+
+ case 'tool_call':
+ return {
+ kind: 'tool_call',
+ toolCallId: update.toolCallId,
+ title: update.title,
+ status: update.status,
+ toolKind: update.kind,
+ rawInput: update.rawInput,
+ rawOutput: update.rawOutput,
+ content: update.content,
+ locations: update.locations,
+ };
+
+ case 'tool_call_update':
+ return {
+ kind: 'tool_call_update',
+ toolCallId: update.toolCallId,
+ title: update.title,
+ status: update.status,
+ toolKind: update.kind,
+ rawInput: update.rawInput,
+ rawOutput: update.rawOutput,
+ content: update.content,
+ locations: update.locations,
+ };
+
+ case 'plan':
+ return {
+ kind: 'plan',
+ entries: update.entries,
+ };
+
+ case 'available_commands_update':
+ return {
+ kind: 'available_commands_update',
+ availableCommands: update.availableCommands,
+ };
+
+ case 'current_mode_update':
+ return {
+ kind: 'current_mode_update',
+ currentModeId: update.currentModeId,
+ };
+
+ default:
+ return null;
+ }
+}
+
+/**
+ * Group consecutive text chunks into messages
+ */
+export function groupTextChunks(activities: AgentActivity[]): Array {
+ const result: Array = [];
+ let currentGroup: {
+ messageType: 'user' | 'agent' | 'thought';
+ chunks: string[];
+ startTimestamp: Date;
+ endTimestamp: Date;
+ } | null = null;
+
+ for (const activity of activities) {
+ const update = activity.update;
+
+ // Check if this is a text chunk
+ if (
+ update.kind === 'user_message_chunk' ||
+ update.kind === 'agent_message_chunk' ||
+ update.kind === 'agent_thought_chunk'
+ ) {
+ const messageType =
+ update.kind === 'user_message_chunk' ? 'user' :
+ update.kind === 'agent_message_chunk' ? 'agent' :
+ 'thought';
+
+ const text = update.content.text;
+
+ // If we have a current group of the same type, add to it
+ if (currentGroup && currentGroup.messageType === messageType) {
+ currentGroup.chunks.push(text);
+ currentGroup.endTimestamp = activity.timestamp;
+ } else {
+ // Flush current group if exists
+ if (currentGroup) {
+ result.push({
+ kind: 'grouped_text',
+ messageType: currentGroup.messageType,
+ text: currentGroup.chunks.join(''),
+ startTimestamp: currentGroup.startTimestamp,
+ endTimestamp: currentGroup.endTimestamp,
+ });
+ }
+
+ // Start new group
+ currentGroup = {
+ messageType,
+ chunks: [text],
+ startTimestamp: activity.timestamp,
+ endTimestamp: activity.timestamp,
+ };
+ }
+ } else {
+ // Flush current group if exists
+ if (currentGroup) {
+ result.push({
+ kind: 'grouped_text',
+ messageType: currentGroup.messageType,
+ text: currentGroup.chunks.join(''),
+ startTimestamp: currentGroup.startTimestamp,
+ endTimestamp: currentGroup.endTimestamp,
+ });
+ currentGroup = null;
+ }
+
+ // Add non-text activity as-is
+ result.push(activity);
+ }
+ }
+
+ // Flush final group if exists
+ if (currentGroup) {
+ result.push({
+ kind: 'grouped_text',
+ messageType: currentGroup.messageType,
+ text: currentGroup.chunks.join(''),
+ startTimestamp: currentGroup.startTimestamp,
+ endTimestamp: currentGroup.endTimestamp,
+ });
+ }
+
+ return result;
+}
diff --git a/packages/server/src/server/acp/agent-manager.ts b/packages/server/src/server/acp/agent-manager.ts
index 5378d35a1..078e27a4e 100644
--- a/packages/server/src/server/acp/agent-manager.ts
+++ b/packages/server/src/server/acp/agent-manager.ts
@@ -349,6 +349,9 @@ export class AgentManager {
}
agent.status = "killed";
+
+ // Notify subscribers BEFORE removing from manager
+ // This ensures subscribers can still query agent info
this.notifySubscribers(agentId);
// Kill the process
@@ -361,8 +364,11 @@ export class AgentManager {
}
}, 5000);
- // Remove from manager
- this.agents.delete(agentId);
+ // Remove from manager after a small delay to allow status updates to propagate
+ setTimeout(() => {
+ this.agents.delete(agentId);
+ console.log(`[Agent ${agentId}] Removed from manager`);
+ }, 100);
}
/**
diff --git a/packages/server/src/server/session.ts b/packages/server/src/server/session.ts
index 3cf857726..881169fcf 100644
--- a/packages/server/src/server/session.ts
+++ b/packages/server/src/server/session.ts
@@ -145,6 +145,41 @@ export class Session {
notification: update.notification,
},
});
+
+ // Check if this is a status change notification
+ // The agent manager sends custom notifications with status field
+ const notification = update.notification as any;
+ if (notification && notification.sessionUpdate && notification.sessionUpdate.status) {
+ const status = notification.sessionUpdate.status;
+
+ // Get current agent info
+ try {
+ const info = this.agentManager!.listAgents().find(a => a.id === agentId);
+ if (info) {
+ // Emit agent_status message
+ this.emit({
+ type: "agent_status",
+ payload: {
+ agentId,
+ status,
+ info: {
+ id: info.id,
+ status: info.status,
+ createdAt: info.createdAt.toISOString(),
+ type: info.type,
+ sessionId: info.sessionId,
+ error: info.error,
+ currentModeId: info.currentModeId,
+ availableModes: info.availableModes,
+ },
+ },
+ });
+ console.log(`[Session ${this.clientId}] Agent ${agentId} status changed to: ${status}`);
+ }
+ } catch (error) {
+ console.error(`[Session ${this.clientId}] Failed to get agent info for status update:`, error);
+ }
+ }
}
);