File size: 4,433 Bytes
854c261
 
 
 
571b292
 
854c261
d2c1b12
854c261
 
 
 
 
 
 
 
 
 
 
 
 
 
 
723c24c
854c261
 
d2c1b12
 
f915e8e
854c261
 
 
 
 
 
 
723c24c
 
 
854c261
723c24c
 
 
 
 
854c261
723c24c
 
00a57cd
723c24c
 
00a57cd
723c24c
854c261
723c24c
 
 
 
854c261
d2c1b12
 
 
 
 
 
f915e8e
 
 
 
854c261
 
f915e8e
854c261
d2c1b12
723c24c
854c261
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
723c24c
854c261
 
 
 
 
 
 
 
 
f915e8e
854c261
 
 
 
 
d2c1b12
f915e8e
854c261
d2c1b12
 
 
 
 
 
 
854c261
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
/**
 * Per-session chat component.
 *
 * Each session renders its own SessionChat. The hook (useAgentChat) always
 * runs — processing events — but only the active session renders visible
 * UI (MessageList + ChatInput).
 */
import { useCallback, useEffect, useState } from 'react';
import { useAgentChat } from '@/hooks/useAgentChat';
import { useAgentStore } from '@/store/agentStore';
import { useSessionStore } from '@/store/sessionStore';
import MessageList from '@/components/Chat/MessageList';
import ChatInput from '@/components/Chat/ChatInput';
import { apiFetch } from '@/utils/api';
import { logger } from '@/utils/logger';

interface SessionChatProps {
  sessionId: string;
  isActive: boolean;
  onSessionDead: (sessionId: string) => void;
}

export default function SessionChat({ sessionId, isActive, onSessionDead }: SessionChatProps) {
  const { isConnected, isProcessing, activityStatus, updateSession } = useAgentStore();
  const { updateSessionTitle } = useSessionStore();

  const [wasCancelled, setWasCancelled] = useState(false);

  const { messages, sendMessage, stop, status, undoLastTurn, approveTools } = useAgentChat({
    sessionId,
    isActive,
    onReady: () => logger.log(`Session ${sessionId} ready`),
    onError: (error) => logger.error(`Session ${sessionId} error:`, error),
    onSessionDead,
  });

  // When this session becomes active, restore its per-session state to the
  // global flat fields. The per-session state map is kept up-to-date by
  // side-channel callbacks even while the session is in the background.
  useEffect(() => {
    if (isActive) {
      useAgentStore.getState().switchActiveSession(sessionId);
      useAgentStore.getState().setConnected(true);
    }
  }, [isActive, sessionId]);

  // Re-sync state when the browser tab regains focus (Chrome throttles
  // timers in background tabs which can stall the AI SDK's update flushing).
  // Fires for ALL sessions so background sessions also recover after sleep.
  useEffect(() => {
    const onVisible = () => {
      if (document.visibilityState === 'visible' && isActive) {
        useAgentStore.getState().switchActiveSession(sessionId);
      }
    };
    document.addEventListener('visibilitychange', onVisible);
    return () => document.removeEventListener('visibilitychange', onVisible);
  }, [isActive, sessionId]);

  // Wrap stop to track cancellation
  const handleStop = useCallback(() => {
    stop();
    setWasCancelled(true);
  }, [stop]);

  // SDK status is the ground truth — if it's streaming/submitted, agent is busy
  const sdkBusy = status === 'streaming' || status === 'submitted';
  const busy = isProcessing || sdkBusy;

  const handleSendMessage = useCallback(
    async (text: string) => {
      if (!text.trim() || busy) return;

      setWasCancelled(false);
      updateSession(sessionId, { isProcessing: true });
      sendMessage({ text: text.trim(), metadata: { createdAt: new Date().toISOString() } });

      // Auto-title the session from the first user message
      const isFirstMessage = messages.filter((m) => m.role === 'user').length <= 1;
      if (isFirstMessage) {
        apiFetch('/api/title', {
          method: 'POST',
          body: JSON.stringify({ session_id: sessionId, text: text.trim() }),
        })
          .then((res) => res.json())
          .then((data) => {
            if (data.title) updateSessionTitle(sessionId, data.title);
          })
          .catch(() => {
            const raw = text.trim();
            updateSessionTitle(sessionId, raw.length > 40 ? raw.slice(0, 40) + '\u2026' : raw);
          });
      }
    },
    [sessionId, sendMessage, messages, updateSessionTitle, busy, updateSession],
  );

  // Don't render UI for background sessions — hooks still run
  if (!isActive) return null;

  return (
    <>
      <MessageList
        messages={messages}
        isProcessing={busy}
        approveTools={approveTools}
        onUndoLastTurn={undoLastTurn}
      />
      <ChatInput
        onSend={handleSendMessage}
        onStop={handleStop}
        isProcessing={busy}
        disabled={!isConnected || activityStatus.type === 'waiting-approval'}
        placeholder={
          activityStatus.type === 'waiting-approval'
            ? 'Approve or reject pending tools first...'
            : wasCancelled
              ? 'What should the agent do instead?'
              : undefined
        }
      />
    </>
  );
}