Skip to content

Commit c8c7041

Browse files
moirahuangoz-agent
andcommitted
TUI: Warm shell completion sources after bootstrap
Co-Authored-By: Oz <oz-agent@warp.dev>
1 parent 08ad6e8 commit c8c7041

6 files changed

Lines changed: 89 additions & 8 deletions

File tree

app/src/terminal/model/session.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1726,7 +1726,7 @@ pub fn get_local_hostname() -> Result<String> {
17261726
}
17271727
}
17281728

1729-
#[cfg(test)]
1729+
#[cfg(any(test, feature = "test-util"))]
17301730
pub mod testing {
17311731
use super::command_executor::testing::TestCommandExecutor;
17321732
use super::*;

app/src/terminal/model/session/command_executor.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -349,7 +349,7 @@ fn new_command_executor_for_local_tty_session(
349349
}
350350
}
351351

352-
#[cfg(test)]
352+
#[cfg(any(test, feature = "test-util"))]
353353
pub mod testing {
354354
use anyhow::anyhow;
355355
use command::r#async::Command;

app/src/tui_export.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,8 +229,8 @@ pub use crate::terminal::model::blocks::{
229229
pub use crate::terminal::model::escape_sequences::{KeystrokeWithDetails, ToEscapeSequence};
230230
pub use crate::terminal::model::grid::grid_handler::{GridHandler, TermMode};
231231
pub use crate::terminal::model::rich_content::RichContentType;
232-
pub use crate::terminal::model::session::Sessions;
233232
pub use crate::terminal::model::session::active_session::{ActiveSession, ActiveSessionEvent};
233+
pub use crate::terminal::model::session::{Session, Sessions, SessionsEvent};
234234
pub use crate::terminal::model::terminal_model::BlockIndex;
235235
pub use crate::terminal::model_events::{ModelEvent, ModelEventDispatcher};
236236
pub use crate::terminal::session_settings::SessionSettings;

crates/warp_tui/src/terminal_session_view.rs

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ use warp::tui_export::{
3232
ParsedSlashCommandInput, PersistenceWriter, PillBarActionKind, PillBarInteractionEvent,
3333
PillBarPillKind, PillSwitchOutcome, PtyIntent, PtyIntentEvent, QueuedQueryEvent,
3434
QueuedQueryModel, RepoDetectionSessionType, RepoDetectionSource, ServerConversationToken,
35-
SessionSettings, Sessions, ShellCommandExecutorEvent, SizeInfo, SizeUpdate, SkillReference,
36-
SlashCommandDataSource as _, SlashCommandKind, SlashCommandSelectionBehavior,
35+
SessionSettings, Sessions, SessionsEvent, ShellCommandExecutorEvent, SizeInfo, SizeUpdate,
36+
SkillReference, SlashCommandDataSource as _, SlashCommandKind, SlashCommandSelectionBehavior,
3737
StartAgentExecutorEvent, StartAgentRequest, StaticCommand, TelemetryEvent, TerminalColorList,
3838
TerminalColors, TerminalModel, TerminalSurface, TerminalSurfaceInit, TranscriptScope,
3939
TuiMcpAction, TuiMcpManager, TuiMcpServerId, TuiMcpVariableValue, TuiSlashCommandDataSource,
@@ -1989,6 +1989,7 @@ impl TuiTerminalSessionView {
19891989
}
19901990
ModelEvent::AfterBlockCompleted(completed) => {
19911991
view.emit_block_completed_telemetry(completed, ctx);
1992+
view.ensure_external_commands_are_warming(ctx);
19921993
}
19931994
ModelEvent::AfterBlockStarted { .. } => {
19941995
view.refresh_input_focus(ctx);
@@ -2044,6 +2045,26 @@ impl TuiTerminalSessionView {
20442045
ctx.notify();
20452046
}
20462047
});
2048+
ctx.subscribe_to_model(&sessions, |view, _, event, ctx| match event {
2049+
SessionsEvent::SessionBootstrapped(bootstrap_event)
2050+
if view.active_session.as_ref(ctx).session_id(ctx)
2051+
== Some(bootstrap_event.session_id) =>
2052+
{
2053+
let Some(session) = view.sessions.as_ref(ctx).get(bootstrap_event.session_id)
2054+
else {
2055+
report_error!(
2056+
"Could not find active TUI session after its bootstrap event",
2057+
extra: { "session_id" => ?bootstrap_event.session_id }
2058+
);
2059+
return;
2060+
};
2061+
view.abort_shell_completion(ctx);
2062+
view.warm_shell_completion_sources(session, ctx);
2063+
}
2064+
SessionsEvent::SessionBootstrapped(_)
2065+
| SessionsEvent::SessionInitialized { .. }
2066+
| SessionsEvent::EnvironmentVariablesUpdated { .. } => {}
2067+
});
20472068
ctx.subscribe_to_model(&active_session, |view, _, event, ctx| match event {
20482069
ActiveSessionEvent::UpdatedPwd => {
20492070
view.abort_shell_completion(ctx);
@@ -2086,7 +2107,7 @@ impl TuiTerminalSessionView {
20862107
});
20872108
ctx.notify();
20882109
}
2089-
ActiveSessionEvent::Bootstrapped => view.abort_shell_completion(ctx),
2110+
ActiveSessionEvent::Bootstrapped => {}
20902111
});
20912112
// The footer's usage entry shows the selected conversation's token/cost
20922113
// totals: re-render when that conversation's usage metadata updates.
@@ -2198,6 +2219,9 @@ impl TuiTerminalSessionView {
21982219
if let Some(error) = initial_settings_file_error {
21992220
view.show_settings_file_error(&error, ctx);
22002221
}
2222+
if let Some(session) = view.active_session.as_ref(ctx).session(ctx) {
2223+
view.warm_shell_completion_sources(session, ctx);
2224+
}
22012225
view
22022226
}
22032227

crates/warp_tui/src/terminal_session_view/completions.rs

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
//! Asynchronous shell-command completion coordination for the TUI composer.
22
3-
use warp::tui_export::{longest_common_prefix, tui_completion_session_context};
3+
use std::sync::Arc;
4+
5+
use warp::tui_export::{Session, longest_common_prefix, tui_completion_session_context};
46
use warp_completer::completer::{
57
CompleterOptions, EngineFileType, Match, SuggestionResults, suggestions,
68
};
@@ -29,6 +31,40 @@ struct CompletionRequestSnapshot {
2931
}
3032

3133
impl TuiTerminalSessionView {
34+
pub(super) fn warm_shell_completion_sources(
35+
&self,
36+
session: Arc<Session>,
37+
ctx: &mut ViewContext<Self>,
38+
) {
39+
let function_names_session = session.clone();
40+
let builtins_session = session.clone();
41+
42+
ctx.spawn(
43+
async move { session.load_external_commands().await },
44+
|_, _, _| {},
45+
);
46+
ctx.background_executor()
47+
.spawn(async move { function_names_session.load_all_function_names().await })
48+
.detach();
49+
ctx.background_executor()
50+
.spawn(async move { builtins_session.load_all_builtins().await })
51+
.detach();
52+
}
53+
54+
pub(super) fn ensure_external_commands_are_warming(&self, ctx: &mut ViewContext<Self>) {
55+
let Some(session) = self.active_session.as_ref(ctx).session(ctx) else {
56+
return;
57+
};
58+
if session.has_attempted_to_load_external_commands() {
59+
return;
60+
}
61+
62+
ctx.spawn(
63+
async move { session.load_external_commands().await },
64+
|_, _, _| {},
65+
);
66+
}
67+
3268
pub(super) fn request_shell_completion(&mut self, ctx: &mut ViewContext<Self>) {
3369
if active_inline_menu(
3470
&self.inline_menus,

crates/warp_tui/src/terminal_session_view_tests.rs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ use warp::tui_export::{
2020
AIConversationAutoexecuteMode, AIConversationId, AgentViewEntryOrigin, BlockPadding,
2121
BlocklistAIHistoryEvent, BlocklistAIHistoryModel, ConversationStatus, ConversationUsageTotals,
2222
Harness, InputTypeAutoDetectionSource, LLMPreferences, LinkedWorkflowData,
23-
LongRunningCommandControlState, PtyIntent, PtyIntentEvent, SizeInfo, SizeUpdate,
23+
LongRunningCommandControlState, PtyIntent, PtyIntentEvent, Session, SizeInfo, SizeUpdate,
2424
SlashCommandDataSource as _, SlashCommandKind, TaskId, TranscriptScope, TuiMcpAction,
2525
TuiMcpServerId, TuiUpArrowHistoryItemKind, UserTakeOverReason, WarpConfig,
2626
WarpConfigUpdateEvent, export_conversation_markdown, light_theme,
@@ -852,6 +852,27 @@ fn shell_mode_reserves_tab_even_when_attachments_render() {
852852
assert!(!attachment_focus_available(true, true));
853853
assert!(!attachment_focus_available(false, false));
854854
}
855+
#[test]
856+
fn shell_completion_source_warmup_loads_path_executables() {
857+
App::test((), |mut app| async move {
858+
let fixture = focus_test_fixture(&mut app);
859+
let (view, _) = add_focus_test_session(&mut app, &fixture, true);
860+
let session = Arc::new(Session::test());
861+
862+
view.update(&mut app, |view, ctx| {
863+
view.warm_shell_completion_sources(session.clone(), ctx);
864+
});
865+
866+
let deadline = Instant::now() + Duration::from_secs(5);
867+
while !session.has_loaded_external_commands() && Instant::now() < deadline {
868+
Timer::after(Duration::from_millis(10)).await;
869+
}
870+
871+
assert!(session.has_attempted_to_load_external_commands());
872+
assert!(session.has_loaded_external_commands());
873+
assert!(session.executable_names().any(|command| command == "git"));
874+
});
875+
}
855876

856877
#[test]
857878
fn nld_reset_only_unlocks_after_agent_control_and_not_on_user_edit() {

0 commit comments

Comments
 (0)