From d832f656599b153be8826bc43e1832209b2a1bf6 Mon Sep 17 00:00:00 2001
From: Matthias Nott <mnott@mnsoft.org>
Date: Sun, 08 Mar 2026 07:10:12 +0100
Subject: [PATCH] feat: per-session typing, incoming toast, remove switched-to messages

---
 contexts/ChatContext.tsx |   56 ++++++++++++++++++++++++++++++++++++++------------------
 1 files changed, 38 insertions(+), 18 deletions(-)

diff --git a/contexts/ChatContext.tsx b/contexts/ChatContext.tsx
index 5269516..b109bb7 100644
--- a/contexts/ChatContext.tsx
+++ b/contexts/ChatContext.tsx
@@ -116,6 +116,12 @@
 
 // --- Context ---
 
+interface IncomingToast {
+  sessionId: string;
+  sessionName: string;
+  preview: string;
+}
+
 interface ChatContextValue {
   messages: Message[];
   sendTextMessage: (text: string) => void;
@@ -136,6 +142,8 @@
   loadMoreMessages: () => void;
   hasMoreMessages: boolean;
   unreadCounts: Record<string, number>;
+  incomingToast: IncomingToast | null;
+  dismissToast: () => void;
   latestScreenshot: string | null;
   requestScreenshot: () => void;
   sendNavKey: (key: string) => void;
@@ -156,8 +164,11 @@
   const [messages, setMessages] = useState<Message[]>([]);
   // Unread counts for non-active sessions
   const [unreadCounts, setUnreadCounts] = useState<Record<string, number>>({});
-  // Typing indicator from server
+  // Per-session typing indicator (sessionId → boolean)
+  const typingMapRef = useRef<Record<string, boolean>>({});
   const [isTyping, setIsTyping] = useState(false);
+  // Toast for other-session incoming messages
+  const [incomingToast, setIncomingToast] = useState<{ sessionId: string; sessionName: string; preview: string } | null>(null);
   // PAI projects list
   const [projects, setProjects] = useState<PaiProject[]>([]);
   // Pagination: does the active session have more messages in storage?
@@ -199,6 +210,9 @@
             delete next[active.id];
             return next;
           });
+          // Sync typing indicator for the new active session
+          const activeTyping = typingMapRef.current[active.id] ?? false;
+          setIsTyping(activeTyping);
         }
         activeSessionIdRef.current = active.id;
         return active.id;
@@ -251,6 +265,16 @@
         ...u,
         [sessionId]: (u[sessionId] ?? 0) + 1,
       }));
+      // Show toast for other-session messages (assistant only, skip system noise)
+      if (msg.role === "assistant") {
+        setSessions((prev) => {
+          const session = prev.find((s) => s.id === sessionId);
+          const name = session?.name ?? sessionId.slice(0, 8);
+          const preview = msg.type === "voice" ? "🎤 Voice note" : msg.type === "image" ? "📷 Image" : (msg.content ?? "").slice(0, 60);
+          setIncomingToast({ sessionId, sessionName: name, preview });
+          return prev;
+        });
+      }
     }
   }, []);
 
@@ -359,26 +383,12 @@
           break;
         }
         case "session_switched": {
-          const msg: Message = {
-            id: generateId(),
-            role: "system",
-            type: "text",
-            content: `Switched to ${data.name}`,
-            timestamp: Date.now(),
-          };
-          addMessageToActive(msg);
+          // Just refresh session list — no system message needed
           sendCommand("sessions");
           break;
         }
         case "session_renamed": {
-          const msg: Message = {
-            id: generateId(),
-            role: "system",
-            type: "text",
-            content: `Renamed to ${data.name}`,
-            timestamp: Date.now(),
-          };
-          addMessageToActive(msg);
+          // Just refresh session list — no system message needed
           sendCommand("sessions");
           break;
         }
@@ -388,7 +398,11 @@
           break;
         }
         case "typing": {
-          setIsTyping(data.typing);
+          const typingSession = (data.sessionId as string) || activeSessionIdRef.current || "_global";
+          typingMapRef.current[typingSession] = !!data.typing;
+          // Only show typing indicator if it's for the active session
+          const activeTyping = typingMapRef.current[activeSessionIdRef.current ?? ""] ?? false;
+          setIsTyping(activeTyping);
           break;
         }
         case "status": {
@@ -545,6 +559,10 @@
     sendCommand("projects");
   }, [sendCommand]);
 
+  const dismissToast = useCallback(() => {
+    setIncomingToast(null);
+  }, []);
+
   const loadMoreMessages = useCallback(() => {
     const sessId = activeSessionIdRef.current;
     if (!sessId) return;
@@ -595,6 +613,8 @@
         loadMoreMessages,
         hasMoreMessages,
         unreadCounts,
+        incomingToast,
+        dismissToast,
         latestScreenshot,
         requestScreenshot,
         sendNavKey,

--
Gitblit v1.3.1