Skip to content
This repository was archived by the owner on Aug 19, 2026. It is now read-only.

Commit 8ccc8d7

Browse files
committed
fix(oss): prevent cloud menu startup crashes
1 parent 84820be commit 8ccc8d7

6 files changed

Lines changed: 197 additions & 24 deletions

File tree

app/src/app_menus.rs

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ use itertools::Itertools;
2727
use settings::manager::SettingsManager;
2828
use settings::Setting as _;
2929
use warp_core::brand;
30+
use warp_core::channel::ChannelState;
3031
use warp_core::context_flag::ContextFlag;
3132
use warp_util::path::user_friendly_path;
3233
use warpui::actions::StandardAction;
@@ -62,18 +63,22 @@ const MAX_RECENT_REPOS_IN_MENU: usize = 10;
6263

6364
/// Creates the root app menu bar
6465
pub fn menu_bar(ctx: &mut AppContext) -> MenuBar {
65-
MenuBar::new(vec![
66+
let mut menus = vec![
6667
make_new_app_menu(ctx),
6768
make_new_file_menu(ctx),
6869
make_new_edit_menu(ctx),
6970
make_new_view_menu(ctx),
7071
make_new_tab_menu(ctx),
7172
make_new_blocks_menu(ctx),
7273
make_new_ai_menu(ctx),
73-
make_new_drive_menu(ctx),
74-
make_new_window_menu(),
75-
make_new_help_menu(),
76-
])
74+
];
75+
76+
if ChannelState::cloud_services_available() {
77+
menus.push(make_new_drive_menu(ctx));
78+
}
79+
80+
menus.extend([make_new_window_menu(), make_new_help_menu()]);
81+
MenuBar::new(menus)
7782
}
7883

7984
// Creates the app dock menu
@@ -150,11 +155,14 @@ fn make_new_app_menu(ctx: &AppContext) -> Menu {
150155
))
151156
}
152157

153-
menu_items.extend([
154-
MenuItem::Separator,
155-
updateable_custom_item_without_checkmark(CustomAction::ReferAFriend, ctx),
156-
MenuItem::Separator,
157-
]);
158+
menu_items.push(MenuItem::Separator);
159+
if ChannelState::cloud_services_available() {
160+
menu_items.push(updateable_custom_item_without_checkmark(
161+
CustomAction::ReferAFriend,
162+
ctx,
163+
));
164+
menu_items.push(MenuItem::Separator);
165+
}
158166

