Skip to content

Commit e120962

Browse files
oz-agentwarp-agent
andcommitted
[CODE-1813] Show TUI settings load failures
Co-Authored-By: Warp <agent@warp.dev> Co-Authored-By: Oz <oz-agent@warp.dev>
1 parent bf56c3c commit e120962

6 files changed

Lines changed: 162 additions & 26 deletions

File tree

app/src/settings/init.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -158,9 +158,8 @@ pub fn init(
158158
None
159159
};
160160

161-
// Always log a settings-load failure. The GUI additionally surfaces this
162-
// via a banner/footer, but headless surfaces (e.g. the TUI) have no such
163-
// UI, so the log is the baseline signal. Final user-facing UX is TBD.
161+
// Always log a settings-load failure with its full details. User-facing
162+
// surfaces may additionally present a shorter summary.
164163
if let Some(err) = &settings_file_error {
165164
match err {
166165
super::SettingsFileError::FileParseFailed(detail) => {

app/src/tui_export.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ pub use crate::code_review::git_repo_model::{
159159
};
160160
pub use crate::code_review::github_repo_model::GitHubRepoModel;
161161
pub use crate::completer::SessionContext;
162+
pub use crate::global_resource_handles::GlobalResourceHandlesProvider;
162163
pub use crate::persistence::PersistenceWriter;
163164
pub use crate::prefix::longest_common_prefix;
164165
pub use crate::search::slash_command_menu::static_commands::commands::{
@@ -243,6 +244,7 @@ pub use crate::tui_test_support::{
243244
blocklist_ai_history_model_with_queries, queue_tui_permission_action,
244245
register_tui_input_mode_test_settings, register_tui_session_view_test_singletons,
245246
};
247+
pub use crate::user_config::{WarpConfig, WarpConfigUpdateEvent};
246248
pub use crate::util::image::{
247249
MAX_IMAGE_COUNT_FOR_QUERY, MAX_IMAGE_SIZE_BYTES, MIME_SNIFF_BYTES, ProcessImageResult,
248250
infer_mime_type, is_supported_image_mime_type, process_image_for_agent,

crates/warp_tui/src/session_registry.rs

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ use std::path::PathBuf;
1010
use pathfinder_geometry::vector::Vector2F;
1111
use warp::tui_export::{
1212
AIConversation, AIConversationAutoexecuteMode, AIConversationId, AmbientAgentTaskId,
13-
BannerState, BlocklistAIHistoryModel, IsSharedSessionCreator, LocalTtyTerminalManager,
14-
PersistenceWriter, ServerConversationToken, TerminalManagerTrait, TerminalSurfaceResult,
15-
oz_run_url,
13+
BannerState, BlocklistAIHistoryModel, GlobalResourceHandlesProvider, IsSharedSessionCreator,
14+
LocalTtyTerminalManager, PersistenceWriter, ServerConversationToken, TerminalManagerTrait,
15+
TerminalSurfaceResult, oz_run_url,
1616
};
1717
use warpui::SingletonEntity;
1818
use warpui_core::runtime::TuiDriverHandle;
@@ -154,14 +154,27 @@ impl TuiSessions {
154154
startup_directory: Option<PathBuf>,
155155
ctx: &mut AppContext,
156156
) -> (TuiSessionId, ViewHandle<TuiTerminalSessionView>) {
157-
let (exit_summary, keyboard_enhancement_supported, default_autoexecute_mode) = sessions
158-
.read(ctx, |sessions, _| {
159-
(
160-
sessions.exit_summary.clone(),
161-
sessions.keyboard_enhancement_supported,
162-
sessions.default_autoexecute_mode,
163-
)
164-
});
157+
let (
158+
exit_summary,
159+
keyboard_enhancement_supported,
160+
default_autoexecute_mode,
161+
is_first_session,
162+
) = sessions.read(ctx, |sessions, _| {
163+
(
164+
sessions.exit_summary.clone(),
165+
sessions.keyboard_enhancement_supported,
166+
sessions.default_autoexecute_mode,
167+
sessions.is_empty(),
168+
)
169+
});
170+
let initial_settings_file_error = is_first_session
171+
.then(|| {
172+
GlobalResourceHandlesProvider::as_ref(ctx)
173+
.get()
174+
.settings_file_error
175+
.clone()
176+
})
177+
.flatten();
165178
// The manager uses this internal model for unsupported-shell state; the
166179
// TUI does not render a separate banner surface.
167180
let banner = ctx.add_model(|_| BannerState::default());
@@ -184,6 +197,7 @@ impl TuiSessions {
184197
exit_summary,
185198
keyboard_enhancement_supported,
186199
default_autoexecute_mode,
200+
initial_settings_file_error,
187201
ctx,
188202
)
189203
});

crates/warp_tui/src/terminal_session_view.rs

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ use parking_lot::FairMutex;
1212
use warp::appearance::AppearanceEvent;
1313
use warp::editor::{CodeEditorModel, CodeEditorModelEvent};
1414
use warp::settings::{
15-
AISettings, AISettingsChangedEvent, AppEditorSettings, TuiStatuslineConfig, TuiTheme,
16-
TuiThemeSettings, TuiVoiceSettings,
15+
AISettings, AISettingsChangedEvent, AppEditorSettings, SettingsFileError, TuiStatuslineConfig,
16+
TuiTheme, TuiThemeSettings, TuiVoiceSettings,
1717
};
1818
use warp::tui_export::{
1919
AIAgentActionId, AIAgentActionResultType, AIAgentContext, AIAgentExchangeId,
@@ -36,11 +36,11 @@ use warp::tui_export::{
3636
TerminalModel, TerminalSurface, TerminalSurfaceInit, TranscriptScope, TuiMcpAction,
3737
TuiMcpManager, TuiSlashCommandDataSource, TuiSlashCommandDataSourceArgs,
3838
TuiUpArrowHistoryItemKind, TuiUserInfoManager, TuiUserInfoManagerEvent, TuiZeroStateDataSource,
39-
UserTakeOverReason, WAKEUP_THROTTLE_PERIOD, block_context_from_terminal_model,
40-
build_slash_command_mixer, detect_possible_git_repo, export_conversation_markdown, log_out_tui,
41-
maybe_build_ai_query_upsert_event, prepare_conversation_block_restoration,
42-
record_autodetection_toggle_from_slash_command, record_saved_prompt_accepted,
43-
record_static_slash_command_accepted, saved_prompt_text_for_id,
39+
UserTakeOverReason, WAKEUP_THROTTLE_PERIOD, WarpConfig, WarpConfigUpdateEvent,
40+
block_context_from_terminal_model, build_slash_command_mixer, detect_possible_git_repo,
41+
export_conversation_markdown, log_out_tui, maybe_build_ai_query_upsert_event,
42+
prepare_conversation_block_restoration, record_autodetection_toggle_from_slash_command,
43+
record_saved_prompt_accepted, record_static_slash_command_accepted, saved_prompt_text_for_id,
4444
slash_command_selection_behavior, slash_commands, throttle,
4545
};
4646
use warp_core::channel::{Channel, ChannelState};
@@ -167,6 +167,15 @@ const RUNNING_COMMAND_DETACH_HINT: &str = "ctrl-c to return to command";
167167
/// Replaces the exit hint when viewing a child agent conversation.
168168
pub(crate) const CTRL_C_KILL_CHILD_HINT: &str = "ctrl-c again to kill child agent";
169169
const STARTING_SHELL_HINT: &str = "Starting shell...";
170+
const SETTINGS_PARSE_FAILED_HINT: &str = "Settings failed to load: invalid syntax.";
171+
const SETTINGS_INVALID_VALUES_HINT: &str = "Settings failed to load: invalid values.";
172+
173+
fn settings_file_error_hint(error: &SettingsFileError) -> &'static str {
174+
match error {
175+
SettingsFileError::FileParseFailed(_) => SETTINGS_PARSE_FAILED_HINT,
176+
SettingsFileError::InvalidSettings(_) => SETTINGS_INVALID_VALUES_HINT,
177+
}
178+
}
170179

171180
/// Fallback strings for the /status status menu.
172181
const STATUS_UNAVAILABLE: &str = "\u{2014}"; // em dash
@@ -1249,6 +1258,7 @@ impl TuiTerminalSessionView {
12491258
exit_summary: TuiExitSummaryHandle,
12501259
keyboard_enhancement_supported: bool,
12511260
default_autoexecute_mode: AIConversationAutoexecuteMode,
1261+
initial_settings_file_error: Option<SettingsFileError>,
12521262
ctx: &mut ViewContext<Self>,
12531263
) -> Self {
12541264
let TerminalSurfaceInit {
@@ -1267,6 +1277,11 @@ impl TuiTerminalSessionView {
12671277
ctx.subscribe_to_model(&Appearance::handle(ctx), |view, _, event, ctx| {
12681278
view.handle_appearance_event(event, ctx);
12691279
});
1280+
ctx.subscribe_to_model(&WarpConfig::handle(ctx), |view, _, event, ctx| {
1281+
if let WarpConfigUpdateEvent::SettingsErrors(error) = event {
1282+
view.show_settings_file_error(error, ctx);
1283+
}
1284+
});
12701285

12711286
let terminal_surface_id: EntityId = ctx.view_id();
12721287
let active_session =
@@ -1977,6 +1992,9 @@ impl TuiTerminalSessionView {
19771992
if let Some(failure) = initial_zero_state_load_failure {
19781993
view.show_zero_state_ascii_load_failure(failure, ctx);
19791994
}
1995+
if let Some(error) = initial_settings_file_error {
1996+
view.show_settings_file_error(&error, ctx);
1997+
}
19801998
view
19811999
}
19822000

@@ -3002,6 +3020,10 @@ impl TuiTerminalSessionView {
30023020
self.show_error_hint(zero_state_ascii_load_failure_hint(failure).to_owned(), ctx);
30033021
}
30043022

3023+
fn show_settings_file_error(&mut self, error: &SettingsFileError, ctx: &mut ViewContext<Self>) {
3024+
self.show_error_hint(settings_file_error_hint(error).to_owned(), ctx);
3025+
}
3026+
30053027
/// Displays success-colored feedback in the transient footer slot.
30063028
fn show_copy_hint(&mut self, ctx: &mut ViewContext<Self>) {
30073029
self.show_success_hint(COPY_SELECTION_HINT.to_owned(), ctx);

crates/warp_tui/src/terminal_session_view_tests.rs

Lines changed: 91 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@ use instant::Instant;
1010
use tempfile::TempDir;
1111
use warp::appearance::Appearance;
1212
use warp::settings::{
13-
AISettings, TuiStatuslineConfig, TuiStatuslineItem, TuiTheme, TuiThemeSettings,
14-
TuiUsageDisplayMode, TuiVoiceInputHoldKey, TuiVoiceSettings, TuiZeroStateObject,
13+
AISettings, SettingsFileError, TuiStatuslineConfig, TuiStatuslineItem, TuiTheme,
14+
TuiThemeSettings, TuiUsageDisplayMode, TuiVoiceInputHoldKey, TuiVoiceSettings,
15+
TuiZeroStateObject,
1516
};
1617
use warp::terminal::model::ansi::{Handler, InputBufferValue, Mode};
1718
use warp::tui_export::{
@@ -21,8 +22,9 @@ use warp::tui_export::{
2122
Harness, InputTypeAutoDetectionSource, LLMPreferences, LinkedWorkflowData,
2223
LongRunningCommandControlState, PtyIntent, PtyIntentEvent, SizeInfo, SizeUpdate,
2324
SlashCommandDataSource as _, SlashCommandKind, TaskId, TranscriptScope, TuiMcpAction,
24-
TuiMcpServerId, TuiUpArrowHistoryItemKind, UserTakeOverReason, export_conversation_markdown,
25-
light_theme, register_tui_session_view_test_singletons, slash_commands,
25+
TuiMcpServerId, TuiUpArrowHistoryItemKind, UserTakeOverReason, WarpConfig,
26+
WarpConfigUpdateEvent, export_conversation_markdown, light_theme,
27+
register_tui_session_view_test_singletons, slash_commands,
2628
};
2729
use warp_core::channel::Channel;
2830
use warp_core::features::FeatureFlag;
@@ -92,7 +94,10 @@ use crate::statusline_config_view::TuiStatuslineConfigEvent;
9294
use crate::terminal_background::TuiHostTerminalBackground;
9395
use crate::terminal_block::{block_content_rows, should_render_terminal_block};
9496
use crate::terminal_use::TuiInputTarget;
95-
use crate::test_fixtures::{add_test_semantic_selection, add_test_terminal_session};
97+
use crate::test_fixtures::{
98+
add_test_semantic_selection, add_test_terminal_session,
99+
add_test_terminal_session_with_settings_file_error,
100+
};
96101
use crate::transcript_view::TRANSCRIPT_BLOCK_SPACING;
97102
use crate::transient_hint::TransientHintTone;
98103
use crate::tui_builder::TuiUiBuilder;
@@ -976,6 +981,42 @@ fn zero_state_reload_failure_renders_as_an_error_footer_hint() {
976981
});
977982
}
978983

984+
#[test]
985+
fn settings_reload_failure_renders_as_an_error_footer_hint() {
986+
App::test((), |mut app| async move {
987+
let fixture = focus_test_fixture(&mut app);
988+
let (view, _) = add_focus_test_session(&mut app, &fixture, true);
989+
990+
app.update(|ctx| {
991+
WarpConfig::handle(ctx).update(ctx, |_, ctx| {
992+
ctx.emit(WarpConfigUpdateEvent::SettingsErrors(
993+
SettingsFileError::InvalidSettings(vec!["Theme".to_owned()]),
994+
));
995+
});
996+
});
997+
998+
assert_eq!(
999+
view.read(&app, |view, _| {
1000+
view.transient_hint
1001+
.current()
1002+
.map(|(text, tone)| (text.to_owned(), tone))
1003+
}),
1004+
Some((
1005+
super::SETTINGS_INVALID_VALUES_HINT.to_owned(),
1006+
TransientHintTone::Error
1007+
))
1008+
);
1009+
1010+
app.read(|ctx| {
1011+
let footer = view.as_ref(ctx).render_footer(ctx).finish();
1012+
assert_eq!(
1013+
render_element(footer, ctx, 120).to_lines(),
1014+
vec![super::SETTINGS_INVALID_VALUES_HINT.to_owned()]
1015+
);
1016+
});
1017+
});
1018+
}
1019+
9791020
#[test]
9801021
fn theme_slash_command_accepts_direct_selection_and_rejects_invalid_values() {
9811022
App::test((), |mut app| async move {
@@ -1100,6 +1141,38 @@ fn zero_state_initial_load_failure_shows_an_error_footer_hint() {
11001141
});
11011142
}
11021143

1144+
#[test]
1145+
fn startup_settings_parse_failure_renders_as_an_error_footer_hint() {
1146+
App::test((), |mut app| async move {
1147+
let fixture = focus_test_fixture(&mut app);
1148+
let view = add_focus_test_session_with_settings_file_error(
1149+
&mut app,
1150+
&fixture,
1151+
SettingsFileError::FileParseFailed("expected a value".to_owned()),
1152+
);
1153+
1154+
assert_eq!(
1155+
view.read(&app, |view, _| {
1156+
view.transient_hint
1157+
.current()
1158+
.map(|(text, tone)| (text.to_owned(), tone))
1159+
}),
1160+
Some((
1161+
super::SETTINGS_PARSE_FAILED_HINT.to_owned(),
1162+
TransientHintTone::Error
1163+
))
1164+
);
1165+
1166+
app.read(|ctx| {
1167+
let footer = view.as_ref(ctx).render_footer(ctx).finish();
1168+
assert_eq!(
1169+
render_element(footer, ctx, 120).to_lines(),
1170+
vec![super::SETTINGS_PARSE_FAILED_HINT.to_owned()]
1171+
);
1172+
});
1173+
});
1174+
}
1175+
11031176
#[test]
11041177
fn listening_voice_input_animates_the_input_border() {
11051178
App::test((), |mut app| async move {
@@ -2133,6 +2206,19 @@ fn add_focus_test_session(
21332206
(view, session_id)
21342207
}
21352208

2209+
fn add_focus_test_session_with_settings_file_error(
2210+
app: &mut App,
2211+
fixture: &FocusTestFixture,
2212+
error: SettingsFileError,
2213+
) -> ViewHandle<super::TuiTerminalSessionView> {
2214+
let (view, manager) =
2215+
add_test_terminal_session_with_settings_file_error(app, fixture.window_id, Some(error));
2216+
app.update(|ctx| {
2217+
TuiSessions::register_session(&fixture.sessions, view.clone(), manager, true, ctx);
2218+
});
2219+
view
2220+
}
2221+
21362222
fn render_element(element: Box<dyn TuiElement>, ctx: &AppContext, width: u16) -> TuiBuffer {
21372223
render_element_with_size(element, ctx, width, 1)
21382224
}

crates/warp_tui/src/test_fixtures.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use std::any::Any;
33
use std::sync::Arc;
44

55
use parking_lot::FairMutex;
6+
use warp::settings::SettingsFileError;
67
use warp::tui_export::{
78
AIConversationAutoexecuteMode, ActiveSession, Appearance, BlocklistAIActionModel,
89
BlocklistAIHistoryModel, ConversationSelection, ConversationSelectionHandle,
@@ -136,6 +137,17 @@ pub(crate) fn add_test_terminal_session(
136137
) -> (
137138
ViewHandle<TuiTerminalSessionView>,
138139
ModelHandle<Box<dyn TerminalManagerTrait>>,
140+
) {
141+
add_test_terminal_session_with_settings_file_error(app, window_id, None)
142+
}
143+
144+
pub(crate) fn add_test_terminal_session_with_settings_file_error(
145+
app: &mut App,
146+
window_id: WindowId,
147+
initial_settings_file_error: Option<SettingsFileError>,
148+
) -> (
149+
ViewHandle<TuiTerminalSessionView>,
150+
ModelHandle<Box<dyn TerminalManagerTrait>>,
139151
) {
140152
app.update(|ctx| {
141153
if !ctx.has_singleton_model::<ZeroStateAnimationConfig>() {
@@ -149,6 +161,7 @@ pub(crate) fn add_test_terminal_session(
149161
TuiExitSummaryHandle::default(),
150162
false,
151163
AIConversationAutoexecuteMode::RespectUserSettings,
164+
initial_settings_file_error,
152165
ctx,
153166
)
154167
});

0 commit comments

Comments
 (0)