botschat 0.1.15 → 0.1.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/package.json +4 -1
  2. package/packages/api/src/do/connection-do.ts +106 -18
  3. package/packages/api/src/index.ts +59 -3
  4. package/packages/plugin/dist/src/channel.d.ts.map +1 -1
  5. package/packages/plugin/dist/src/channel.js +75 -6
  6. package/packages/plugin/dist/src/channel.js.map +1 -1
  7. package/packages/plugin/dist/src/types.d.ts +1 -0
  8. package/packages/plugin/dist/src/types.d.ts.map +1 -1
  9. package/packages/plugin/dist/src/ws-client.d.ts.map +1 -1
  10. package/packages/plugin/dist/src/ws-client.js +11 -6
  11. package/packages/plugin/dist/src/ws-client.js.map +1 -1
  12. package/packages/plugin/package.json +1 -1
  13. package/packages/web/dist/assets/{index-cm_3YFsA.css → index-B5GU1yVt.css} +1 -1
  14. package/packages/web/dist/assets/index-CO9YgLst.js +2 -0
  15. package/packages/web/dist/assets/index-ClDrCe_c.js +1 -0
  16. package/packages/web/dist/assets/{index-DsWBWQD6.js → index-D3T7sc-R.js} +1 -1
  17. package/packages/web/dist/assets/index-DPEosppm.js +2 -0
  18. package/packages/web/dist/assets/{index-dMn_npR3.js → index-DzYqprDN.js} +1 -1
  19. package/packages/web/dist/assets/index-IVUdSd9w.js +1516 -0
  20. package/packages/web/dist/assets/{index.esm-DdTIpXjl.js → index.esm-COzWPkKi.js} +1 -1
  21. package/packages/web/dist/assets/{web-Dft_LGIH.js → web-CxXbaApe.js} +1 -1
  22. package/packages/web/dist/assets/{web-DIeOUVhn.js → web-DFQypSd0.js} +1 -1
  23. package/packages/web/dist/index.html +2 -2
  24. package/packages/web/dist/sw.js +9 -1
  25. package/packages/web/src/App.tsx +34 -5
  26. package/packages/web/src/api.ts +7 -4
  27. package/packages/web/src/components/ChatWindow.tsx +139 -74
  28. package/packages/web/src/components/CronSidebar.tsx +5 -1
  29. package/packages/web/src/components/LoginPage.tsx +3 -1
  30. package/packages/web/src/components/OnboardingPage.tsx +3 -1
  31. package/packages/web/src/components/ScheduleEditor.tsx +120 -47
  32. package/packages/web/src/components/SessionTabs.tsx +5 -1
  33. package/packages/web/src/components/Sidebar.tsx +8 -2
  34. package/packages/web/src/components/TaskBar.tsx +5 -1
  35. package/packages/web/src/components/ThreadPanel.tsx +26 -3
  36. package/packages/web/src/firebase.ts +3 -2
  37. package/packages/web/src/foreground.ts +40 -10
  38. package/packages/web/src/hooks/useIMEComposition.ts +36 -0
  39. package/packages/web/src/main.tsx +3 -2
  40. package/packages/web/src/push.ts +88 -1
  41. package/packages/web/src/store.ts +4 -0
  42. package/packages/web/src/ws.ts +4 -3
  43. package/packages/web/dist/assets/index-CbCpFrA9.js +0 -2
  44. package/packages/web/dist/assets/index-Ct0m11C8.js +0 -2
  45. package/packages/web/dist/assets/index-CvbTpaza.js +0 -1516
  46. package/packages/web/dist/assets/index-GwprVhDP.js +0 -1
@@ -2,6 +2,7 @@ import React, { useState, useRef, useEffect, useCallback } from "react";
2
2
  import { useAppState, useAppDispatch } from "../store";
3
3
  import { sessionsApi, channelsApi, agentsApi } from "../api";
4
4
  import { dlog } from "../debug-log";
5
+ import { useIMEComposition } from "../hooks/useIMEComposition";
5
6
 
6
7
  // ---------------------------------------------------------------------------
