Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
95df1b6
feat(notify): user-notify 桌面通知与点击深链
petros-double-test1 Jul 28, 2026
13a921a
fix(notify): restore needs-you refresh and harden click deep links
petros-double-test1 Jul 28, 2026
6371276
fix(notify): prevent deep-link races and re-query permission
petros-double-test1 Jul 28, 2026
270ae67
fix(notify): honor denied permission and stabilize click delivery
petros-double-test1 Jul 28, 2026
305c6dc
fix(notify): stop rememberThreads recursive self-call
petros-double-test1 Jul 28, 2026
43a9a5b
fix(notify): surface main-window focus failures on click
petros-double-test1 Jul 28, 2026
18d5506
fix(notify): wait for workspace load before deep-link apply
petros-double-test1 Jul 28, 2026
d9b11e1
fix(notify): expose workspaceLoadSeq on store value
petros-double-test1 Jul 28, 2026
ed03adc
fix(notify): badge action-required, global quota, settings deep-link
petros-double-test1 Jul 29, 2026
1ce13bc
fix(notify): settle workspace loads and harden deep-link edge cases
petros-double-test1 Jul 29, 2026
cf88f87
fix(notify): add open_curator fields to os_notify payloads
petros-double-test1 Jul 29, 2026
778b689
fix(notify): open Repo Map before curator drawer from notifications
petros-double-test1 Jul 29, 2026
4231a7c
fix(notify): harden deep-link loading, ownership, and permission state
petros-double-test1 Jul 31, 2026
d9daacf
fix(notify): include workspace_id in IM Ask test fixtures
petros-double-test1 Jul 31, 2026
661ed78
fix(notify): close deep-link review races
petros-double-test1 Jul 31, 2026
b8c3557
fix(notify): wait for source hydration
petros-double-test1 Jul 31, 2026
161b7b8
fix(notify): close hydration and routing edges
petros-double-test1 Jul 31, 2026
728097d
fix(notify): close concurrent load review gaps
petros-double-test1 Jul 31, 2026
d5149fe
fix(notify): harden hydration and click delivery
petros-double-test1 Jul 31, 2026
7a018ab
fix(notify): preserve click ownership across races
petros-double-test1 Jul 31, 2026
10be1bd
fix(notify): align needs and refresh ownership
petros-double-test1 Jul 31, 2026
76f4d9f
fix(notify): guard deferred workspace navigation
petros-double-test1 Jul 31, 2026
a67df3e
fix(notify): preserve category events during hydration
petros-double-test1 Jul 31, 2026
b14bfe7
fix(notify): preserve live stall transitions
petros-double-test1 Jul 31, 2026
fa8b44c
fix(notify): finish workspace hydration guards
petros-double-test1 Jul 31, 2026
c2502e5
fix(notify): serialize hydration and ownership
petros-double-test1 Jul 31, 2026
99e845d
fix(notifications): close latest review follow-ups
petros-double-test1 Jul 31, 2026
ec84890
fix(notifications): preserve cross-workspace event state
petros-double-test1 Jul 31, 2026
ff587ac
fix(notifications): cover workspace-wide review refresh
petros-double-test1 Jul 31, 2026
3b9809d
fix(notifications): harden workspace overview polling
petros-double-test1 Jul 31, 2026
0b7ff03
fix(notifications): isolate ready sources and stale navigation
petros-double-test1 Jul 31, 2026
3946632
fix(notifications): rebaseline muted review state
petros-double-test1 Jul 31, 2026
aa2f817
fix(notifications): preserve early quota transitions
petros-double-test1 Jul 31, 2026
6307880
fix(notifications): harden hydration and deep links
petros-double-test1 Jul 31, 2026
d636da6
fix(notifications): retain first quota transition
petros-double-test1 Jul 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
549 changes: 549 additions & 0 deletions src-tauri/Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ tauri-plugin-opener = "2"
tauri-plugin-dialog = "2"
tauri-plugin-updater = "2"
tauri-plugin-notification = "2"
user-notify = "0.4.2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
serde_yaml = "0.9"
Expand Down
3 changes: 3 additions & 0 deletions src-tauri/capabilities/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
},
"dialog:default",
"notification:default",
"core:window:allow-set-badge-count",
"core:window:allow-request-user-attention",
"core:window:allow-is-focused",
"updater:default",
"mcp-bridge:default",
"process:default"
Expand Down
4 changes: 4 additions & 0 deletions src-tauri/src/ask.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1302,6 +1302,9 @@ pub struct Ask {
pub thread_title: String,
#[serde(default)]
pub dir_name: String,
/// Owning workspace id, filled when listed (pending_asks).
#[serde(default)]
pub workspace_id: Option<i32>,
/// The canonical, EXACT action identity (full command / full path set /
/// full args) set by the engine's ask-creation call — distinct from
/// `summary`, which may truncate for display. Used ONLY for Always-grant
Expand Down Expand Up @@ -1688,6 +1691,7 @@ impl AskRegistry {
ts: now(),
thread_title: String::new(),
dir_name: String::new(),
workspace_id: None,
action_key: action_key.to_string(),
};
g.open.push(ask.clone());
Expand Down
25 changes: 18 additions & 7 deletions src-tauri/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1971,6 +1971,7 @@ pub async fn pending_asks(
for a in &mut open {
if let Ok(Some(t)) = repo::get_thread(&db, a.thread).await {
a.thread_title = t.title;
a.workspace_id = Some(t.workspace_id);
}
if let Ok(id) = a.dir.parse::<i32>() {
if let Ok(Some(d)) = repo::get_direction(&db, id).await {
Expand Down Expand Up @@ -2369,27 +2370,37 @@ pub async fn workspace_needs_counts(
let open_asks = asks.open();
let mut out = Vec::new();
for w in repo::list_workspaces(&db).await.map_err(e)? {
let threads: Vec<_> = repo::list_threads(&db, w.id)
let all_threads = repo::list_threads(&db, w.id)
.await
.map_err(e)?
.map_err(e)?;
let workspace_tids: HashSet<i32> = all_threads.iter().map(|t| t.id).collect();
let threads: Vec<_> = all_threads
.into_iter()
.filter(|t| t.kind != "curator") // hidden curator chat isn't a board issue
.collect();
let tids: HashSet<i32> = threads.iter().map(|t| t.id).collect();
let mut count: u32 = 0;
for t in &threads {
// Excludes self-clearing NOTICEs (e.g. the stall hint) — they carry
// no action, so they must not inflate the "needs you" count that
// flags other workspaces (issue #105).
// Questions still come from open_answerable_ask_count (issue #105).
// Action-required notices are not answerable, but they leave a durable
// Retry control in Needs-you, so the dock badge and workspace switcher
// should count them. Self-clearing notices stay out.
count += bus.open_answerable_ask_count(t.id) as u32;
count += bus
.open_asks(t.id)
.iter()
.filter(|a| a.kind == crate::bus::AskKind::NoticeActionRequired)
.count() as u32;
Comment thread
SoloJiang marked this conversation as resolved.
count += crate::planner::pending_writes(&db, t.id)
.await
.map_err(e)?
.len() as u32;
}
count += open_asks
.iter()
.filter(|a| tids.contains(&a.thread))
// Permission asks are global and the hidden curator thread is not
// part of `threads` above, but its asks still belong to this
// workspace and appear in Needs-you.
.filter(|a| workspace_tids.contains(&a.thread))
.count() as u32;
out.push((w.id, count));
}
Expand Down
1 change: 1 addition & 0 deletions src-tauri/src/im/outbound.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ mod tests {
ts: 0,
thread_title: "登录超时修复".into(),
dir_name: "backend".into(),
workspace_id: None,
action_key: "claude:npm test".into(),
}
}
Expand Down
12 changes: 12 additions & 0 deletions src-tauri/src/lead_chat/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ fn push_model_arg(extra: &mut Vec<String>, model: Option<&str>) {
#[derive(serde::Serialize, Clone)]
pub struct SessionInfo {
pub session_id: i32,
pub thread_id: i32,
pub repo: String,
pub worktree: String,
pub branch: String,
Expand Down Expand Up @@ -1086,6 +1087,9 @@ pub struct LiveWorkerSlot {
/// The worker's OWN thread (from EngineInner.thread_id) — adoption must not
/// assume the active thread.
pub thread_id: i32,
/// Owning workspace of the worker's thread, so the frontend can deep-link
/// adopted/revived workers even before that workspace is visited.
pub workspace_id: Option<i32>,
pub busy: bool,
pub queue: Vec<engine::QueuedItem>,
}
Expand Down Expand Up @@ -1119,9 +1123,15 @@ async fn build_worker_slots(db: &Db, snaps: Vec<WorkerSnapshot>) -> Vec<LiveWork
continue;
};
let command = crate::tool_command::effective(sess.command.as_deref(), &sess.tool);
let workspace_id = repo::get_thread(db, s.thread_id)
.await
.ok()
.flatten()
.map(|t| t.workspace_id);
out.push(LiveWorkerSlot {
info: SessionInfo {
session_id: sess.id,
thread_id: s.thread_id,
repo: wt.path.clone(),
worktree: wt.path,
branch: wt.branch,
Expand All @@ -1133,6 +1143,7 @@ async fn build_worker_slots(db: &Db, snaps: Vec<WorkerSnapshot>) -> Vec<LiveWork
direction_id: sess.direction_id,
repo_id: sess.repo_id,
thread_id: s.thread_id,
workspace_id,
busy: s.busy,
queue: s.queue,
});
Expand Down Expand Up @@ -1560,6 +1571,7 @@ pub(crate) async fn chat_open_worker_impl(
let command = crate::tool_command::effective(sess.command.as_deref(), &session_tool);
Ok(SessionInfo {
session_id: sess.id,
thread_id: dir.thread_id,
repo: wt.path.clone(),
worktree: wt.path,
branch: wt.branch,
Expand Down
10 changes: 10 additions & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ mod power;
/// 派生并发上限都挂靠此接口(单一口径 `is_ours`,计数==孤儿判定)。
pub mod proc_registry;
mod process_quota;
mod os_notify;
pub mod profile;
/// issue #112 本地资源仪表盘:只读聚合 proc_registry / process_quota /
/// session_gate 已有的采样,不新增采样、不碰任何安全网写路径。
Expand Down Expand Up @@ -235,6 +236,9 @@ pub fn run() {
host::automerge::spawn_pr_automerge_watch(app.handle().clone());
power::spawn_sweep(app.handle().clone());
process_quota::spawn_monitor(app.handle().clone());
if let Err(err) = os_notify::init(app.handle()) {
eprintln!("[weft] os_notify init: {err}");
}
gc::spawn_periodic(app.handle().clone());
skills::spawn_periodic(app.handle().clone());
im::spawn(app.handle().clone());
Expand Down Expand Up @@ -311,6 +315,12 @@ pub fn run() {
commands::set_guardrails,
process_quota::process_quota_status,
resource_dashboard::resource_dashboard_snapshot,
os_notify::os_notify_permission,
os_notify::os_notify_request_permission,
os_notify::os_notify_send,
os_notify::os_notify_take_pending_open,
os_notify::os_notify_ack_open,
os_notify::os_notify_restore_pending_open,
commands::session_for,
commands::session_meta,
commands::effective_config,
Expand Down
Loading
Loading