159167
let preferences_menu_items = vec![
160168
updateable_custom_item_without_checkmark(CustomAction::ShowSettings, ctx),
@@ -377,14 +385,21 @@ fn make_new_edit_menu(ctx: &AppContext) -> Menu {
377385
}
378386

379387
fn make_new_view_menu(ctx: &AppContext) -> Menu {
380-
let mut items = vec![
381-
updateable_custom_item_without_checkmark(CustomAction::ToggleWarpDrive, ctx),
382-
MenuItem::Separator,
388+
let mut items = vec![];
389+
if ChannelState::cloud_services_available() {
390+
items.push(updateable_custom_item_without_checkmark(
391+
CustomAction::ToggleWarpDrive,
392+
ctx,
393+
));
394+
items.push(MenuItem::Separator);
395+
}
396+
397+
items.extend([
383398
updateable_custom_item_without_checkmark(CustomAction::CommandPalette, ctx),
384399
updateable_custom_item_without_checkmark(CustomAction::NavigationPalette, ctx),
385400
updateable_custom_item_without_checkmark(CustomAction::LaunchConfigPalette, ctx),
386401
updateable_custom_item_without_checkmark(CustomAction::FilesPalette, ctx),
387-
];
402+
]);
388403

389404
if FeatureFlag::AgentViewConversationListView.is_enabled() {
390405
items.push(updateable_custom_item_without_checkmark(
@@ -598,9 +613,17 @@ fn make_new_blocks_menu(ctx: &AppContext) -> Menu {
598613
ctx,
599614
));
600615
items.push(MenuItem::Separator);
616+
items.push(updateable_custom_item_without_checkmark(
617+
CustomAction::CreateBlockPermalink,
618+
ctx,
619+
));
620+
if ChannelState::cloud_services_available() {
621+
items.push(non_updateable_custom_item(
622+
CustomAction::ViewSharedBlocks,
623+
ctx,
624+
));
625+
}
601626
items.extend([
602-
updateable_custom_item_without_checkmark(CustomAction::CreateBlockPermalink, ctx),
603-
non_updateable_custom_item(CustomAction::ViewSharedBlocks, ctx),
604627
updateable_custom_item_without_checkmark(CustomAction::ToggleBookmarkBlock, ctx),
605628
updateable_custom_item_without_checkmark(CustomAction::FindWithinBlock, ctx),
606629
MenuItem::Separator,

app/src/castcodes_public_surface_tests.rs

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const AI_BLOCK_SOURCE: &str = include_str!("ai/blocklist/block.rs");
99
const AI_BLOCK_STATUS_BAR_SOURCE: &str = include_str!("ai/blocklist/block/status_bar.rs");
1010
const AI_FACT_RULE_SOURCE: &str = include_str!("ai/facts/view/rule.rs");
1111
const AI_TELEMETRY_BANNER_SOURCE: &str = include_str!("ai/blocklist/telemetry_banner.rs");
12+
const APP_MENUS_SOURCE: &str = include_str!("app_menus.rs");
1213
const AGENT_VIEW_ZERO_STATE_BLOCK_SOURCE: &str =
1314
include_str!("ai/blocklist/agent_view/zero_state_block.rs");
1415
const AGENT_PANEL_MOD_SOURCE: &str = include_str!("agent_panel/mod.rs");
@@ -54,6 +55,117 @@ const WORKFLOW_VIEW_SOURCE: &str = include_str!("workflows/workflow_view.rs");
5455
const WORKSPACE_MOD_SOURCE: &str = include_str!("workspace/mod.rs");
5556
const WORKSPACE_VIEW_SOURCE: &str = include_str!("workspace/view.rs");
5657

58+
fn function_source<'a>(source: &'a str, function: &str, next_function: &str) -> &'a str {
59+
source
60+
.split_once(function)
61+
.unwrap_or_else(|| panic!("{function} should exist"))
62+
.1
63+
.split_once(next_function)
64+
.unwrap_or_else(|| panic!("{next_function} should follow {function}"))
65+
.0
66+
}
67+
68+
fn matching_closing_brace(source: &str, opening_brace_offset: usize) -> Option<usize> {
69+
if source.as_bytes().get(opening_brace_offset) != Some(&b'{') {
70+
return None;
71+
}
72+
73+
let mut depth = 0;
74+
for (relative_offset, character) in source[opening_brace_offset..].char_indices() {
75+
match character {
76+
'{' => depth += 1,
77+
'}' => {
78+
depth -= 1;
79+
if depth == 0 {
80+
return Some(opening_brace_offset + relative_offset);
81+
}
82+
}
83+
_ => {}
84+
}
85+
}
86+
None
87+
}
88+
89+
fn assert_item_is_cloud_service_only(function: &str, item: &str, description: &str) {
90+
let item_offset = function
91+
.find(item)
92+
.unwrap_or_else(|| panic!("{description} should exist"));
93+
let cloud_gate = "if ChannelState::cloud_services_available()";
94+
let gate_offset = function[..item_offset]
95+
.rfind(cloud_gate)
96+
.unwrap_or_else(|| panic!("{description} should have a cloud-service gate"));
97+
let gate_open_offset = function[gate_offset..item_offset]
98+
.find('{')
99+
.map(|offset| gate_offset + offset)
100+
.unwrap_or_else(|| panic!("{description} cloud-service gate should open"));
101+
let gate_end_offset = matching_closing_brace(function, gate_open_offset)
102+
.unwrap_or_else(|| panic!("{description} cloud-service gate should close"));
103+
104+
assert!(
105+
item_offset < gate_end_offset,
106+
"{description} should be hidden when hosted cloud services are unavailable"
107+
);
108+
}
109+
110+
#[test]
111+
fn cloud_service_gate_matching_supports_nested_braces() {
112+
let function = r#"
113+
if ChannelState::cloud_services_available() {
114+
if nested_condition {
115+
nested_action();
116+
}
117+
CustomAction::ReferAFriend;
118+
}
119+
"#;
120+
assert_item_is_cloud_service_only(
121+
function,
122+
"CustomAction::ReferAFriend",
123+
"nested referral action",
124+
);
125+
}
126+
127+
#[test]
128+
fn referral_app_menu_item_is_cloud_service_only() {
129+
let app_menu = function_source(
130+
APP_MENUS_SOURCE,
131+
"fn make_new_app_menu",
132+
"fn make_new_file_menu",
133+
);
134+
assert_item_is_cloud_service_only(
135+
app_menu,
136+
"CustomAction::ReferAFriend",
137+
"referral app menu action",
138+
);
139+
}
140+
141+
#[test]
142+
fn oss_app_menus_do_not_build_cloud_only_items() {
143+
let menu_bar = function_source(APP_MENUS_SOURCE, "pub fn menu_bar", "pub fn dock_menu");
144+
assert_item_is_cloud_service_only(menu_bar, "make_new_drive_menu(ctx)", "Drive app menu");
145+
146+
let view_menu = function_source(
147+
APP_MENUS_SOURCE,
148+
"fn make_new_view_menu",
149+
"fn make_new_tab_menu",
150+
);
151+
assert_item_is_cloud_service_only(
152+
view_menu,
153+
"CustomAction::ToggleWarpDrive",
154+
"Cast Drive view menu action",
155+
);
156+
157+
let blocks_menu = function_source(
158+
APP_MENUS_SOURCE,
159+
"fn make_new_blocks_menu",
160+
"fn make_new_drive_menu",
161+
);
162+
assert_item_is_cloud_service_only(
163+
blocks_menu,
164+
"CustomAction::ViewSharedBlocks",
165+
"shared blocks menu action",
166+
);
167+
}
168+
57169
#[test]
58170
fn public_app_surfaces_use_castcodes_links_and_labels() {
59171
assert!(LOGIN_SLIDE_SOURCE.contains("PRIVACY_POLICY_URL"));

crates/integration/src/bin/integration.rs

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,17 @@ pub struct Args {
2323
}
2424

2525
pub fn main() -> Result<()> {
26+
let args = Args::parse();
27+
let tests = register_tests();
28+
let channel = args
29+
.integration_test_name
30+
.as_deref()
31+
.and_then(|test_name| tests.get(test_name))
32+
.map(|(channel, _)| *channel)
33+
.unwrap_or(Channel::Integration);
34+
2635
ChannelState::set(ChannelState::new(
27-
Channel::Integration,
36+
channel,
2837
ChannelConfig {
2938
app_id: AppId::new(
3039
"dev",
@@ -56,8 +65,6 @@ pub fn main() -> Result<()> {
5665
},
5766
));
5867

59-
let args = Args::parse();
60-
6168
if let Some(command) = &args.command {
6269
match command {
6370
#[cfg(unix)]
@@ -75,14 +82,13 @@ pub fn main() -> Result<()> {
7582
}
7683
}
7784

78-
let tests = register_tests();
7985
let test_name = args
8086
.integration_test_name
8187
.as_deref()
8288
.expect("Integration test name is required");
8389

8490
println!("Running integration test: {test_name}");
85-
let Some(builder) = tests.get(test_name).map(|func| func()) else {
91+
let Some(builder) = tests.get(test_name).map(|(_, builder_fn)| builder_fn()) else {
8692
panic!("test not found for args: {:#?}", env::args());
8793
};
8894
#[cfg_attr(not(unix), allow(unused_variables))]
@@ -109,21 +115,27 @@ pub fn main() -> Result<()> {
109115

110116
/// Type of a function that produces an integration test builder.
111117
type BoxedBuilderFn = Box<dyn Fn() -> Builder>;
118+
type RegisteredTest = (Channel, BoxedBuilderFn);
112119

113-
fn register_tests() -> HashMap<&'static str, BoxedBuilderFn> {
114-
let mut tests: HashMap<&str, BoxedBuilderFn> = HashMap::new();
120+
fn register_tests() -> HashMap<&'static str, RegisteredTest> {
121+
let mut tests: HashMap<&str, RegisteredTest> = HashMap::new();
115122

116123
// A tiny macro to simplify the act of registering a test. This avoids
117124
// any inconsistencies between the test function name and the key in the
118125
// map, and makes it easier to change how we register the tests (if we
119126
// decide to do so in the future).
120127
macro_rules! register_test {
121128
($name:ident) => {
122-
tests.insert(stringify!($name), Box::new(|| $name()));
129+
register_test!($name, Channel::Integration);
130+
};
131+
($name:ident, $channel:expr) => {
132+
tests.insert(stringify!($name), ($channel, Box::new(|| $name())));
123133
};
124134
}
125135

126136
// Add new tests here
137+
#[cfg(target_os = "macos")]
138+
register_test!(test_oss_app_menu_startup, Channel::Oss);
127139
register_test!(test_single_command);
128140
register_test!(test_add_and_close_session);
129141
register_test!(test_add_many_sessions);

crates/integration/src/test.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
55
mod agent_mode;
66
mod agent_panel;
7+
#[cfg(target_os = "macos")]
8+
mod app_startup;
79
mod block_filtering;
810
mod bootstrapping;
911
mod code_review;
@@ -36,6 +38,8 @@ mod workspace;
3638

3739
pub use agent_mode::*;
3840
pub use agent_panel::*;
41+
#[cfg(target_os = "macos")]
42+
pub use app_startup::*;
3943
pub use block_filtering::*;
4044
pub use bootstrapping::*;
4145
pub use code_review::*;
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
use crate::Builder;
2+
use warpui::integration::{AssertionOutcome, TestStep};
3+
4+
pub fn test_oss_app_menu_startup() -> Builder {
5+
Builder::new().with_step(TestStep::new("Assert OSS app startup").add_named_assertion(
6+
"initial root view exists",
7+
|app, window_id| {
8+
if app
9+
.root_view::<warp::root_view::RootView>(window_id)
10+
.is_some()
11+
{
12+
AssertionOutcome::Success
13+
} else {
14+
AssertionOutcome::failure(format!(
15+
"root view should exist for window_id={window_id}"
16+
))
17+
}
18+
},
19+
))
20+
}

crates/integration/tests/integration/ui_tests.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
use super::integration_tests;
88

99
integration_tests! {
10+
#[cfg(target_os = "macos")]
11+
test_oss_app_menu_startup,
1012
test_add_many_sessions,
1113
test_daemon_conversation_composer_names_daemon_fix,
1214
test_ctrl_tab_session_switching,

0 commit comments

Comments
 (0)