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:
co-authored by
Claude Fable 5.1
parent
eb74de2634
commit
89fc8fab45
@@ -3,6 +3,7 @@ import { toast } from "sonner"
|
|||||||
import { useQueryClient, type QueryClient } from "@tanstack/react-query"
|
import { useQueryClient, type QueryClient } from "@tanstack/react-query"
|
||||||
import { useSnapChatStore } from "../store/snapChatStore"
|
import { useSnapChatStore } from "../store/snapChatStore"
|
||||||
import { snapStream, cancelStream } from "../api/snap.stream"
|
import { snapStream, cancelStream } from "../api/snap.stream"
|
||||||
|
import { notifyChatDone } from "@/lib/bridge/webviewBridge"
|
||||||
import type { SnapImageInput, SnapSessionDetail } from "../contract/types"
|
import type { SnapImageInput, SnapSessionDetail } from "../contract/types"
|
||||||
|
|
||||||
// title 이벤트 → 상세 캐시는 즉시 패치(헤더 제목 실시간 반영), 목록은 invalidate.
|
// title 이벤트 → 상세 캐시는 즉시 패치(헤더 제목 실시간 반영), 목록은 invalidate.
|
||||||
@@ -40,10 +41,15 @@ export function useSnapChat(sessionId: string) {
|
|||||||
onToken: (d) => useSnapChatStore.getState().appendChunk(d),
|
onToken: (d) => useSnapChatStore.getState().appendChunk(d),
|
||||||
// done 시점엔 백엔드가 이미 답변을 DB 에 저장함(streaming.py: persist→usage→done).
|
// done 시점엔 백엔드가 이미 답변을 DB 에 저장함(streaming.py: persist→usage→done).
|
||||||
// detail 캐시를 무효화해 재진입 시 stale 스냅샷 대신 완성본을 받게 함.
|
// detail 캐시를 무효화해 재진입 시 stale 스냅샷 대신 완성본을 받게 함.
|
||||||
onDone: () =>
|
onDone: () => {
|
||||||
void queryClient.invalidateQueries({
|
void queryClient.invalidateQueries({ queryKey: ["snap", "session", sessionId] })
|
||||||
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),
|
onTitle: (title) => patchSessionTitle(queryClient, sessionId, title),
|
||||||
onUsage: (u) =>
|
onUsage: (u) =>
|
||||||
useSnapChatStore.getState().applyUsage({
|
useSnapChatStore.getState().applyUsage({
|
||||||
|
|||||||
@@ -14,5 +14,11 @@ export const pasteToApp = (text: string) => send({ type: "paste.code", text })
|
|||||||
/** 현재 React route를 데스크톱 호스트에 알림. */
|
/** 현재 React route를 데스크톱 호스트에 알림. */
|
||||||
export const reportRoute = (path: string) => send({ type: "route.changed", path })
|
export const reportRoute = (path: string) => send({ type: "route.changed", path })
|
||||||
|
|
||||||
|
/** 답변 스트림 끝 — 데스크톱이 창 안 보고 있으면 토스트 + 트레이 점. 브라우저에선 no-op. */
|
||||||
|
export const notifyChatDone = (title?: string) => {
|
||||||
|
if (hostKind() !== "tauri") return
|
||||||
|
send({ type: "chat.done", title: title ?? "" })
|
||||||
|
}
|
||||||
|
|
||||||
/** 프레임리스 창을 native 창 이동으로 끌 수 있게 함. */
|
/** 프레임리스 창을 native 창 이동으로 끌 수 있게 함. */
|
||||||
export const startWindowDrag = () => send({ type: "window.drag" })
|
export const startWindowDrag = () => send({ type: "window.drag" })
|
||||||
|
|||||||
Generated
+98
@@ -495,6 +495,7 @@ dependencies = [
|
|||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
"tauri-plugin-global-shortcut",
|
"tauri-plugin-global-shortcut",
|
||||||
|
"tauri-plugin-notification",
|
||||||
"tauri-plugin-single-instance",
|
"tauri-plugin-single-instance",
|
||||||
"tauri-plugin-window-state",
|
"tauri-plugin-window-state",
|
||||||
"windows",
|
"windows",
|
||||||
@@ -2105,6 +2106,20 @@ version = "0.4.33"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
|
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mac-notification-sys"
|
||||||
|
version = "0.6.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fd604973958ddcc11b561193c0fb96ba146506ef2f231ef2e7c35fd2cbc9beca"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"log",
|
||||||
|
"objc2",
|
||||||
|
"objc2-foundation",
|
||||||
|
"time",
|
||||||
|
"uuid",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "markup5ever"
|
name = "markup5ever"
|
||||||
version = "0.38.0"
|
version = "0.38.0"
|
||||||
@@ -2219,6 +2234,20 @@ version = "1.0.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
|
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "notify-rust"
|
||||||
|
version = "4.18.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c5b4c1b4f2aa9f25f63a7a49d3dd0ed567b3670da15330a66b29434be899b891"
|
||||||
|
dependencies = [
|
||||||
|
"futures-lite",
|
||||||
|
"log",
|
||||||
|
"mac-notification-sys",
|
||||||
|
"serde",
|
||||||
|
"tauri-winrt-notification",
|
||||||
|
"zbus",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-conv"
|
name = "num-conv"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
@@ -2379,6 +2408,7 @@ checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.13.1",
|
"bitflags 2.13.1",
|
||||||
"block2",
|
"block2",
|
||||||
|
"libc",
|
||||||
"objc2",
|
"objc2",
|
||||||
"objc2-core-foundation",
|
"objc2-core-foundation",
|
||||||
]
|
]
|
||||||
@@ -2692,6 +2722,15 @@ version = "0.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ppv-lite86"
|
||||||
|
version = "0.2.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
|
||||||
|
dependencies = [
|
||||||
|
"zerocopy",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "precomputed-hash"
|
name = "precomputed-hash"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
@@ -2796,6 +2835,35 @@ version = "6.0.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand"
|
||||||
|
version = "0.9.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41"
|
||||||
|
dependencies = [
|
||||||
|
"rand_chacha",
|
||||||
|
"rand_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_chacha"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||||
|
dependencies = [
|
||||||
|
"ppv-lite86",
|
||||||
|
"rand_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_core"
|
||||||
|
version = "0.9.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.3.4",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "raw-window-handle"
|
name = "raw-window-handle"
|
||||||
version = "0.6.2"
|
version = "0.6.2"
|
||||||
@@ -3630,6 +3698,25 @@ dependencies = [
|
|||||||
"thiserror 2.0.20",
|
"thiserror 2.0.20",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-plugin-notification"
|
||||||
|
version = "2.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ad2fd40946aef810c4be9fd33a2d1b9b397cb79042b2d21c81a0a8f204354fd1"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"notify-rust",
|
||||||
|
"rand",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_repr",
|
||||||
|
"tauri",
|
||||||
|
"tauri-plugin",
|
||||||
|
"thiserror 2.0.20",
|
||||||
|
"time",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-single-instance"
|
name = "tauri-plugin-single-instance"
|
||||||
version = "2.4.3"
|
version = "2.4.3"
|
||||||
@@ -3761,6 +3848,17 @@ dependencies = [
|
|||||||
"toml 1.1.4+spec-1.1.0",
|
"toml 1.1.4+spec-1.1.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-winrt-notification"
|
||||||
|
version = "0.7.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9ed071c670382e85fc2f48ae706492d8c338f4f89bf72520d32f8abfe880aade"
|
||||||
|
dependencies = [
|
||||||
|
"thiserror 2.0.20",
|
||||||
|
"windows",
|
||||||
|
"windows-version",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tempfile"
|
name = "tempfile"
|
||||||
version = "3.27.0"
|
version = "3.27.0"
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ tauri = { version = "2", features = ["tray-icon", "image-png"] }
|
|||||||
tauri-plugin-global-shortcut = "2" # 전역 핫키 (.NET HotKeyService 대체)
|
tauri-plugin-global-shortcut = "2" # 전역 핫키 (.NET HotKeyService 대체)
|
||||||
tauri-plugin-single-instance = "2" # 단일 인스턴스 (.NET SingleInstanceGuard 대체)
|
tauri-plugin-single-instance = "2" # 단일 인스턴스 (.NET SingleInstanceGuard 대체)
|
||||||
tauri-plugin-window-state = "2" # 창 위치·크기 저장 (.NET JsonWindowPlacementStore 대체)
|
tauri-plugin-window-state = "2" # 창 위치·크기 저장 (.NET JsonWindowPlacementStore 대체)
|
||||||
|
tauri-plugin-notification = "2" # 답변 완료 Windows 토스트
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
rusqlite = { version = "0.32.1", features = ["bundled"] }
|
rusqlite = { version = "0.32.1", features = ["bundled"] }
|
||||||
|
|||||||
@@ -3,5 +3,5 @@
|
|||||||
"identifier": "default",
|
"identifier": "default",
|
||||||
"description": "메인 창 권한. 프론트는 core API 를 직접 안 부르고 우리 #[tauri::command] 만 invoke 하므로 core:default 로 충분 (listen/emit 포함).",
|
"description": "메인 창 권한. 프론트는 core API 를 직접 안 부르고 우리 #[tauri::command] 만 invoke 하므로 core:default 로 충분 (listen/emit 포함).",
|
||||||
"windows": ["main"],
|
"windows": ["main"],
|
||||||
"permissions": ["core:default"]
|
"permissions": ["core:default", "notification:default"]
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 3.2 KiB |
@@ -11,7 +11,7 @@ use std::sync::Mutex;
|
|||||||
|
|
||||||
use tauri::{AppHandle, State};
|
use tauri::{AppHandle, State};
|
||||||
|
|
||||||
use crate::shell::{paste, paste::PasteState, window};
|
use crate::shell::{notify, paste, paste::PasteState, window};
|
||||||
|
|
||||||
/// JS 가 보고하는 현재 route + 마지막 챗봇(`/snap`) route.
|
/// JS 가 보고하는 현재 route + 마지막 챗봇(`/snap`) route.
|
||||||
/// (.NET `App.xaml.cs` 의 `_currentRoute` / `_lastSnapRoute` 대응)
|
/// (.NET `App.xaml.cs` 의 `_currentRoute` / `_lastSnapRoute` 대응)
|
||||||
@@ -54,6 +54,13 @@ pub fn window_hide(app: AppHandle) {
|
|||||||
let _ = window::hide(&app);
|
let _ = window::hide(&app);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 답변 스트림이 끝났음 — 사용자가 창을 안 보고 있으면 토스트 + 트레이 점.
|
||||||
|
/// 프론트 `send({type:"chat.done", title})`.
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn chat_done(app: AppHandle, title: Option<String>) {
|
||||||
|
notify::answer_done(&app, title.as_deref().unwrap_or(""));
|
||||||
|
}
|
||||||
|
|
||||||
/// 프레임리스 창이라 제목표시줄이 없음 — React 헤더 mousedown 이 이걸 불러 창을 끈다.
|
/// 프레임리스 창이라 제목표시줄이 없음 — React 헤더 mousedown 이 이걸 불러 창을 끈다.
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn window_drag(app: AppHandle) {
|
pub fn window_drag(app: AppHandle) {
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ pub fn run() {
|
|||||||
.build(),
|
.build(),
|
||||||
)
|
)
|
||||||
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
|
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
|
||||||
|
.plugin(tauri_plugin_notification::init())
|
||||||
.manage(commands::RouteState::default())
|
.manage(commands::RouteState::default())
|
||||||
.manage(paste::PasteState::default())
|
.manage(paste::PasteState::default())
|
||||||
.manage(window::TemporaryWindowState::default())
|
.manage(window::TemporaryWindowState::default())
|
||||||
@@ -73,6 +74,7 @@ pub fn run() {
|
|||||||
commands::snippets_update,
|
commands::snippets_update,
|
||||||
commands::snippets_delete,
|
commands::snippets_delete,
|
||||||
commands::snippets_record_use,
|
commands::snippets_record_use,
|
||||||
|
commands::chat_done,
|
||||||
])
|
])
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
let handle = app.handle().clone();
|
let handle = app.handle().clone();
|
||||||
@@ -103,6 +105,10 @@ pub fn run() {
|
|||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
.on_window_event(|window, event| {
|
.on_window_event(|window, event| {
|
||||||
|
// 창을 다시 보면 트레이 "새 답변" 점 원복.
|
||||||
|
if matches!(event, tauri::WindowEvent::Focused(true)) {
|
||||||
|
shell::notify::seen(window.app_handle());
|
||||||
|
}
|
||||||
if matches!(event, tauri::WindowEvent::CloseRequested { .. }) {
|
if matches!(event, tauri::WindowEvent::CloseRequested { .. }) {
|
||||||
if let Err(error) = window::restore_size(window.app_handle()) {
|
if let Err(error) = window::restore_size(window.app_handle()) {
|
||||||
eprintln!("[window] {error}");
|
eprintln!("[window] {error}");
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ pub mod capture;
|
|||||||
#[path = "capture_stub.rs"]
|
#[path = "capture_stub.rs"]
|
||||||
pub mod capture;
|
pub mod capture;
|
||||||
pub mod hotkey;
|
pub mod hotkey;
|
||||||
|
pub mod notify;
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
pub mod paste;
|
pub mod paste;
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
//! 답변 완료 알림 — 사용자가 창을 안 보고 있을 때만.
|
||||||
|
//!
|
||||||
|
//! 토스트(Windows 알림센터)는 몇 초 뒤 사라지고, 트레이 점은 창을 다시 볼 때까지 남는다. 둘 다 씀.
|
||||||
|
//! 창이 앞에 있고 포커스면 아무것도 안 함 — 보고 있는데 알림 뜨면 짜증.
|
||||||
|
//! 토스트는 설치본(msi)에서만 알림센터에 등록돼 보이고 `tauri dev` 에선 안 뜰 수 있음 — 그땐 트레이 점으로 확인.
|
||||||
|
|
||||||
|
use tauri::{AppHandle, Manager};
|
||||||
|
use tauri_plugin_notification::NotificationExt;
|
||||||
|
|
||||||
|
use super::{tray, window};
|
||||||
|
|
||||||
|
pub const TOOLTIP: &str = "CodeAssist";
|
||||||
|
const TOOLTIP_ALERT: &str = "CodeAssist — 새 답변 도착";
|
||||||
|
|
||||||
|
/// 창이 보이고 포커스까지 있으면 "보고 있음".
|
||||||
|
fn user_is_watching(app: &AppHandle) -> bool {
|
||||||
|
window::is_visible(app)
|
||||||
|
&& window::main_window(app)
|
||||||
|
.and_then(|w| w.is_focused().ok())
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn answer_done(app: &AppHandle, title: &str) {
|
||||||
|
if user_is_watching(app) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tray::mark_alert(app, TOOLTIP_ALERT);
|
||||||
|
let body = if title.trim().is_empty() {
|
||||||
|
"새 답변이 도착했어".to_string()
|
||||||
|
} else {
|
||||||
|
title.trim().to_string()
|
||||||
|
};
|
||||||
|
if let Err(error) = app
|
||||||
|
.notification()
|
||||||
|
.builder()
|
||||||
|
.title("답변 완료")
|
||||||
|
.body(body)
|
||||||
|
.show()
|
||||||
|
{
|
||||||
|
eprintln!("[notify] 토스트 실패(설치본 아니면 정상): {error}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 창이 포커스를 받으면 트레이 점 원복. `on_window_event(Focused(true))` 에서 부른다.
|
||||||
|
pub fn seen(app: &AppHandle) {
|
||||||
|
tray::clear_alert(app, TOOLTIP);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manager 는 tray_by_id/default_window_icon 용으로 tray.rs 가 쓰고, 여기선 is_focused 경로에서 씀.
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
use Manager as _;
|
||||||
@@ -2,10 +2,13 @@
|
|||||||
//!
|
//!
|
||||||
//! 메뉴 구성은 .NET 판과 똑같이 열기 / 항상 위에 고정(체크) / 종료 3개.
|
//! 메뉴 구성은 .NET 판과 똑같이 열기 / 항상 위에 고정(체크) / 종료 3개.
|
||||||
|
|
||||||
|
use tauri::image::Image;
|
||||||
use tauri::menu::{CheckMenuItem, Menu, MenuItem};
|
use tauri::menu::{CheckMenuItem, Menu, MenuItem};
|
||||||
use tauri::tray::TrayIconBuilder;
|
use tauri::tray::TrayIconBuilder;
|
||||||
use tauri::AppHandle;
|
use tauri::AppHandle;
|
||||||
|
|
||||||
|
const TRAY_ID: &str = "main";
|
||||||
|
|
||||||
/// 트레이에서 사용자가 고른 것. 실제로 뭘 할지는 조립부(lib.rs)가 정한다.
|
/// 트레이에서 사용자가 고른 것. 실제로 뭘 할지는 조립부(lib.rs)가 정한다.
|
||||||
pub enum TrayAction {
|
pub enum TrayAction {
|
||||||
Open,
|
Open,
|
||||||
@@ -28,7 +31,7 @@ where
|
|||||||
.expect("번들 아이콘 없음 — tauri.conf.json 의 bundle.icon 확인")
|
.expect("번들 아이콘 없음 — tauri.conf.json 의 bundle.icon 확인")
|
||||||
.clone();
|
.clone();
|
||||||
|
|
||||||
TrayIconBuilder::with_id("main")
|
TrayIconBuilder::with_id(TRAY_ID)
|
||||||
.icon(icon)
|
.icon(icon)
|
||||||
.tooltip(tooltip)
|
.tooltip(tooltip)
|
||||||
.menu(&menu)
|
.menu(&menu)
|
||||||
@@ -48,3 +51,25 @@ where
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 답변 도착 표시 — 아이콘에 파란 점 + 툴팁. 사용자가 창을 볼 때까지 남는다(토스트는 사라지니까).
|
||||||
|
pub fn mark_alert(app: &AppHandle, tooltip: &str) {
|
||||||
|
let Some(tray) = app.tray_by_id(TRAY_ID) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if let Ok(icon) = Image::from_bytes(include_bytes!("../../icons/tray-alert.png")) {
|
||||||
|
let _ = tray.set_icon(Some(icon));
|
||||||
|
}
|
||||||
|
let _ = tray.set_tooltip(Some(tooltip));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 알림 표시 원복 — 창이 포커스를 받으면 부른다.
|
||||||
|
pub fn clear_alert(app: &AppHandle, tooltip: &str) {
|
||||||
|
let Some(tray) = app.tray_by_id(TRAY_ID) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if let Some(icon) = app.default_window_icon() {
|
||||||
|
let _ = tray.set_icon(Some(icon.clone()));
|
||||||
|
}
|
||||||
|
let _ = tray.set_tooltip(Some(tooltip));
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,3 +10,4 @@
|
|||||||
| 13:30 | 스트림 타임아웃 env 화 — 첫 이벤트 60→300s, FabriX 조각 간 120→300s (Gemma4 '안녕' 200초 대비). 한 턴 600 유지 |
|
| 13:30 | 스트림 타임아웃 env 화 — 첫 이벤트 60→300s, FabriX 조각 간 120→300s (Gemma4 '안녕' 200초 대비). 한 턴 600 유지 |
|
||||||
| 13:39 | 관리자 대시보드 — 백엔드 /admin/stats 집계(총 토큰·비용·평균 응답, 사용자별·일별, superuser 만) + 앱 /admin 페이지(카드 4·일별 막대·사용자 표·기간 프리셋). 헤더에 관리자만 아이콘 |
|
| 13:39 | 관리자 대시보드 — 백엔드 /admin/stats 집계(총 토큰·비용·평균 응답, 사용자별·일별, superuser 만) + 앱 /admin 페이지(카드 4·일별 막대·사용자 표·기간 프리셋). 헤더에 관리자만 아이콘 |
|
||||||
| 14:04 | 고객사 -12 를 PostgreSQL 로 전환 — -13 과 같은 외부 PG 서버(8851), 스키마 codeassist 분리. 내부 PG 가 SSL 미지원이라 sslmode 를 env 화(prefer). SAP MCP 도 -12 에 별도 기동, 도구 6개 확인 |
|
| 14:04 | 고객사 -12 를 PostgreSQL 로 전환 — -13 과 같은 외부 PG 서버(8851), 스키마 codeassist 분리. 내부 PG 가 SSL 미지원이라 sslmode 를 env 화(prefer). SAP MCP 도 -12 에 별도 기동, 도구 6개 확인 |
|
||||||
|
| 14:13 | 답변 완료 알림 — 창 안 보고 있으면 Windows 토스트 + 트레이 아이콘 파란 점, 창 포커스 받으면 원복. tauri-plugin-notification, 프론트 onDone 에서 chat.done 한 줄 |
|
||||||
|
|||||||
Reference in New Issue
Block a user