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.
- package/package.json +4 -1
- package/packages/api/src/do/connection-do.ts +106 -18
- package/packages/api/src/index.ts +59 -3
- package/packages/plugin/dist/src/channel.d.ts.map +1 -1
- package/packages/plugin/dist/src/channel.js +75 -6
- package/packages/plugin/dist/src/channel.js.map +1 -1
- package/packages/plugin/dist/src/types.d.ts +1 -0
- package/packages/plugin/dist/src/types.d.ts.map +1 -1
- package/packages/plugin/dist/src/ws-client.d.ts.map +1 -1
- package/packages/plugin/dist/src/ws-client.js +11 -6
- package/packages/plugin/dist/src/ws-client.js.map +1 -1
- package/packages/plugin/package.json +1 -1
- package/packages/web/dist/assets/{index-cm_3YFsA.css → index-B5GU1yVt.css} +1 -1
- package/packages/web/dist/assets/index-CO9YgLst.js +2 -0
- package/packages/web/dist/assets/index-ClDrCe_c.js +1 -0
- package/packages/web/dist/assets/{index-DsWBWQD6.js → index-D3T7sc-R.js} +1 -1
- package/packages/web/dist/assets/index-DPEosppm.js +2 -0
- package/packages/web/dist/assets/{index-dMn_npR3.js → index-DzYqprDN.js} +1 -1
- package/packages/web/dist/assets/index-IVUdSd9w.js +1516 -0
- package/packages/web/dist/assets/{index.esm-DdTIpXjl.js → index.esm-COzWPkKi.js} +1 -1
- package/packages/web/dist/assets/{web-Dft_LGIH.js → web-CxXbaApe.js} +1 -1
- package/packages/web/dist/assets/{web-DIeOUVhn.js → web-DFQypSd0.js} +1 -1
- package/packages/web/dist/index.html +2 -2
- package/packages/web/dist/sw.js +9 -1
- package/packages/web/src/App.tsx +34 -5
- package/packages/web/src/api.ts +7 -4
- package/packages/web/src/components/ChatWindow.tsx +139 -74
- package/packages/web/src/components/CronSidebar.tsx +5 -1
- package/packages/web/src/components/LoginPage.tsx +3 -1
- package/packages/web/src/components/OnboardingPage.tsx +3 -1
- package/packages/web/src/components/ScheduleEditor.tsx +120 -47
- package/packages/web/src/components/SessionTabs.tsx +5 -1
- package/packages/web/src/components/Sidebar.tsx +8 -2
- package/packages/web/src/components/TaskBar.tsx +5 -1
- package/packages/web/src/components/ThreadPanel.tsx +26 -3
- package/packages/web/src/firebase.ts +3 -2
- package/packages/web/src/foreground.ts +40 -10
- package/packages/web/src/hooks/useIMEComposition.ts +36 -0
- package/packages/web/src/main.tsx +3 -2
- package/packages/web/src/push.ts +88 -1
- package/packages/web/src/store.ts +4 -0
- package/packages/web/src/ws.ts +4 -3
- package/packages/web/dist/assets/index-CbCpFrA9.js +0 -2
- package/packages/web/dist/assets/index-Ct0m11C8.js +0 -2
- package/packages/web/dist/assets/index-CvbTpaza.js +0 -1516
- 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
|
-
|
|
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
|
|
63
|
+
// In native WKWebViews (Capacitor or macOS), IndexedDB can hang Firebase Auth.
|
|
64
64
|
// Use in-memory persistence to avoid this.
|
|
65
|
-
|
|
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
|
|
3
|
-
*
|
|
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
|
|
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
|
-
|
|
13
|
-
):
|
|
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
|
|
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
|
|
38
|
-
|
|
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
|
package/packages/web/src/push.ts
CHANGED
|
@@ -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 (
|
|
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";
|
package/packages/web/src/ws.ts
CHANGED
|
@@ -35,10 +35,11 @@ export class BotsChatWSClient {
|
|
|
35
35
|
connect(): void {
|
|
36
36
|
this.intentionalClose = false;
|
|
37
37
|
|
|
38
|
-
// In
|
|
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 (
|
|
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};
|