From 0e888d62af1434fef231e11a5c307a5b48a8deb1 Mon Sep 17 00:00:00 2001
From: Matthias Nott <mnott@mnsoft.org>
Date: Sat, 07 Mar 2026 10:49:07 +0100
Subject: [PATCH] feat: singleton audio, transcript reflection, voice persistence

---
 contexts/ChatContext.tsx |   48 ++++++++++++++++++++++++++++++++++++++++++------
 1 files changed, 42 insertions(+), 6 deletions(-)

diff --git a/contexts/ChatContext.tsx b/contexts/ChatContext.tsx
index 0865375..151f6f7 100644
--- a/contexts/ChatContext.tsx
+++ b/contexts/ChatContext.tsx
@@ -26,7 +26,9 @@
 
 const MESSAGES_DIR = "pailot-messages";
 
-/** Strip heavy fields (base64 images, audio URIs) before persisting. */
+/** Strip heavy fields (base64 images, audio URIs) before persisting.
+ *  Voice messages keep their content (transcript) but lose audioUri
+ *  since cache files won't survive app restarts. */
 function lightMessage(m: Message): Message {
   const light = { ...m };
   if (light.imageBase64) light.imageBase64 = undefined;
@@ -63,7 +65,16 @@
       if (!file.endsWith(".json")) continue;
       const sessionId = file.replace(".json", "");
       const content = await fs.readAsStringAsync(`${dir}${file}`);
-      result[sessionId] = JSON.parse(content) as Message[];
+      result[sessionId] = (JSON.parse(content) as Message[])
+        // Drop voice messages with no audio and no content (empty chunks)
+        .filter((m) => !(m.type === "voice" && !m.audioUri && !m.content))
+        .map((m) => {
+          // Voice messages without audio but with transcript → show as text
+          if (m.type === "voice" && !m.audioUri && m.content) {
+            return { ...m, type: "text" };
+          }
+          return m;
+        });
     }
     return result;
   } catch {
@@ -179,12 +190,15 @@
     }
   }, [messages]);
 
-  // On connect: ask gateway to detect the focused iTerm2 session and sync
+  // On connect: ask gateway to sync sessions. If we already had a session
+  // selected, tell the gateway so it preserves our selection instead of
+  // jumping to whatever iTerm has focused on the Mac.
   useEffect(() => {
     if (status === "connected") {
       needsSync.current = true;
-      sendCommand("sync");
+      sendCommand("sync", activeSessionId ? { activeSessionId } : undefined);
     }
+    // eslint-disable-next-line react-hooks/exhaustive-deps — only fire on status change
   }, [status, sendCommand]);
 
   // Helper: add a message to the active session
@@ -233,6 +247,23 @@
     },
     []
   );
+
+  // Update a message's content (e.g., voice transcript reflection)
+  const updateMessageContent = useCallback((id: string, content: string) => {
+    setMessages((prev) => {
+      const next = prev.map((m) =>
+        m.id === id ? { ...m, content } : m
+      );
+      setActiveSessionId((sessId) => {
+        if (sessId) {
+          messagesMapRef.current[sessId] = next;
+          debouncedSave(messagesMapRef.current);
+        }
+        return sessId;
+      });
+      return next;
+    });
+  }, []);
 
   // Handle incoming WebSocket messages
   useEffect(() => {
@@ -322,6 +353,11 @@
           sendCommand("sessions");
           break;
         }
+        case "transcript": {
+          // Voice → text reflection: replace voice bubble with transcribed text
+          updateMessageContent(data.messageId, data.content);
+          break;
+        }
         case "error": {
           const msg: Message = {
             id: generateId(),
@@ -339,7 +375,7 @@
     return () => {
       onMessageReceived.current = null;
     };
-  }, [onMessageReceived, sendCommand, addMessageToActive, syncActiveFromSessions]);
+  }, [onMessageReceived, sendCommand, addMessageToActive, updateMessageContent, syncActiveFromSessions]);
 
   const sendTextMessage = useCallback(
     (text: string) => {
@@ -375,7 +411,7 @@
       addMessageToActive(msg);
       try {
         const base64 = await encodeAudioToBase64(audioUri);
-        const sent = wsVoice(base64);
+        const sent = wsVoice(base64, "", id);
         updateMessageStatus(id, sent ? "sent" : "error");
       } catch (err) {
         console.error("Failed to encode audio:", err);

--
Gitblit v1.3.1