- #1: URLs in chat are now clickable links (open externally on desktop).
- #2: group 'Seen by X, Y +N more' now shows under the LATEST message to EVERYONE
(server computes reads for all messages; click shows the names).
- #5: tapping the conversation-header avatar previews the DP/group photo full-size.
- #4: in-call invite lists only people you've messaged, excludes those already in the
call/invited, and the Invite button is sticky.
- #3 (0.1.6 shell): auto-updater emits checking/available/downloading/ready/error to
the web UI, which shows a progress banner (+ Restart button) so updates aren't silent.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Desktop notifications were slow (~15s), vanished in ~1s, and clicks did nothing:
* don't block the toast on the avatar download (race a 600ms cap) → shows instantly
* keep a strong reference to each Notification (Electron GC'd them → premature close
+ dead click)
* call invites use timeoutType:'never' + a 45s window so they stay until clicked/ended;
web marks call notifications persistent.
- #9: enable the OS screen/window PICKER (useSystemPicker) so users choose what to share
(a single window avoids the whole-screen mirror); falls back to primary display.
- desktop 0.1.5 -> 0.1.6.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- #3: profile avatar now shows the photo (avatarUrl) not just initials.
- #5: header is position:sticky so the brand + profile stay visible while scrolling.
- #4: App installs table gets a search box (user/platform/version/OS) and 10-per-page
pagination with prev/next + count.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- #9 REAL cause: the tile was forced to avatar whenever the peer's CAMERA was off
(addTile + meeting-peer-state), so a screen-share only appeared if the camera was
also on. Now camera-off only shows the avatar when the peer is NOT sharing a screen.
- #9: screen-share button is highlighted (blue) while active, like mic/cam/record.
- Unanswered call text is viewer-relative: the caller sees 'Call not answered', the
callee sees 'Missed call'.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- REGRESSION (More#1): removed overflow-anchor:none (it stopped the view from staying
at the bottom as images settle) + renderThread re-asserts scroll-to-bottom after late
content, unless a 'New messages' divider is shown → chats open at the latest message.
- #9: dynacast off — a screen-share layer was being paused unless a camera track was
also flowing (blank / only-with-camera / slow). Now every published track flows.
- More#7/#9: 1:1 calls track 'answered'; unanswered calls auto-end after ~40s (caller
no longer stuck ringing) and post 'Missed call' instead of a duration; answered calls
show duration from the answer time. meeting-ended reason 'unanswered' → 'No answer'.
- More#6: selected chat now has a thin yellow (brand) boundary.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- #9: sharing your whole screen no longer creates a 'hall of mirrors' — your own
screen is NOT shown back to you (you see your camera/avatar + a 'You're sharing'
badge); other participants still see your screen. Your own tile is never the local
stage.
- #1: opening a DM now syncs the sidebar tick to the thread's true delivered/read
state, so it can't show a single tick while the conversation shows double.
- #3: nativePlatform() now also detects older desktop builds that expose
bizConnectNative but not __NATIVE__. Those were wrongly subscribing to Web Push, so
the service worker's openWindow RELOADED the page on notification click (losing the
header DP). Desktop now uses only the native toast → opens in-place.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- #7: outgoing 1:1 call shows the person you're calling (DP + name + 'Ringing…')
instead of only your own tile; removed when they answer or the call ends.
- #8: declineDmCall now ignores the decline if that user has ALREADY accepted on
another device (they're in the room) — declining the ringing invite on a 2nd
device no longer tears down the active call.
- #9: sfuAttach retries (bounded) when the uid→peerId map lags the LiveKit track,
so a shared screen isn't silently dropped due to a race.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- #4: forward modal buttons now branded (bzc-* un-scoped from .bz-confirm); target
rows get real checkboxes + border highlight; selected MESSAGES highlight in green
with a filled tick (distinct from blue bubbles), checkbox on every message.
- #5: forwarded messages show an italic 'Forwarded from <origin>' label; new fwd_from
column preserves the true origin across re-forwards.
- #6: image-preview arrows are smaller (32px) chevron icons, clear of the image
(image max 82vw); icons.js?v=5.
- #10: image preview now opens ABOVE the media modal (z 9900) and Esc/close returns
to the media view instead of closing everything.
- #11: Media/Links/Docs active tab underline is brand-blue, not green.
- #2: chat-list + conversation scrollbars 6px, stepper arrows hidden; pagination adds
a cooldown + overflow-anchor:none so scrolling up no longer sticks/jumps.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When you open/read a conversation, the server now also pushes a notif-clear to your
OWN other sockets. Each device tags notifications by conversation (kind:id), so on
notif-clear it closes the matching page Notification + any Service-Worker (Web Push)
notifications, and drops matching activity-center entries so the bell badge stays in
sync. (Desktop Electron native toasts are transient/auto-expire; the web+PWA surface
is covered.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Settings now shows 'Biz Connect for Desktop · Version x.y.z' with a Check for
updates button (desktop only, feature-detected).
- preload exposes checkForUpdates(); main.js adds the check-updates IPC (returns
available/current/dev/error) and, when a build finishes downloading, shows a
Restart now / Later dialog instead of only the silent on-next-launch install.
- desktop version bumped 0.1.4 -> 0.1.5.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Message action pill gains a Forward button; tapping it enters selection mode
(tap bubbles to multi-select, footer bar shows count + Forward/Cancel, Esc exits).
- Forward picker lists EXISTING conversations only (DMs + groups from the sidebar),
searchable, multi-target. POST /api/messages/forward copies body+attachment into
each target (authorized as participant/member), live-pushed like a normal send.
- /files auth now accepts ANY message carrying an attachment (allByAttachment), so
forwarded images stay viewable for the new recipients.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- #8: reply quotes are now clickable — jump to the original message (paging older
history in if needed), then flash it. reply DTO carries the target's timestamp.
- #3: image lightbox now has ← / → arrows + keyboard nav to flip through all images
in the conversation.
- #5: the composer queues MULTIPLE files (file input is multiple; paste still works);
each is shown as a removable chip and sent as its own message (first carries the
typed text as caption), in order.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- #2/#7/#19: openFromNotif opened chats via a full-page reload (location.assign),
which was slow AND dropped live state — the sidebar hadn't reloaded so the header
showed 'Conversation' with no DP, and an incoming call's Join/invite popup was lost.
Now it opens the chat IN-PLACE (selectChat) since the app is already running;
full-page nav only as a fallback when the app isn't initialised.
- #9: slimmer 6px scrollbar; explicitly hide all webkit stepper-arrow variants.
- #16: 1:1/small call no longer shows a stray scrollbar — meet-grid centers tiles
and clips overflow (call UI fits the viewport).
- #17: caller now hears a gentle ringback while waiting; stops on answer, on any
call exit, and auto-stops after 45s so it never rings forever.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pull-down-at-top called reloadThread (reload newest 500 + scroll to bottom), so
reaching the oldest message yanked the view to the newest — jarring, especially on a
single page. Now the pull gesture calls loadOlder: it pages in older history keeping
scroll position, or no-ops when there's nothing older. Never jumps to the bottom.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Older messages (beyond the newest 500) now load as you scroll to the top — the
thread endpoint takes a ?before=<created_at> cursor, the client prepends the older
page and preserves scroll position (renderThread keepScroll). _hasMoreOlder stops
paging when a short page returns.
Search now covers the ENTIRE thread, not just the loaded window: new
/api/messages/search (DM + group, LIKE with escaped wildcards) returns all matching
message ids; the client debounces the query, and jumping to a hit older than the
loaded window pages history back (ensureLoadedBack) until the match is in view, then
highlights + flashes it. Cap raised to 500.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
THE disappearing-messages root cause (found via THREAD.length=300 vs 351 on server).
messages.thread and threadByConversation did 'ORDER BY created_at ASC LIMIT 300' —
the oldest 300. Once a DM/group passed 300 messages, every newer message was silently
dropped from the fetch, so anything sent after that point 'disappeared' (persisted
server-side, never returned to the client). Now: inner 'ORDER BY created_at DESC LIMIT'
takes the NEWEST N, outer ASC presents them oldest-first. Cap raised 300 -> 500.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
THE disappearing-messages root cause. openConvo did: let msgs=[]; try{ msgs=fetch()
}catch{}; THREAD = Array.isArray(msgs)?msgs:[]. On a flaky desktop network a failed
fetch left msgs=[] (still an array) → THREAD=[] AND THREAD_CACHE.set(ckey,[]) — so
reopening a chat blanked it AND overwrote the cache, making messages vanish and stay
gone even though they persist server-side (confirmed: 982 DMs, zero dangling ids).
Now msgs stays null on a failed/non-OK fetch; we only replace THREAD/cache on a real
array response, and otherwise keep the cached render instead of blanking.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Root cause of 'messages to some contacts disappear (gone after reopen)': the #2
account merge deletes the merged-away user row. Any lingering reference to that old
id — a cached contact, an in-flight DM — then saved against a dead recipient / 404'd
on thread fetch, so messages silently vanished.
- New user_aliases table records old_id -> survivor on every merge (mergeInto).
- users.resolve(id) follows the redirect.
- DM send (recipient), thread fetch (with), and read now resolve() the peer id, so a
stale id transparently routes to the surviving account.
Fixes future merges fully. Contacts merged BEFORE this (no alias recorded) may need a
one-off data check.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Broken images: UPLOADS_DIR/REC_DIR/TRANS_DIR were server/<dir> INSIDE the image,
so every deploy.sh rebuild wiped uploaded files — old images 404'd ('broken
image') though their DB rows survived. Make them env-overridable and point prod
at /data/uploads|recordings|transcripts (persistent volume), matching DB/downloads.
NOTE: files already lost to prior rebuilds can't be recovered; new uploads persist.
- Duplicate notifications: harden connectChatWs — close/detach any prior socket
before opening a new one and keep a single pending reconnect timer, so a flaky
reconnect can't leave two live sockets delivering every event/notification twice.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
batch42's resync-on-reconnect reopened the conversation via openConvo, which
replaces THREAD wholesale. On the desktop app (flaky WS), a reload racing with a
just-sent message could momentarily blank the chat ('messages disappear after
sending', seen on the Manasa chat). resync now MERGES: adds messages missed while
disconnected + refreshes read/delivered/seen/edited/deleted flags, but never
removes messages already on screen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The bundled livekit-client@2.20 uses signaling protocol 17 and the /rtc/v1 path,
which the v1.7 server didn't implement (404). The client fell back to the legacy
path, leaving the track publisher in a bad state so mic/cam publishing failed with
'InvalidAccessError: The sender was not created by this peer connection' — surfaced
to users as a misleading 'permission required' toast. v1.9 supports protocol 17.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- #10/#18/#7: on WebSocket RE-connect, re-pull sidebar + re-open the current
thread so the desktop/mobile apps recover missed messages/ticks without a
refresh (the old onopen only re-sent chat-hello).
- #11: branded bzConfirm() dialog replaces window.confirm for delete (the OS/
Electron default dialog looked off-brand on desktop).
- #4: global Esc no longer closes the conversation while an image preview is
open — the preview closes first, the conversation on a second Esc.
- #6: live 'typing…' now also shows inside the conversation panel (animated),
not only the header/sidebar.
- #15 (diagnostic): mic/cam failures now report the REAL cause (permission vs
no-device vs in-use) with desktop-specific guidance, and log the raw error,
instead of always saying 'permission required'.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The prod box is behind NAT (private 192.168.88.61 behind public 118.95.33.89), so
LiveKit auto-IP-detection would pick the wrong (outbound) address. Pin
rtc.node_ip=118.95.33.89 and collapse media to one UDP port (50000) + TCP 7881 to
minimize the upstream gateway port-forward the network team must add. Docs updated
with the exact forward table.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A normal deploy no longer starts the livekit container (it would crashloop with
empty keys before provisioning). Enable with 'docker compose --profile sfu up -d'.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Step-by-step to enable meetings SFU: generate key/secret, .env vars, DNS record,
NPM proxy host for wss signaling, VPS firewall for UDP media + TCP fallback, deploy,
verify. Includes the one-line rollback to mesh (remove the LIVEKIT_* vars).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Meeting media flows through LiveKit when the server reports sfu:true; otherwise the
P2P mesh is unchanged. Reuses the ENTIRE existing meeting UI + WS control plane
(join/host/mute/screen/recording/transcript) and only swaps the transport:
- vendored livekit-client 2.20 UMD (lazy-loaded; no build step).
- sfuInit() reads /api/meetings/config at boot; sfuConnect() joins the LiveKit room
with the minted token after the WS meeting-join.
- remote tracks map back to the WS peerId via uid (LiveKit identity = app user id);
per-peer stream prefers screen over camera, driving the existing sharing/stage UI.
- toggleMic/Cam/Screen publish via LiveKit; local tracks reflected into meetLocalStream
so tiles, active-speaker meter, canvas recording and transcript keep working.
- meetMakePeer/peer-joined/peer-left/leaveMeeting branch on SFU.on; mesh path intact.
Needs a running LiveKit server + live test (phase 3 ops) to exercise end-to-end.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds the server side of scaling meetings past the ~5-peer mesh:
- config.js: LIVEKIT_URL/API_KEY/API_SECRET + LIVEKIT_ENABLED flag. All optional;
when unset the app keeps the built-in P2P mesh (fully additive, like push).
- routes.js: GET /api/meetings/config (tells the client sfu on/off + wss url) and
POST /api/meetings/token (mints a per-user, per-room LiveKit join token — hand-rolled
HS256 JWT like the FCM/APNs tokens, no new dependency; secret stays server-side).
- docker-compose.yml: optional livekit service (single-node, no Redis), keys injected
via LIVEKIT_KEYS from the same .env; media over published UDP 50000-50100 + TCP 7881,
signaling proxied by NPM.
- livekit.yaml + .env.example documented.
Client (mesh->LiveKit media swap, behind the flag) lands in phase 2.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Thread: every group message I send now carries a receipt tick — sent (1 grey)
-> seen-by-some (2 grey) -> seen-by-all (2 yellow) — computed from the per-message
seenBy the server already returns and kept live by onGroupRead. Tap the tick to see
exactly who ('Seen by X, Y') with an 'N of M' tooltip. Replaces the old last-message-
only 'Seen by' line with a universal, tappable per-message receipt.
Sidebar: the group row tick now reflects real read state (read/delivered/sent) via
memberReads vs member count, instead of a hardcoded 'sent' — and refreshes live when
the open group is read by all.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ephemeral over the chat WebSocket (no DB). Composer emits chat-typing on/off
(throttled 2.5s, auto-stop after 4s idle / on send / on leaving the chat).
Server relays to the DM peer or fans out to group members (membership-checked).
Receiver shows 'typing…' in the conversation header subtitle and the sidebar
row preview (brand-blue italic), with per-sender auto-expiry so a dropped 'off'
can't stick. Group shows names ('Alice is typing…', 'N people are typing…').
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mirrors redeploy.sh conventions (pinned host key, DEPLOY_PASS/deploy.secret/
prompt password resolution, plink discovery). Uploads the three electron-builder
artifacts via pscp to a temp dir, then docker cp's them into the app container's
/data/downloads (volume-path-independent), and verifies the public feed serves
the new version. Keeps older versions; only latest.yml is overwritten.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
/data is a named Docker volume, not a host bind-mount, so the prior
scp root@host:/data/downloads/ path was wrong. Document the real host
path (/var/lib/docker/volumes/bizgaze_support_data/_data/downloads) plus
the docker cp alternative, and note latest.yml is the only overwritten file.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Message actions (reply/react/edit/delete) consolidated into ONE hover pill anchored to the
bubble's top-right, overlapping it so on short messages the icons no longer float off into
empty space and vanish before you can click.
- Desktop toast + mobile(FCM)/web-background push now show the sender's real DP:
* renderer passes the DP URL through; desktop shell downloads it for the toast icon
(canvas-drawing an external DP tainted it → initials). Desktop bumped to 0.1.4.
* DM push payload carries icon=sender avatar; sw.js already uses it (web background),
sendFcm sets notification.image (Android).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sender can edit their own text messages: a pencil action on the bubble loads the text into the
composer in an 'Editing' mode; saving updates the body, marks it 'edited', and pushes the change
live to the other side/tabs (chat-edited, mirroring delete). Adds messages.edited_at + editBody().
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- #5: use the ACTUAL brand C (extracted transparent white C + gold dot from the master
app-icon) on the blue headers instead of a hand-drawn mark; login card keeps the app icon.
- #3: read-receipt double tick on my own (blue) bubble is now brand yellow (was blue-on-blue,
invisible).
- #2: Sign-in button shows a spinner + 'Signing in…' on submit.
- #1: web notification uses the sender/group DP URL directly as the icon (drawing an external
DP to canvas tainted it → silently fell back to initials).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The header is already blue, so the app-icon-in-a-white-pill looked like blue→white→blue→white
nesting. Use a transparent mark (mark-light.svg: white C + gold dot) placed directly on the blue
header (home/index/connect/dashboard), dropping the white background + padding. share.html keeps
the full app icon (it's a light card, not the blue bar).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Replace the leftover 'Your Virtual CXO' logo.png with the app icon (C mark) in every
header (home/index/connect/dashboard) + the app icon on share.html; delete logo.png.
- Loading screen now shows the orbit mark + 'Biz Connect' wordmark + 'Loading…' (was bare).
- Keep the branded splash up until the chat list finishes loading — no blank/stuck gap
after login (previously hidden before loadSidebar()).
- Login accepts email OR phone (field was type=email, which rejected phone numbers and so
blocked the mobile-login that triggers the account merge). Label → 'Email or phone'.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- #6: the server echoes your own sent message back over the socket; onChatMessage reset
last_status='sent', overwriting a delivered/read that arrived first → the chat-list tick
flipped back to single. Track last_msg_id and never downgrade the same message's tick.
- #3: pin the 'You' note-to-self chat in its own slot — tinted row + 'Note to self' tag +
a divider separating it from the conversation list.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- docker-compose: DOWNLOADS_DIR=/data/downloads so uploaded installers/latest.yml
survive image rebuilds (deploy.sh) instead of being wiped.
- DEPLOY.md: step-by-step for publishing a desktop release (build → upload the
3 feed files → verify) so the Download button + auto-update go live.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Live presence (fixes stale in-call/status until refresh — impossible in apps):
- server broadcasts a user's status over the chat socket on connect/disconnect, call
join/leave, and status change (chat.js broadcastPresence; signaling + routes hooks).
- client onPresence() updates the contact dot + open-chat header live.
Chat delivery ticks (#6): chat-list row now mirrors the thread (delivered→double grey,
read→blue) via a new 'with' field on the delivered relay + onChatRead/onChatDelivered.
Call fixes: no bogus 'host handed over' when a 1:1 call ends (leaveMeeting forced);
branded call-connecting + chat-thread loaders; header subtitle tracks live call state.
Notifications: web notify + sw.js use sender/group DP + brand icon (not old wordmark);
desktop shell drops Web Push so only the single native toast fires (#5).
Brand: master icon/splash/loaders wired everywhere (PWA/favicon/apple-touch/.ico),
branded login (blue + gold CTA), branded toasts (BZToast) on all pages, Electron splash.
Desktop: dev auto-targets localhost (packaged→prod); version 0.1.3 with new multi-size
icon; dropped unused node-notifier; removed home-mockup.html.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
From brand-assets/ masters (C + orbiting-dot on brand blue):
- Regenerated PWA icons (192/512/maskable), apple-touch (180), favicons
(32/16 + root favicon.ico) from the 1024 master; bumped manifest to v3.
- Rebuilt desktop/build/icon.ico as a proper multi-size ICO (16..256,
PNG-compressed) to fix the tiny/blurry taskbar icon.
- Updated mobile masters (resources/icon.png 1024, splash.png/splash-dark.png
2732) for capacitor-assets to regenerate native icons/splash.
- Wired the animated branded orbit loader into the app boot 'Loading…' screen;
added favicon/theme-color links to index + home heads.
- logo.png (horizontal wordmark) left untouched — masters have no wordmark.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Provisioning now keys the Biz Connect account on bz.bizgazeUserId (the same
value whether the person signs in with email or mobile) instead of the typed
identifier, so both logins resolve to a single contact. Legacy rows get the
person-id stamped on next login; an existing duplicate created under the same
identifier is folded in via a transactional users.mergeInto() that reassigns
all messages/memberships/reactions/votes/favorites/ownership to the survivor.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Loads @twemoji/api (jsDelivr) and renders emojis as Twemoji images in messages, the emoji
picker, and reactions (twemojify at each render point). Picker inserts via data-emoji so it
survives the <img> swap. Falls back to plain Unicode if the CDN is unavailable. build batch25.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pinned 'You' chat at the top of the list (always available); messaging yourself works
(to===me), with no self push/echo notification and a 'Message yourself' header. Self is
filtered from the normal contacts and shows no status dot. build batch24.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Conversations DTO now returns last_status (sent/delivered/read for DMs; sent for groups) for
my last message. The list row shows the matching tick (single/double, blue when read) and a
red 'Draft:' indicator when there's unsent text. Live 'sent' on send/receive; upgrades to
delivered/read on refresh. e2e 119.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Unsent text is saved per conversation (survives switching chats and reloads) and restored
when you reopen the chat; cleared on send. (#1)
- Emoji picker: stopPropagation on tab/grid clicks so switching category no longer closes it
(the re-render was detaching the clicked node -> outside-click handler fired). (#6)
build batch23.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
node-notifier/SnoreToast click callback never fired without the crashing wait mode. Switch the
chat toast to Electron's built-in Notification: shows avatar + message and its 'click' event
reliably raises the app + opens the chat. No SnoreToast, no external tools.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The bundled SnoreToast crashes (0xC0000409) handling a text reply, and that crash triggered
a second (fallback) toast. Removed the direct-SnoreToast reply path entirely; the chat toast
now reliably shows avatar + message via node-notifier and opens the chat on click. Inline
text reply needs a different toast engine (deferred).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>