Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 15 additions & 0 deletions cli/src/commands/agent/run/mode_interactive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,20 @@ pub async fn run_interactive(
};

let send_init_prompt_on_start = config.send_init_prompt_on_start;

let banner_message = if agent_context
.as_ref()
.map(|ctx| ctx.apps_md.is_none())
.unwrap_or(true)
{
Some(stakpak_tui::BannerMessage::new(
"❕ System not scanned - /init to generate an APPS.md file with your context",
stakpak_tui::BannerStyle::Info,
))
} else {
None
};

let tui_handle = tokio::spawn(async move {
let latest_version = get_latest_cli_version().await;
stakpak_tui::run_tui(
Expand All @@ -310,6 +324,7 @@ pub async fn run_interactive(
init_prompt_content_for_tui,
send_init_prompt_on_start,
recent_models_for_tui,
banner_message,
)
.await
.map_err(|e| e.to_string())
Expand Down
11 changes: 10 additions & 1 deletion tui/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub use types::*;

use crate::services::approval_bar::ApprovalBar;
use crate::services::auto_approve::AutoApproveManager;
use crate::services::banner::BannerMessage;
use crate::services::board_tasks::TaskProgress;
use crate::services::changeset::{Changeset, SidePanelSection, TodoItem};
use crate::services::detect_term::ThemeColors;
Expand All @@ -24,7 +25,7 @@ use crate::services::shell_mode::{SHELL_PROMPT_PREFIX, ShellCommand, ShellEvent}
use crate::services::text_selection::SelectionState;
use crate::services::textarea::{TextArea, TextAreaState};
use crate::services::toast::Toast;
use ratatui::layout::Size;
use ratatui::layout::{Rect, Size};
use ratatui::text::Line;
use stakpak_api::models::ListRuleBook;
use stakpak_shared::models::integrations::openai::{ToolCall, ToolCallResult};
Expand Down Expand Up @@ -241,6 +242,11 @@ pub struct AppState {
// ========== Text Selection State ==========
pub selection: SelectionState,
pub toast: Option<Toast>,
pub banner_message: Option<BannerMessage>,
/// Stores the banner area rect for mouse click detection
pub banner_area: Option<Rect>,
/// Clickable command regions within the banner: (command_text, bounding_rect)
pub banner_click_regions: Vec<(String, Rect)>,
/// Auto-scroll direction during drag selection: -1 (up), 0 (none), 1 (down)
pub selection_auto_scroll: i32,

Expand Down Expand Up @@ -596,6 +602,9 @@ impl AppState {
// Text selection initialization
selection: SelectionState::default(),
toast: None,
banner_message: None,
banner_area: None,
banner_click_regions: Vec::new(),
selection_auto_scroll: 0,
input_content_area: None,

Expand Down
3 changes: 3 additions & 0 deletions tui/src/app/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use stakpak_shared::models::{
use uuid::Uuid;

use crate::app::{ExistingPlanPrompt, LoadingOperation, SessionInfo};
use crate::services::banner::BannerStyle;
use crate::services::board_tasks::FetchTasksResult;

#[derive(Debug)]
Expand All @@ -31,6 +32,7 @@ pub enum InputEvent {
BillingInfoLoaded(stakpak_shared::models::billing::BillingResponse),
Error(String),
SetSessions(Vec<SessionInfo>),
SetBannerMessage(String, BannerStyle),
InputBackspace,
InputChangedNewline,
InputSubmitted,
Expand Down Expand Up @@ -253,6 +255,7 @@ impl InputEvent {
| InputEvent::ShowAskUserPopup(_, _)
| InputEvent::ExistingPlanFound(_)
| InputEvent::SetSessions(_)
| InputEvent::SetBannerMessage(_, _)
| InputEvent::GetStatus(_)
| InputEvent::BillingInfoLoaded(_)
| InputEvent::TotalUsage(_)
Expand Down
112 changes: 61 additions & 51 deletions tui/src/event_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
//! Contains the main TUI event loop and related helper functions.

use crate::app::{AppState, AppStateOptions, InputEvent, OutputEvent};
use crate::services::banner::BannerMessage;
use crate::services::detect_term::ThemeColors;
use crate::services::handlers::tool::{
clear_streaming_tool_results, handle_tool_result, update_session_tool_calls_queue,
Expand Down Expand Up @@ -56,6 +57,7 @@ pub async fn run_tui(
init_prompt_content: Option<String>,
send_init_prompt_on_start: bool,
recent_models: Vec<String>,
banner_message: Option<BannerMessage>,
) -> io::Result<()> {
let _guard = TerminalGuard;

Expand Down Expand Up @@ -100,6 +102,8 @@ pub async fn run_tui(
recent_models,
});

state.banner_message = banner_message;

// Mouse capture is always enabled
state.mouse_capture_enabled = true;

Expand Down Expand Up @@ -224,20 +228,22 @@ pub async fn run_tui(
// The approval bar will be visible, so input and dropdown are hidden
let approval_bar_height = state.approval_bar.calculate_height(term_rect.width).max(7); // Use expected height

let outer_chunks = ratatui::layout::Layout::default()
.direction(ratatui::layout::Direction::Vertical)
.constraints([
ratatui::layout::Constraint::Min(1), // messages
ratatui::layout::Constraint::Length(1), // loading
ratatui::layout::Constraint::Length(0), // shell popup
ratatui::layout::Constraint::Length(approval_bar_height), // approval bar
ratatui::layout::Constraint::Length(0), // input (hidden when approval bar visible)
ratatui::layout::Constraint::Length(0), // dropdown (hidden when approval bar visible)
ratatui::layout::Constraint::Length(hint_height), // hint
])
.split(term_rect);
let message_area_width = outer_chunks[0].width.saturating_sub(2) as usize;
let message_area_height = outer_chunks[0].height as usize;
let banner_h = crate::services::banner::banner_height(&state);
let outer_chunks = ratatui::layout::Layout::default()
.direction(ratatui::layout::Direction::Vertical)
.constraints([
ratatui::layout::Constraint::Length(banner_h), // banner (0 if no message)
ratatui::layout::Constraint::Min(1), // messages
ratatui::layout::Constraint::Length(1), // loading
ratatui::layout::Constraint::Length(0), // shell popup
ratatui::layout::Constraint::Length(approval_bar_height), // approval bar
ratatui::layout::Constraint::Length(0), // input (hidden when approval bar visible)
ratatui::layout::Constraint::Length(0), // dropdown (hidden when approval bar visible)
ratatui::layout::Constraint::Length(hint_height), // hint
])
.split(term_rect);
let message_area_width = outer_chunks[1].width.saturating_sub(2) as usize;
let message_area_height = outer_chunks[1].height as usize;

crate::services::update::update(&mut state, InputEvent::ShowConfirmationDialog(tool_call.clone()), message_area_height, message_area_width, &internal_tx, &output_tx, cancel_tx.clone(), &shell_event_tx, term_size);
state.poll_file_search_results();
Expand Down Expand Up @@ -389,28 +395,30 @@ pub async fn run_tui(
let dropdown_showing = state.show_helper_dropdown
&& ((!state.filtered_helpers.is_empty() && state.input().starts_with('/'))
|| !state.filtered_files.is_empty());
let dropdown_height = if dropdown_showing {
state.filtered_helpers.len() as u16
} else {
0
};
let hint_height = if dropdown_showing { 0 } else { margin_height };
let outer_chunks = ratatui::layout::Layout::default()
.direction(ratatui::layout::Direction::Vertical)
.constraints([
ratatui::layout::Constraint::Min(1), // messages
ratatui::layout::Constraint::Length(1), // loading indicator
ratatui::layout::Constraint::Length(input_height as u16),
ratatui::layout::Constraint::Length(dropdown_height),
ratatui::layout::Constraint::Length(hint_height),
])
.split(term_rect);
// Subtract 2 for padding (matches view.rs padded_message_area)
let message_area_width = outer_chunks[0].width.saturating_sub(2) as usize;
let message_area_height = outer_chunks[0].height as usize;
crate::services::update::update(&mut state, event, message_area_height, message_area_width, &internal_tx, &output_tx, cancel_tx.clone(), &shell_event_tx, term_size);
state.poll_file_search_results();
// Handle pending editor open request
let dropdown_height = if dropdown_showing {
state.filtered_helpers.len() as u16
} else {
0
};
let hint_height = if dropdown_showing { 0 } else { margin_height };
let banner_h = crate::services::banner::banner_height(&state);
let outer_chunks = ratatui::layout::Layout::default()
.direction(ratatui::layout::Direction::Vertical)
.constraints([
ratatui::layout::Constraint::Length(banner_h), // banner (0 if no message)
ratatui::layout::Constraint::Min(1), // messages
ratatui::layout::Constraint::Length(1), // loading indicator
ratatui::layout::Constraint::Length(input_height as u16),
ratatui::layout::Constraint::Length(dropdown_height),
ratatui::layout::Constraint::Length(hint_height),
])
.split(term_rect);
// Subtract 2 for padding (matches view.rs padded_message_area)
let message_area_width = outer_chunks[1].width.saturating_sub(2) as usize;
let message_area_height = outer_chunks[1].height as usize;
crate::services::update::update(&mut state, event, message_area_height, message_area_width, &internal_tx, &output_tx, cancel_tx.clone(), &shell_event_tx, term_size);
state.poll_file_search_results();
// Handle pending editor open request
if let Some(file_path) = state.pending_editor_open.take() {
// Disable mouse capture before opening editor to prevent weird input
let was_mouse_capture_enabled = state.mouse_capture_enabled;
Expand Down Expand Up @@ -479,21 +487,23 @@ pub async fn run_tui(
} else {
0
};
let hint_height = if dropdown_showing { 0 } else { margin_height };
let outer_chunks = ratatui::layout::Layout::default()
.direction(ratatui::layout::Direction::Vertical)
.constraints([
ratatui::layout::Constraint::Min(1), // messages
ratatui::layout::Constraint::Length(1), // loading indicator
ratatui::layout::Constraint::Length(input_height as u16),
ratatui::layout::Constraint::Length(dropdown_height),
ratatui::layout::Constraint::Length(hint_height),
])
.split(term_rect);
// Subtract 2 for padding (matches view.rs padded_message_area)
let message_area_width = outer_chunks[0].width.saturating_sub(2) as usize;
let message_area_height = outer_chunks[0].height as usize;
if let InputEvent::EmergencyClearTerminal = event {
let hint_height = if dropdown_showing { 0 } else { margin_height };
let banner_h = crate::services::banner::banner_height(&state);
let outer_chunks = ratatui::layout::Layout::default()
.direction(ratatui::layout::Direction::Vertical)
.constraints([
ratatui::layout::Constraint::Length(banner_h), // banner (0 if no message)
ratatui::layout::Constraint::Min(1), // messages
ratatui::layout::Constraint::Length(1), // loading indicator
ratatui::layout::Constraint::Length(input_height as u16),
ratatui::layout::Constraint::Length(dropdown_height),
ratatui::layout::Constraint::Length(hint_height),
])
.split(term_rect);
// Subtract 2 for padding (matches view.rs padded_message_area)
let message_area_width = outer_chunks[1].width.saturating_sub(2) as usize;
let message_area_height = outer_chunks[1].height as usize;
if let InputEvent::EmergencyClearTerminal = event {
emergency_clear_and_redraw(&mut terminal, &mut state)?;
continue;
}
Expand Down
1 change: 1 addition & 0 deletions tui/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pub use app::{
};
pub use event_loop::{RulebookConfig, run_tui};
pub use ratatui::style::Color;
pub use services::banner::{BannerMessage, BannerStyle};

pub mod services;

Expand Down
Loading
Loading