@@ -36,6 +36,64 @@ enum AudioCmd {
3636static AUDIO_TX : OnceCell < mpsc:: Sender < AudioCmd > > = OnceCell :: new ( ) ;
3737static VOLUME_LEVEL_TX : OnceCell < mpsc:: Sender < f32 > > = OnceCell :: new ( ) ;
3838
39+ // Agent API URL - dev vs prod
40+ #[ cfg( debug_assertions) ]
41+ const AGENT_API_URL : & str = "http://localhost:1337/agent" ;
42+
43+ #[ cfg( not( debug_assertions) ) ]
44+ const AGENT_API_URL : & str = "https://t2t-agent-api.YOUR_SUBDOMAIN.workers.dev/agent" ;
45+
46+ #[ derive( serde:: Deserialize ) ]
47+ struct AgentResponse {
48+ success : bool ,
49+ script : Option < String > ,
50+ blocked : Option < bool > ,
51+ error : Option < String > ,
52+ }
53+
54+ fn call_agent_api ( transcript : & str ) -> Result < AgentResponse , String > {
55+ let client = reqwest:: blocking:: Client :: new ( ) ;
56+ let resp = client
57+ . post ( AGENT_API_URL )
58+ . json ( & serde_json:: json!( { "transcript" : transcript } ) )
59+ . timeout ( std:: time:: Duration :: from_secs ( 30 ) )
60+ . send ( )
61+ . map_err ( |e| format ! ( "Agent API request failed: {e}" ) ) ?;
62+
63+ if !resp. status ( ) . is_success ( ) {
64+ return Err ( format ! ( "Agent API returned {}" , resp. status( ) ) ) ;
65+ }
66+
67+ resp. json :: < AgentResponse > ( )
68+ . map_err ( |e| format ! ( "Failed to parse agent response: {e}" ) )
69+ }
70+
71+ #[ cfg( target_os = "macos" ) ]
72+ fn execute_applescript ( script : & str ) -> Result < String , String > {
73+ use std:: process:: Command ;
74+ let output = Command :: new ( "osascript" )
75+ . arg ( "-e" )
76+ . arg ( script)
77+ . output ( )
78+ . map_err ( |e| format ! ( "Failed to run osascript: {e}" ) ) ?;
79+
80+ if output. status . success ( ) {
81+ Ok ( String :: from_utf8_lossy ( & output. stdout ) . to_string ( ) )
82+ } else {
83+ Err ( String :: from_utf8_lossy ( & output. stderr ) . to_string ( ) )
84+ }
85+ }
86+
87+ #[ cfg( target_os = "macos" ) ]
88+ fn show_notification ( title : & str , message : & str ) {
89+ let script = format ! (
90+ r#"display notification "{}" with title "{}""# ,
91+ message. replace( '"' , "\\ \" " ) ,
92+ title. replace( '"' , "\\ \" " )
93+ ) ;
94+ let _ = execute_applescript ( & script) ;
95+ }
96+
3997fn log_line ( msg : & str ) {
4098 // Best-effort persistent log to help debug Finder vs Terminal launch differences.
4199 let ts = SystemTime :: now ( )
@@ -598,20 +656,12 @@ mod macos_fn_key {
598656 }
599657 }
600658
601- fn handle_fn_key ( pressed : bool , control_held : bool ) {
659+ fn handle_fn_key ( pressed : bool , _control_held : bool ) {
602660 let was_recording = IS_RECORDING . load ( Ordering :: SeqCst ) ;
603661
604662 if pressed && !was_recording {
605663 IS_RECORDING . store ( true , Ordering :: SeqCst ) ;
606664
607- // Fn alone = typing mode, Fn + Control = agent mode
608- let is_text = !control_held;
609- IS_TEXT_INPUT_MODE . store ( is_text, Ordering :: SeqCst ) ;
610-
611- if control_held {
612- log_line ( "Fn + Control detected -> agent mode" ) ;
613- }
614-
615665 // Remember where the user was typing so we can restore focus before pasting.
616666 let pid = capture_frontmost_pid ( ) ;
617667 FRONTMOST_PID . store ( pid, Ordering :: SeqCst ) ;
@@ -639,49 +689,44 @@ mod macos_fn_key {
639689 . and_then ( |e| focused_fingerprint ( e) ) ;
640690 }
641691
642- // Send mode to frontend
643- let mode_str = if is_text { "typing" } else { "agent" } ;
692+ // Start in "pending" state - frontend shows neutral color
644693 if let Some ( w) = app_clone. get_webview_window ( "main" ) {
645- let _ = w. eval ( & format ! ( "window.__setMode && window.__setMode('{} ')" , mode_str ) ) ;
694+ let _ = w. eval ( "window.__setMode && window.__setMode('typing ')" ) ;
646695 }
647696
648- log_line ( & format ! ( "Captured AX focused element (best effort), mode={mode_str}" ) ) ;
697+ log_line ( "Captured AX focused element (best effort)" ) ;
649698 } ) ;
650699 }
651700 log_line ( "Fn pressed - start recording" ) ;
652701
653- // Watchdog: poll modifier flags, update mode on-the-fly when Control changes
702+ // Watchdog: monitor for Ctrl (one-way switch to agent) and Fn release
654703 std:: thread:: spawn ( || {
655- // hard cap so we never get stuck forever
656704 let max_ms = 60_000u64 ; // 60 seconds max recording
657705 let start = std:: time:: Instant :: now ( ) ;
658706 let control_flag: u64 = 1u64 << 18 ;
659- let mut last_control = IS_TEXT_INPUT_MODE . load ( Ordering :: SeqCst ) == false ;
660707
661708 loop {
662709 std:: thread:: sleep ( std:: time:: Duration :: from_millis ( 25 ) ) ;
663710 if !IS_RECORDING . load ( Ordering :: SeqCst ) {
664711 break ;
665712 }
713+
714+ let elapsed_ms = start. elapsed ( ) . as_millis ( ) as u64 ;
666715 let flags = unsafe { CGEventSourceFlagsState ( K_CG_EVENT_SOURCE_STATE_COMBINED_SESSION_STATE ) } ;
667716 let fn_down = ( flags & K_CG_EVENT_FLAG_MASK_SECONDARY_FN ) != 0 ;
668717 let control_down = ( flags & control_flag) != 0 ;
669718
670- // Update mode on-the-fly if Control state changed
671- if control_down != last_control {
672- last_control = control_down;
673- let is_text = !control_down;
674- IS_TEXT_INPUT_MODE . store ( is_text, Ordering :: SeqCst ) ;
675- let mode_str = if is_text { "typing" } else { "agent" } ;
676- log_line ( & format ! ( "Mode switched to {} (Control {})" , mode_str, if control_down { "pressed" } else { "released" } ) ) ;
719+ // One-way switch: if Ctrl pressed at any time, switch to agent mode permanently
720+ if control_down && IS_TEXT_INPUT_MODE . load ( Ordering :: SeqCst ) {
721+ IS_TEXT_INPUT_MODE . store ( false , Ordering :: SeqCst ) ;
722+ log_line ( "Control pressed -> agent mode (locked)" ) ;
677723
678724 // Update frontend
679725 if let Some ( app) = APP_HANDLE . get ( ) . cloned ( ) {
680- let mode = mode_str. to_string ( ) ;
681726 let app_clone = app. clone ( ) ;
682727 let _ = app. run_on_main_thread ( move || {
683728 if let Some ( w) = app_clone. get_webview_window ( "main" ) {
684- let _ = w. eval ( & format ! ( "window.__setMode && window.__setMode('{} ')" , mode ) ) ;
729+ let _ = w. eval ( "window.__setMode && window.__setMode('agent ')" ) ;
685730 }
686731 } ) ;
687732 }
@@ -692,7 +737,7 @@ mod macos_fn_key {
692737 handle_fn_key ( false , false ) ;
693738 break ;
694739 }
695- if start . elapsed ( ) . as_millis ( ) as u64 > max_ms {
740+ if elapsed_ms > max_ms {
696741 log_line ( "Fn watchdog timeout - forcing stop" ) ;
697742 handle_fn_key ( false , false ) ;
698743 break ;
@@ -804,17 +849,43 @@ mod macos_fn_key {
804849 }
805850 log_line ( & format ! ( "Pasted native text len={} (clipboard preserved)" , text. len( ) ) ) ;
806851 } else {
807- // Agent mode - emit event to frontend
808- if let Some ( app) = app. clone ( ) {
809- let text_clone = text. clone ( ) ;
810- let app_clone = app. clone ( ) ;
811- let _ = app. run_on_main_thread ( move || {
812- if let Some ( w) = app_clone. get_webview_window ( "main" ) {
813- let _ = w. eval ( & format ! ( "window.__agentInput && window.__agentInput('{}')" , text_clone. replace( '\\' , "\\ \\ " ) . replace( '\'' , "\\ '" ) ) ) ;
852+ // Agent mode - call worker API, execute AppleScript
853+ log_line ( & format ! ( "Agent mode: calling API with '{}'" , text) ) ;
854+
855+ match call_agent_api ( & text) {
856+ Ok ( response) => {
857+ if response. success {
858+ if let Some ( script) = response. script {
859+ log_line ( & format ! ( "Agent: executing script: {}" , script) ) ;
860+ #[ cfg( target_os = "macos" ) ]
861+ match execute_applescript ( & script) {
862+ Ok ( output) => {
863+ log_line ( & format ! ( "Agent: script succeeded: {}" , output) ) ;
864+ show_notification ( "t2t" , "Done" ) ;
865+ }
866+ Err ( e) => {
867+ log_line ( & format ! ( "Agent: script failed: {}" , e) ) ;
868+ show_notification ( "t2t" , & format ! ( "Script error: {}" , e) ) ;
869+ }
870+ }
871+ }
872+ } else if response. blocked == Some ( true ) {
873+ log_line ( "Agent: script blocked by safety filter" ) ;
874+ #[ cfg( target_os = "macos" ) ]
875+ show_notification ( "t2t" , "Action blocked for safety" ) ;
876+ } else {
877+ let err = response. error . unwrap_or_else ( || "Unknown error" . to_string ( ) ) ;
878+ log_line ( & format ! ( "Agent: API error: {}" , err) ) ;
879+ #[ cfg( target_os = "macos" ) ]
880+ show_notification ( "t2t" , & format ! ( "Error: {}" , err) ) ;
814881 }
815- } ) ;
882+ }
883+ Err ( e) => {
884+ log_line ( & format ! ( "Agent: API call failed: {}" , e) ) ;
885+ #[ cfg( target_os = "macos" ) ]
886+ show_notification ( "t2t" , "Could not reach agent" ) ;
887+ }
816888 }
817- log_line ( & format ! ( "Agent mode: text len={}" , text. len( ) ) ) ;
818889 }
819890 }
820891 }
@@ -1397,9 +1468,13 @@ fn main() {
13971468 . on_menu_event ( |app, event| {
13981469 match event. id . as_ref ( ) {
13991470 "stats" => {
1400- // Show the stats window
1471+ // Show the stats window and bring to front
14011472 if let Some ( w) = app. get_webview_window ( "stats" ) {
14021473 let _ = w. show ( ) ;
1474+ let _ = w. unminimize ( ) ;
1475+ // Force to front by briefly setting always-on-top
1476+ let _ = w. set_always_on_top ( true ) ;
1477+ let _ = w. set_always_on_top ( false ) ;
14031478 let _ = w. set_focus ( ) ;
14041479 log_line ( "tray: view stats (existing window)" ) ;
14051480 } else {
0 commit comments