7
8
  // Session history — tracks per-channel usage order in localStorage
@@ -88,6 +89,7 @@ export function SessionTabs({ channelId }: SessionTabsProps) {
88
89
  const state = useAppState();
89
90
  const dispatch = useAppDispatch();
90
91
  const [editingId, setEditingId] = useState<string | null>(null);
92
+ const { onCompositionStart, onCompositionEnd, isIMEActive } = useIMEComposition();
91
93
  const [editValue, setEditValue] = useState("");
92
94
  const [confirmDeleteId, setConfirmDeleteId] = useState<string | null>(null);
93
95
  const editRef = useRef<HTMLInputElement>(null);
@@ -267,7 +269,7 @@ export function SessionTabs({ channelId }: SessionTabsProps) {
267
269
  onChange={(e) => setEditValue(e.target.value)}
268
270
  onBlur={commitRename}
269
271
  onKeyDown={(e) => {
270
- if (e.key === "Enter" && !e.nativeEvent.isComposing) {
272
+ if (e.key === "Enter" && !e.nativeEvent.isComposing && !isIMEActive()) {
271
273
  e.preventDefault();
272
274
  commitRename();
273
275
  }
@@ -275,6 +277,8 @@ export function SessionTabs({ channelId }: SessionTabsProps) {
275
277
  setEditingId(null);
276
278
  }
277
279
  }}
280
+ onCompositionStart={onCompositionStart}
281
+ onCompositionEnd={onCompositionEnd}
278
282
  className="px-2.5 py-1 text-caption rounded-t-md focus:outline-none"
279
283
  style={{
280
284
  background: "var(--bg-hover)",
@@ -2,6 +2,7 @@ import React, { useState, useRef, useEffect } from "react";
2
2
  import { useAppState, useAppDispatch } from "../store";
3
3
  import { agentsApi, channelsApi } from "../api";
4
4
  import { dlog } from "../debug-log";
5
+ import { useIMEComposition } from "../hooks/useIMEComposition";
5
6
 
6
7
  export function Sidebar({ onOpenSettings, onNavigate }: { onOpenSettings?: () => void; onNavigate?: () => void } = {}) {
7
8
  const state = useAppState();
@@ -9,6 +10,7 @@ export function Sidebar({ onOpenSettings, onNavigate }: { onOpenSettings?: () =>
9
10
  const [showCreate, setShowCreate] = useState(false);
10
11
  const [newName, setNewName] = useState("");
11
12
  const [newDesc, setNewDesc] = useState("");
13
+ const { onCompositionStart, onCompositionEnd, isIMEActive } = useIMEComposition();
12
14
  const [channelsExpanded, setChannelsExpanded] = useState(true);
13
15
 
14
16
  const handleCreate = async () => {
@@ -189,7 +191,9 @@ export function Sidebar({ onOpenSettings, onNavigate }: { onOpenSettings?: () =>
189
191
  placeholder="Channel name"
190
192
  value={newName}
191
193
  onChange={(e) => setNewName(e.target.value)}
192
- onKeyDown={(e) => e.key === "Enter" && !e.nativeEvent.isComposing && handleCreate()}
194
+ onKeyDown={(e) => e.key === "Enter" && !e.nativeEvent.isComposing && !isIMEActive() && handleCreate()}
195
+ onCompositionStart={onCompositionStart}
196
+ onCompositionEnd={onCompositionEnd}
193
197
  className="w-full px-2 py-1.5 text-caption text-[--text-sidebar] rounded-sm focus:outline-none placeholder:text-[--text-muted]"
194
198
  style={{ background: "var(--sidebar-hover)", border: "1px solid var(--sidebar-border)" }}
195
199
  autoFocus
@@ -199,7 +203,9 @@ export function Sidebar({ onOpenSettings, onNavigate }: { onOpenSettings?: () =>
199
203
  placeholder="Description (optional)"
200
204
  value={newDesc}
201
205
  onChange={(e) => setNewDesc(e.target.value)}
202
- onKeyDown={(e) => e.key === "Enter" && !e.nativeEvent.isComposing && handleCreate()}
206
+ onKeyDown={(e) => e.key === "Enter" && !e.nativeEvent.isComposing && !isIMEActive() && handleCreate()}
207
+ onCompositionStart={onCompositionStart}
208
+ onCompositionEnd={onCompositionEnd}
203
209
  className="w-full px-2 py-1.5 text-caption text-[--text-sidebar] rounded-sm focus:outline-none placeholder:text-[--text-muted]"
204
210
  style={{ background: "var(--sidebar-hover)", border: "1px solid var(--sidebar-border)" }}
205
211
  />
@@ -1,6 +1,7 @@
1
1
  import React, { useState } from "react";
2
2
  import { useAppState, useAppDispatch } from "../store";
3
3
  import { tasksApi, type Task } from "../api";
4
+ import { useIMEComposition } from "../hooks/useIMEComposition";
4
5
 
5
6
  const SCHEDULE_PRESETS = [
6
7
  { label: "Every 30 min", value: "every 30m" },
@@ -18,6 +19,7 @@ export function TaskBar() {
18
19
  const state = useAppState();
19
20
  const dispatch = useAppDispatch();
20
21
  const [showCreate, setShowCreate] = useState(false);
22
+ const { onCompositionStart, onCompositionEnd, isIMEActive } = useIMEComposition();
21
23
  const [newTaskName, setNewTaskName] = useState("");
22
24
  const [newTaskKind, setNewTaskKind] = useState<"adhoc" | "background">("adhoc");
23
25
  const [newSchedule, setNewSchedule] = useState("");
@@ -200,7 +202,9 @@ export function TaskBar() {
200
202
  placeholder="Task name"
201
203
  value={newTaskName}
202
204
  onChange={(e) => setNewTaskName(e.target.value)}
203
- onKeyDown={(e) => e.key === "Enter" && !e.nativeEvent.isComposing && handleCreate()}
205
+ onKeyDown={(e) => e.key === "Enter" && !e.nativeEvent.isComposing && !isIMEActive() && handleCreate()}
206
+ onCompositionStart={onCompositionStart}
207
+ onCompositionEnd={onCompositionEnd}
204
208
  className="px-2 py-1 text-caption rounded-sm focus:outline-none w-36 placeholder:text-[--text-muted]"
205
209
  style={{
206
210
  background: "var(--bg-hover)",
@@ -3,9 +3,11 @@ import { useAppState, useAppDispatch, type ChatMessage } from "../store";
3
3
  import { messagesApi } from "../api";
4
4
  import type { WSMessage } from "../ws";
5
5
  import { MessageContent } from "./MessageContent";
6
+ import { E2eService } from "../e2e";
6
7
  import { dlog } from "../debug-log";
7
8
  import { randomUUID } from "../utils/uuid";
8
9
  import { formatMessageTime, formatFullDateTime } from "../utils/time";
10
+ import { useIMEComposition } from "../hooks/useIMEComposition";
9
11
 
10
12
  /** Simple string hash for action prompt keys (matches MessageContent / ChatWindow) */
11
13
  function simpleHash(str: string): string {
@@ -25,6 +27,7 @@ export function ThreadPanel({ sendMessage }: ThreadPanelProps) {
25
27
  const state = useAppState();
26
28
  const dispatch = useAppDispatch();
27
29
  const [input, setInput] = useState("");
30
+ const { onCompositionStart, onCompositionEnd, isIMEActive } = useIMEComposition();
28
31
 
29
32
  // Load thread message history when a thread is opened
30
33
  useEffect(() => {
@@ -33,10 +36,24 @@ export function ThreadPanel({ sendMessage }: ThreadPanelProps) {
33
36
  dlog.info("Thread", `Loading history for thread ${state.activeThreadId}`);
34
37
  messagesApi
35
38
  .list(state.user.id, threadSessionKey, state.activeThreadId)
36
- .then(({ messages }) => {
39
+ .then(async ({ messages }) => {
37
40
  dlog.info("Thread", `Loaded ${messages.length} thread messages`);
38
41
  if (messages.length > 0) {
39
- dispatch({ type: "OPEN_THREAD", threadId: state.activeThreadId!, messages });
42
+ const decrypted = await Promise.all(messages.map(async (m) => {
43
+ if (m.encrypted && E2eService.hasKey()) {
44
+ try {
45
+ const plaintext = await E2eService.decrypt(m.text, m.id);
46
+ return { ...m, text: plaintext, isEncryptedLocked: false };
47
+ } catch (err) {
48
+ dlog.warn("Thread", `Failed to decrypt message ${m.id}`, err);
49
+ return { ...m, isEncryptedLocked: true };
50
+ }
51
+ } else if (m.encrypted) {
52
+ return { ...m, isEncryptedLocked: true };
53
+ }
54
+ return m;
55
+ }));
56
+ dispatch({ type: "OPEN_THREAD", threadId: state.activeThreadId!, messages: decrypted });
40
57
  }
41
58
  })
42
59
  .catch((err) => {
@@ -201,6 +218,8 @@ export function ThreadPanel({ sendMessage }: ThreadPanelProps) {
201
218
  <MessageContent
202
219
  text={parentMessage.text}
203
220
  mediaUrl={parentMessage.mediaUrl}
221
+ messageId={parentMessage.id}
222
+ encrypted={!!parentMessage.mediaEncrypted && !!parentMessage.mediaUrl && E2eService.hasKey()}
204
223
  a2ui={parentMessage.a2ui}
205
224
  onAction={handleA2UIAction}
206
225
  onResolveAction={(value, label) => handleResolveAction(parentMessage.id, value, label)}
@@ -259,6 +278,8 @@ export function ThreadPanel({ sendMessage }: ThreadPanelProps) {
259
278
  <MessageContent
260
279
  text={msg.text}
261
280
  mediaUrl={msg.mediaUrl}
281
+ messageId={msg.id}
282
+ encrypted={!!msg.mediaEncrypted && !!msg.mediaUrl && E2eService.hasKey()}
262
283
  a2ui={msg.a2ui}
263
284
  isStreaming={msg.isStreaming}
264
285
  onAction={handleA2UIAction}
@@ -315,11 +336,13 @@ export function ThreadPanel({ sendMessage }: ThreadPanelProps) {
315
336
  value={input}
316
337
  onChange={(e) => setInput(e.target.value)}
317
338
  onKeyDown={(e) => {
318
- if (e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing) {
339
+ if (e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing && !isIMEActive()) {
319
340
  e.preventDefault();
320
341
  handleSend();
321
342
  }
322
343
  }}
344
+ onCompositionStart={onCompositionStart}
345
+ onCompositionEnd={onCompositionEnd}
323
346
  placeholder="Reply…"
324
347
  rows={1}
325
348
  className="w-full px-3 py-2 text-body bg-transparent resize-none focus:outline-none placeholder:text-[--text-muted]"
@@ -60,9 +60,10 @@ function getFirebaseAuth(): Auth {
60
60
  app = initializeApp(firebaseConfig);
61
61
  auth = getAuth(app);
62
62
 
63
- // In Capacitor native, WKWebView's IndexedDB can hang Firebase Auth.
63
+ // In native WKWebViews (Capacitor or macOS), IndexedDB can hang Firebase Auth.
64
64
  // Use in-memory persistence to avoid this.
65
- if (Capacitor.isNativePlatform()) {
65
+ const isNative = Capacitor.isNativePlatform() || !!(window as any).__BOTSCHAT_NATIVE__;
66
+ if (isNative) {
66
67
  setPersistence(auth, inMemoryPersistence).catch(() => {});
67
68
  }
68
69
  }
@@ -1,18 +1,43 @@
1
1
  /**
2
- * Foreground/background detection notifies the ConnectionDO via WebSocket
3
- * so it knows whether to send push notifications.
2
+ * Foreground/background detection & channel-level focus tracking.
3
+ *
4
+ * Notifies the ConnectionDO via WebSocket so it knows whether to send push
5
+ * notifications and which session the user is currently viewing.
4
6
  */
5
7
 
6
8
  import { Capacitor } from "@capacitor/core";
7
9
  import type { BotsChatWSClient } from "./ws";
8
10
  import { dlog } from "./debug-log";
9
11
 
10
- export function setupForegroundDetection(
12
+ export interface ForegroundOptions {
13
+ wsClient: BotsChatWSClient;
14
+ getActiveSessionKey: () => string | null;
15
+ onResume?: () => void;
16
+ }
17
+
18
+ export interface ForegroundHandle {
19
+ cleanup: () => void;
20
+ /** Re-send the current foreground/background state. Call after WS auth succeeds. */
21
+ resend: () => void;
22
+ }
23
+
24
+ /**
25
+ * Send a focus.update message when the user switches channels/sessions
26
+ * while already in the foreground.
27
+ */
28
+ export function sendFocusUpdate(
11
29
  wsClient: BotsChatWSClient,
12
- onResume?: () => void,
13
- ): () => void {
30
+ sessionKey: string | null,
31
+ ): void {
32
+ wsClient.send({ type: "focus.update", sessionKey });
33
+ dlog.info("Foreground", `Focus updated: ${sessionKey ?? "(none)"}`);
34
+ }
35
+
36
+ export function setupForegroundDetection(opts: ForegroundOptions): ForegroundHandle {
37
+ const { wsClient, getActiveSessionKey, onResume } = opts;
38
+
14
39
  const notifyForeground = () => {
15
- wsClient.send({ type: "foreground.enter" });
40
+ wsClient.send({ type: "foreground.enter", sessionKey: getActiveSessionKey() });
16
41
  onResume?.();
17
42
  dlog.info("Foreground", "Entered foreground");
18
43
  };
@@ -24,19 +49,23 @@ export function setupForegroundDetection(
24
49
 
25
50
  if (Capacitor.isNativePlatform()) {
26
51
  let cleanup: (() => void) | null = null;
52
+ let nativeIsActive = true;
27
53
 
28
54
  import("@capacitor/app").then(({ App }) => {
29
55
  const handle = App.addListener("appStateChange", ({ isActive }) => {
56
+ nativeIsActive = isActive;
30
57
  if (isActive) notifyForeground();
31
58
  else notifyBackground();
32
59
  });
33
60
  cleanup = () => handle.then((h) => h.remove());
34
61
  });
35
62
 
36
- // Report initial foreground state once WS is connected
37
63
  notifyForeground();
38
64
 
39
- return () => cleanup?.();
65
+ return {
66
+ cleanup: () => cleanup?.(),
67
+ resend: () => { if (nativeIsActive) notifyForeground(); else notifyBackground(); },
68
+ };
40
69
  }
41
70
 
42
71
  // Web: Use Page Visibility API
@@ -49,7 +78,8 @@ export function setupForegroundDetection(
49
78
 
50
79
  if (!document.hidden) notifyForeground();
51
80
 
52
- return () => {
53
- document.removeEventListener("visibilitychange", handleVisibilityChange);
81
+ return {
82
+ cleanup: () => document.removeEventListener("visibilitychange", handleVisibilityChange),
83
+ resend: () => { if (!document.hidden) notifyForeground(); else notifyBackground(); },
54
84
  };
55
85
  }
@@ -0,0 +1,36 @@
1
+ import { useRef, useCallback } from "react";
2
+
3
+ /**
4
+ * Tracks IME composition state to prevent Enter-to-send during
5
+ * Chinese/Japanese/Korean input confirmation.
6
+ *
7
+ * In WebKit/WKWebView, when a user presses Enter to accept an IME candidate,
8
+ * the event order is: compositionend → keydown(Enter, isComposing=false).
9
+ * The native `isComposing` flag is already false by the time keydown fires,
10
+ * so checking it alone is insufficient. This hook keeps a flag active for one
11
+ * animation frame after compositionend to swallow that trailing Enter.
12
+ */
13
+ export function useIMEComposition() {
14
+ const composingRef = useRef(false);
15
+ const justEndedRef = useRef(false);
16
+
17
+ const onCompositionStart = useCallback(() => {
18
+ composingRef.current = true;
19
+ justEndedRef.current = false;
20
+ }, []);
21
+
22
+ const onCompositionEnd = useCallback(() => {
23
+ composingRef.current = false;
24
+ justEndedRef.current = true;
25
+ requestAnimationFrame(() => {
26
+ justEndedRef.current = false;
27
+ });
28
+ }, []);
29
+
30
+ const isIMEActive = useCallback(
31
+ () => composingRef.current || justEndedRef.current,
32
+ [],
33
+ );
34
+
35
+ return { onCompositionStart, onCompositionEnd, isIMEActive } as const;
36
+ }
@@ -34,8 +34,9 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
34
34
  </React.StrictMode>,
35
35
  );
36
36
 
37
- // Register service worker for PWA support — skip in Capacitor (native has its own caching)
38
- if (!Capacitor.isNativePlatform() && "serviceWorker" in navigator) {
37
+ // Register service worker for PWA support — skip in native apps (iOS/Android/macOS)
38
+ const isNativePlatform = Capacitor.isNativePlatform() || !!(window as any).__BOTSCHAT_NATIVE__;
39
+ if (!isNativePlatform && "serviceWorker" in navigator) {
39
40
  window.addEventListener("load", () => {
40
41
  navigator.serviceWorker.register("/sw.js").catch(() => {
41
42
  // SW registration failed — non-critical, app still works
@@ -96,11 +96,96 @@ export async function clearE2eKeyFromSW(): Promise<void> {
96
96
  }
97
97
  }
98
98
 
99
+ // ---- macOS native notification bridge ----
100
+
101
+ declare global {
102
+ interface Window {
103
+ __BOTSCHAT_NATIVE__?: boolean;
104
+ __BOTSCHAT_PLATFORM__?: string;
105
+ __BOTSCHAT_NATIVE_NOTIFY__?: (payload: {
106
+ title: string;
107
+ body: string;
108
+ sessionKey?: string;
109
+ }) => void;
110
+ __BOTSCHAT_NATIVE_REQUEST_PERMISSION__?: () => void;
111
+ }
112
+ }
113
+
114
+ function isMacOSNative(): boolean {
115
+ return !!(window.__BOTSCHAT_NATIVE__ && window.__BOTSCHAT_PLATFORM__ === "macos");
116
+ }
117
+
118
+ async function initMacOSPush(): Promise<void> {
119
+ try {
120
+ window.__BOTSCHAT_NATIVE_REQUEST_PERMISSION__?.();
121
+ dlog.info("Push", "macOS native notification permission requested");
122
+ } catch (err) {
123
+ dlog.error("Push", "macOS notification init failed", err);
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Show a native macOS notification when a message arrives via WS and
129
+ * the window is not focused. Call this from the WS message handler.
130
+ */
131
+ export function notifyIfBackground(msg: {
132
+ type: string;
133
+ text?: string;
134
+ caption?: string;
135
+ sessionKey?: string;
136
+ agentName?: string;
137
+ }): void {
138
+ if (!isMacOSNative()) return;
139
+ if (!document.hidden && document.hasFocus()) return;
140
+ if (!window.__BOTSCHAT_NATIVE_NOTIFY__) return;
141
+
142
+ let body = "";
143
+ const title = msg.agentName || "BotsChat";
144
+
145
+ if (msg.type === "agent.text" && msg.text) {
146
+ body = msg.text.length > 200 ? msg.text.slice(0, 200) + "…" : msg.text;
147
+ } else if (msg.type === "agent.media") {
148
+ body = msg.caption || "Sent a media file";
149
+ } else {
150
+ return;
151
+ }
152
+
153
+ window.__BOTSCHAT_NATIVE_NOTIFY__({ title, body, sessionKey: msg.sessionKey });
154
+ }
155
+
156
+ // ---- Service Worker message listener (notification click → navigation) ----
157
+
158
+ function setupSWMessageListener(): void {
159
+ if (!("serviceWorker" in navigator)) return;
160
+ navigator.serviceWorker.addEventListener("message", (event) => {
161
+ if (event.data?.type === "push-nav" && event.data.sessionKey) {
162
+ dlog.info("Push", `SW postMessage push-nav: ${event.data.sessionKey}`);
163
+ firePushNav(event.data.sessionKey);
164
+ }
165
+ });
166
+
167
+ // Also check URL for push_session param (when SW opens a new window)
168
+ const params = new URLSearchParams(window.location.search);
169
+ const pushSession = params.get("push_session");
170
+ if (pushSession) {
171
+ dlog.info("Push", `URL push_session param: ${pushSession}`);
172
+ firePushNav(pushSession);
173
+ // Clean up the URL parameter
174
+ params.delete("push_session");
175
+ const clean = params.toString();
176
+ const newUrl = window.location.pathname + (clean ? "?" + clean : "") + window.location.hash;
177
+ window.history.replaceState({}, "", newUrl);
178
+ }
179
+ }
180
+
99
181
  // ---- Push initialization ----
100
182
 
101
183
  export async function initPushNotifications(): Promise<void> {
102
184
  if (initialized) return;
103
185
 
186
+ // Listen for SW notification-click messages (must be before any early return)
187
+ setupSWMessageListener();
188
+
104
189
  // Sync E2E key so push notifications can be decrypted
105
190
  await syncE2eKeyToSW();
106
191
 
@@ -109,7 +194,9 @@ export async function initPushNotifications(): Promise<void> {
109
194
  syncE2eKeyToSW().catch(() => {});
110
195
  });
111
196
 
112
- if (Capacitor.isNativePlatform()) {
197
+ if (isMacOSNative()) {
198
+ await initMacOSPush();
199
+ } else if (Capacitor.isNativePlatform()) {
113
200
  await initNativePush();
114
201
  } else {
115
202
  await initWebPush();
@@ -15,6 +15,10 @@ export type ChatMessage = {
15
15
  /** Tracks which action blocks have been resolved, keyed by prompt hash */
16
16
  resolvedActions?: Record<string, { value: string; label: string }>;
17
17
  isEncryptedLocked?: boolean;
18
+ /** Whether the message text was E2E encrypted (bitmask from API: bit0=text, bit1=media) */
19
+ encrypted?: boolean | number;
20
+ /** Whether the media binary was E2E encrypted (derived from sender + encrypted flag) */
21
+ mediaEncrypted?: boolean;
18
22
  };
19
23
 
20
24
  export type ActiveView = "messages" | "automations";
@@ -35,10 +35,11 @@ export class BotsChatWSClient {
35
35
  connect(): void {
36
36
  this.intentionalClose = false;
37
37
 
38
- // In Capacitor (native app), WebView runs from capacitor:// so we must
39
- // use the full production WebSocket URL.
38
+ // In native apps (Capacitor or macOS), the WebView runs from a custom
39
+ // scheme so we must use the full production WebSocket URL.
40
+ const isNative = Capacitor.isNativePlatform() || !!(window as any).__BOTSCHAT_NATIVE__;
40
41
  let url: string;
41
- if (Capacitor.isNativePlatform()) {
42
+ if (isNative) {
42
43
  url = `wss://console.botschat.app/api/ws/${this.opts.userId}/${this.opts.sessionId}`;
43
44
  } else {
44
45
  const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
@@ -1,2 +0,0 @@
1
- const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/web-DIeOUVhn.js","assets/index-CvbTpaza.js","assets/index-cm_3YFsA.css"])))=>i.map(i=>d[i]);
2
- import{r as o,f as t}from"./index-CvbTpaza.js";const n=o("SocialLogin",{web:()=>t(()=>import("./web-DIeOUVhn.js"),__vite__mapDeps([0,1,2])).then(e=>new e.SocialLoginWeb)});export{n as SocialLogin};
@@ -1,2 +0,0 @@
1
- const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/web-Dft_LGIH.js","assets/index-CvbTpaza.js","assets/index-cm_3YFsA.css"])))=>i.map(i=>d[i]);
2
- import{r as p,f as r}from"./index-CvbTpaza.js";const o=p("App",{web:()=>r(()=>import("./web-Dft_LGIH.js"),__vite__mapDeps([0,1,2])).then(e=>new e.AppWeb)});export{o as App};