feat(app): 답변 완료 알림 — 창 안 보고 있으면 토스트 + 트레이 점

- 프론트: 스트림 onDone 에서 chat.done(title) 한 번. 브라우저에선 no-op
- Rust shell/notify: 창 보이고 포커스면 아무것도 안 함. 아니면 Windows 토스트(tauri-plugin-notification)
  + 트레이 아이콘을 점 찍힌 tray-alert.png 로. 창 Focused(true) 에서 원복
- 토스트는 설치본(msi)에서만 알림센터에 등록돼 보임. dev 에선 트레이 점으로 확인

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
lee-hyeon-cheol
2026-09-21 14:13:40 +09:00
co-authored by Claude Fable 5.1
parent eb74de2634
commit 89fc8fab45
12 changed files with 5522 additions and 5320 deletions
@@ -3,6 +3,7 @@ import { toast } from "sonner"
import { useQueryClient, type QueryClient } from "@tanstack/react-query"
import { useSnapChatStore } from "../store/snapChatStore"
import { snapStream, cancelStream } from "../api/snap.stream"
import { notifyChatDone } from "@/lib/bridge/webviewBridge"
import type { SnapImageInput, SnapSessionDetail } from "../contract/types"
// title 이벤트 → 상세 캐시는 즉시 패치(헤더 제목 실시간 반영), 목록은 invalidate.
@@ -40,10 +41,15 @@ export function useSnapChat(sessionId: string) {
onToken: (d) => useSnapChatStore.getState().appendChunk(d),
// done 시점엔 백엔드가 이미 답변을 DB 에 저장함(streaming.py: persist→usage→done).
// detail 캐시를 무효화해 재진입 시 stale 스냅샷 대신 완성본을 받게 함.
onDone: () =>
void queryClient.invalidateQueries({
queryKey: ["snap", "session", sessionId],
}),
onDone: () => {
void queryClient.invalidateQueries({ queryKey: ["snap", "session", sessionId] })
// 창 안 보고 있으면 데스크톱 알림(토스트+트레이 점). 제목은 캐시에 있으면 그걸로.
const detail = queryClient.getQueryData<{
title?: string | null
titleLlm?: string | null
}>(["snap", "session", sessionId])
notifyChatDone(detail?.titleLlm ?? detail?.title ?? undefined)
},
onTitle: (title) => patchSessionTitle(queryClient, sessionId, title),
onUsage: (u) =>
useSnapChatStore.getState().applyUsage({