Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- Diff reviews can be annotated line by line in both Unified and Editor views. Use the comment action on a changed line to write a note and add its file, line number, and code context to the active composer; collect multiple comments and send them to the agent in one prompt.
- Settings: Notifications, off by default. With it on, a system notification appears when a turn finishes or an agent waits on an approval or question in a session that is not on screen, whether MonoCode is in the background or another session is open; clicking it jumps to that session. Turning it on asks macOS for permission, and a blocked state links to System Settings. The Sounds setting decides whether the notification plays a sound, and the in-app cue is skipped when the banner fires.

## [0.1.32] - 2026-09-04

Expand Down
40 changes: 40 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,12 @@ block2 = "0.6.2"
objc2 = "0.6"
objc2-app-kit = { version = "0.3", features = ["NSApplication", "NSButton", "NSColor", "NSControl", "NSDockTile", "NSImage", "NSLayoutAnchor", "NSLayoutConstraint", "NSMenu", "NSMenuItem", "NSResponder", "NSView", "NSWindow", "objc2-core-foundation"] }
objc2-foundation = { version = "0.3", features = ["NSGeometry", "NSError", "NSString"] }
objc2-user-notifications = { version = "0.3.2", features = ["UNUserNotificationCenter", "block2"] }
objc2-user-notifications = { version = "0.3.2", features = ["UNUserNotificationCenter", "UNNotification", "UNNotificationAction", "UNNotificationCategory", "UNNotificationContent", "UNNotificationRequest", "UNNotificationResponse", "UNNotificationSettings", "UNNotificationSound", "block2"] }
raw-window-handle = "0.6"

[target.'cfg(target_os = "linux")'.dependencies]
notify-rust = "4.18"

[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-updater = "2"

Expand Down
6 changes: 6 additions & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ mod linear;
mod macos;
mod menu;
mod notes;
mod notifications;
mod project_logo;
mod pty;
mod rate_limits;
Expand Down Expand Up @@ -165,6 +166,10 @@ pub fn run() {
.invoke_handler(tauri::generate_handler![
default_cwd,
home_dir,
notifications::notification_permission,
notifications::request_notification_permission,
notifications::show_notification,
notifications::open_notification_settings,
fs::list_dir,
fs::list_project_files,
fs::git_diff_stats,
Expand Down Expand Up @@ -300,6 +305,7 @@ pub fn run() {
#[cfg(target_os = "macos")]
{
macos::request_badge_authorization();
notifications::install_delegate(handle);
#[cfg(debug_assertions)]
macos::prefer_bundle_dock_icon();
}
Expand Down
54 changes: 44 additions & 10 deletions src-tauri/src/macos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -378,21 +378,39 @@ thread_local! {
/// Since macOS 12, `NSDockTile` badge updates are ignored unless the app has
/// requested `UNUserNotificationCenter` authorization with the badge option.
/// Must run on the main thread after launch (`RunEvent::Ready`), not in setup.
///
/// Only re-requests once the user has already answered the prompt: the
/// one-time system dialog is reserved for the Notifications toggle, so a
/// badge-only request at startup must not consume it. Until then the badge
/// stays off.
pub(crate) fn request_badge_authorization() {
let Some(mtm) = MainThreadMarker::new() else {
if MainThreadMarker::new().is_none() {
return;
};
}

use block2::RcBlock;
use objc2::runtime::Bool;
use objc2_foundation::NSError;
use objc2_user_notifications::{UNAuthorizationOptions, UNUserNotificationCenter};
use objc2_user_notifications::{
UNAuthorizationOptions, UNAuthorizationStatus, UNNotificationSettings,
UNUserNotificationCenter,
};
use std::ptr::NonNull;

let center = UNUserNotificationCenter::currentNotificationCenter();
let options = UNAuthorizationOptions::Badge;
let handler = RcBlock::new(|_granted: Bool, _error: *mut NSError| {});
center.requestAuthorizationWithOptions_completionHandler(options, &handler);
let _ = mtm;
let handler = RcBlock::new(|settings: NonNull<UNNotificationSettings>| {
let settings = unsafe { settings.as_ref() };
if settings.authorizationStatus() == UNAuthorizationStatus::NotDetermined {
return;
}
let done = RcBlock::new(|_granted: Bool, _error: *mut NSError| {});
UNUserNotificationCenter::currentNotificationCenter()
.requestAuthorizationWithOptions_completionHandler(
UNAuthorizationOptions::Badge,
&done,
);
});
center.getNotificationSettingsWithCompletionHandler(&handler);
}

pub(crate) fn install_dock_menu(app: &AppHandle) {
Expand Down Expand Up @@ -497,15 +515,28 @@ fn relaunch_from_dev_bundle() -> Result<(), String> {

let bundled = macos_dir.join("monocode");
let _ = std::fs::remove_file(&bundled);
if std::fs::hard_link(&exe, &bundled).is_err() {
std::fs::copy(&exe, &bundled).map_err(|e| e.to_string())?;
}
// A copy, not a hard link: re-signing below rewrites the file, and the
// linked original is the executable running this code.
std::fs::copy(&exe, &bundled).map_err(|e| e.to_string())?;
let mut perms = std::fs::metadata(&bundled)
.map_err(|e| e.to_string())?
.permissions();
perms.set_mode(0o755);
std::fs::set_permissions(&bundled, perms).map_err(|e| e.to_string())?;

// The linker's ad-hoc signature carries a `monocode-<hash>` identifier.
// UNUserNotificationCenter refuses authorization, without prompting,
// unless the signing identifier matches CFBundleIdentifier.
let signed = Command::new("/usr/bin/codesign")
.args(["--force", "--sign", "-", "--identifier", DEV_BUNDLE_ID])
.arg(&app)
.status()
.map(|status| status.success())
.unwrap_or(false);
if !signed {
eprintln!("monocode: macos dev bundle: codesign failed; notifications stay off");
}

let err = Command::new(&bundled)
.args(std::env::args_os().skip(1))
.exec();
Expand All @@ -525,6 +556,9 @@ fn write_dev_bundle_icons(app: &std::path::Path) -> Result<(), String> {
Ok(())
}

/// Must match `CFBundleIdentifier` in `DEV_BUNDLE_PLIST` and tauri.conf.json.
#[cfg(debug_assertions)]
const DEV_BUNDLE_ID: &str = "com.monocode.desktop";
#[cfg(debug_assertions)]
const DEV_ICNS: &[u8] = include_bytes!("../icons/icon.icns");
#[cfg(debug_assertions)]
Expand Down
Loading
Loading