Skip to main content

rustyclaw_tui/components/
root.rs

1// ── Root ────────────────────────────────────────────────────────────────────
2//
3// Top-level layout. Receives terminal size explicitly (as iocraft fullscreen
4// examples do) and composes Messages+Sidebar, InputBar, StatusBar.
5
6use iocraft::prelude::*;
7
8use crate::components::auth_dialog::AuthDialog;
9use crate::components::command_menu::CommandMenu;
10use crate::components::hatching_dialog::HatchingDialog;
11use crate::components::input_bar::InputBar;
12use crate::components::messages::Messages;
13use crate::components::secrets_dialog::{SecretInfo, SecretsDialog};
14use crate::components::sidebar::Sidebar;
15use crate::components::skills_dialog::{SkillInfo, SkillsDialog};
16use crate::components::status_bar::StatusBar;
17use crate::components::tool_approval_dialog::ToolApprovalDialog;
18use crate::components::tool_perms_dialog::{ToolPermInfo, ToolPermsDialog};
19use crate::components::user_prompt_dialog::UserPromptDialog;
20use crate::components::vault_unlock_dialog::VaultUnlockDialog;
21use crate::theme;
22use crate::types::DisplayMessage;
23
24#[derive(Default, Props)]
25pub struct RootProps {
26    // terminal
27    pub width: u16,
28    pub height: u16,
29
30    // identity / model (shown in status bar)
31    pub soul_name: String,
32    pub model_label: String,
33
34    // gateway (used by input bar & sidebar)
35    pub gateway_icon: String,
36    pub gateway_label: String,
37    pub gateway_color: Option<Color>,
38
39    // messages
40    pub messages: Vec<DisplayMessage>,
41    pub scroll_offset: i32,
42
43    // command menu (slash completions)
44    pub command_completions: Vec<String>,
45    pub command_selected: Option<usize>,
46
47    // input
48    pub input_value: String,
49    pub on_change: HandlerMut<'static, String>,
50    pub on_submit: HandlerMut<'static, String>,
51    pub input_has_focus: bool,
52
53    // sidebar
54    pub task_text: String,
55    pub streaming: bool,
56    pub elapsed: String,
57    pub threads: Vec<crate::action::ThreadInfo>,
58    pub sidebar_focused: bool,
59    pub sidebar_selected: usize,
60
61    // status bar
62    pub hint: String,
63    pub spinner_tick: usize,
64
65    // auth dialog overlay
66    pub show_auth_dialog: bool,
67    pub auth_code: String,
68    pub auth_error: String,
69
70    // tool approval dialog overlay
71    pub show_tool_approval: bool,
72    pub tool_approval_name: String,
73    pub tool_approval_args: String,
74    pub tool_approval_selected: bool,
75
76    // vault unlock dialog overlay
77    pub show_vault_unlock: bool,
78    pub vault_password_len: usize,
79    pub vault_error: String,
80
81    // user prompt dialog overlay
82    pub show_user_prompt: bool,
83    pub user_prompt_title: String,
84    pub user_prompt_desc: String,
85    pub user_prompt_input: String,
86    pub user_prompt_type: Option<rustyclaw_core::user_prompt_types::PromptType>,
87    pub user_prompt_selected: usize,
88
89    // secrets dialog overlay
90    pub show_secrets_dialog: bool,
91    pub secrets_data: Vec<SecretInfo>,
92    pub secrets_agent_access: bool,
93    pub secrets_has_totp: bool,
94    pub secrets_selected: Option<usize>,
95    pub secrets_scroll_offset: usize,
96    pub secrets_add_step: u8,
97    pub secrets_add_name: String,
98    pub secrets_add_value: String,
99
100    // skills dialog overlay
101    pub show_skills_dialog: bool,
102    pub skills_data: Vec<SkillInfo>,
103    pub skills_selected: Option<usize>,
104    pub skills_scroll_offset: usize,
105
106    // tool permissions dialog overlay
107    pub show_tool_perms_dialog: bool,
108    pub tool_perms_data: Vec<ToolPermInfo>,
109    pub tool_perms_selected: Option<usize>,
110    pub tool_perms_scroll_offset: usize,
111
112    // hatching dialog overlay (first run)
113    pub show_hatching: bool,
114    pub hatching_state: crate::components::hatching_dialog::HatchState,
115    pub hatching_agent_name: String,
116}
117
118#[component]
119pub fn Root(props: &mut RootProps) -> impl Into<AnyElement<'static>> {
120    let show_auth = props.show_auth_dialog;
121    let show_approval = props.show_tool_approval;
122    let show_vault = props.show_vault_unlock;
123    let show_prompt = props.show_user_prompt;
124
125    let secrets_data = std::mem::take(&mut props.secrets_data);
126    let secrets_agent = props.secrets_agent_access;
127    let secrets_totp = props.secrets_has_totp;
128    let secrets_selected = props.secrets_selected;
129    let secrets_scroll = props.secrets_scroll_offset;
130    let secrets_add_step = props.secrets_add_step;
131    let secrets_add_name = std::mem::take(&mut props.secrets_add_name);
132    let secrets_add_value = std::mem::take(&mut props.secrets_add_value);
133    #[allow(unused_variables)]
134    let show_secrets = props.show_secrets_dialog;
135    let skills_data = std::mem::take(&mut props.skills_data);
136    let skills_selected = props.skills_selected;
137    let skills_scroll = props.skills_scroll_offset;
138    #[allow(unused_variables)]
139    let show_skills = props.show_skills_dialog;
140    let tool_perms_data = std::mem::take(&mut props.tool_perms_data);
141    let tool_perms_selected = props.tool_perms_selected;
142    let tool_perms_scroll = props.tool_perms_scroll_offset;
143    #[allow(unused_variables)]
144    let show_tool_perms = props.show_tool_perms_dialog;
145    
146    let show_hatching = props.show_hatching;
147    let hatching_state = props.hatching_state.clone();
148    let hatching_agent_name = props.hatching_agent_name.clone();
149
150    element! {
151        View(
152            width: props.width,
153            height: props.height,
154            flex_direction: FlexDirection::Column,
155            background_color: theme::BG_MAIN,
156        ) {
157            // ── Main area (flex grow) ───────────────────────────────────
158            View(
159                flex_grow: 1.0,
160                flex_direction: FlexDirection::Row,
161                width: 100pct,
162            ) {
163                // Chat area: messages + input
164                View(
165                    flex_grow: 1.0,
166                    flex_direction: FlexDirection::Column,
167                ) {
168                    Messages(
169                        messages: props.messages.clone(),
170                        scroll_offset: props.scroll_offset,
171                        streaming: props.streaming,
172                        spinner_tick: props.spinner_tick,
173                        elapsed: props.elapsed.clone(),
174                        assistant_name: if props.soul_name.is_empty() {
175                            None
176                        } else {
177                            Some(props.soul_name.clone())
178                        },
179                    )
180                    CommandMenu(
181                        completions: props.command_completions.clone(),
182                        selected: props.command_selected,
183                    )
184                    InputBar(
185                        value: props.input_value.clone(),
186                        on_change: props.on_change.take(),
187                        on_submit: props.on_submit.take(),
188                        gateway_icon: props.gateway_icon.clone(),
189                        gateway_label: props.gateway_label.clone(),
190                        gateway_color: props.gateway_color,
191                        has_focus: props.input_has_focus,
192                    )
193                }
194                // Sidebar
195                Sidebar(
196                    gateway_label: props.gateway_label.clone(),
197                    task_text: props.task_text.clone(),
198                    streaming: props.streaming,
199                    elapsed: props.elapsed.clone(),
200                    spinner_tick: props.spinner_tick,
201                    threads: props.threads.clone(),
202                    focused: props.sidebar_focused,
203                    selected: props.sidebar_selected,
204                )
205            }
206
207            // ── Status bar (1 row) ──────────────────────────────────────
208            StatusBar(
209                hint: props.hint.clone(),
210                streaming: props.streaming,
211                elapsed: props.elapsed.clone(),
212                spinner_tick: props.spinner_tick,
213                soul_name: props.soul_name.clone(),
214                model_label: props.model_label.clone(),
215            )
216
217            // ── Auth dialog overlay ─────────────────────────────────────
218            #(if show_auth {
219                element! {
220                    View(
221                        width: props.width,
222                        height: props.height,
223                        position: Position::Absolute,
224                        top: 0,
225                        left: 0,
226                    ) {
227                        AuthDialog(
228                            code: props.auth_code.clone(),
229                            error: props.auth_error.clone(),
230                        )
231                    }
232                }.into_any()
233            } else {
234                element! { View() }.into_any()
235            })
236
237            // ── Tool approval dialog overlay ────────────────────────────
238            #(if show_approval {
239                element! {
240                    View(
241                        width: props.width,
242                        height: props.height,
243                        position: Position::Absolute,
244                        top: 0,
245                        left: 0,
246                    ) {
247                        ToolApprovalDialog(
248                            tool_name: props.tool_approval_name.clone(),
249                            arguments: props.tool_approval_args.clone(),
250                            selected_allow: props.tool_approval_selected,
251                        )
252                    }
253                }.into_any()
254            } else {
255                element! { View() }.into_any()
256            })
257
258            // ── Vault unlock dialog overlay ─────────────────────────────
259            #(if show_vault {
260                element! {
261                    View(
262                        width: props.width,
263                        height: props.height,
264                        position: Position::Absolute,
265                        top: 0,
266                        left: 0,
267                    ) {
268                        VaultUnlockDialog(
269                            password_len: props.vault_password_len,
270                            error: props.vault_error.clone(),
271                        )
272                    }
273                }.into_any()
274            } else {
275                element! { View() }.into_any()
276            })
277
278            // ── User prompt dialog overlay ──────────────────────────────
279            #(if show_prompt {
280                element! {
281                    View(
282                        width: props.width,
283                        height: props.height,
284                        position: Position::Absolute,
285                        top: 0,
286                        left: 0,
287                    ) {
288                        UserPromptDialog(
289                            title: props.user_prompt_title.clone(),
290                            description: props.user_prompt_desc.clone(),
291                            input: props.user_prompt_input.clone(),
292                            prompt_type: props.user_prompt_type.clone(),
293                            selected: props.user_prompt_selected,
294                        )
295                    }
296                }.into_any()
297            } else {
298                element! { View() }.into_any()
299            })
300
301            // ── Secrets dialog overlay ──────────────────────────────────
302            #(if show_secrets {
303                element! {
304                    View(
305                        width: props.width,
306                        height: props.height,
307                        position: Position::Absolute,
308                        top: 0,
309                        left: 0,
310                    ) {
311                        SecretsDialog(
312                            secrets: secrets_data,
313                            agent_access: secrets_agent,
314                            has_totp: secrets_totp,
315                            selected: secrets_selected,
316                            scroll_offset: secrets_scroll,
317                            add_step: secrets_add_step,
318                            add_name: secrets_add_name,
319                            add_value: secrets_add_value,
320                        )
321                    }
322                }.into_any()
323            } else {
324                element! { View() }.into_any()
325            })
326
327            // ── Skills dialog overlay ───────────────────────────────────
328            #(if show_skills {
329                element! {
330                    View(
331                        width: props.width,
332                        height: props.height,
333                        position: Position::Absolute,
334                        top: 0,
335                        left: 0,
336                    ) {
337                        SkillsDialog(
338                            skills: skills_data,
339                            selected: skills_selected,
340                            scroll_offset: skills_scroll,
341                        )
342                    }
343                }.into_any()
344            } else {
345                element! { View() }.into_any()
346            })
347
348            // ── Tool permissions dialog overlay ─────────────────────────
349            #(if show_tool_perms {
350                element! {
351                    View(
352                        width: props.width,
353                        height: props.height,
354                        position: Position::Absolute,
355                        top: 0,
356                        left: 0,
357                    ) {
358                        ToolPermsDialog(
359                            tools: tool_perms_data,
360                            selected: tool_perms_selected,
361                            scroll_offset: tool_perms_scroll,
362                        )
363                    }
364                }.into_any()
365            } else {
366                element! { View() }.into_any()
367            })
368
369            // ── Hatching dialog overlay (first run) ─────────────────────
370            #(if show_hatching {
371                element! {
372                    View(
373                        width: props.width,
374                        height: props.height,
375                        position: Position::Absolute,
376                        top: 0,
377                        left: 0,
378                    ) {
379                        HatchingDialog(
380                            state: hatching_state,
381                            agent_name: hatching_agent_name,
382                        )
383                    }
384                }.into_any()
385            } else {
386                element! { View() }.into_any()
387            })
388        }
389    }
390}