Matthias Nott
2026-03-07 0e888d62af1434fef231e11a5c307a5b48a8deb1
contexts/ChatContext.tsx
....@@ -26,7 +26,9 @@
2626
2727 const MESSAGES_DIR = "pailot-messages";
2828
29
-/** Strip heavy fields (base64 images, audio URIs) before persisting. */
29
+/** Strip heavy fields (base64 images, audio URIs) before persisting.
30
+ * Voice messages keep their content (transcript) but lose audioUri
31
+ * since cache files won't survive app restarts. */
3032 function lightMessage(m: Message): Message {
3133 const light = { ...m };
3234 if (light.imageBase64) light.imageBase64 = undefined;
....@@ -63,7 +65,16 @@
6365 if (!file.endsWith(".json")) continue;
6466 const sessionId = file.replace(".json", "");
6567 const content = await fs.readAsStringAsync(`${dir}${file}`);
66
- result[sessionId] = JSON.parse(content) as Message[];
68
+ result[sessionId] = (JSON.parse(content) as Message[])
69
+ // Drop voice messages with no audio and no content (empty chunks)
70
+ .filter((m) => !(m.type === "voice" && !m.audioUri && !m.content))
71
+ .map((m) => {
72
+ // Voice messages without audio but with transcript → show as text
73
+ if (m.type === "voice" && !m.audioUri && m.content) {
74
+ return { ...m, type: "text" };
75
+ }
76
+ return m;
77
+ });
6778 }
6879 return result;
6980 } catch {
....@@ -179,12 +190,15 @@
179190 }
180191 }, [messages]);
181192
182
- // On connect: ask gateway to detect the focused iTerm2 session and sync
193
+ // On connect: ask gateway to sync sessions. If we already had a session
194
+ // selected, tell the gateway so it preserves our selection instead of
195
+ // jumping to whatever iTerm has focused on the Mac.
183196 useEffect(() => {
184197 if (status === "connected") {
185198 needsSync.current = true;
186
- sendCommand("sync");
199
+ sendCommand("sync", activeSessionId ? { activeSessionId } : undefined);
187200 }
201
+ // eslint-disable-next-line react-hooks/exhaustive-deps — only fire on status change
188202 }, [status, sendCommand]);
189203
190204 // Helper: add a message to the active session
....@@ -233,6 +247,23 @@
233247 },
234248 []
235249 );
250
+
251
+ // Update a message's content (e.g., voice transcript reflection)
252
+ const updateMessageContent = useCallback((id: string, content: string) => {
253
+ setMessages((prev) => {
254
+ const next = prev.map((m) =>
255
+ m.id === id ? { ...m, content } : m
256
+ );
257
+ setActiveSessionId((sessId) => {
258
+ if (sessId) {
259
+ messagesMapRef.current[sessId] = next;
260
+ debouncedSave(messagesMapRef.current);
261
+ }
262
+ return sessId;
263
+ });
264
+ return next;
265
+ });
266
+ }, []);
236267
237268 // Handle incoming WebSocket messages
238269 useEffect(() => {
....@@ -322,6 +353,11 @@
322353 sendCommand("sessions");
323354 break;
324355 }
356
+ case "transcript": {
357
+ // Voice → text reflection: replace voice bubble with transcribed text
358
+ updateMessageContent(data.messageId, data.content);
359
+ break;
360
+ }
325361 case "error": {
326362 const msg: Message = {
327363 id: generateId(),
....@@ -339,7 +375,7 @@
339375 return () => {
340376 onMessageReceived.current = null;
341377 };
342
- }, [onMessageReceived, sendCommand, addMessageToActive, syncActiveFromSessions]);
378
+ }, [onMessageReceived, sendCommand, addMessageToActive, updateMessageContent, syncActiveFromSessions]);
343379
344380 const sendTextMessage = useCallback(
345381 (text: string) => {
....@@ -375,7 +411,7 @@
375411 addMessageToActive(msg);
376412 try {
377413 const base64 = await encodeAudioToBase64(audioUri);
378
- const sent = wsVoice(base64);
414
+ const sent = wsVoice(base64, "", id);
379415 updateMessageStatus(id, sent ? "sent" : "error");
380416 } catch (err) {
381417 console.error("Failed to encode audio:", err);