diff --git a/packages/app/src/components/agent-status-bar.tsx b/packages/app/src/components/agent-status-bar.tsx index 15c20ad12..0a3d6ffcd 100644 --- a/packages/app/src/components/agent-status-bar.tsx +++ b/packages/app/src/components/agent-status-bar.tsx @@ -1,7 +1,7 @@ import { View, Text, Pressable } from "react-native"; import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { ChevronDown } from "lucide-react-native"; -import { useState, useCallback } from "react"; +import { useState, useCallback, useRef } from "react"; import { ModeSelectorModal } from "./mode-selector-modal"; import { useDaemonSession } from "@/hooks/use-daemon-session"; import type { SessionContextValue, Agent } from "@/contexts/session-context"; @@ -12,6 +12,10 @@ interface AgentStatusBarProps { } export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) { + // INVESTIGATION: Log rerenders + const renderCountRef = useRef(0); + renderCountRef.current++; + console.log(`[INVESTIGATION] AgentStatusBar render #${renderCountRef.current}`, { agentId }); const { theme } = useUnistyles(); const session = useDaemonSession(serverId, { allowUnavailable: true, suppressUnavailableAlert: true }); const noopSetAgentMode = useCallback(() => {}, []); diff --git a/packages/app/src/components/agent-stream-view.tsx b/packages/app/src/components/agent-stream-view.tsx index 9070af1dd..60a5757fe 100644 --- a/packages/app/src/components/agent-stream-view.tsx +++ b/packages/app/src/components/agent-stream-view.tsx @@ -62,6 +62,14 @@ export function AgentStreamView({ streamItems, pendingPermissions, }: AgentStreamViewProps) { + // INVESTIGATION: Log rerenders + const renderCountRef = useRef(0); + renderCountRef.current++; + console.log(`[INVESTIGATION] AgentStreamView render #${renderCountRef.current}`, { + agentId, + streamItemsLength: streamItems.length, + pendingPermissionsSize: pendingPermissions.size, + }); const flatListRef = useRef>(null); const insets = useSafeAreaInsets(); const [isNearBottom, setIsNearBottom] = useState(true); diff --git a/packages/app/src/components/create-agent-modal.tsx b/packages/app/src/components/create-agent-modal.tsx index 8a923ad54..e5e3fc3d6 100644 --- a/packages/app/src/components/create-agent-modal.tsx +++ b/packages/app/src/components/create-agent-modal.tsx @@ -2153,6 +2153,9 @@ function AgentFlowModal({ value={initialPrompt} isLoading={isLoading} onChange={(text) => { + console.log('[INVESTIGATION] CreateModal onChange called', { + textLength: text.length, + }); setInitialPrompt(text); setErrorMessage(""); }} diff --git a/packages/app/src/contexts/session-context.tsx b/packages/app/src/contexts/session-context.tsx index c6ec05cef..06953de31 100644 --- a/packages/app/src/contexts/session-context.tsx +++ b/packages/app/src/contexts/session-context.tsx @@ -672,11 +672,21 @@ export function SessionProvider({ children, serverUrl, serverId }: SessionProvid setAgentStreamState(serverId, (prev) => { const currentStream = prev.get(agentId) || []; + const lastItem = currentStream[currentStream.length - 1]; + const lastItemText = lastItem && "text" in lastItem ? lastItem.text : "(no text)"; + console.log(`[AGENT_STREAM BEFORE] agentId=${agentId} currentStreamLength=${currentStream.length} lastText="${lastItemText}"`); + console.log(`[AGENT_STREAM CHUNK] incoming type=${event.type}`); + const newStream = reduceStreamUpdate( currentStream, event as AgentStreamEventPayload, parsedTimestamp ); + + const newLastItem = newStream[newStream.length - 1]; + const newLastText = newLastItem && "text" in newLastItem ? newLastItem.text : "(no text)"; + console.log(`[AGENT_STREAM AFTER] newStreamLength=${newStream.length} newLastText="${newLastText}"`); + const next = new Map(prev); next.set(agentId, newStream); return next; @@ -1079,12 +1089,7 @@ export function SessionProvider({ children, serverUrl, serverId }: SessionProvid }); // Remove draft input - const session = getSession(serverId); - if (session) { - const nextDraftInputs = new Map(session.draftInputs); - nextDraftInputs.delete(agentId); - saveDraftInput(serverId, agentId, { text: "", images: [] }); // Clear it - } + saveDraftInput(agentId, { text: "", images: [] }); setPendingPermissions(serverId, (prev) => { let changed = false; diff --git a/packages/app/src/hooks/use-daemon-session.ts b/packages/app/src/hooks/use-daemon-session.ts index fe9c1d2b5..51326cf88 100644 --- a/packages/app/src/hooks/use-daemon-session.ts +++ b/packages/app/src/hooks/use-daemon-session.ts @@ -71,6 +71,7 @@ export function useDaemonSession( options: UseDaemonSessionOptions & { allowUnavailable: true } ): DaemonSession | null; export function useDaemonSession(serverId?: string | null, options?: UseDaemonSessionOptions) { + const selectSession = useCallback( (state: ReturnType) => { if (!serverId) { @@ -93,14 +94,14 @@ export function useDaemonSession(serverId?: string | null, options?: UseDaemonSe const setMessagesAction = useSessionStore((state) => state.setMessages); const setQueuedMessagesAction = useSessionStore((state) => state.setQueuedMessages); - // Create stable wrapper functions that bind serverId (must be before conditional returns) + // Create stable wrapper functions (must be before conditional returns) const boundGetDraftInput = useCallback( - (agentId: string) => serverId ? getDraftInputAction(serverId, agentId) : undefined, - [serverId, getDraftInputAction] + (agentId: string) => getDraftInputAction(agentId), + [getDraftInputAction] ); const boundSaveDraftInput = useCallback( - (agentId: string, draft: any) => { if (serverId) saveDraftInputAction(serverId, agentId, draft); }, - [serverId, saveDraftInputAction] + (agentId: string, draft: any) => { saveDraftInputAction(agentId, draft); }, + [saveDraftInputAction] ); const boundSetFocusedAgentId = useCallback( (agentId: string | null) => { if (serverId) setFocusedAgentIdAction(serverId, agentId); }, diff --git a/packages/app/src/hooks/use-websocket.ts b/packages/app/src/hooks/use-websocket.ts index 3dd9802ff..fd0a49813 100644 --- a/packages/app/src/hooks/use-websocket.ts +++ b/packages/app/src/hooks/use-websocket.ts @@ -159,6 +159,12 @@ export function useWebSocket(url: string, conversationId?: string | null): UseWe (sessionMessage as { payload?: { agentId?: string } }).payload?.agentId; console.log(`[WS] ← ${sessionMessage.type}`, { size, id: id ?? "-" }); + // Log agent_stream messages for debugging + if (sessionMessage.type === "agent_stream") { + const payload = (sessionMessage as { payload: { agentId: string; event: { type: string } } }).payload; + console.log(`[WS AGENT_STREAM] timestamp=${Date.now()} agentId=${payload.agentId} eventType=${payload.event.type}`); + } + // Track conversation ID when loaded if (sessionMessage.type === "conversation_loaded") { setCurrentConversationId(sessionMessage.payload.conversationId); diff --git a/packages/app/src/stores/session-store.ts b/packages/app/src/stores/session-store.ts index 08707f7c5..5cedaee25 100644 --- a/packages/app/src/stores/session-store.ts +++ b/packages/app/src/stores/session-store.ts @@ -230,9 +230,6 @@ export interface SessionState { // Provider models providerModels: Map; - // Draft inputs (non-serializable, stored in Map) - draftInputs: Map; - // Queued messages queuedMessages: Map }>>; } @@ -240,6 +237,12 @@ export interface SessionState { // Global store state interface SessionStoreState { sessions: Record; + + // Top-level drafts (keyed by agentId, not serverId:agentId since agent IDs are globally unique) + drafts: Map; + + // Single draft for the create agent modal + createModalDraft: DraftInput | null; } // Action types @@ -284,9 +287,13 @@ interface SessionStoreActions { // Provider models setProviderModels: (serverId: string, models: Map | ((prev: Map) => Map)) => void; - // Draft inputs - getDraftInput: (serverId: string, agentId: string) => DraftInput | undefined; - saveDraftInput: (serverId: string, agentId: string, draft: DraftInput) => void; + // Draft inputs (top-level, not in sessions) + getDraftInput: (agentId: string) => DraftInput | undefined; + saveDraftInput: (agentId: string, draft: DraftInput) => void; + + // Create modal draft + getCreateModalDraft: () => DraftInput | null; + saveCreateModalDraft: (draft: DraftInput | null) => void; // Queued messages setQueuedMessages: (serverId: string, value: Map }>> | ((prev: Map }>>) => Map }>>)) => void; @@ -347,7 +354,6 @@ function createInitialSessionState(serverId: string, ws: UseWebSocketReturn, aud gitDiffs: new Map(), fileExplorer: new Map(), providerModels: new Map(), - draftInputs: new Map(), queuedMessages: new Map(), }; } @@ -355,6 +361,8 @@ function createInitialSessionState(serverId: string, ws: UseWebSocketReturn, aud export const useSessionStore = create()( subscribeWithSelector((set, get) => ({ sessions: {}, + drafts: new Map(), + createModalDraft: null, // Session management initializeSession: (serverId, ws, audioPlayer) => { @@ -675,34 +683,34 @@ export const useSessionStore = create()( }); }, - // Draft inputs - getDraftInput: (serverId, agentId) => { - const session = get().sessions[serverId]; - if (!session) { - return undefined; - } - return session.draftInputs.get(agentId); + // Draft inputs (top-level, keyed by agentId only) + getDraftInput: (agentId) => { + return get().drafts.get(agentId); }, - saveDraftInput: (serverId, agentId, draft) => { + saveDraftInput: (agentId, draft) => { set((prev) => { - const session = prev.sessions[serverId]; - if (!session) { - return prev; - } - const nextDraftInputs = new Map(session.draftInputs); - nextDraftInputs.set(agentId, draft); - logSessionStoreUpdate("saveDraftInput", serverId, { agentId }); + const nextDrafts = new Map(prev.drafts); + nextDrafts.set(agentId, draft); return { ...prev, - sessions: { - ...prev.sessions, - [serverId]: { ...session, draftInputs: nextDraftInputs }, - }, + drafts: nextDrafts, }; }); }, + // Create modal draft + getCreateModalDraft: () => { + return get().createModalDraft; + }, + + saveCreateModalDraft: (draft) => { + set((prev) => ({ + ...prev, + createModalDraft: draft, + })); + }, + // Queued messages setQueuedMessages: (serverId, value) => { set((prev) => { diff --git a/packages/server/src/server/session.ts b/packages/server/src/server/session.ts index 09aeb24b4..3e4dc2713 100644 --- a/packages/server/src/server/session.ts +++ b/packages/server/src/server/session.ts @@ -514,6 +514,7 @@ export class Session { timestamp: new Date().toISOString(), } as const; + console.log(`[SERVER AGENT_STREAM] timestamp=${Date.now()} agentId=${event.agentId} eventType=${event.event.type}`); this.emit({ type: "agent_stream", payload,