Skip to content
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
fe59353
docs(superpowers): spec for global push-to-talk hotkey (#3090)
CodeGhost21 Jun 2, 2026
4c3f560
docs(superpowers): correct PTT spec to use channel.web_chat RPC
CodeGhost21 Jun 2, 2026
e6ce558
docs(superpowers): implementation plan for global push-to-talk (#3090)
CodeGhost21 Jun 2, 2026
8827c6b
feat(channels/web): accept optional speak_reply/source/session_id on …
CodeGhost21 Jun 2, 2026
2da81c0
refactor(channels/web): group ptt fields into ChatRequestMetadata; ti…
CodeGhost21 Jun 2, 2026
d24fa3b
feat(voice/bus): publish DomainEvent::Voice::PttTranscriptCommitted (…
CodeGhost21 Jun 2, 2026
26b84aa
refactor(voice/bus): re-export publish from mod; assert full event pa…
CodeGhost21 Jun 2, 2026
ce061f0
feat(tauri/ptt): add ptt_hotkeys module with shortcut expansion + val…
CodeGhost21 Jun 2, 2026
83ad4e7
refactor(tauri/ptt): tighten PttError visibility; zero-alloc modifier…
CodeGhost21 Jun 2, 2026
f4189cf
feat(channels/web): invoke reply_speech + publish PttTranscriptCommit…
CodeGhost21 Jun 3, 2026
eff5645
refactor(channels/web,voice): harden TTS test seam + tighten T4 revie…
CodeGhost21 Jun 3, 2026
9159709
feat(tauri/ptt): register/unregister IPC + dictation conflict guard (…
CodeGhost21 Jun 3, 2026
368d2e7
fix(tauri/ptt): CAS-guard press/release; robust unregister + rollback…
CodeGhost21 Jun 3, 2026
474b5bc
feat(tauri/ptt): lazy borderless always-on-top overlay window (#3090)
CodeGhost21 Jun 3, 2026
ef9e05d
refactor(tauri/ptt): macOS-gate accept_first_mouse; note CEF shadow n…
CodeGhost21 Jun 3, 2026
a19f1d4
assets(ptt): bundle CC0 open/close/error chimes (#3090)
CodeGhost21 Jun 3, 2026
e16e0c2
feat(store/ptt): redux slice for ptt hotkey + settings (#3090)
CodeGhost21 Jun 3, 2026
c258e92
refactor(store/ptt): align selectors; add resetUserScopedState test (…
CodeGhost21 Jun 3, 2026
aeb99c6
feat(chatService): forward speakReply/source/sessionId; add ptt tauri…
CodeGhost21 Jun 3, 2026
1367250
style(ptt-wrappers): add skip/done debug logs to showPttOverlay (#3090)
CodeGhost21 Jun 3, 2026
c3c642f
feat(pttService): state machine, watchdog, preempt, fallback thread (…
CodeGhost21 Jun 3, 2026
0aec9d2
refactor(pttService): close preempt race; cover cancel/start-fail/tra…
CodeGhost21 Jun 3, 2026
782806f
feat(ptt): mount PttHotkeyManager + wire service to real audio/STT/ch…
CodeGhost21 Jun 3, 2026
13dcee2
feat(ptt/ui): overlay page at /ptt-overlay with idle/active states (#…
CodeGhost21 Jun 3, 2026
88b5d3e
fix(ptt/overlay): close listener leak on fast unmount via cancelled g…
CodeGhost21 Jun 3, 2026
7759af9
feat(settings/voice): PttSettingsPanel + 13-locale i18n (#3090)
CodeGhost21 Jun 3, 2026
8c72740
feat(ptt/settings): surface registration errors with localized messag…
CodeGhost21 Jun 4, 2026
fe7d781
test(ptt/e2e): full bind→hold→commit flow with mocked STT (#3090)
CodeGhost21 Jun 4, 2026
2acab20
feat(about_app): register voice.ptt capability (#3090)
CodeGhost21 Jun 4, 2026
8e5ce8d
style(ptt): apply prettier + cargo fmt + fix empty-block lint (#3090)
CodeGhost21 Jun 4, 2026
b4796d0
Merge branch 'main' into feat/global-ptt-3090
CodeGhost21 Jun 4, 2026
37c65b5
Merge remote-tracking branch 'upstream/main' into feat/global-ptt-3090
CodeGhost21 Jun 5, 2026
2368405
fix(ptt): catch rejected promises from onStart/onStop service calls (…
CodeGhost21 Jun 5, 2026
fc3f36a
i18n(ptt): make description conditional on speakReplies setting acros…
CodeGhost21 Jun 5, 2026
6625342
fix(ptt/settings): normalize Space key label and allow Tab to pass th…
CodeGhost21 Jun 5, 2026
20bf4c2
fix(ptt/service): catch thread-resolution and sendMessage failures in…
CodeGhost21 Jun 5, 2026
a8b5a2a
docs(ptt): fix locale count (12→13) in spec; add PTT IPC commands to …
CodeGhost21 Jun 5, 2026
ef2dfe8
chore(pr-manager): apply formatting
CodeGhost21 Jun 5, 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
192 changes: 192 additions & 0 deletions app/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ mod notch_window;
mod notification_settings;
mod process_kill;
mod process_recovery;
mod ptt_hotkeys;
mod ptt_overlay;
#[cfg(target_os = "windows")]
mod reset_reboot_schedule;
mod screen_capture;
Expand Down Expand Up @@ -758,6 +760,18 @@ async fn register_dictation_hotkey(
expanded_shortcuts.join(", ")
);

// Reject overlap with the currently-registered PTT hotkey.
let ptt_current = {
let state = app.state::<ptt_hotkeys::PttHotkeyState>();
let guard = state.shortcut.lock().unwrap();
guard.clone()
};
if let Some(conflict) = ptt_hotkeys::first_conflict_with(&expanded_shortcuts, &ptt_current) {
return Err(format!(
"dictation shortcut '{conflict}' conflicts with the push-to-talk hotkey"
));
}

let register_shortcut = |shortcut_variant: &str| -> Result<(), String> {
let app_clone = app.clone();
app.global_shortcut()
Expand Down Expand Up @@ -852,6 +866,180 @@ async fn unregister_dictation_hotkey(app: AppHandle<AppRuntime>) -> Result<(), S
Ok(())
}

/// Register (or re-register) the global push-to-talk hotkey. Emits
/// `ptt://start { session_id }` on press and `ptt://stop { session_id }`
/// on release.
#[tauri::command]
async fn register_ptt_hotkey(app: AppHandle<AppRuntime>, shortcut: String) -> Result<(), String> {
log::info!("[ptt] register_ptt_hotkey: shortcut={shortcut}");

let expanded = ptt_hotkeys::expand_ptt_shortcuts(&shortcut).map_err(|e| e.to_string())?;

// Reject overlap with the currently-registered dictation hotkey.
let dictation_current = {
let state = app.state::<dictation_hotkeys::DictationHotkeyState>();
let guard = state.0.lock().unwrap();
guard.clone()
};
if let Some(conflict) = ptt_hotkeys::first_conflict_with(&expanded, &dictation_current) {
return Err(ptt_hotkeys::PttError::ConflictsWithDictation(conflict).to_string());
}

let old_shortcuts = {
let state = app.state::<ptt_hotkeys::PttHotkeyState>();
let guard = state.shortcut.lock().unwrap();
guard.clone()
};

// Lazy-instantiate the overlay window so it's ready before the first press.
if let Err(e) = ptt_overlay::ensure_window(&app) {
log::warn!("[ptt] overlay window create failed (continuing): {e}");
}

let register_shortcut = |variant: &str| -> Result<(), String> {
let app_pressed = app.clone();
let app_released = app.clone();
let variant_owned = variant.to_string();
app.global_shortcut()
.on_shortcut(variant, move |app_inner, _sc, event| {
let state = app_inner.state::<ptt_hotkeys::PttHotkeyState>();
match event.state {
ShortcutState::Pressed => {
// Drop OS key-repeat events; only the first Pressed of a hold opens a session.
if state
.is_held
.compare_exchange(
false,
true,
std::sync::atomic::Ordering::AcqRel,
std::sync::atomic::Ordering::Acquire,
)
.is_err()
{
log::trace!(
"[ptt] press dropped (already held) shortcut={variant_owned}"
);
return;
}
let session_id = state
.session_counter
.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
+ 1;
log::debug!(
"[ptt] pressed shortcut={variant_owned} session_id={session_id}"
);
if let Err(e) = app_pressed.emit(
"ptt://start",
serde_json::json!({
"session_id": session_id,
}),
) {
log::warn!("[ptt] emit start failed: {e}");
}
}
ShortcutState::Released => {
if !state
.is_held
.swap(false, std::sync::atomic::Ordering::AcqRel)
{
// No corresponding Pressed in our state — stale event, drop.
log::trace!(
"[ptt] release dropped (not held) shortcut={variant_owned}"
);
return;
}
let session_id = state
.session_counter
.load(std::sync::atomic::Ordering::Relaxed);
log::debug!(
"[ptt] released shortcut={variant_owned} session_id={session_id}"
);
if let Err(e) = app_released.emit(
"ptt://stop",
serde_json::json!({
"session_id": session_id,
}),
) {
log::warn!("[ptt] emit stop failed: {e}");
}
}
}
})
.map_err(|e| format!("Failed to register ptt shortcut '{variant}': {e}"))
};

// Unregister previous PTT variants.
let mut unregistered: Vec<String> = Vec::new();
for old in &old_shortcuts {
if let Err(e) = app.global_shortcut().unregister(old.as_str()) {
// Rollback already-unregistered ones.
for r in &unregistered {
if let Err(re) = register_shortcut(r) {
log::warn!("[ptt] rollback failed for '{r}': {re}");
}
}
return Err(format!(
"Failed to unregister previous ptt shortcut '{old}': {e}"
));
}
unregistered.push(old.clone());
}

// Register the new variants. Rollback on first failure.
let mut newly_registered: Vec<String> = Vec::new();
for v in &expanded {
if let Err(e) = register_shortcut(v) {
for r in &newly_registered {
if let Err(re) = app.global_shortcut().unregister(r.as_str()) {
log::warn!("[ptt] rollback failed for '{r}': {re}");
}
}
for old in &old_shortcuts {
if let Err(re) = register_shortcut(old) {
log::warn!("[ptt] rollback failed for '{old}': {re}");
}
}
return Err(e);
}
newly_registered.push(v.clone());
}

{
let state = app.state::<ptt_hotkeys::PttHotkeyState>();
let mut guard = state.shortcut.lock().unwrap();
*guard = expanded.clone();
}

log::info!("[ptt] registered: {}", expanded.join(", "));
Ok(())
}

/// Unregister the global PTT hotkey (if any).
#[tauri::command]
async fn unregister_ptt_hotkey(app: AppHandle<AppRuntime>) -> Result<(), String> {
log::info!("[ptt] unregister_ptt_hotkey: called");
let state = app.state::<ptt_hotkeys::PttHotkeyState>();
let old = {
let guard = state.shortcut.lock().unwrap();
guard.clone()
};
let mut still_registered: Vec<String> = Vec::new();
for s in &old {
if let Err(e) = app.global_shortcut().unregister(s.as_str()) {
log::warn!("[ptt] unregister '{s}' failed: {e}");
still_registered.push(s.clone());
}
}
// Only retain variants that genuinely failed to unregister; the rest are gone.
{
let mut guard = state.shortcut.lock().unwrap();
*guard = still_registered;
}
// Destroy the overlay window so resources are released.
ptt_overlay::destroy_window(&app);
Ok(())
}

fn is_daemon_mode() -> bool {
std::env::args().any(|arg| arg == "daemon" || arg == "--daemon")
}
Expand Down Expand Up @@ -2487,6 +2675,7 @@ pub fn run() {
.manage(dictation_hotkeys::DictationHotkeyState(
std::sync::Mutex::new(Vec::new()),
))
.manage(ptt_hotkeys::PttHotkeyState::new())
.manage(companion_commands::CompanionHotkeyState(
std::sync::Mutex::new(Vec::new()),
))
Expand Down Expand Up @@ -3240,6 +3429,9 @@ pub fn run() {
schedule_cef_profile_purge,
register_dictation_hotkey,
unregister_dictation_hotkey,
register_ptt_hotkey,
unregister_ptt_hotkey,
ptt_overlay::show_ptt_overlay,
webview_accounts::webview_account_open,
webview_accounts::webview_account_prewarm,
webview_accounts::webview_account_close,
Expand Down
Loading
Loading