@@ -93,6 +93,8 @@ use crate::ui::events::{render_session, sanitize_output};
9393use crate :: ui:: input:: InputEditor ;
9494use crate :: ui:: keymap:: { KeyAction , Keymaps } ;
9595use crate :: ui:: panel_render:: { build_left_panel_info, build_panel_data} ;
96+ #[ cfg( feature = "vigil" ) ]
97+ use crate :: ui:: renderer:: VigilStatusRow ;
9698use crate :: ui:: renderer:: { LineEntry , Renderer } ;
9799use crate :: ui:: search_rewind:: {
98100 allow_always_downgrade_reason, is_placeholder_pattern, open_rewind_picker, rewind_session,
@@ -281,6 +283,16 @@ pub async fn run_interactive(
281283 // ui-redesign: shared system-load snapshot. Polled in the
282284 // background; read at panel paint time. Cheap clone (Arc bump).
283285 sysload : crate :: ui:: sysload:: SharedSysLoad ,
286+ #[ cfg( feature = "vigil" ) ] mut vigil_wake_rx : Option < tokio:: sync:: mpsc:: UnboundedReceiver < ( ) > > ,
287+ #[ cfg( feature = "vigil" ) ] mut vigil_observance_rx : Option <
288+ tokio:: sync:: mpsc:: Receiver < crate :: extras:: vigil:: reaper:: Observance > ,
289+ > ,
290+ #[ cfg( feature = "vigil" ) ] vigil_ctl_tx : Option <
291+ tokio:: sync:: mpsc:: Sender < crate :: extras:: vigil:: types:: VigilCtl > ,
292+ > ,
293+ #[ cfg( feature = "vigil" ) ] mut vigil_hook_rx : Option <
294+ tokio:: sync:: mpsc:: Receiver < crate :: extras:: vigil:: types:: HookDispatchRequest > ,
295+ > ,
284296) -> anyhow:: Result < ( ) > {
285297 let _guard = TerminalGuard :: new ( cfg. keyboard_enhancement . unwrap_or ( true ) ) ?;
286298
@@ -538,6 +550,13 @@ pub async fn run_interactive(
538550 #[ cfg( feature = "loop" ) ]
539551 let mut loop_state: Option < crate :: extras:: r#loop:: LoopState > = None ;
540552
553+ #[ cfg( feature = "vigil" ) ]
554+ let mut vigil_state: Option < crate :: extras:: vigil:: VigilState > =
555+ Some ( crate :: extras:: vigil:: VigilState {
556+ active : vigil_wake_rx. is_some ( ) ,
557+ pending_observance : None ,
558+ } ) ;
559+
541560 // Snapshot plugin-registered shortcuts (P9c). Seeded at UI
542561 // startup; refreshed at the top of each event loop iteration
543562 // (M2) so a plugin that registers a shortcut from a hook —
@@ -1652,6 +1671,36 @@ pub async fn run_interactive(
16521671 gitstat. snapshot ( ) ,
16531672 ) ) ;
16541673 }
1674+ #[ cfg( feature = "vigil" ) ]
1675+ {
1676+ if let Some ( ref vigil_ctl) = vigil_ctl_tx {
1677+ let ( tx, rx) = tokio:: sync:: oneshot:: channel ( ) ;
1678+ let _ = vigil_ctl
1679+ . send ( crate :: extras:: vigil:: types:: VigilCtl :: StatusReq { respond_to : tx } )
1680+ . await ;
1681+ if let Ok ( statuses) = rx. await {
1682+ let rows: Vec < VigilStatusRow > = statuses
1683+ . into_iter ( )
1684+ . map ( |s| VigilStatusRow {
1685+ name : s. name ,
1686+ trigger : s. trigger . as_str ( ) . to_string ( ) ,
1687+ interval_secs : s. reap_interval_secs ,
1688+ running : s. running ,
1689+ paused : s. paused ,
1690+ last_event_count : s. last_event_count ,
1691+ last_event_age : s. last_event_at . and_then ( |ts| {
1692+ chrono:: DateTime :: parse_from_rfc3339 ( & ts) . ok ( ) . map ( |dt| {
1693+ let elapsed =
1694+ chrono:: Utc :: now ( ) . signed_duration_since ( dt. to_utc ( ) ) ;
1695+ crate :: ui:: panel_data:: format_duration_short ( elapsed)
1696+ } )
1697+ } ) ,
1698+ } )
1699+ . collect ( ) ;
1700+ renderer. set_vigil_status ( rows) ;
1701+ }
1702+ }
1703+ }
16551704 }
16561705
16571706 // H-R1: loop-top PM acquisitions use `try_lock` so a
@@ -1775,6 +1824,28 @@ pub async fn run_interactive(
17751824 }
17761825 }
17771826
1827+ // Drain vigil plugin hook dispatch requests (on-vigil-event,
1828+ // on-vigil-reap) every iteration rather than only after a
1829+ // successful observance wake. Rite failures, paused vigils, and
1830+ // commands-mode dispatches never wake the loop, so their hooks
1831+ // would otherwise sit undelivered.
1832+ #[ cfg( feature = "vigil" ) ]
1833+ if let Some ( ref mut hook_rx) = vigil_hook_rx {
1834+ while let Ok ( req) = hook_rx. try_recv ( ) {
1835+ #[ cfg( feature = "plugin" ) ]
1836+ if let Some ( pm) = plugin_manager {
1837+ let pm = pm. clone ( ) ;
1838+ let hook = req. hook_name ;
1839+ let ctx = req. context ;
1840+ tokio:: task:: spawn_blocking ( move || {
1841+ pm. lock_ignore_poison ( ) . dispatch_tool_hook ( & hook, & ctx)
1842+ } )
1843+ . await
1844+ . ok ( ) ;
1845+ }
1846+ }
1847+ }
1848+
17781849 // #387: single paint per event. Render the model (the previous
17791850 // event's mutations + this iteration's loop-top updates) exactly
17801851 // once, THEN block on the next event. Because every handler returns
@@ -1786,6 +1857,22 @@ pub async fn run_interactive(
17861857 // mount-timer select! arm can move it into its async block.
17871858 let mount_deadline = ui. shell_mount_deadline ;
17881859
1860+ // When vigil is active, slow the idle poll from 20Hz to 1Hz so the
1861+ // CPU isn't constantly waking during a quiet observance window.
1862+ #[ cfg( feature = "vigil" ) ]
1863+ let idle_sleep_ms: u64 = if vigil_state. as_ref ( ) . is_some_and ( |vs| vs. active ) {
1864+ 1000
1865+ } else {
1866+ 50
1867+ } ;
1868+ #[ cfg( not( feature = "vigil" ) ) ]
1869+ let idle_sleep_ms: u64 = 50 ;
1870+
1871+ // When vigil is compiled out, declare a dummy wake receiver so
1872+ // the vigil select! arm is syntactically present but inert.
1873+ #[ cfg( not( feature = "vigil" ) ) ]
1874+ let mut vigil_wake_rx: Option < tokio:: sync:: mpsc:: UnboundedReceiver < ( ) > > = None ;
1875+
17891876 tokio:: select! {
17901877 // #387: poll arms in order so USER INPUT takes priority — when a
17911878 // keystroke and an agent event are both ready, the keystroke is
@@ -2959,7 +3046,7 @@ pub async fn run_interactive(
29593046 // /help) have no UserMessage event, so we keep the echo.
29603047 write_user_lines( & mut renderer, & text) ?;
29613048 renderer. write_line( "" , Color :: White ) ?;
2962- let result = handle_slash( & expanded, & mut agent, & mut client, & mut renderer, session, cli, cfg, context, & mut ui. show_reasoning, & mut ui. is_running, & mut input, & permission, & ask_tx, & question_tx, & plan_tx, & mut ui. todo_tools_enabled, & bg_store, & sandbox, #[ cfg( unix) ] & user_tx, #[ cfg( feature = "loop" ) ] & mut loop_state, #[ cfg( feature = "mcp" ) ] mcp_manager. as_ref( ) , #[ cfg( feature = "semantic" ) ] semantic_manager, #[ cfg( feature = "lsp" ) ] lsp_manager. as_ref( ) , & mut ui. plan_phase) . await ;
3049+ let result = handle_slash( & expanded, & mut agent, & mut client, & mut renderer, session, cli, cfg, context, & mut ui. show_reasoning, & mut ui. is_running, & mut input, & permission, & ask_tx, & question_tx, & plan_tx, & mut ui. todo_tools_enabled, & bg_store, & sandbox, #[ cfg( unix) ] & user_tx, #[ cfg( feature = "loop" ) ] & mut loop_state, #[ cfg( feature = "vigil" ) ] & mut vigil_state , # [ cfg ( feature = "vigil" ) ] & vigil_ctl_tx , # [ cfg ( feature = " mcp") ] mcp_manager. as_ref( ) , #[ cfg( feature = "semantic" ) ] semantic_manager, #[ cfg( feature = "lsp" ) ] lsp_manager. as_ref( ) , & mut ui. plan_phase) . await ;
29633050 match result {
29643051 Ok ( SlashOutcome :: DeferCompress { instructions } ) => {
29653052 let instructions = instructions. as_deref( ) . and_then( |s| {
@@ -3606,6 +3693,10 @@ pub async fn run_interactive(
36063693 state: & mut loop_state,
36073694 label: & mut ui. loop_label,
36083695 } ;
3696+ #[ cfg( feature = "vigil" ) ]
3697+ let vigil_bits = run_handlers:: done:: VigilBits {
3698+ state: & mut vigil_state,
3699+ } ;
36093700 run_handlers:: handle_done(
36103701 & mut ctx,
36113702 response,
@@ -3626,6 +3717,8 @@ pub async fn run_interactive(
36263717 & mut ui. done_phase,
36273718 #[ cfg( feature = "loop" ) ]
36283719 loop_bits,
3720+ #[ cfg( feature = "vigil" ) ]
3721+ vigil_bits,
36293722 ) . await ?;
36303723 }
36313724 AgentEvent :: Usage {
@@ -4098,6 +4191,10 @@ pub async fn run_interactive(
40984191 state: & mut loop_state,
40994192 label: & mut ui. loop_label,
41004193 } ;
4194+ #[ cfg( feature = "vigil" ) ]
4195+ let vigil_bits = run_handlers:: done:: VigilBits {
4196+ state: & mut vigil_state,
4197+ } ;
41014198 run_handlers:: done:: finish_done(
41024199 & mut ctx,
41034200 result. response,
@@ -4116,6 +4213,8 @@ pub async fn run_interactive(
41164213 plugin_manager,
41174214 #[ cfg( feature = "loop" ) ]
41184215 loop_bits,
4216+ #[ cfg( feature = "vigil" ) ]
4217+ vigil_bits,
41194218 )
41204219 . await ?;
41214220 }
@@ -5266,9 +5365,67 @@ pub async fn run_interactive(
52665365 // active path already re-asserts.
52675366 _ = tokio:: time:: sleep( tokio:: time:: Duration :: from_secs( 1 ) ) , if !ui. is_running => {
52685367 renderer. reassert_terminal_modes( ) ;
5368+ } ,
5369+ // Vigil wake — triggered by the reaper after a
5370+ // successful observance. The vigils may be disabled
5371+ // at compile time; the `if vigil_wake_rx.is_some()`
5372+ // guard ensures the arm is inert when vigil is off.
5373+ _ = async {
5374+ match & mut vigil_wake_rx {
5375+ Some ( rx) => rx. recv( ) . await ,
5376+ None => std:: future:: pending:: <Option <( ) >>( ) . await ,
5377+ }
5378+ } , if vigil_wake_rx. is_some( ) => {
5379+ #[ cfg( feature = "vigil" ) ]
5380+ {
5381+ if !ui. is_running
5382+ && let Some ( ref mut rx) = vigil_observance_rx
5383+ && let Ok ( obs) = rx. try_recv( )
5384+ {
5385+ // Store observance metadata so the post-turn
5386+ // handler can dispatch on-vigil-observance
5387+ // with :response and :exit after the agent turn.
5388+ if let Some ( ref mut vs) = vigil_state {
5389+ vs. pending_observance = Some (
5390+ crate :: extras:: vigil:: PendingObservance {
5391+ vigil_name: obs. vigil_name. clone( ) ,
5392+ event_count: obs. event_count,
5393+ running: obs. running. clone( ) ,
5394+ } ,
5395+ ) ;
5396+ }
5397+ let prompt = if obs. prompt. is_empty( ) {
5398+ format!( "[vigil] {} - {} event(s)" , obs. vigil_name, obs. event_count)
5399+ } else {
5400+ obs. prompt. clone( )
5401+ } ;
5402+ ui. last_user_prompt. clone_from( & prompt) ;
5403+ let history = crate :: agent:: runner:: convert_history( session) ;
5404+ session. add_message( MessageRole :: User , & prompt) ;
5405+ begin_snapshot_turn( session) ;
5406+ let runner = agent. clone( ) . spawn_runner(
5407+ crate :: provider:: Prompt :: text(
5408+ crate :: agent:: tools:: background:: prepend_pending_notifications(
5409+ & prompt,
5410+ bg_store. as_ref( ) ,
5411+ ) ,
5412+ ) ,
5413+ history,
5414+ Some ( ui. interjection_queue. clone( ) ) ,
5415+ Some ( session. assets_dir( ) ) ,
5416+ ) ;
5417+ runner. install_into(
5418+ & mut ui. agent_rx,
5419+ & mut ui. agent_abort,
5420+ & mut ui. agent_interject,
5421+ & mut ui. agent_cancel,
5422+ & mut ui. is_running,
5423+ ) ;
5424+ }
5425+ }
52695426 }
52705427 else => {
5271- tokio:: time:: sleep( tokio:: time:: Duration :: from_millis( 50 ) ) . await ;
5428+ tokio:: time:: sleep( tokio:: time:: Duration :: from_millis( idle_sleep_ms ) ) . await ;
52725429 }
52735430 }
52745431 }
0 commit comments