netget/tui/modal/intercept.rs
1//! Answering a request yourself.
2//!
3//! A `manual` routing rule parks each matched event as a pending question
4//! (`crate::state::intercepts`); this modal is where the human answers it. It
5//! shows what arrived and offers exactly three things to do:
6//!
7//! - **Compose answer…** opens the same composer the `[ send ]` rows use —
8//! pick one of the protocol's actions from a list, fill its parameters as
9//! fields (booleans toggle), and send. Raw JSON is one button away for the
10//! rare shape the fields cannot express, never the starting point.
11//! - **Answer with nothing** delivers zero actions: acknowledge, say nothing.
12//! That is a real answer — the same as an empty static handler, and what a
13//! lifecycle event like connection-opened usually deserves — so it is
14//! delivered (Ok) and distinct from a timeout (Err).
15//! - **Fail closed** refuses: the waiting dispatcher errors immediately and
16//! the peer gets the protocol's category error, exactly as if the model had
17//! failed. Refusing is honest; inventing nothing is the fail-closed rule.
18
19use crate::llm::actions::ActionDefinition;
20use crate::tui::app::UiKey;
21use crate::tui::hit::ModalAction;
22
23#[derive(Debug, Clone)]
24pub struct InterceptModel {
25 /// The pending intercept being answered.
26 pub id: u64,
27 pub owner: UiKey,
28 pub protocol: String,
29 pub event_type: String,
30 pub description: String,
31 pub event_data: Option<serde_json::Value>,
32 /// The protocol's action vocabulary — what the composer offers.
33 pub vocabulary: Vec<ActionDefinition>,
34 pub error: Option<String>,
35 /// Index into [`Self::buttons`].
36 pub focused: usize,
37}
38
39impl InterceptModel {
40 pub fn buttons(&self) -> Vec<ModalAction> {
41 vec![
42 ModalAction::InterceptCompose,
43 ModalAction::InterceptSend,
44 ModalAction::InterceptDismiss,
45 ]
46 }
47
48 pub fn focused_action(&self) -> Option<ModalAction> {
49 self.buttons().get(self.focused).copied()
50 }
51
52 pub fn cycle_focus(&mut self, backward: bool) {
53 let total = self.buttons().len();
54 self.focused = if backward {
55 (self.focused + total - 1) % total
56 } else {
57 (self.focused + 1) % total
58 };
59 }
60}